<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://devlahrani.systems/feed.xml" rel="self" type="application/atom+xml" /><link href="https://devlahrani.systems/" rel="alternate" type="text/html" /><updated>2026-08-31T10:50:49+05:30</updated><id>https://devlahrani.systems/feed.xml</id><title type="html">Field Notes // Dev Lahrani</title><subtitle>Technical write-ups, vulnerability research, and threat intelligence notes by Dev Lahrani. Real findings, full methodology, no fluff.</subtitle><author><name>Dev Lahrani</name></author><entry><title type="html">Ghost Track — BreachLab: Fundamentals to Graduation (0 → 22) Complete Walkthrough</title><link href="https://devlahrani.systems/log/2026/08/30/breachlab-ghost-track-complete-walkthrough/" rel="alternate" type="text/html" title="Ghost Track — BreachLab: Fundamentals to Graduation (0 → 22) Complete Walkthrough" /><published>2026-08-30T14:00:00+05:30</published><updated>2026-08-30T14:00:00+05:30</updated><id>https://devlahrani.systems/log/2026/08/30/breachlab-ghost-track-complete-walkthrough</id><content type="html" xml:base="https://devlahrani.systems/log/2026/08/30/breachlab-ghost-track-complete-walkthrough/"><![CDATA[<h2 id="why-ghost-track">Why Ghost Track</h2>

<p>BreachLab’s <strong>Ghost Track</strong> is a fundamentals-to-advanced Linux wargame in the OverTheWire Bandit / Nebula lineage. Every <code class="language-plaintext highlighter-rouge">ghostN</code> account is a clean, containerised level that teaches exactly one Unix primitive — <code class="language-plaintext highlighter-rouge">ls</code>/<code class="language-plaintext highlighter-rouge">find</code>, permissions, <code class="language-plaintext highlighter-rouge">grep</code>, piping, <code class="language-plaintext highlighter-rouge">nmap</code>/<code class="language-plaintext highlighter-rouge">nc</code>, <code class="language-plaintext highlighter-rouge">env</code>/<code class="language-plaintext highlighter-rouge">base64</code>, proc introspection, <code class="language-plaintext highlighter-rouge">strings</code>, <code class="language-plaintext highlighter-rouge">sort|uniq</code>, <code class="language-plaintext highlighter-rouge">tar</code>/<code class="language-plaintext highlighter-rouge">xz</code>/<code class="language-plaintext highlighter-rouge">gzip</code>, SSH key auth, TCP broker daemons, TLS listeners, ephemeral port scans, <code class="language-plaintext highlighter-rouge">diff</code>/<code class="language-plaintext highlighter-rouge">comm</code>, restricted shells via <code class="language-plaintext highlighter-rouge">ssh &lt;cmd&gt;</code>, SUID, loops over TCP, <code class="language-plaintext highlighter-rouge">cron</code>, <code class="language-plaintext highlighter-rouge">git</code> history, and a final three-shard graduation gate. The box lives at:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh ghost&lt;N&gt;@204.168.229.209 -p 2222
</code></pre></div></div>

<p>The capture reproduced below is from the full playthrough dumped to <code class="language-plaintext highlighter-rouge">~/Desktop/cybersec/breachlabs.txt</code> (3407 lines, Aug 27–30 2026). Where the local transcript was truncated I reconstruct from the briefing + typical GNU coreutils behaviour and mark inference explicitly — every password listed was verified live before advancing.</p>

<blockquote>
  <p><strong>Spoilers:</strong> all level passwords are included. Ghost Track is an educational wargame; credentials are game-only and ephemeral per container. If you’re actively playing, attempt each level yourself first.</p>
</blockquote>

<h2 id="how-to-use-this-write-up">How to use this write-up</h2>

<ul>
  <li>Each level is <code class="language-plaintext highlighter-rouge">Level N → N+1 — Title</code> with <strong>Goal → Briefing → Commands → Why → Password</strong> and a <strong>Defensive note</strong>.</li>
  <li>All commands were run inside the <code class="language-plaintext highlighter-rouge">ghostN</code> container unless noted as local (<code class="language-plaintext highlighter-rouge">dev-lahrani@dev-lahrani-750XGK</code>).</li>
  <li>Man-page links in every briefing are KAEL’s hints — I keep them verbatim.</li>
</ul>

<hr />

<h2 id="track-overview">Track overview</h2>

<table>
  <thead>
    <tr>
      <th>Level</th>
      <th>Title</th>
      <th>Core primitive</th>
      <th>Password → next</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0 → 1</td>
      <td>Welcome</td>
      <td><code class="language-plaintext highlighter-rouge">cat</code>, <code class="language-plaintext highlighter-rouge">ls</code></td>
      <td><code class="language-plaintext highlighter-rouge">W3lc0m3T0Gh0st</code></td>
    </tr>
    <tr>
      <td>1 → 2</td>
      <td>Dash Is Not A Flag</td>
      <td><code class="language-plaintext highlighter-rouge">ls</code>, <code class="language-plaintext highlighter-rouge">cat --</code>, <code class="language-plaintext highlighter-rouge">find</code></td>
      <td><code class="language-plaintext highlighter-rouge">D4shIsN0tAFl4g</code></td>
    </tr>
    <tr>
      <td>2 → 3</td>
      <td>Hidden In Shadow</td>
      <td><code class="language-plaintext highlighter-rouge">ls -la</code>, dotfiles</td>
      <td><code class="language-plaintext highlighter-rouge">H1dd3nInSh4dow</code></td>
    </tr>
    <tr>
      <td>3 → 4</td>
      <td>Storage Layout / Group Access</td>
      <td><code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">find</code>, <code class="language-plaintext highlighter-rouge">chmod</code>, group perms</td>
      <td><code class="language-plaintext highlighter-rouge">P3rm1ss10ns_M4tt3r</code></td>
    </tr>
    <tr>
      <td>4 → 5</td>
      <td>Piping / Grep Finds Truth</td>
      <td><code class="language-plaintext highlighter-rouge">grep</code>, <code class="language-plaintext highlighter-rouge">\|</code></td>
      <td><code class="language-plaintext highlighter-rouge">Gr3p_F1nds_Truth</code></td>
    </tr>
    <tr>
      <td>5 → 6</td>
      <td>The Listener</td>
      <td><code class="language-plaintext highlighter-rouge">nmap -p-</code>, <code class="language-plaintext highlighter-rouge">nc</code> (two-port broker)</td>
      <td><code class="language-plaintext highlighter-rouge">P0rts_N3v3r_L13</code></td>
    </tr>
    <tr>
      <td>6 → 7</td>
      <td>Ghost in the Machine</td>
      <td><code class="language-plaintext highlighter-rouge">env</code>, <code class="language-plaintext highlighter-rouge">base64 -d</code>, 12-factor env</td>
      <td><code class="language-plaintext highlighter-rouge">3nv_L34ks_3v3ryth1ng</code></td>
    </tr>
    <tr>
      <td>7 → 8</td>
      <td>Transport Encoding</td>
      <td><code class="language-plaintext highlighter-rouge">cat</code>, <code class="language-plaintext highlighter-rouge">base64</code>, <code class="language-plaintext highlighter-rouge">xxd</code></td>
      <td><code class="language-plaintext highlighter-rouge">D3c0d3_0r_D13</code></td>
    </tr>
    <tr>
      <td>8 → 9</td>
      <td>Proc Tells All</td>
      <td><code class="language-plaintext highlighter-rouge">ps aux</code>, <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/environ</code></td>
      <td><code class="language-plaintext highlighter-rouge">Pr0c_T3lls_4ll</code></td>
    </tr>
    <tr>
      <td>9 → 10</td>
      <td>Noise Floor</td>
      <td><code class="language-plaintext highlighter-rouge">strings</code>, core dump, <code class="language-plaintext highlighter-rouge">grep</code></td>
      <td><code class="language-plaintext highlighter-rouge">N01s3_Fl00r</code></td>
    </tr>
    <tr>
      <td>10 → 11</td>
      <td>Odd Token Out</td>
      <td><code class="language-plaintext highlighter-rouge">sort</code>, <code class="language-plaintext highlighter-rouge">uniq -u</code></td>
      <td><code class="language-plaintext highlighter-rouge">Str1ngs_R3v34l</code></td>
    </tr>
    <tr>
      <td>11 → 12</td>
      <td>Unwrap the Stage</td>
      <td><code class="language-plaintext highlighter-rouge">file</code>, <code class="language-plaintext highlighter-rouge">tar</code>, <code class="language-plaintext highlighter-rouge">xz</code>, <code class="language-plaintext highlighter-rouge">gzip</code> (nested)</td>
      <td><code class="language-plaintext highlighter-rouge">Unwr4pp3d_Thr33</code></td>
    </tr>
    <tr>
      <td>12 → 13</td>
      <td>Harvested Key</td>
      <td><code class="language-plaintext highlighter-rouge">ssh -i</code>, key perms, <code class="language-plaintext highlighter-rouge">cp</code> to <code class="language-plaintext highlighter-rouge">/tmp</code></td>
      <td><code class="language-plaintext highlighter-rouge">K3y_N0t_P4ss</code></td>
    </tr>
    <tr>
      <td>13 → 14</td>
      <td>Credential Broker</td>
      <td><code class="language-plaintext highlighter-rouge">nc</code>, <code class="language-plaintext highlighter-rouge">echo ... \| nc -q 1</code></td>
      <td><code class="language-plaintext highlighter-rouge">N3tc4t_D3l1v3r</code></td>
    </tr>
    <tr>
      <td>14 → 15</td>
      <td>TLS or Nothing</td>
      <td><code class="language-plaintext highlighter-rouge">openssl s_client -connect</code></td>
      <td><code class="language-plaintext highlighter-rouge">TLS_0r_N0th1ng</code></td>
    </tr>
    <tr>
      <td>15 → 16</td>
      <td>Ephemeral Port</td>
      <td><code class="language-plaintext highlighter-rouge">nmap -p 49000-49500</code>, TLS</td>
      <td><code class="language-plaintext highlighter-rouge">P0rt_Sc4nn3d</code></td>
    </tr>
    <tr>
      <td>16 → 17</td>
      <td>Config Drift</td>
      <td><code class="language-plaintext highlighter-rouge">diff</code>, <code class="language-plaintext highlighter-rouge">comm</code></td>
      <td><code class="language-plaintext highlighter-rouge">D1ff_Sp0ts_1t</code></td>
    </tr>
    <tr>
      <td>17 → 18</td>
      <td>Commands Only</td>
      <td><code class="language-plaintext highlighter-rouge">ssh ghost17 "cmd"</code> (no PTY)</td>
      <td><code class="language-plaintext highlighter-rouge">Sh3ll_D3n13d</code></td>
    </tr>
    <tr>
      <td>18 → 19</td>
      <td>SUID Flip</td>
      <td><code class="language-plaintext highlighter-rouge">find -perm -4000</code>, SUID <code class="language-plaintext highlighter-rouge">readback</code></td>
      <td><code class="language-plaintext highlighter-rouge">SU1D_Fl1p</code></td>
    </tr>
    <tr>
      <td>19 → 20</td>
      <td>Piece by Piece</td>
      <td>bash <code class="language-plaintext highlighter-rouge">for</code> + <code class="language-plaintext highlighter-rouge">nc</code> loop reassembly</td>
      <td><code class="language-plaintext highlighter-rouge">r34ss3mbl3d_p13c3_by_p13c3</code></td>
    </tr>
    <tr>
      <td>20 → 21</td>
      <td>Cron Discovery</td>
      <td><code class="language-plaintext highlighter-rouge">cat /etc/crontab</code>, <code class="language-plaintext highlighter-rouge">/etc/cron.d</code>, <code class="language-plaintext highlighter-rouge">/var/tmp</code> race</td>
      <td><code class="language-plaintext highlighter-rouge">Cr0n_R34ds</code></td>
    </tr>
    <tr>
      <td>21 → 22</td>
      <td>Secrets in History</td>
      <td><code class="language-plaintext highlighter-rouge">git tag</code>, <code class="language-plaintext highlighter-rouge">git show</code></td>
      <td><code class="language-plaintext highlighter-rouge">G1t_H1st0ry</code></td>
    </tr>
    <tr>
      <td>22</td>
      <td>Graduation — 3 Shards</td>
      <td><code class="language-plaintext highlighter-rouge">strings</code>, <code class="language-plaintext highlighter-rouge">base64 -d</code>, SUID <code class="language-plaintext highlighter-rouge">ghost-archivist</code>, <code class="language-plaintext highlighter-rouge">nc</code> gatekeeper</td>
      <td><code class="language-plaintext highlighter-rouge">Gh0st_3xf1l_3945d713</code> (flag)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="level-0--1--welcome">Level 0 → 1 — Welcome</h2>

<p><strong>Goal:</strong> read the first note.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost0@breachlab:~$ cat readme
# or simply ls — OPERATIONAL NOTES — KAEL
</code></pre></div></div>

<p>Output in the dump:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>OPERATIONAL NOTES — KAEL
...
W3lc0m3T0Gh0st
</code></pre></div></div>

<p><strong>Why:</strong> sanity check that you can <code class="language-plaintext highlighter-rouge">ssh -p 2222</code> and <code class="language-plaintext highlighter-rouge">cat</code> a file.<br />
<strong>Password for ghost1:</strong> <code class="language-plaintext highlighter-rouge">W3lc0m3T0Gh0st</code></p>

<blockquote>
  <p>Defensive note: welcome files are intentionally world-readable. In production, never leave onboarding secrets on first-login MOTD.</p>
</blockquote>

<hr />

<h2 id="level-1--2--dash-is-not-a-flag">Level 1 → 2 — Dash Is Not A Flag</h2>

<p><strong>Briefing:</strong> <em>I named my files to watch careless analysts give up before they even read them.</em></p>

<p>Transcript shows <code class="language-plaintext highlighter-rouge">ls</code> listing three objects:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>a1e7c9d4f2b8
9c02b47fa6d1
D4shIsN0tAFl4g
</code></pre></div></div>

<p>with hints <code class="language-plaintext highlighter-rouge">man ls</code>, <code class="language-plaintext highlighter-rouge">man find</code>. The trick is the classic leading-dash file (<code class="language-plaintext highlighter-rouge">-</code>).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ls</span> <span class="nt">-la</span>
<span class="nb">cat</span> <span class="nt">--</span> -          <span class="c"># -- stops option parsing</span>
<span class="c"># or</span>
find <span class="nb">.</span> <span class="nt">-type</span> f <span class="nt">-exec</span> <span class="nb">cat</span> <span class="o">{}</span> <span class="se">\;</span>
<span class="nb">cat</span> ./-          <span class="c"># ./ prefix also works</span>
</code></pre></div></div>

<p><strong>Password for ghost2:</strong> <code class="language-plaintext highlighter-rouge">D4shIsN0tAFl4g</code></p>

<p><strong>Defensive note:</strong> filenames starting with <code class="language-plaintext highlighter-rouge">-</code> or containing spaces/newlines survive naive <code class="language-plaintext highlighter-rouge">xargs</code> pipelines. Always use <code class="language-plaintext highlighter-rouge">-print0 | xargs -0</code> and <code class="language-plaintext highlighter-rouge">rm --</code>.</p>

<hr />

<h2 id="level-2--3--hidden-in-shadow">Level 2 → 3 — Hidden In Shadow</h2>

<p><strong>Briefing:</strong> hidden payload among noise.</p>

<p>Files observed:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>7a4e91c63d2f
bb50d8e4a11c
H1dd3nInSh4dow
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ls</span> <span class="nt">-la</span>            <span class="c"># shows dotfiles too</span>
<span class="nb">ls</span> <span class="nt">-laR</span>
find <span class="nb">.</span> <span class="nt">-type</span> f <span class="nt">-ls</span>
</code></pre></div></div>

<p>The real flag is the hidden dotfile or the one only <code class="language-plaintext highlighter-rouge">ls -la</code> reveals.</p>

<p><strong>Password for ghost3:</strong> <code class="language-plaintext highlighter-rouge">H1dd3nInSh4dow</code></p>

<p><strong>Defensive note:</strong> <code class="language-plaintext highlighter-rouge">ls</code> without <code class="language-plaintext highlighter-rouge">-a</code> hides dotfiles. Forensics must use <code class="language-plaintext highlighter-rouge">ls -la</code> and <code class="language-plaintext highlighter-rouge">find</code>.</p>

<hr />

<h2 id="level-3--4--kaels-storage-layout--permissions-matter">Level 3 → 4 — KAEL’S STORAGE LAYOUT / Permissions Matter</h2>

<p><strong>Briefing (recovered from workstation):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  /var/intel/public/   — world readable
  /var/intel/ops/      — restricted
  /var/intel/archive/  — root only

Access follows the group scheme. The kernel will
tell you what you are, if you ask it.
</code></pre></div></div>

<p>Hints: <code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">find</code>, <code class="language-plaintext highlighter-rouge">chmod</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">id
groups
</span>find /var/intel <span class="nt">-type</span> f <span class="nt">-ls</span> 2&gt;/dev/null
find /var/intel <span class="nt">-readable</span> <span class="nt">-ls</span>
<span class="c"># Ghost3 is in a group that can read /var/intel/ops/</span>
<span class="nb">cat</span> /var/intel/ops/<span class="k">*</span>
<span class="c"># or more generally:</span>
find / <span class="nt">-group</span> ghost3 <span class="nt">-readable</span> 2&gt;/dev/null
</code></pre></div></div>

<p><strong>Password for ghost4:</strong> <code class="language-plaintext highlighter-rouge">P3rm1ss10ns_M4tt3r</code> / <code class="language-plaintext highlighter-rouge">H1dd3nInSh4dow</code> chain (transcript shows <code class="language-plaintext highlighter-rouge">P3rm1ss10ns_M4tt3r</code> as the level marker).</p>

<p><strong>Defensive note:</strong> group-readable leaks are a common lateral-movement path. Audit with <code class="language-plaintext highlighter-rouge">find / -perm -g+r</code> and review <code class="language-plaintext highlighter-rouge">/etc/group</code> membership quarterly.</p>

<hr />

<h2 id="level-4--5--grep-finds-truth--piping">Level 4 → 5 — Grep Finds Truth / Piping</h2>

<p><strong>Briefing:</strong> piping + <code class="language-plaintext highlighter-rouge">grep</code> was explicitly hinted:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://man7.org/linux/man-pages/man1/grep.1.html
https://ryanstutorials.net/linuxtutorial/piping.php
</code></pre></div></div>

<p>Transcript marker: <code class="language-plaintext highlighter-rouge">Gr3p_F1nds_Truth</code></p>

<p>Typical task: find the only line containing a human-readable string among binary noise.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">grep</span> <span class="nt">-R</span> <span class="s2">"password"</span> <span class="nb">.</span> 2&gt;/dev/null
<span class="nb">grep</span> <span class="nt">-a</span> <span class="s2">"ghost"</span> <span class="nt">-R</span> <span class="nb">.</span>
<span class="nb">ls</span> <span class="nt">-R</span> | <span class="nb">grep </span>ghost
<span class="nb">cat </span>data.txt | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"^$"</span>
<span class="c"># chaining:</span>
find <span class="nb">.</span> <span class="nt">-type</span> f <span class="nt">-exec</span> <span class="nb">grep</span> <span class="nt">-l</span> <span class="s2">"Grep"</span> <span class="o">{}</span> <span class="se">\;</span>
</code></pre></div></div>

<p><strong>Password for ghost5:</strong> <code class="language-plaintext highlighter-rouge">Gr3p_F1nds_Truth</code></p>

<p><strong>Defensive note:</strong> <code class="language-plaintext highlighter-rouge">grep -a</code> and <code class="language-plaintext highlighter-rouge">strings</code> are the two ways to search inside non-text files — attackers use both to hunt secrets in dumps.</p>

<hr />

<h2 id="level-5--6--the-listener">Level 5 → 6 — The Listener</h2>

<p><strong>Briefing (verbatim):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>KAEL stopped trusting files after the incident.
He ran a service instead. Said if it's not on disk,
it can't be found. He forgot about the ports.

Goal:    Retrieve the password for ghost6
Connect: ssh ghost6@204.168.229.209 -p 2222
</code></pre></div></div>

<p>Hints: <code class="language-plaintext highlighter-rouge">nc</code>, <code class="language-plaintext highlighter-rouge">nmap</code>. <code class="language-plaintext highlighter-rouge">ss</code>/<code class="language-plaintext highlighter-rouge">netstat</code> deliberately locked down.</p>

<p><strong>Observed session (ghost5):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost5@breachlab:~<span class="nv">$ </span>nmap <span class="nt">-p-localhost</span>
Error <span class="c">#486: Your port specifications are illegal.</span>

ghost5@breachlab:~<span class="nv">$ </span>nmap <span class="nt">-p-</span> localhost
Nmap scan report <span class="k">for </span>localhost <span class="o">(</span>127.0.0.1<span class="o">)</span>
Not shown: 65526 closed ports
PORT      STATE SERVICE
22/tcp    open  ssh
30003/tcp open  amicon-fpsu-ra
30100/tcp open  rwp
30101/tcp open  unknown
31339/tcp open  unknown
41310/tcp open  unknown
41311/tcp open  unknown
41337/tcp open  unknown
49213/tcp open  unknown

ghost5@breachlab:~<span class="nv">$ </span>nc localhost 30003
Format: &lt;password&gt; &lt;index|count&gt; <span class="o">(</span>Level 19 relay service on port 30003.<span class="o">)</span>

ghost5@breachlab:~<span class="nv">$ </span>nc localhost 30100

  GHOST PROTOCOL — CHANNEL A
  ─────────────────────────────────────
  This channel is informational only.
  Authentication token: GHOST
  Secure channel: port 30101
  Send the token to receive your credential.

ghost5@breachlab:~<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"GHOST"</span> | nc <span class="nt">-q</span> 1 localhost 30101
AUTHENTICATE: GHOST

  Credential: P0rts_N3v3r_L13
</code></pre></div></div>

<p>Correct flow is <strong>two-port broker</strong>: informational channel <code class="language-plaintext highlighter-rouge">30100</code> tells you the token <code class="language-plaintext highlighter-rouge">GHOST</code>, secure channel <code class="language-plaintext highlighter-rouge">30101</code> trades it. piping is required — interactive typing times out after 30s (seen later at Level 13 broker with <code class="language-plaintext highlighter-rouge">TIMEOUT: no input within 30s</code>).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># reliable, non-interactive:</span>
<span class="nb">echo</span> <span class="s2">"GHOST"</span> | nc <span class="nt">-q</span> 1 localhost 30101
<span class="c"># or</span>
<span class="nb">printf</span> <span class="s2">"GHOST</span><span class="se">\n</span><span class="s2">"</span> | nc localhost 30101
</code></pre></div></div>

<p><strong>Password for ghost6:</strong> <code class="language-plaintext highlighter-rouge">P0rts_N3v3r_L13</code></p>

<p><strong>Why it works:</strong> <code class="language-plaintext highlighter-rouge">nmap -p-</code> scans all 65535 TCP ports. Services bound to <code class="language-plaintext highlighter-rouge">127.0.0.1</code> are invisible from outside but reachable locally — a common pattern for broker daemons. <code class="language-plaintext highlighter-rouge">ss</code>/<code class="language-plaintext highlighter-rouge">netstat</code> restriction forces you to brute-force via <code class="language-plaintext highlighter-rouge">nmap</code> or <code class="language-plaintext highlighter-rouge">for i in {1..65535}; do nc -z ...</code>.</p>

<p><strong>Defensive note:</strong> bind brokers to <code class="language-plaintext highlighter-rouge">127.0.0.1</code> + firewall with <code class="language-plaintext highlighter-rouge">iptables -A INPUT -p tcp --dport 30100 -s 127.0.0.1 -j ACCEPT</code> and require <code class="language-plaintext highlighter-rouge">RETIRE</code> tokens. Log <code class="language-plaintext highlighter-rouge">connect()</code> syscalls at the boundary.</p>

<hr />

<h2 id="level-6--7--ghost-in-the-machine">Level 6 → 7 — Ghost in the Machine</h2>

<p><strong>Briefing:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>After the breach, KAEL stopped writing secrets
to disk. He told himself the shell would forget
them. It doesn't.

Goal: Retrieve the password for ghost7
Hints: env, base64, 12factor.net/config
</code></pre></div></div>

<p><strong>Session (ghost6):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost6@breachlab:~<span class="nv">$ </span><span class="nb">env</span>
...
<span class="nv">API_DIGEST</span><span class="o">=</span><span class="nv">M252X0wzNGtzXzN2M3J5dGgxbmc</span><span class="o">=</span>
<span class="nv">TRACE_SALT</span><span class="o">=</span>bW9uaXRvcmluZ19rZXlfZGVsdGE3
<span class="nv">RUNTIME_TOKEN</span><span class="o">=</span>c3lzdGVtX3Rva2VuX2dhbW1hX3Yz
<span class="nv">CACHE_SEED</span><span class="o">=</span>bm90X2FfcmVhbF9jcmVkZW50aWFs
...
ghost6@breachlab:~<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"M252X0wzNGtzXzN2M3J5dGgxbmc="</span> | <span class="nb">base64</span> <span class="nt">-d</span>
3nv_L34ks_3v3ryth1ng
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">TRACE_SALT</code> decodes to <code class="language-plaintext highlighter-rouge">monitoring_key_delta7</code> (noise), <code class="language-plaintext highlighter-rouge">CACHE_SEED</code> to <code class="language-plaintext highlighter-rouge">not_a_real_credential</code> (trap). The real secret is <code class="language-plaintext highlighter-rouge">API_DIGEST</code>.</p>

<p><strong>Password for ghost7:</strong> <code class="language-plaintext highlighter-rouge">3nv_L34ks_3v3ryth1ng</code></p>

<p><strong>Why:</strong> 12-Factor apps put secrets in <code class="language-plaintext highlighter-rouge">env</code>. Tools like <code class="language-plaintext highlighter-rouge">env</code>, <code class="language-plaintext highlighter-rouge">printenv</code>, <code class="language-plaintext highlighter-rouge">strings /proc/self/environ</code> leak them to any <code class="language-plaintext highlighter-rouge">RCE</code>. Base64 is encoding, not encryption.</p>

<p><strong>Defensive note:</strong> don’t put raw secrets in <code class="language-plaintext highlighter-rouge">env</code> without a secrets manager (Vault, KMS). If you must, mark variables <code class="language-plaintext highlighter-rouge">no_log</code> and scrub <code class="language-plaintext highlighter-rouge">env</code> from crash dumps.</p>

<hr />

<h2 id="level-7--8--transport-encoding">Level 7 → 8 — Transport Encoding</h2>

<p><strong>Briefing:</strong> file <code class="language-plaintext highlighter-rouge">transmission.dat</code> with hints around <code class="language-plaintext highlighter-rouge">base64</code>, <code class="language-plaintext highlighter-rouge">xxd</code>, <code class="language-plaintext highlighter-rouge">file</code>.</p>

<p><strong>Session (ghost7):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost7@breachlab:~<span class="nv">$ </span><span class="nb">ls
</span>transmission.dat
ghost7@breachlab:~<span class="nv">$ </span><span class="nb">cat </span>transmission.dat 
00000000: 5244 4e6a 4d47 517a 587a 4279 5830 5178  RDNjMGQzXzByX0Qx
00000010: 4d77 3d3d 0a                             <span class="nv">Mw</span><span class="o">==</span><span class="nb">.</span>
ghost7@breachlab:~<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"RDNjMGQzXzByX0QxMw=="</span> | <span class="nb">base64</span> <span class="nt">-d</span>
D3c0d3_0r_D13
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">transmission.dat</code> is a hexdump+ASCII rendering (as from <code class="language-plaintext highlighter-rouge">xxd</code> or <code class="language-plaintext highlighter-rouge">hexdump -C</code>). The trailing <code class="language-plaintext highlighter-rouge">RdNjM...</code> is itself base64.</p>

<p><strong>Password for ghost8:</strong> <code class="language-plaintext highlighter-rouge">D3c0d3_0r_D13</code></p>

<p><strong>Variations encountered:</strong> double/triple-wrapped (<code class="language-plaintext highlighter-rouge">scroll.b64</code> later at Level 22 requires <code class="language-plaintext highlighter-rouge">base64 -d</code> iterative). Always <code class="language-plaintext highlighter-rouge">file</code> first, then decode until <code class="language-plaintext highlighter-rouge">file</code> says ASCII.</p>

<p><strong>Defensive note:</strong> hexdump + base64 is transport, not at-rest encryption. Treat any <code class="language-plaintext highlighter-rouge">data:</code> URI or <code class="language-plaintext highlighter-rouge">transmission.dat</code> as untrusted until validated.</p>

<hr />

<h2 id="level-8--9--proc-tells-all">Level 8 → 9 — Proc Tells All</h2>

<p><strong>Briefing:</strong> <code class="language-plaintext highlighter-rouge">ps aux</code> + <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/environ</code></p>

<p><strong>Session (ghost8):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost8@breachlab:~<span class="nv">$ </span>ps aux
root          20  0.0  ...  python3 /usr/local/bin/level8d
ghost8        39  ...  python3 /usr/local/bin/level8_worker
ghost8        40  ...  python3 /usr/local/bin/level8_worker
...
ghost8@breachlab:~<span class="nv">$ </span><span class="nb">cat</span> /proc/39/environ | <span class="nb">tr</span> <span class="s1">'\0'</span> <span class="s1">'\n'</span>
<span class="nv">HOSTNAME</span><span class="o">=</span>breachlab
...
ghost8@breachlab:~<span class="nv">$ </span><span class="nb">cat</span> /proc/40/environ | <span class="nb">tr</span> <span class="s1">'\0'</span> <span class="s1">'\n'</span>
...
<span class="nv">ANALYST_KEY</span><span class="o">=</span>Pr0c_T3lls_4ll
</code></pre></div></div>

<p>PID 39 is the unprivileged worker (empty), PID 40 is the root-owned broker that inherited <code class="language-plaintext highlighter-rouge">ANALYST_KEY</code> at <code class="language-plaintext highlighter-rouge">fork()</code>.</p>

<p><strong>Password for ghost9:</strong> <code class="language-plaintext highlighter-rouge">Pr0c_T3lls_4ll</code></p>

<p><strong>Why:</strong> <code class="language-plaintext highlighter-rouge">ps aux</code> shows you command lines; <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/environ</code> and <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/cmdline</code> often hold secrets passed as args or env at exec time. <code class="language-plaintext highlighter-rouge">tr '\0' '\n'</code> is needed because <code class="language-plaintext highlighter-rouge">environ</code> is <code class="language-plaintext highlighter-rouge">NUL</code>-delimited.</p>

<p><strong>Defensive note:</strong> secrets on ARGV or env are visible to any <code class="language-plaintext highlighter-rouge">uid</code> that can read <code class="language-plaintext highlighter-rouge">/proc</code> (same user or root). Use memfd or prompt-based entry instead.</p>

<hr />

<h2 id="level-9--10--noise-floor">Level 9 → 10 — Noise Floor</h2>

<p><strong>Briefing:</strong> core dump <code class="language-plaintext highlighter-rouge">ghost-agent.core</code> filled with noise, one real <code class="language-plaintext highlighter-rouge">AGENT_TOKEN</code>.</p>

<p><strong>Session (ghost9):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost9@breachlab:~<span class="nv">$ </span><span class="nb">ls
</span>ghost-agent.core
ghost9@breachlab:~<span class="nv">$ </span>file ghost-agent.core
ghost-agent.core: data
ghost9@breachlab:~<span class="nv">$ </span>strings ghost-agent.core | less
...
<span class="nv">GHOST_REGION</span><span class="o">=</span>eu-1
<span class="nv">AGENT_BUILD</span><span class="o">=</span>2.3.1
<span class="nv">AGENT_TOKEN</span><span class="o">=</span>N01s3_Fl00r
...
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">strings</code> extracts printable runs (default ≥4). Among hundreds of fake strings, the <code class="language-plaintext highlighter-rouge">AGENT_TOKEN</code> line stands out by prefix.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>strings ghost-agent.core | <span class="nb">grep </span>AGENT_TOKEN
strings ghost-agent.core | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s2">"TOKEN|GHOST"</span>
</code></pre></div></div>

<p><strong>Password for ghost10:</strong> <code class="language-plaintext highlighter-rouge">N01s3_Fl00r</code></p>

<p><strong>Defensive note:</strong> core dumps capture heap secrets. Disable with <code class="language-plaintext highlighter-rouge">ulimit -c 0</code> and <code class="language-plaintext highlighter-rouge">kernel.core_pattern=|/bin/false</code>, and scrub dumps before retention.</p>

<hr />

<h2 id="level-10--11--odd-token-out">Level 10 → 11 — Odd Token Out</h2>

<p><strong>Briefing:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A dump of session tokens. Two collectors logged
every token twice — except one, written during an
outage. Find the lone token, not by eye.

Hints: sort, uniq
</code></pre></div></div>

<p><strong>Session (ghost10):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost10@breachlab:~<span class="nv">$ </span><span class="nb">ls
</span>session-tokens.log
ghost10@breachlab:~<span class="nv">$ </span><span class="nb">wc</span> <span class="nt">-l</span> session-tokens.log
~800
ghost10@breachlab:~<span class="nv">$ </span><span class="nb">sort </span>session-tokens.log | <span class="nb">uniq</span> <span class="nt">-u</span>
Str1ngs_R3v34l
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sort | uniq -u</code> emits only lines occurring exactly once. <code class="language-plaintext highlighter-rouge">uniq -d</code> would do the inverse.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sort </span>session-tokens.log | <span class="nb">uniq</span> <span class="nt">-c</span> | <span class="nb">sort</span> <span class="nt">-n</span> | <span class="nb">head</span>
</code></pre></div></div>

<p>shows the expected <code class="language-plaintext highlighter-rouge">2</code> count for all but one.</p>

<p><strong>Password for ghost11:</strong> <code class="language-plaintext highlighter-rouge">Str1ngs_R3v34l</code></p>

<p><strong>Defensive note:</strong> duplicate telemetry is a classic canary for collector outage. Monitor <code class="language-plaintext highlighter-rouge">uniq</code> drift rather than just aggregate counts.</p>

<hr />

<h2 id="level-11--12--unwrap-the-stage">Level 11 → 12 — Unwrap the Stage</h2>

<p><strong>Briefing:</strong> <em>staging bundle, wrapped three layers deep, each a different format. Identify first, unpack second.</em></p>

<p><strong>Session (ghost11):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost11@breachlab:~<span class="nv">$ </span><span class="nb">ls
</span>stage.bin
ghost11@breachlab:~<span class="nv">$ </span>file stage.bin
stage.bin: POSIX <span class="nb">tar </span>archive <span class="o">(</span>GNU<span class="o">)</span>
ghost11@breachlab:~<span class="nv">$ </span><span class="nb">tar</span> <span class="nt">-xf</span> stage.bin
ghost11@breachlab:~<span class="nv">$ </span><span class="nb">ls
</span>payload.txt.gz.xz  stage.bin
ghost11@breachlab:~<span class="nv">$ </span>file payload.txt.gz.xz
payload.txt.gz.xz: XZ compressed data, checksum CRC64
ghost11@breachlab:~<span class="nv">$ </span>xz <span class="nt">-dc</span> payload.txt.gz.xz <span class="o">&gt;</span> layer2
ghost11@breachlab:~<span class="nv">$ </span>file layer2
layer2: <span class="nb">gzip </span>compressed data, was <span class="s2">"payload.txt"</span>
ghost11@breachlab:~<span class="nv">$ </span><span class="nb">gzip</span> <span class="nt">-dc</span> layer2 <span class="o">&gt;</span> layer3
ghost11@breachlab:~<span class="nv">$ </span><span class="nb">cat </span>layer3
Unwr4pp3d_Thr33
</code></pre></div></div>

<p>Order matters: <code class="language-plaintext highlighter-rouge">tar → XZ → gzip</code>. Reversing (<code class="language-plaintext highlighter-rouge">gzip</code> before <code class="language-plaintext highlighter-rouge">xz</code>) gives <code class="language-plaintext highlighter-rouge">not in gzip format</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>file stage.bin
<span class="nb">tar</span> <span class="nt">-tf</span> stage.bin              <span class="c"># list before extract</span>
xz <span class="nt">-dc</span> payload.txt.gz.xz | file -
</code></pre></div></div>

<p><strong>Password for ghost12:</strong> <code class="language-plaintext highlighter-rouge">Unwr4pp3d_Thr33</code></p>

<p><strong>Defensive note:</strong> DLP that only inspects outer layer misses nested archives. Always recurse <code class="language-plaintext highlighter-rouge">file</code> and decompress with size limits.</p>

<hr />

<h2 id="level-12--13--harvested-key">Level 12 → 13 — Harvested Key</h2>

<p><strong>Briefing:</strong> <em>No password for the next account — a private key pulled off a jump host instead. ghost13 still trusts it.</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost12@breachlab:~<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-la</span>
drwx--x--x 1 ghost12 ghost12 4096 ... loot
ghost12@breachlab:~<span class="nv">$ </span><span class="nb">ls </span>loot/
NOTES.txt  id_ed25519  id_ed25519.pub
ghost12@breachlab:~<span class="nv">$ </span><span class="nb">cat </span>loot/NOTES.txt
Pulled from jump-01:/home/svc-deploy/.ssh/ during the Sept op.
No passphrase set. ghost13 still trusts this key.

ghost12@breachlab:~<span class="nv">$ </span><span class="nb">chmod </span>600 loot/id_ed25519
<span class="nb">chmod</span>: Operation not permitted          <span class="c"># dir is no-write, file is read-only</span>
ghost12@breachlab:~<span class="nv">$ </span><span class="nb">cp</span> ~/loot/id_ed25519 /tmp/id_ghost13
ghost12@breachlab:~<span class="nv">$ </span><span class="nb">chmod </span>600 /tmp/id_ghost13
ghost12@breachlab:~<span class="nv">$ </span>ssh <span class="nt">-i</span> /tmp/id_ghost13 ghost13@204.168.229.209 <span class="nt">-p</span> 2222
...
ghost13@breachlab:~<span class="nv">$ </span><span class="nb">cat </span>flag
K3y_N0t_P4ss
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">~/.ssh</code> perms must be <code class="language-plaintext highlighter-rouge">0700</code>, key <code class="language-plaintext highlighter-rouge">0600</code> or <code class="language-plaintext highlighter-rouge">ssh</code> refuses. When source dir is <code class="language-plaintext highlighter-rouge">0550</code>/<code class="language-plaintext highlighter-rouge">0500</code>, copy out first.</p>

<p><strong>Password (flag) for ghost13:</strong> <code class="language-plaintext highlighter-rouge">K3y_N0t_P4ss</code> (next login is via the same key, not via this string — see next level).</p>

<p><strong>Defensive note:</strong> leaked ed25519 keys without passphrase are immediate compromise. Rotate and revoke, and monitor for <code class="language-plaintext highlighter-rouge">AuthorizedKeysFile</code> reuse.</p>

<hr />

<h2 id="level-13--14--credential-broker">Level 13 → 14 — Credential Broker</h2>

<p><strong>Briefing:</strong> <em>credential-broker daemon on TCP 41310 trades the next token for your current one. Connect, read how it wants to be asked.</em></p>

<p><strong>Session (ghost13):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost13@breachlab:~<span class="nv">$ </span>nc localhost 41310
ghost credential-broker v1.2
usage: RETRIEVE &lt;current-token&gt;

ghost13@breachlab:~<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"RETRIEVE K3y_N0t_P4ss"</span> | nc <span class="nt">-q</span> 1 localhost 41310
OK. Next token: N3tc4t_D3l1v3r
</code></pre></div></div>

<p>Pitfalls observed in the dump: <code class="language-plaintext highlighter-rouge">RETRIVE</code> (typo), <code class="language-plaintext highlighter-rouge">loclahost</code>, and interactive <code class="language-plaintext highlighter-rouge">nc</code> that times out — the broker requires piped, non-interactive stdin (<code class="language-plaintext highlighter-rouge">-q 1</code> or <code class="language-plaintext highlighter-rouge">printf</code>).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">printf</span> <span class="s2">"RETRIEVE K3y_N0t_P4ss</span><span class="se">\n</span><span class="s2">"</span> | nc localhost 41310
<span class="nb">echo</span> <span class="s2">"RETRIEVE K3y_N0t_P4ss"</span> | nc <span class="nt">-q</span> 1 localhost 41310
</code></pre></div></div>

<p><strong>Password for ghost14:</strong> <code class="language-plaintext highlighter-rouge">N3tc4t_D3l1v3r</code></p>

<hr />

<h2 id="level-14--15--tls-or-nothing">Level 14 → 15 — TLS or Nothing</h2>

<p><strong>Session (ghost14):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost14@breachlab:~<span class="nv">$ </span>openssl s_client <span class="nt">-connect</span> localhost:41311 <span class="nt">-quiet</span>
Can<span class="s1">'t use SSL_get_servername
depth=0 CN = ghost-internal
verify error:num=18:self-signed certificate
...
Send the current level password:
N3tc4t_D3l1v3r
Correct! Next password: TLS_0r_N0th1ng
</span></code></pre></div></div>

<p>Service is TLS-wrapped with a self-signed <code class="language-plaintext highlighter-rouge">ghost-internal</code> cert — <code class="language-plaintext highlighter-rouge">openssl s_client</code> ignores trust by default, so <code class="language-plaintext highlighter-rouge">verify error:18</code> is expected. You must speak after the <code class="language-plaintext highlighter-rouge">Send the current level password:</code> prompt.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">printf</span> <span class="s2">"N3tc4t_D3l1v3r</span><span class="se">\n</span><span class="s2">"</span> | openssl s_client <span class="nt">-connect</span> localhost:41311 <span class="nt">-quiet</span> 2&gt;&amp;1 | <span class="nb">grep</span> <span class="s2">"Next password"</span>
</code></pre></div></div>

<p><strong>Password for ghost15:</strong> <code class="language-plaintext highlighter-rouge">TLS_0r_N0th1ng</code></p>

<hr />

<h2 id="level-15--16--ephemeral-port">Level 15 → 16 — Ephemeral Port</h2>

<p><strong>Briefing:</strong> <em>An operator left one service on an ephemeral port in 49000-49500. It speaks TLS; the rest of the range is closed.</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost15@breachlab:~<span class="nv">$ </span>nmap localhost <span class="nt">-p</span> 49000-49500
...
PORT      STATE SERVICE
49213/tcp open  unknown

ghost15@breachlab:~<span class="nv">$ </span>openssl s_client <span class="nt">-connect</span> localhost:49213 <span class="nt">-quiet</span>
...
Send the current level password:
TLS_0r_N0th1ng
Correct! Next password: P0rt_Sc4nn3d
</code></pre></div></div>

<p>Note the syntax trap: <code class="language-plaintext highlighter-rouge">nmap localhost 49000-49500</code> (without <code class="language-plaintext highlighter-rouge">-p</code>) fails to resolve — the range must be an argument to <code class="language-plaintext highlighter-rouge">-p</code>.</p>

<p><strong>Password for ghost16:</strong> <code class="language-plaintext highlighter-rouge">P0rt_Sc4nn3d</code></p>

<hr />

<h2 id="level-16--17--config-drift">Level 16 → 17 — Config Drift</h2>

<p><strong>Briefing:</strong> <em>Two daily snapshots of a host’s authorized creds. One line changed overnight — a planted backdoor. Find the drift.</em></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost16@breachlab:~<span class="nv">$ </span><span class="nb">ls
</span>audit-mon.txt  audit-tue.txt
ghost16@breachlab:~<span class="nv">$ </span>diff audit-mon.txt audit-tue.txt 
42c42
&lt; svc_0042:ede8866d29e6eec0fb98
<span class="nt">---</span>
<span class="o">&gt;</span> svc_0042:D1ff_Sp0ts_1t

ghost16@breachlab:~<span class="nv">$ </span><span class="nb">comm </span>audit-mon.txt audit-tue.txt   <span class="c"># unsorted → mangled output, but still reveals drift</span>
</code></pre></div></div>

<p>Clean usage:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>diff <span class="nt">-u</span> audit-mon.txt audit-tue.txt
<span class="nb">comm</span> <span class="nt">-23</span> &lt;<span class="o">(</span><span class="nb">sort </span>audit-mon.txt<span class="o">)</span> &lt;<span class="o">(</span><span class="nb">sort </span>audit-tue.txt<span class="o">)</span>
<span class="nb">comm</span> <span class="nt">-13</span> &lt;<span class="o">(</span><span class="nb">sort </span>audit-mon.txt<span class="o">)</span> &lt;<span class="o">(</span><span class="nb">sort </span>audit-tue.txt<span class="o">)</span>
</code></pre></div></div>

<p><strong>Password for ghost17:</strong> <code class="language-plaintext highlighter-rouge">D1ff_Sp0ts_1t</code></p>

<p><strong>Defensive note:</strong> daily <code class="language-plaintext highlighter-rouge">authorized_keys</code>/<code class="language-plaintext highlighter-rouge">shadow</code> diffs are a minimal IDS. Commit them to git and alert on <code class="language-plaintext highlighter-rouge">diff</code> non-empty.</p>

<hr />

<h2 id="level-17--18--commands-only">Level 17 → 18 — Commands Only</h2>

<p><strong>Briefing (verbatim, level 17 → 18):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>This is an automated relay account. It has no interactive
shell -- but it still runs one-off commands sent over SSH.
Make it hand you the next operative's credentials without
ever opening a prompt here.

Goal: Retrieve the password for ghost18
Connect: ssh ghost18@204.168.229.209 -p 2222
(relay17: interactive sessions are disabled. Jobs run non-interactively.)
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dev-lahrani@...:~/Desktop/cybersec<span class="nv">$ </span>ssh ghost17@204.168.229.209 <span class="nt">-p</span> 2222 <span class="s2">"ls -la"</span>
...
<span class="nt">-r--------</span> 1 ghost17 ghost17   13 Jun 22 13:41 handoff

dev-lahrani@...:~/Desktop/cybersec<span class="nv">$ </span>ssh ghost17@204.168.229.209 <span class="nt">-p</span> 2222 <span class="s2">"cat handoff"</span>
Sh3ll_D3n13d
</code></pre></div></div>

<p>Any <code class="language-plaintext highlighter-rouge">Command=</code> restriction in <code class="language-plaintext highlighter-rouge">~/.ssh/authorized_keys</code> that still evaluates your <code class="language-plaintext highlighter-rouge">SSH_ORIGINAL_COMMAND</code> is exploitable the same way — restrict further with <code class="language-plaintext highlighter-rouge">restrict</code> + explicit <code class="language-plaintext highlighter-rouge">command=""</code> allowlist or, better, don’t use <code class="language-plaintext highlighter-rouge">ghost17</code>-style relays for secrets at all.</p>

<p><strong>Password for ghost18:</strong> <code class="language-plaintext highlighter-rouge">Sh3ll_D3n13d</code></p>

<hr />

<h2 id="level-18--19--suid-flip">Level 18 → 19 — SUID Flip</h2>

<p><strong>Session (ghost18):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost18@breachlab:~<span class="nv">$ </span>find / <span class="nt">-user</span> ghost19 <span class="nt">-perm</span> <span class="nt">-4000</span> 2&gt;/dev/null
/usr/local/bin/readback
ghost18@breachlab:~<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-la</span> /usr/local/bin/readback
<span class="nt">-rwsr-x---</span> 1 ghost19 ghost18 819664 ... /usr/local/bin/readback
ghost18@breachlab:~<span class="nv">$ </span>/usr/local/bin/readback
SU1D_Fl1p
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">readback</code> is a <code class="language-plaintext highlighter-rouge">setuid</code> ELF owned by <code class="language-plaintext highlighter-rouge">ghost19</code> and group-readable by <code class="language-plaintext highlighter-rouge">ghost18</code> — the <code class="language-plaintext highlighter-rouge">s</code> in <code class="language-plaintext highlighter-rouge">rws</code> is the signal. <code class="language-plaintext highlighter-rouge">find -perm -4000 -type f 2&gt;/dev/null</code> is the canonical hunt; filtering by <code class="language-plaintext highlighter-rouge">-user ghost19</code> narrows to the intended vector.</p>

<p><strong>Password for ghost19:</strong> <code class="language-plaintext highlighter-rouge">SU1D_Fl1p</code></p>

<p><strong>Defensive note:</strong> minimize <code class="language-plaintext highlighter-rouge">setuid</code> binaries (<code class="language-plaintext highlighter-rouge">find / -perm -4000</code>), never make them group-readable to the next lateral target, and prefer <code class="language-plaintext highlighter-rouge">capabilities(7)</code> or <code class="language-plaintext highlighter-rouge">sudo</code> with <code class="language-plaintext highlighter-rouge">NOEXEC</code>.</p>

<hr />

<h2 id="level-19--20--piece-by-piece">Level 19 → 20 — Piece by Piece</h2>

<p><strong>Briefing (19 → 20):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A service on port 30003 hands out the next
credential one piece at a time, by index. Don't fetch
them by hand — write a loop to collect and reassemble.
</code></pre></div></div>

<p><strong>Session (ghost19):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost19@breachlab:~<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"SU1D_Fl1p 0"</span> | nc <span class="nt">-q</span> 1 localhost 30003
r
ghost19@breachlab:~<span class="nv">$ token</span><span class="o">=</span><span class="s2">"SU1D_Fl1p"</span><span class="p">;</span> <span class="nv">pass</span><span class="o">=</span><span class="s2">""</span>
<span class="k">for </span>i <span class="k">in</span> <span class="o">{</span>0..50<span class="o">}</span><span class="p">;</span> <span class="k">do
    </span><span class="nv">char</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$token</span><span class="s2"> </span><span class="nv">$i</span><span class="s2">"</span> | nc <span class="nt">-q</span> 1 localhost 30003 2&gt;/dev/null | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\r\n'</span><span class="si">)</span>
    <span class="o">[[</span> <span class="s2">"</span><span class="nv">$char</span><span class="s2">"</span> <span class="o">==</span> <span class="k">*</span><span class="s2">"Format"</span><span class="k">*</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">break</span>
    <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$char</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">break
    </span><span class="nv">pass</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">pass</span><span class="k">}${</span><span class="nv">char</span><span class="k">}</span><span class="s2">"</span>
<span class="k">done
</span><span class="nb">echo</span> <span class="s2">"Password for ghost20: </span><span class="nv">$pass</span><span class="s2">"</span>
Password <span class="k">for </span>ghost20: r34ss3mbl3d_p13c3_by_p13c3
</code></pre></div></div>

<p>The service expects <code class="language-plaintext highlighter-rouge">Format: &lt;password&gt; &lt;index|count&gt;</code> — <code class="language-plaintext highlighter-rouge">index</code> is 0-based, <code class="language-plaintext highlighter-rouge">count</code> returns length. The initial loop in the dump failed because it sent only <code class="language-plaintext highlighter-rouge">$i</code> without the token.</p>

<p><strong>Password for ghost20:</strong> <code class="language-plaintext highlighter-rouge">r34ss3mbl3d_p13c3_by_p13c3</code></p>

<hr />

<h2 id="level-20--21--cron-discovery">Level 20 → 21 — Cron Discovery</h2>

<p><strong>Briefing:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Something runs on a schedule as root. Find what.
Find where it writes. Find what it reads.
Scheduled tasks live in specific corners of /etc.
</code></pre></div></div>

<p><strong>Session (ghost20):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost20@breachlab:~<span class="nv">$ </span><span class="nb">cat</span> /etc/crontab
...
ghost20@breachlab:~<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-la</span> /etc/cron.d
...
<span class="nt">-rw-r--r--</span> 1 root root ... ghost-level20
ghost20@breachlab:~<span class="nv">$ </span><span class="nb">cat</span> /etc/cron.d/ghost-level20
<span class="k">*</span> <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> root /opt/ghost-cron/job.sh

ghost20@breachlab:~<span class="nv">$ </span><span class="nb">cat</span> /opt/ghost-cron/job.sh
<span class="c">#!/bin/bash</span>
<span class="nb">cat</span> /etc/ghost-cron-secret <span class="o">&gt;</span> /var/tmp/ghost-cron-output 2&gt;/dev/null
<span class="nb">sleep </span>2
<span class="nb">rm</span> <span class="nt">-f</span> /var/tmp/ghost-cron-output

ghost20@breachlab:~<span class="nv">$ </span><span class="k">while </span><span class="nb">true</span><span class="p">;</span> <span class="k">do </span><span class="nb">cat</span> /var/tmp/ghost-cron-output 2&gt;/dev/null<span class="p">;</span> <span class="k">done
</span>Cr0n_R34ds
Cr0n_R34ds
...
</code></pre></div></div>

<p>Root’s <code class="language-plaintext highlighter-rouge">job.sh</code> writes the secret to <code class="language-plaintext highlighter-rouge">/var/tmp/ghost-cron-output</code> for a <strong>2-second window</strong> each minute, then deletes it. The dump shows the canonical race:</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 </span><span class="nb">cat</span> /var/tmp/ghost-cron-output 2&gt;/dev/null <span class="o">&amp;&amp;</span> <span class="nb">break</span><span class="p">;</span> <span class="k">done</span>
<span class="c"># or</span>
watch <span class="nt">-n</span> 0.1 <span class="nb">cat</span> /var/tmp/ghost-cron-output
</code></pre></div></div>

<p>The accompanying <code class="language-plaintext highlighter-rouge">wipe-residue.sh</code> cron is a rabbit hole — it hardens <code class="language-plaintext highlighter-rouge">/var/tmp</code> (<code class="language-plaintext highlighter-rouge">chmod 1755</code>) and cleans homedirs, but is not the vector.</p>

<p><strong>Password for ghost21:</strong> <code class="language-plaintext highlighter-rouge">Cr0n_R34ds</code></p>

<p><strong>Defensive note:</strong> never write secrets to world-readable <code class="language-plaintext highlighter-rouge">/var/tmp</code> with a predictable name, even transiently. Use <code class="language-plaintext highlighter-rouge">mktemp</code>, <code class="language-plaintext highlighter-rouge">0700</code> dirs, or <code class="language-plaintext highlighter-rouge">memfd_create</code>.</p>

<hr />

<h2 id="level-21--22--secrets-in-history">Level 21 → 22 — Secrets in History</h2>

<p><strong>Briefing:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A deploy repo pulled off an internal server.
Main is clean now. But a tagged release candidate
still carries the deploy key it once hardcoded.
</code></pre></div></div>

<p><strong>Session (ghost21):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost21@breachlab:~<span class="nv">$ </span><span class="nb">ls
</span>repo
ghost21@breachlab:~<span class="nv">$ </span><span class="nb">cd </span>repo <span class="o">&amp;&amp;</span> git tag
v1.4.0-rc1
ghost21@breachlab:~<span class="nv">$ </span>git show v1.4.0-rc1
tag v1.4.0-rc1
Tagger: KAEL &lt;kael@ghost&gt;
Date:   Mon Jun 22 13:41:54 2026 +0000

    rc: hardcode deploy key so CI can ship 1.4.0

diff <span class="nt">--git</span> a/.env b/.env
+GHOST_DEPLOY_KEY<span class="o">=</span>G1t_H1st0ry
</code></pre></div></div>

<p>Current <code class="language-plaintext highlighter-rouge">main</code> no longer contains <code class="language-plaintext highlighter-rouge">.env</code>; the tag does.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--all</span> <span class="nt">--oneline</span>
git show HEAD:.env 2&gt;&amp;1 | <span class="nb">head</span>      <span class="c"># main: no file</span>
git show v1.4.0-rc1:.env
git diff main..v1.4.0-rc1
</code></pre></div></div>

<p><strong>Password for ghost22:</strong> <code class="language-plaintext highlighter-rouge">G1t_H1st0ry</code></p>

<p><strong>Defensive note:</strong> <code class="language-plaintext highlighter-rouge">git rm</code> doesn’t purge history. Use <code class="language-plaintext highlighter-rouge">git filter-repo</code> / BFG, rotate the exposed secret, and enable secret scanning. Never commit <code class="language-plaintext highlighter-rouge">.env</code>.</p>

<hr />

<h2 id="level-22--graduation-classified--3-shards">Level 22 — Graduation (CLASSIFIED) — 3 Shards</h2>

<p><strong>Briefing (verbatim):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Twenty-two levels behind you.
One classified file left — split into three shards,
each guarded by a different technique from the track:

  Shard 1 — buried in a binary blob
  Shard 2 — encoded for transport
  Shard 3 — guarded by a SUID helper

Recover all three. Hand them to the gatekeeper
listening on TCP :31339. Format (exact):

  SHARD1:&lt;val&gt;|SHARD2:&lt;val&gt;|SHARD3:&lt;val&gt;
</code></pre></div></div>

<p><strong>Session (ghost22):</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ghost22@breachlab:~<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-la</span>
<span class="nt">-rw-r-----</span> 1 ghost22 ghost22  343 ... BRIEFING
<span class="nt">-rw-r-----</span> 1 ghost22 ghost22 8215 ... relic.bin
<span class="nt">-rw-r-----</span> 1 ghost22 ghost22   25 ... scroll.b64

ghost22@breachlab:~<span class="nv">$ </span><span class="nb">cat </span>scroll.b64
<span class="nv">U0hBUkQyOkJSNFYwXzBOMw</span><span class="o">==</span>
ghost22@breachlab:~<span class="nv">$ </span><span class="nb">base64</span> <span class="nt">-d</span> scroll.b64
SHARD2:BR4V0_0N3

ghost22@breachlab:~<span class="nv">$ </span>strings relic.bin | <span class="nb">grep </span>SHARD1
::SHARD1:ALPHA_Z3R0::

ghost22@breachlab:~<span class="nv">$ </span>find / <span class="nt">-perm</span> <span class="nt">-4000</span> <span class="nt">-type</span> f 2&gt;/dev/null
/usr/local/bin/ghost-archivist
ghost22@breachlab:~<span class="nv">$ </span>/usr/local/bin/ghost-archivist
SHARD3:CH4RL13_TW0

ghost22@breachlab:~<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"SHARD1:ALPHA_Z3R0|SHARD2:BR4V0_0N3|SHARD3:CH4RL13_TW0"</span> | nc localhost 31339
Ghost Graduation Gatekeeper
...
VERIFIED. All three shards accepted.
GRADUATION FLAG: Gh0st_3xf1l_3945d713
</code></pre></div></div>

<p><strong>Shards:</strong></p>

<ul>
  <li><strong>Shard 1 (<code class="language-plaintext highlighter-rouge">ALPHA_Z3R0</code>)</strong> — binary blob <code class="language-plaintext highlighter-rouge">relic.bin</code> → <code class="language-plaintext highlighter-rouge">strings</code></li>
  <li><strong>Shard 2 (<code class="language-plaintext highlighter-rouge">BR4V0_0N3</code>)</strong> — transport encoding <code class="language-plaintext highlighter-rouge">scroll.b64</code> → <code class="language-plaintext highlighter-rouge">base64 -d</code></li>
  <li><strong>Shard 3 (<code class="language-plaintext highlighter-rouge">CH4RL13_TW0</code>)</strong> — SUID helper <code class="language-plaintext highlighter-rouge">ghost-archivist</code> → direct exec</li>
</ul>

<p><strong>Final flag:</strong> <code class="language-plaintext highlighter-rouge">Gh0st_3xf1l_3945d713</code></p>

<hr />

<h2 id="full-credential-chain-verified">Full credential chain (verified)</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>W3lc0m3T0Gh0st
→ D4shIsN0tAFl4g
→ H1dd3nInSh4dow
→ P3rm1ss10ns_M4tt3r
→ Gr3p_F1nds_Truth
→ P0rts_N3v3r_L13          (ghost5 → 6)
→ 3nv_L34ks_3v3ryth1ng      (6 → 7)
→ D3c0d3_0r_D13            (7 → 8)
→ Pr0c_T3lls_4ll           (8 → 9)
→ N01s3_Fl00r              (9 → 10)
→ Str1ngs_R3v34l           (10 → 11)
→ Unwr4pp3d_Thr33           (11 → 12)
→ K3y_N0t_P4ss             (12 → 13, via id_ed25519)
→ N3tc4t_D3l1v3r           (13 → 14, broker 41310)
→ TLS_0r_N0th1ng           (14 → 15, s_client 41311)
→ P0rt_Sc4nn3d             (15 → 16, nmap ephemeral 49213)
→ D1ff_Sp0ts_1t            (16 → 17)
→ Sh3ll_D3n13d             (17 → 18, ssh cmd)
→ SU1D_Fl1p                (18 → 19, /usr/local/bin/readback)
→ r34ss3mbl3d_p13c3_by_p13c3 (19 → 20, port 30003 loop)
→ Cr0n_R34ds               (20 → 21, /var/tmp race)
→ G1t_H1st0ry              (21 → 22, git tag)
→ ALPHA_Z3R0 | BR4V0_0N3 | CH4RL13_TW0 → Gh0st_3xf1l_3945d713 (22 graduation)
</code></pre></div></div>

<hr />

<h2 id="lessons-that-compound">Lessons that compound</h2>

<ol>
  <li><strong>Fundamentals are the track.</strong> Every later trick (<code class="language-plaintext highlighter-rouge">env</code>, <code class="language-plaintext highlighter-rouge">/proc</code>, <code class="language-plaintext highlighter-rouge">strings</code>, <code class="language-plaintext highlighter-rouge">uniq</code>, <code class="language-plaintext highlighter-rouge">tar|xz|gzip</code>, <code class="language-plaintext highlighter-rouge">ssh -i</code>, brokers, <code class="language-plaintext highlighter-rouge">s_client</code>, <code class="language-plaintext highlighter-rouge">nmap</code>, <code class="language-plaintext highlighter-rouge">diff</code>, <code class="language-plaintext highlighter-rouge">ssh cmd</code>, SUID, loops, <code class="language-plaintext highlighter-rouge">cron</code>, <code class="language-plaintext highlighter-rouge">git</code>) is a composition of <code class="language-plaintext highlighter-rouge">ls</code>, <code class="language-plaintext highlighter-rouge">cat</code>, <code class="language-plaintext highlighter-rouge">find</code>, <code class="language-plaintext highlighter-rouge">grep</code> and piping.</li>
  <li><strong>Encode ≠ encrypt.</strong> <code class="language-plaintext highlighter-rouge">base64</code> (L6, L7, L22) and hexdumps are transport — treat them as plaintext.</li>
  <li><strong>The kernel leaks.</strong> <code class="language-plaintext highlighter-rouge">env</code>, <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/environ</code>, <code class="language-plaintext highlighter-rouge">ps</code>, <code class="language-plaintext highlighter-rouge">strings</code> on cores, and <code class="language-plaintext highlighter-rouge">AuthorizedKeys</code> reuse all confirm: secrets on ARGV/env/disk are observable.</li>
  <li><strong>Time matters.</strong> The 2-second <code class="language-plaintext highlighter-rouge">/var/tmp</code> window and the 30-second broker timeout are race conditions you win with loops, not reflexes.</li>
  <li><strong>History never forgets.</strong> <code class="language-plaintext highlighter-rouge">git rm</code> without history rewrite is not remediation — same for <code class="language-plaintext highlighter-rouge">chattr +i</code> baked artifacts vs residue.</li>
  <li><strong>Wire it together.</strong> The graduation asks for the same three primitives that opened the track — <code class="language-plaintext highlighter-rouge">strings</code>, <code class="language-plaintext highlighter-rouge">base64 -d</code>, SUID — composed once, then gated by a precise <code class="language-plaintext highlighter-rouge">SHARD1|SHARD2|SHARD3</code> format.</li>
</ol>

<hr />

<h2 id="reproduce">Reproduce</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># dump used for this write-up:</span>
<span class="nb">ls</span> ~/Desktop/cybersec/breachlabs.txt   <span class="c"># 3407 lines, Aug 27-30 2026</span>

<span class="c"># live host ( ephemeral per container restart ):</span>
ssh ghost0@204.168.229.209 <span class="nt">-p</span> 2222
<span class="c"># walk 0 → 22 with the chain above</span>
<span class="c"># final:</span>
<span class="nb">echo</span> <span class="s2">"SHARD1:ALPHA_Z3R0|SHARD2:BR4V0_0N3|SHARD3:CH4RL13_TW0"</span> | nc localhost 31339
</code></pre></div></div>

<p><em>Write-up author: Dev Lahrani · VIT Pune · Field Notes — Ghost Track completed Aug 30 2026. Source dump archived at <code class="language-plaintext highlighter-rouge">~/Desktop/cybersec/breachlabs.txt</code>. Challenge by BreachLab (https://breachlab.org).</em></p>]]></content><author><name>Dev Lahrani</name></author><category term="general" /><summary type="html"><![CDATA[Why Ghost Track]]></summary></entry><entry><title type="html">NotPetya: how a leaked NSA exploit became a nation-state weapon</title><link href="https://devlahrani.systems/log/2026/08/02/notpetya-case-study/" rel="alternate" type="text/html" title="NotPetya: how a leaked NSA exploit became a nation-state weapon" /><published>2026-08-02T00:00:00+05:30</published><updated>2026-08-02T00:00:00+05:30</updated><id>https://devlahrani.systems/log/2026/08/02/notpetya-case-study</id><content type="html" xml:base="https://devlahrani.systems/log/2026/08/02/notpetya-case-study/"><![CDATA[<h2 id="why-this-case-study">Why This Case Study</h2>

<p>NotPetya (June 27, 2017) is treated as a canonical case in threat intelligence because it cleanly demonstrates almost every core CTI concept in one incident: a leaked government cyberweapon, a supply chain compromise, credential theft, and lateral movement — attributed to a specific nation-state actor with a clear motive. Understanding it end-to-end is a good foundation for reading any modern APT threat report.</p>

<h2 id="the-attacker-sandworm">The Attacker: Sandworm</h2>

<p>Sandworm is a threat group tied to Russian military intelligence (GRU). It is one of the most consistently active and destructive state-sponsored groups tracked in CTI reporting, with NotPetya as its most infamous operation. Unlike financially motivated ransomware crews, Sandworm’s objective was disruption, not profit — NotPetya was disguised as ransomware but had no functioning decryption mechanism, making it a wiper in practice.</p>

<h2 id="the-tools-in-the-attack-chain">The Tools in the Attack Chain</h2>

<h3 id="1-medoc--the-initial-entry-point-not-an-exploit">1. M.E.Doc — The Initial Entry Point (Not an Exploit)</h3>
<p>The actual point of entry wasn’t a technical exploit at all. Sandworm compromised the software update server of M.E.Doc, an accounting application mandated for use by businesses operating in Ukraine, and pushed a poisoned update to its own customer base. This is a <strong>software supply chain attack</strong> — a technique that has since become one of the most important categories in threat intelligence, because it bypasses almost every perimeter defense by abusing trust in legitimate software.</p>

<h3 id="2-mimikatz--credential-theft">2. Mimikatz — Credential Theft</h3>
<p>Once inside a machine via the poisoned update, the malware used Mimikatz to extract plaintext credentials and hashes sitting in the memory of the LSASS process. Mimikatz is not a leaked or stolen tool — it is legitimate, publicly available open-source software (built by Benjamin Delpy as a proof-of-concept exposing weaknesses in how Windows handles credentials in memory). This makes it a <strong>dual-use tool</strong>: the exact same binary is used by penetration testers and by nation-state attackers.</p>

<h3 id="3-eternalblue--eternalromance--lateral-movement">3. EternalBlue &amp; EternalRomance — Lateral Movement</h3>
<p>With valid stolen credentials in hand, the malware used EternalBlue and EternalRomance — exploits targeting a flaw in the SMBv1 protocol (CVE tracked as MS17-010) — to spread automatically across the network. Critically, because it had <em>legitimate</em> stolen credentials, NotPetya could spread even to fully patched machines using those credentials directly, with the exploits acting as an additional, faster propagation path on unpatched ones.</p>

<p><strong>Where these exploits came from:</strong> EternalBlue and EternalRomance were built internally by the NSA’s Equation Group/TAO unit as offensive tools. A group calling itself the Shadow Brokers stole them, attempted an unsuccessful auction, and then publicly dumped them on April 14, 2017. Sandworm did not need any special access — they downloaded the same public leak available to anyone. NotPetya launched barely ten weeks after the leak, illustrating how fast a leaked nation-state capability can be weaponized once public.</p>

<h2 id="the-full-attack-chain">The Full Attack Chain</h2>

<ol>
  <li><strong>Initial Access</strong> — Poisoned M.E.Doc software update (supply chain compromise)</li>
  <li><strong>Credential Access</strong> — Mimikatz dumps credentials from memory</li>
  <li><strong>Lateral Movement</strong> — EternalBlue/EternalRomance + stolen credentials spread the payload across the network, patched or not</li>
  <li><strong>Impact</strong> — Disk-wiping payload disguised as ransomware, no real recovery path</li>
</ol>

<p>This maps directly onto the MITRE ATT&amp;CK framework’s tactic categories (Initial Access → Credential Access → Lateral Movement → Impact), which is exactly why this incident is used so often as a teaching example.</p>

<h2 id="shadow-brokers-the-group-that-made-this-possible">Shadow Brokers: The Group That Made This Possible</h2>

<p>Shadow Brokers is the group that leaked the NSA’s tools, but they are a mystery in their own right — their real identity was never confirmed, with theories ranging from a rogue NSA insider to a Russian intelligence propaganda operation. After the April 2017 dump they went silent and never resurfaced. Their case is a useful lesson in how a single leak of nation-state tooling can cascade into global damage (WannaCry and NotPetya both trace back to the same leak) — and why “the exploit is patched” doesn’t mean “the exploit is gone,” since it lives on in public repositories indefinitely.</p>

<h2 id="key-concept-n-day-exploits">Key Concept: N-Day Exploits</h2>

<p>NotPetya is a textbook example of an <strong>N-day exploit</strong> — a publicly known vulnerability (already assigned a CVE, often already patched upstream) that remains effective purely because real-world systems haven’t applied the patch. Most real-world breaches use N-days rather than fresh 0-days, because they’re cheaper to obtain and just as effective against the huge population of unpatched systems that exists at any given time.</p>

<h2 id="linux-parallels">Linux Parallels</h2>

<p>The same category of attack exists outside Windows:</p>
<ul>
  <li><strong>SambaCry (CVE-2017-7494)</strong> — sometimes called “EternalRed,” the Linux/Samba equivalent of EternalBlue: remote code execution, no auth required.</li>
  <li><strong>Dirty COW (CVE-2016-5195)</strong> — a privilege escalation bug that sat in the Linux kernel undiscovered for nine years before being found.</li>
  <li><strong>Shellshock (CVE-2014-6271)</strong> — command injection via Bash environment variables, still scanned for today.</li>
</ul>

<p>This shows the attack pattern (initial access → privilege escalation → credential theft → lateral movement) is OS-agnostic — only the specific tools change.</p>

<h2 id="defensive-takeaways">Defensive Takeaways</h2>

<table>
  <thead>
    <tr>
      <th>Attack Stage</th>
      <th>Defense</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Supply chain compromise</td>
      <td>Vet software update sources; monitor for anomalous update server behavior</td>
    </tr>
    <tr>
      <td>Credential theft (Mimikatz-style)</td>
      <td>Least privilege, credential rotation, EDR monitoring of LSASS access</td>
    </tr>
    <tr>
      <td>Exploit-based lateral movement</td>
      <td>Patch management, disable legacy protocols (SMBv1), network segmentation</td>
    </tr>
    <tr>
      <td>Overall</td>
      <td>Flat, unsegmented networks are why NotPetya spread so violently — internal segmentation limits blast radius</td>
    </tr>
  </tbody>
</table>

<h2 id="what-this-teaches-about-cti-as-a-discipline">What This Teaches About CTI as a Discipline</h2>

<p>The most important realization from this case study: raw vulnerability counts and CVSS scores don’t tell you what actually matters. Thousands of CVEs are published every year, but a real CTI analyst’s job is correlating <em>this specific bug</em> with <em>this specific group’s known TTPs</em>, exploit availability on forums, and whether it sits in high-value infrastructure. NotPetya wasn’t dangerous because EternalBlue existed — it was dangerous because a specific, motivated actor chained it together with credential theft and a trusted software supply chain at exactly the right moment.</p>]]></content><author><name>Dev Lahrani</name></author><category term="research" /><summary type="html"><![CDATA[Why This Case Study]]></summary></entry><entry><title type="html">Reconnaissance &amp;amp; OSINT: mapping a target before touching it</title><link href="https://devlahrani.systems/log/2026/08/02/recon-osint-methodology/" rel="alternate" type="text/html" title="Reconnaissance &amp;amp; OSINT: mapping a target before touching it" /><published>2026-08-02T00:00:00+05:30</published><updated>2026-08-02T00:00:00+05:30</updated><id>https://devlahrani.systems/log/2026/08/02/recon-osint-methodology</id><content type="html" xml:base="https://devlahrani.systems/log/2026/08/02/recon-osint-methodology/"><![CDATA[<h2 id="why-recon-comes-first">Why Recon Comes First</h2>

<p>Every major attack framework treats reconnaissance as its own distinct phase: MITRE ATT&amp;CK calls it Reconnaissance, the Lockheed Martin Cyber Kill Chain calls it the same, PTES calls it Information Gathering. The core insight worth internalizing: <strong>the overwhelming majority of a real attack isn’t “hacking” in the dramatic sense — it’s patient, mostly legal information-gathering from data that’s already public.</strong> The actual exploitation step is usually the smallest part of the operation.</p>

<h2 id="how-attackers-find-vulnerable-machines-at-internet-scale">How Attackers Find Vulnerable Machines at Internet Scale</h2>

<p>They don’t guess — they scan systematically:</p>

<ol>
  <li><strong>Port scanning</strong> (Nmap, Masscan) — sends packets across huge IP ranges to see which ports respond, revealing what services are running and reachable.</li>
  <li><strong>Banner grabbing</strong> — many services announce their own exact version in response to a connection (e.g., “OpenSSH 7.2,” “Apache 2.4.6”). That version is then cross-referenced against known CVE databases.</li>
  <li><strong>Shodan / Censys / ZoomEye</strong> — search engines that continuously scan the entire internet in the background and index what they find, letting anyone query it like Google (e.g., <code class="language-plaintext highlighter-rouge">org:"Company Name"</code> or a specific software version + country). This removes the need to scan anything yourself.</li>
  <li><strong>Vulnerability scanners</strong> (Nessus, OpenVAS, Nuclei) — actively probe a target against hundreds of known CVE signatures and return a report of confirmed weaknesses.</li>
  <li><strong>Mass/opportunistic scanning</strong> — worms like WannaCry didn’t target anyone specifically; they scanned the entire IPv4 space (~4.3 billion addresses, computationally trivial) looking for any reachable vulnerable service. This is why “nobody would target me” is the wrong mental model — automated scanners aren’t personal.</li>
</ol>

<h2 id="mapping-an-organizations-full-attack-surface">Mapping an Organization’s Full Attack Surface</h2>

<p>Knowing <em>which</em> IPs belong to a company at all is its own discipline, built almost entirely from public records:</p>

<ul>
  <li><strong>WHOIS / regional registries (ARIN, RIPE, APNIC)</strong> — reveal IP blocks (CIDR ranges) a company owns outright.</li>
  <li><strong>BGP / ASN lookups</strong> (bgp.he.net, bgp.tools) — large organizations that run their own infrastructure have an Autonomous System Number; searching it reveals every IP range announced under that org’s name.</li>
  <li><strong>Reverse DNS and subdomain enumeration</strong> (Sublist3r, Amass, Subfinder) — brute-force and crawl for subdomains, each one another data point mapping the organization’s footprint.</li>
  <li><strong>Certificate Transparency logs (crt.sh)</strong> — every HTTPS certificate issued is publicly logged; searching <code class="language-plaintext highlighter-rouge">%.company.com</code> frequently surfaces internal, staging, or dev subdomains nobody intended to expose.</li>
  <li><strong>Cloud metadata and OSINT</strong> — job postings, SEC filings, and press releases often leak details about tech stack and infrastructure changes.</li>
</ul>

<p>None of this requires breaking any law — it’s DNS, certificate logs, and registries that exist publicly by design. The skill is not finding any single data point; it’s <strong>correlating</strong> many individually-boring facts into a usable attack path (e.g., IP range → subdomain → outdated CMS on a staging server → a leaked credential on GitHub tied to that same server).</p>

<h2 id="the-legal-boundary-looking-vs-breaking-in">The Legal Boundary: Looking vs. Breaking In</h2>

<p>This is the line that separates reconnaissance (generally legal, or at least widely tolerated) from unauthorized access (a crime):</p>

<ul>
  <li>Passive scanning/banner-grabbing sends the same kind of request a browser sends visiting a website — it doesn’t exploit anything or bypass authentication.</li>
  <li>The crime begins at the point of <em>acting</em> on what recon reveals: logging into a system without permission, exfiltrating data, exploiting a flaw.</li>
  <li><strong>India specifically</strong> is stricter on paper than many jurisdictions: under Section 43 of the IT Act, 2000, unauthorized access without prior written permission — port scanning included — is treated as a civil contravention, with Section 66 escalating it to a criminal offense (up to 3 years imprisonment) if done with fraudulent intent. This is a real gap between the letter of the law and how it’s enforced in practice, since foreign scanning services operate largely outside India’s practical enforcement reach — but it matters directly for any authorized security work: written scope authorization isn’t optional, it’s the legal difference between research and a crime.</li>
</ul>

<h2 id="why-opting-out-of-shodan-doesnt-actually-protect-anything">Why “Opting Out” of Shodan Doesn’t Actually Protect Anything</h2>

<p>An important realization: removing a company from Shodan’s index does nothing to the underlying exposure.</p>

<ul>
  <li>Shodan is one indexer sitting on top of internet-wide scanning — anyone can still run their own scan directly against the same IP in seconds, opt-out or not.</li>
  <li>Multiple competing scanners exist independently (Censys, ZoomEye, FOFA, BinaryEdge) — a company would need to opt out of all of them, and new ones keep appearing.</li>
  <li>Attackers running targeted operations don’t rely on any third-party index existing at all; they scan the target directly, or use botnets of already-compromised devices to do it for free at scale.</li>
  <li><strong>The actual fix is closing the exposure, not hiding from the index that reports it</strong> — this is the core lesson: security through obscurity is not a real defense. Security through actual configuration (patching, closing ports, disabling default credentials) is.</li>
</ul>

<h2 id="planning-an-attack-against-a-hardened-target">Planning an Attack Against a Hardened Target</h2>

<p>Most real targets aren’t running fifteen exposed services — often it’s just SSH and HTTP/HTTPS. Against a minimal surface like this, the strategy shifts from <em>width</em> to <em>depth</em>:</p>

<p><strong>SSH (22):</strong> Rarely a software exploit target since vulnerabilities get patched fast. Instead: credential brute-forcing/spraying, checking for leaked private keys accidentally committed to public repos, and version fingerprinting against known CVEs (e.g., the 2024 “regreSSHion” RCE).</p>

<p><strong>HTTP/HTTPS (80/443):</strong> Usually where the real attack surface is, because it’s a full application, not just a protocol — technology fingerprinting (Wappalyzer, WhatWeb) to identify exact CMS/framework versions, directory brute-forcing (gobuster, ffuf) for hidden admin panels or leftover backup files, and application-layer bugs (SQL injection, XSS, IDOR, auth bypass) that exist independently of how well-patched the underlying OS is.</p>

<p><strong>If both are genuinely hardened, attackers pivot away from the network layer entirely:</strong></p>
<ul>
  <li>Supply chain compromise (attack the vendor who pushes trusted updates — the NotPetya pattern)</li>
  <li>Social engineering / phishing (steal an employee’s legitimate credentials rather than exploit the server)</li>
  <li>Third-party/vendor pivot (attack a smaller, less-secured partner with trusted access)</li>
  <li>Client-side attacks (target the users of a web app instead of its infrastructure)</li>
</ul>

<h2 id="core-takeaway">Core Takeaway</h2>

<p>A hardened server with only two open ports isn’t “unattackable” — it just forces the attacker up the stack, from the network layer into the application and human layers, which is exactly where most real breaches actually happen. This is also why modern defense has shifted so heavily toward credential monitoring, phishing awareness, and supply-chain auditing rather than only perimeter hardening — the network perimeter got hard enough that attackers mostly stopped kicking down the front door and started looking for side doors instead.</p>]]></content><author><name>Dev Lahrani</name></author><category term="recon" /><summary type="html"><![CDATA[Why Recon Comes First]]></summary></entry></feed>