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

Alt text

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

Alt text

Enfin , on peut envoyer un token aléatoire dans les headers et attendre le retour des logs du serveur !

Alt text

ctfauth_patched.so

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>