- A+
不过今天再看的时候发现,远程代码执行的
SE说的是Drupal的callbacks造成的RCE。那么我们审计的point就要在callback上。PHP内置的一个函数
第一个参数是function的名称,第二个参数是要调用的参数的参数。
利用这几个特性,可以实现Drupal的各种花式getshell。我只审计出来一种,还有很多方式并没有去实现(毕竟渣渣看不出来)。
Drupal有一个特性,可以让管理员添加PHPtag来写文章,叫做PHPfilter。这个特性默认是关闭的。当然,由于PDO可以执行多行SQL语句的特性,我们可以直接添加管理用户然后登录上去,打开PHPfilter的Module,然后发表文章,最后getshell。
想要自动化也简单,那就是把从打开PHPfilter到发表文章的所有执行过的SQL语句合在一起当作Payload,最后getshell。这个思路的POC我也做过,可惜太过于繁琐,最后Payload出来很长很长,一点也不优雅,而且并没有利用到callback的特性。为了钻个牛角尖,咱就来审计一下如何花式getshell。
上文中说了PHPfilter是一个可以执行PHP代码的特性,而PHPfilter所在的文件是/modules/php/php.module这个文件,其中:
..
ob_start();
die($code);
print eval('?>' . $code);
$output = ob_get_contents();
ob_end_clean();
$theme_path = $old_theme_path;
return $output;
}
这个函数就是用来执行customphpcode的函数。
对于审计的工具,我这种渣渣直接就find然后grep定位文件,在用phpstorm+ideavim(pluginofphpstorm),来跟踪函数,轻松愉快ow<。
我们很确定的是我们要找call_user_func_array这个函数,利用如下命令来查找:
find . -type f -name "*" | xargs grep call_user_func_array
其实我们可以进一步缩小范围,因为第一个函数是我们调用的函数,而如果我们想利用的话,call_user_func_array这个函数的第一个参数我们要可以控制,那么进一步缩小范围:
结果如下:
我们接下来就要查看上下文,看从哪里可以控制那个变量。经过逐一排查,我定位到/include/menu.inc这个文件中的menu_execute_active_handler函数。
$page_callback_result = _menu_site_is_offline() ? MENU_SITE_OFFLINE : MENU_SITE_ONLINE;
$read_only_path = !empty($path) ? $path : $_GET['q'];
drupal_alter('menu_site_status', $page_callback_result, $read_only_path);
if ($page_callback_result == MENU_SITE_ONLINE) {
if ($router_item = menu_get_item($path)) {
if ($router_item['access']) {
if ($router_item['include_file']) {
require_once DRUPAL_ROOT . '/' . $router_item['include_file'];
}
$page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
}
else {
$page_callback_result = MENU_ACCESS_DENIED;
}
}
else {
$page_callback_result = MENU_NOT_FOUND;
}
}
}
阅读可以发现,我们可以控制$_GET['q']这个参数,接着进入menu_get_item这个函数。这个函数的核心代码是这里:
if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
menu_rebuild();
}
$original_map = arg(NULL, $path);
$parts = array_slice($original_map, 0, MENU_MAX_PARTS);
$ancestors = menu_get_ancestors($parts);
$router_item = db_query_range('SELECT * FROM {menu_router} WHERE path IN (:ancestors) ORDER BY fit DESC', 0, 1, array(':ancestors' => $ancestors))->fetchAssoc();
if ($router_item) {
drupal_alter('menu_get_item', $router_item, $path, $original_map);
$map = _menu_translate($router_item, $original_map);
$router_item['original_map'] = $original_map;
if ($map === FALSE) {
$router_items[$path] = FALSE;
return FALSE;
}
if ($router_item['access']) {
$router_item['map'] = $map;
$router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts']));
$router_item['theme_arguments'] = array_merge(menu_unserialize($router_item['theme_arguments'], $map), array_slice($map, $router_item['number_parts']));
}
}
在menu_router里查询我们输入的$_GET['q'],然后从返回所有字段。接着回到menu_execute_active_handler函数。
require_once DRUPAL_ROOT . '/' . $router_item['include_file'];
}
这里取出router_item中的include_file,然后用require_once来包含。这里是一个point,因为Drupal默认不开启PHPfilter,这里包含了就可以不用开启PHPfilter了。
接着取出router_item中的page_callback,带入call_user_func_array执行。
到此为止整个流程我们已经很清楚了。
需要注意的是page_arguments的第一个参数才会被执行,而第一个参数正是$_GET['q']的值。
通过注入向menu_router表中插入一段数据:
include_file 为 PHP filter Module 的路径;
page_callback 为 php_eval;
access_callback 为 1(可以让任意用户访问)。
访问地址即可造成RCE。
我们来测试一下。首先在数据库执行语句:
然后访问http://192.168.1.109/drupal/?q=%3C?php%20phpinfo();?%3E。
exp已经放出了,地址戳
- 我的微信
- 这是我的微信扫一扫
- 我的微信公众号
- 我的微信公众号扫一扫