Mod ctfauth | Tjctf 2022
Fichier(s)
Nécessaires
- Python3 + Ida + Ghidra
Flag
CTF{http_headers_amiright}
Solution détaillée
Partie 1: Découverte du challenge
Ce challenge mélange un peu de Web et une majorité de reverse. Deux fichiers sont fournis,
- Une configuration apache2
- Un fichier
.so
- Le lien d’un site web (Hébergé en local pour le writeup)
Voici ce que nous avons quand nous nous rendons sur le site :
<html><body><h1>It works!</h1></body></html>
Rien de très pertinent , jetons un œil au fichier de configuration httpd.conf
:
DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
AllowOverride None
Header set Cache-Control no-cache
Require all granted
</Directory>
<Directory "/usr/local/apache2/htdocs/secret">
AllowOverride None
Header set Cache-Control no-cache
Require ctfauth
</Directory>
Ça c’est intéressant ! On apprend l’existence de /secret
Malheuresement , l’accés est bloqué !
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
</body></html>
Tous ceci est logique , on remarque que l’accès est limité par un module : Require ctfauth
En remontant dans le fichier :
LoadModule ctfauth_module modules/ctfauth.so
Voilà , maintenant nous savons que le .so est un module apache2 permettant de gérer l’accès ou non à la page en question !
Partie 2: Mise en place d’un environnement de travail.
Pour continuer ce challenge, j’ai utilisé un serveur en local grâce à Docker et celui va me permettre par la suite de tester les manipulations et modification que je vais faire .
Grâce à ce serveur, je pourrais accéder aux logs et donc avoir plus d’informations sur mon avancé .
Voici le DockerFile:
FROM httpd:2.4
COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf
COPY ./ctfauth3.so /usr/local/apache2/modules/ctfauth.so
RUN \
mkdir /usr/local/apache2/htdocs/secret; \
echo '<html><body><h1>flag{randomflag}</h1></body></html>' > /usr/local/apache2/htdocs/secret/secret.html;
EXPOSE 80
Partie 3: Compréhension du plugin.
Ouvrons le fichier ctfauth.so
avec Ida
On retrouve une fonction qui gère l’authentification :
Voici le retour de fin :
if ( *v13 == v17 )
{
ap_log_rerror_("./ctfauth.c", 87LL, 0xFFFFFFFFLL, 4LL, 0LL, a1, "CTF: Looks good!");
free(v13);
result = 1LL;
}
else
{
ap_log_rerror_("./ctfauth.c", 82LL, 0xFFFFFFFFLL, 4LL, 0LL, a1, "CTF: Token doesn't match!");
free(v13);
result = 0LL;
}
return result;
Reprenons depuis le début de la fonction :
v3 = (const char *)apr_table_get(v2, "X-CTF-User");
if ( !v3 )
{
ap_log_rerror_("./ctfauth.c", 38LL, 0xFFFFFFFFLL, 4LL, 0LL, a1, "CTF: No username detected");
return 0LL;
}
v4 = v3;
if ( strcmp(v3, "ctf") )
{
ap_log_rerror_("./ctfauth.c", 42LL, 0xFFFFFFFFLL, 4LL, 0LL, a1, "CTF: Incorrect username");
return 0LL;
}
Le début est Trivial, on voit tout de suite qu’un Header X-CTF-User
égal à ctf
est nécessaire
v5 = apr_table_get(*(_QWORD *)(a1 + 232), "X-CTF-Authorization");
v6 = 0;
v7 = v5 == 0;
if ( !v5 )
{
ap_log_rerror_("./ctfauth.c", 49LL, 0xFFFFFFFFLL, 4LL, 0LL, a1, "CTF: Missing authorization header");
return 0LL;
}
v8 = 6LL;
v9 = "Token ";
v10 = (const char *)v5;
do
{
if ( !v8 )
break;
v6 = *v10 < (unsigned int)*v9;
v7 = *v10++ == *v9++;
--v8;
}
while ( v7 );
if ( (!v6 && !v7) != v6 )
{
ap_log_rerror_(
"./ctfauth.c",
54LL,
0xFFFFFFFFLL,
4LL,
0LL,
a1,
"CTF: Incorrect header format for authorization header");
return 0LL;
}
Un autre Header est requis. X-CTF-Authorization
et après avoir passé un petit moment un bien comprendre le pseudo-code , le seul prérequis ici est que la valeur commence par Token
Par la suite on a :
v11 = v5 + 6;
v12 = apr_base64_decode_len(v5 + 6, v10);
v13 = malloc(v12);
if ( (unsigned int)apr_base64_decode(v13, v11) != 16 )
{
ap_log_rerror_(
"./ctfauth.c",
67LL,
0xFFFFFFFFLL,
4LL,
0LL,
a1,
"CTF: Incorrect decoded length for authorization header (expected: %d bytes)");
return 0LL;
}
Ici , on a une vérification de la taille de ce qui suit Token
apr_base64_decode
nous indique que le Header est de la forme :
X-CTF-Authorization: Token BASE64
et que la taille de la base64 décodée est égale à 16
Enfin , on a :
apr_md5_init(v16);
apr_md5_update(v16, "GuardingTheGatesFromEvilCTFPlayers", 34LL);
v14 = strlen(v4);
apr_md5_update(v16, v4, v14);
apr_md5_update(v16, "GuardingTheGatesFromEvilCTFPlayers", 34LL);
apr_md5_final(&v17, v16);
if ( *v13 == v17 )
{
ap_log_rerror_("./ctfauth.c", 87LL, 0xFFFFFFFFLL, 4LL, 0LL, a1, "CTF: Looks good!");
free(v13);
result = 1LL;
}
else
{
ap_log_rerror_("./ctfauth.c", 82LL, 0xFFFFFFFFLL, 4LL, 0LL, a1, "CTF: Token doesn't match!");
free(v13);
result = 0LL;
}
return result;
lire cette documentation
Ici , un hash MD5 de
GuardingTheGatesFromEvilCTFPlayers + v4 + GuardingTheGatesFromEvilCTFPlayers
avec:
v4 = v3 = (const char *)apr_table_get(v2, "X-CTF-User");
= 'ctf'
Finalement , la valeur du Token serai:
base64(md5('GuardingTheGatesFromEvilCTFPlayerscfGuardingTheGatesFromEvilCTFPlayers')[16])
Malheureusement , cette technique n’a pas fonctionné et je n’ai toujours pas la réponse à ce soucis .
Contournement du Token:
Comme expliqué précédement , le serveur sur le Docker donne accès à des logs :
[Fri Jun 10 16:17:07.819589 2022] [:warn] [pid 99:tid 140199342372608] [client 172.17.0.1:33192] CTF: No username detected
[Fri Jun 10 16:17:07.819623 2022] [authz_core:error] [pid 99:tid 140199342372608] [client 172.17.0.1:33192] AH01630: client denied by server configuration: /usr/local/apache2/htdocs/secret
172.17.0.1 - - [10/Jun/2022:16:17:07 +0000] "GET /secret HTTP/1.1" 403 199
172.17.0.1 - - [10/Jun/2022:16:17:57 +0000] "-" 408 -
172.17.0.1 - - [10/Jun/2022:16:18:06 +0000] "-" 408 -
Utilisons cette fois Ghidra
Le code :
ap_log_rerror_("./ctfauth.c",0x52,0xffffffff,4,0,param_1,"CTF: Token doesn\'t match!",
est effectué durant la dernière vérification , ainsi : le bon hash md5 de comparaison est déjà calculé et présent en mémoire . De plus ; celui-ci est constant !
On voit ici dans l’assembleur que la valeur du md5 est push dans le registre RDI
Par chance , le code d’affichage d’un token invalide et ce stockage dans RSI sont très proche .
On peut vérifier que ce registre RSI n’est pas modifié !
On peut donc patcher le binaire pour remplacer CTF: Token doesn\'t match!
par RSI
Enfin , on peut envoyer un token aléatoire dans les headers et attendre le retour des logs du serveur !
Bingo ! Nous avons maintenant le MD5
import base64
a = b'm\xd8Q\b\x14\x1bzXE\x01\xb5\x86\x1b,\x83X)\xb6.p\x8f\x7f'
print(base64.b64encode(a[:16]).decode())
Output:
root@DESKTOP-HNQJECB: /c/mod_ctfauth
➜ python3 find.py
bdhRCBQbelhFAbWGGyyDWA==
Il ne nous reste plus qu’à tous envoyer et le tour est joué :
<p>Great job! Here's your flag</p>
<pre>CTF{http_headers_amiright}</pre>