Drupal 7.X服务模块从反序列化到远程命令执行

前言

Ambionics团队在审计Drupal的服务模块后发现unserialize()的不安全使用造成了严重的漏洞。这个漏洞允许特权升级,SQL注入,最后造成远程代码执行。

服务模块

Drupal的服务模块是一种构建API的标准化解决方案,以便外部客户端可以与Drupal通信。它允许任何人构建SOAP,REST或XMLRPC,以便以多种输出格式发送和获取信息。它目前是Drupal的150个最常用插件之一,大约45000正在使用。

服务允许使用不同的资源创建不同的端点,允许以面向API的方式与网站及其内容进行交互。例如,可以启用/user/login资源通过JSON或XML登录。

返回

漏洞

模块的一个特点是可以通过更改Content-Type/Acceptheaders 来控制输入/输出格式。默认情况下允许以下输入格式:

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

最后一个是给PHP序列化的数据类型。让我们再试一次:

返回:

因此确实存在一个unserialize()使用不当的漏洞。

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

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

我们能用它做什么?

利用

即使Drupal没有unserialize()小工具,服务中可用的许多端点,以及发送序列化数据的能力,提供了很多方法来利用此漏洞:用户提交的数据可以在SQL查询中使用,回显在结果等。开发关注/user/login,因为它是客户中最常用的终端。尽管如此,只要PHP反序列化被激活,仍然可以构造任何URL上的RCE Payload。

SQL注入

显然,/user/login端点的主要功能是允许人们进行身份验证。为此,服务使用通常的Drupal内部API,它从数据库中提取用户名,然后将密码哈希值与用户提交的密码进行比较。这意味着我们发送的用户名将使用Drupal数据库API的SQL查询。调用就像这样:

<?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

unserialize()的bug一样,框架的漏洞来自于自身的功能。实际上不像我们通常那样提交像字符串这样的基本类型,API提供了通过SelectQueryInterface给它一个实现Drupal的对象来进行子查询的可能性。

<?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()
  • 它的字符串必须由我们控制

SelectQueryExtender是实现SelectQueryInterface的其中两个对象之一,意在包围一个标准的SelectQuery对象。它的$query属性包含所述对象。当调用SelectQueryExtender的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对象满足了两个最后的条件:出于性能原因,它有一个stringVersion属性,意味着在编译之后包含它的字符串。

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

我们还可以在其他字段中存储其他数据库数据,例如将管理员的原始hash放在他的签名中。

现在我们可以以管理员身份登录从数据库中读取任何内容。

远程代码执行

Drupal有一个缓存表,将要序列化的数据相关联。Services模块为每个端点缓存资源列表及其期望的参数以及与其相关联的回调函数。修改缓存会产生巨大的影响,因为我们可以模块调用任何PHP函数,任何参数。而且DrupalCacheArray类允许我们这样做:

  • 修改/user/login资源的行为以在服务器上的任何位置写入文件
  • 点击/user/login创建文件
  • 恢复标准行为

为了在攻击期间不中断端点,我们使用SQL注入来获取原始缓存数据,以便我们只修改特定值。我们使用file_put_contents()和两个参数在任何地方写一个文件。

补丁

如果您使用此模块的漏洞版本,请尽快更新。在您无法更新的情况下,我们强烈建议在Drupal Services设置中禁用application/vnd.php.serialized

Exploit

以下exploit结合了这两个漏洞的利用,执行特权升级,SQL注入和RCE。

https://github.com/mottoin/Drupal-Exploit

转载来源:MottoIN

原文地址:http://www.mottoin.com/98140.html