Write-ups del Cybercamp 2018: medium
Cada año, el INCIBE (una agencia española que se encarga de concienciar sobre temas de ciberseguridad) organiza la CyberCamp.
Estos son los write-ups de los clasificatorios del CTF, que fueron hace ya un par de semanas. Como los resultados ya se han anunciado1 y han dicho que podemos subir nuestros write-ups, estoy escribiendo aquí mis soluciones para los retos que resolví. Los materiales para los de este post se pueden descargar aquí:
En este artículo explicaré mis respuestas para los retos etiquetados como medium.
5.- Cosas del Wi-Fi
La descripción de este reto dice así:
Se ha realizado la monitorización de tráfico wireless de una de las redes de tu
organización. Se tiene la sospecha de que uno de los usuarios se está logueando contra
una web que está realizando phishing y suplantando la legítima. Tu objetivo es recuperar
las credenciales, la FLAG será la contraseña que utiliza el usuario para loguear.
Bien, abramos con Wireshark el .pcap
que nos dan y veamos lo que hay dentro…
Perfecto, no hay problema. Estamos en la categoría medium. ¿Pensabas que que iba a ser tan sencillo como abrir el archivo y buscar HTTP? Ya, yo también pensaba eso :( Pero no hay que preocuparse, que somos l33t hax0rs y sabemos crackear el Wi-Fi, ¿verdad?
Hay muchas herramientas diferentes para sacar la contraseña de un punto de acceso (AP,
Access Point); pero ahora vamos a usar una de las más populares: aircrack-ng
.
Normalmente tenemos que descubrir el BSSID del AP para calcular las claves; pero
aircrack-ng ya nos muestra un mensaje al abrir el archivo que nos permite seleccionar
el AP que queremos usar y realiza todos los cálculos necesarios.
Para una primera prueba, podemos usar alguno de los diccionarios que vienen por defecto
en la mayoría de sistemas de tipo Unix, /usr/share/dict/words
, que contiene una pequeña
lista de palabras en inglés que se comprueba de manera muy rápida:
$ aircrack-ng medium_5.cap -w /usr/share/dict/words
(...)
Passphrase not in dictionary
Quitting aircrack-ng...
Rayos… Vamos a usar un diccionario más potente y grande (pero más lento, también): el archifamoso rockyou. En lugar de pasar un buen rato intentando buscarlo por Internet, yo suelo usar el repositorio en Github de SecLists, que contiene una cantidad enorme de diccionarios, reglas para fuzzing, nombres de usuario comunes… Tengo el repositorio clonado en mi ordenador2 poder actualizarlo cuando lo necesite.
Probemos ahora con este nuevo diccionario:
$ aircrack-ng medium_5.cap -w /usr/share/dict/seclists/Passwords/rockyou.txt
Opening medium_5.cap
Read 3166 packets.
# BSSID ESSID Encryption
1 D4:63:FE:C1:09:91 ECORP WPA (1 handshake)
Choosing first network as target.
Opening medium_5.cap
Reading packets, please wait...
Aircrack-ng 1.2 beta3
[00:03:34] 870344 keys tested (3616.36 k/s)
KEY FOUND! [ iw4108604 ]
(...)
¡Genial! Tras unos 10 minutos obtenemos la clave: iw4108604
. Ahora toca descifrar el
archivo con la captura. De nuevo, podemos hacerlo de varias formas (por ejemplo, es muy
fácil hacerlo en Wireshark). En este caso, voy a usar otra de las herramientas de la
suite aircrack-ng: airdecap-ng
.
$ airdecap-ng -p "iw4108604" -b "D4:63:FE:C1:09:91" -e "ECORP" medium_5.cap
Total number of packets read 3166
Total number of WEP data packets 0
Total number of WPA data packets 1466
Number of plaintext data packets 0
Number of decrypted WEP packets 0
Number of corrupted WEP packets 0
Number of decrypted WPA packets 328
Ahora es tan fácil como abrir el archivo medi_5-dec.pcap
y buscar el tráfico HTTP
sospechoso… o quizá no. Si usamos el filtro de visualización HTTP
en Wireshark, sólo
vemos un mensaje que parece ser una prueba de conexión, pero no parece que haya nada
raro. Tendremos que seguir buscando.
Por suerte este archivo es pequeño, pero con otras capturas más grandes no sería posible buscar entre todos los paquetes. Así que necesitamos usar filtros y otras herramientas que nos permite usar Wireshark. Una cosa que se puede buscar, por ejemplo, son paquetes anormalmente grandes, basándose en el protocolo que usan. Por ejemplo, si ordenamos los paquetes de más a menos pesado, vemos un paquete de ping ICMP con 605 Bytes y ninguna respuesta:
Los datos de este paquete sospechoso parecen ser una petición HTTP. Al copiar los valores nos devuelve los datos capturados por el atacante (recordad que nuestro objetivo es recuperar las credenciales robadas):
POST /login.a4p HTTP/1.1
Host: 10.0.1.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.0.1.1/
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Pragma: no-cache
Cache-Control: no-cache
Content-Length: 100
f_Login_Name=john&f_Login_Password=qwertyFpass1234&bt_Login=Submit&Login_Page=%2FLogin_Santander.a4d
Parece que la página de phishing se estaba intentando hacer pasar por el Banco Santander, y el usuario john cayó en la trapa y puso su contraseña (la flag): qwertyFpass1234.
Como nota aparte, podríamos haber completado este reto de una forma un poco más rápida (una vez tenemos los paquetes descifrados):
$ strings ../medium_5-dec.cap | grep -i pass
<label class="lb02" for="password">Password
<input class="it02" id="password" type="password" name="f_Login_Password" value="">
f_Login_Name=john&f_Login_Password=qwertyFpass1234&bt_Login=Submit&Login_Page=%2FLogin_Santander.a4dmc3[</label>
Y a otra cosa XD
La flag es qwertyFpass1234
.
6.- Redundancia innecesaria
La descripción de este reto dice así:
Nuestros expertos han capturado un pendrive que contenía estos dos ficheros, pero parece
que uno de ellos ha sufrido daños... (Respuesta: flag{X})
Los dos archivos que se nos dan son secret.txt
, que sólo contiene datos binarios, y
key.pem
. Parece que este segundo archivo es la clave privada que hace falta para
descifrar el otro. Sin embargo, la clave privada no está completa :(
$ cat key.pem
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAMSwf+/I42wFwNpDQiGuv0fb9w5Ria2JJAjzrYEYKp4HAKB8nXxm
yGx6OWAhI+4PYFYT3pf95J/mg5buCvP19fMCAwEAAQJAKuxRnyR57PL8eSVAY1Vd
TPNF4QwOPZ62DHYRISEC++UtRemqE1eBPkRgswiJ91+r9y8EnVw/SvL4GYQmeovS
sQIhAOq8Heinxe4udriNOd35SgJV9e87YglCCIfCoAirR0qtAiEA1oIMcKaiRiUj
2S/Q4YFTNySdT+fH16huoSQrEapD9x8*********************************
****************************************************************
********************************************
-----END RSA PRIVATE KEY-----
Antes de continuar con el reto, tenemos que aprender un par de cosas sobre el formato usado para almacenar la información de la clave privada, porque esta es la clave (he he) para solucionar el problema.
Hay dos formatos para almacenar claves privadas: PEM y DER. De hecho, sólo es un formato; porque el PEM es sólo los codificar los datos del DER un base64 y añadirle una cabecera y un pie. Así pues, sólo tenemos que entender cómo funciona la codificación DER3
Como ya sabemos, una clave privada de RSA se compone (en teoría) de un módulo y de un exponente privado. Sin embargo, en la práctica, la clave privada tiene otros parámetros que se usan para agilizar las operaciones. En el RFC 3447 se especifica que una clave privada debería tener los siguientes campos (codificados en ASN.1):
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}
Como se ve, hay algunos parámetros redundantes (en realidad sólo necesitamos n
y e
).
De esta forma, puede que en nuestra clave privada corrupta haya suficiente información
como para descifrar el archivo.
Empecemos por crear un archivo, partial_key.der
, con lo que conocemos de nuestra clave:
$ base64 -d > partial_key.der
MIIBOwIBAAJBAMSwf+/I42wFwNpDQiGuv0fb9w5Ria2JJAjzrYEYKp4HAKB8nXxm
yGx6OWAhI+4PYFYT3pf95J/mg5buCvP19fMCAwEAAQJAKuxRnyR57PL8eSVAY1Vd
TPNF4QwOPZ62DHYRISEC++UtRemqE1eBPkRgswiJ91+r9y8EnVw/SvL4GYQmeovS
sQIhAOq8Heinxe4udriNOd35SgJV9e87YglCCIfCoAirR0qtAiEA1oIMcKaiRiUj
2S/Q4YFTNySdT+fH16huoSQrEapD9x8
base64: invalid input
El decodificador se queja de invalid input
porque el último bloque de base64 no está
completo y no puede usarse para recuperar el último Byte de información. Sin embargo, eso
nos da igual ahora mismo.
El siguiente paso es interpretar los Bytes con el formato ASN.1 descrito. Para ello, podemos hacerlo a mano (para ser sinceros, preferiría no hacerlo), o usando alguna de las bibliotecas que tenemos disponibles. Por ejemplo, yo he elegido la biblioteca de Python pyasn1. La herramienta pyasn1gen también es bastante útil para automatizar el proceso lo más posible.
Estos son los pasos para obtener la información:
Crear la definición de la clave en ASN.1 y guardarla en el archivo pkcs1.asn
Partimos de la definición original, la del RFC:
$ cat -> pkcs1.asn
PKCS-1 {iso(1) member(2) us(840) rsadsi(113549) pkcs(1) pkcs-1(1) modules(0) pkcs-1(1)}
DEFINITIONS EXPLICIT TAGS ::= BEGIN
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER,
publicExponent INTEGER,
privateExponent INTEGER,
prime1 INTEGER,
prime2 INTEGER,
exponent1 INTEGER,
exponent2 INTEGER,
coefficient INTEGER
}
Version ::= INTEGER
END
Traducirlo a Python
$ ./asn1ate-master/asn1ate/pyasn1gen.py pkcs1.asn
# Auto-generated by asn1ate v.0.6.1.dev0 from pkcs-1.asn
# (last modified on 2018-10-21 16:00:07.678764)
from pyasn1.type import univ, char, namedtype, namedval, tag, constraint, useful
class Version(univ.Integer):
pass
class RSAPrivateKey(univ.Sequence):
pass
RSAPrivateKey.componentType = namedtype.NamedTypes(
namedtype.NamedType('version', Version()),
namedtype.NamedType('modulus', univ.Integer()),
namedtype.NamedType('publicExponent', univ.Integer()),
namedtype.NamedType('privateExponent', univ.Integer()),
namedtype.NamedType('prime1', univ.Integer()),
namedtype.NamedType('prime2', univ.Integer()),
namedtype.NamedType('exponent1', univ.Integer()),
namedtype.NamedType('exponent2', univ.Integer()),
namedtype.NamedType('coefficient', univ.Integer())
)
Cargar la clase RSAPrivateKey
en el intérprete e intentar decodificarlo
Primero necesitamos un par de imports y leer algunos datos antes de intentar decodificar el archivo:
>>> data = open ("partial_key.der", "rb").read ()
>>> from pyasn1.codec.der.decoder import decode as der_decoder
>>> from pkcs1 import RSAPrivateKey
Ahora podemos intentar decodificar:
>>> pk, rest = der_decoder (data, asn1spec = RSAPrivateKey ())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.5/dist-packages/pyasn1/codec/ber/decoder.py", line 1182, in __call__
raise error.SubstrateUnderrunError('%d-octet short' % (length - len(substrate)))
pyasn1.error.SubstrateUnderrunError: 104-octet short
No esperaba este error (104-octet short
); pero supuse que tiene algo que ver con la
manera de funcionar de ASN.1: al principio de cada campo indicamos su tipo (INTEGER,
SEQ…). Luego ponemos su longitud ( en octetos ), y luego empezamos con el valor del
objeto codificado. El mensaje 104-octet short
nos dice que el objeto necesita tener 104
octetos más de los que le estamos pasando. Para resolver esto podríamos analizar
cuidadosamente partial_key.der
para modificar la longitud y decodificarlo con éxito (en
cuyo caso casi mejor nos hubiera valido decodificar a mano desde el principio)… O
podríamos añadir 104 octetos más de basura. Por supuesto, decidí hacerlo por este segundo
método :D
$ cp partial_key.der partial_key_append.der
$ cat /dev/zero | fold -w 140 | head -n 1 | tr -d '\n' >> partial_key_append.der
Podemos continuar con nuestros esfuerzos.
Tras intentarlo sin suerte durante bastante tiempo (la biblioteca seguía lanzando
errores), casi doy mi brazo a torcer… Hasta que me di cuenta de que aún no había
intentado usar asn1parse
de OpenSSL. Con la clave original devolvía el mismo error que
pyasn1 (ASN1_get_object:too long
). Sin embargo, con la nueva clave con los ceros
añadidos al final, podemos decodificar sin problema:
$ openssl asn1parse -inform der -in partial_key_append.der
0:d=0 hl=4 l= 315 cons: SEQUENCE
4:d=1 hl=2 l= 1 prim: INTEGER :00
7:d=1 hl=2 l= 65 prim: INTEGER :C4B07FEFC8E36C05C0DA434221AEBF47DBF70E5189AD892408F3AD81182A9E0700A07C9D7C66C86C7A39602123EE0F605613DE97FDE49FE68396EE0AF3F5F5F3
74:d=1 hl=2 l= 3 prim: INTEGER :010001
79:d=1 hl=2 l= 64 prim: INTEGER :2AEC519F2479ECF2FC79254063555D4CF345E10C0E3D9EB60C7611212102FBE52D45E9AA1357813E4460B30889F75FABF72F049D5C3F4AF2F81984267A8BD2B1
145:d=1 hl=2 l= 33 prim: INTEGER :EABC1DE8A7C5EE2E76B88D39DDF94A0255F5EF3B6209420887C2A008AB474AAD
180:d=1 hl=2 l= 33 prim: INTEGER :D6820C70A6A2462523D92FD0E1815337249D4FE7C7D7A86EA1242B11AA43F71F
215:d=1 hl=2 l= 0 prim: EOC
217:d=1 hl=2 l= 0 prim: EOC
(...)
317:d=1 hl=2 l= 0 prim: EOC
319:d=0 hl=2 l= 0 prim: EOC
Obviamente, el intérprete sólo nos va mostrando los objetos que se va encontrando; pero sabemos qué registros se están leyendo:
- Primero va la SEQUENCE RSAPrivateKey
- Luego, viene un INTEGER: version (
0x00
) - El primer dato dentro de la SEQUENCE es un INTEGER: modulus (
0xC4B0...F5F3
) - El segundo dato es otro INTEGER: publicExponent (
0x010001
) - Por último, viene privateExponent (
0x2AEC...D2B1
).
Con esta información ya podemos descifrar el mensaje; pero vamos a ver qué mas datos hay en la clave:
- prime1 (p):
0xEABC...4AAD
- prime2 (q):
0xD682...F71F
Podemos comprobar que y verificar que tenemos los datos correctos.
Para descifrar el mensaje original, podemos usar Python de nuevo y crear una clave
privada para descifrar usando OpenSSL4. Para ello, vamos a usar la clase
Crypto.PublicKey.RSA
, disponible en la biblioteca estándar:
>>> # First, we load the data
>>> mod = 0xC4B07FEFC8E36C05C0DA434221AEBF47DBF70E5189AD892408F3AD81182A9E0700A07C9D7C66C86C7A39602123EE0F605613DE97FDE49FE68396EE0AF3F5F5F3
>>> pub_exp = 0x65537
>>> pub_exp = 0x010001
>>> priv_exp = 0x2AEC519F2479ECF2FC79254063555D4CF345E10C0E3D9EB60C7611212102FBE52D45E9AA1357813E4460B30889F75FABF72F049D5C3F4AF2F81984267A8BD2B1
>>> prime_1 = 0xEABC1DE8A7C5EE2E76B88D39DDF94A0255F5EF3B6209420887C2A008AB474AAD
>>> prime_2 = 0xD6820C70A6A2462523D92FD0E1815337249D4FE7C7D7A86EA1242B11AA43F71F
>>>
>>>
>>> # Then, we create the private key with the previous data
>>> from Crypto.PublicKey import RSA
>>> rsa_private = RSA.construct ((mod, pub_exp, priv_exp, prime_1, prime_2))
>>>
>>> # Finnally, we store the private key to decrypt with OpenSSL
>>> open ("key_NEW.pem", "wb").write (rsa_private.exportKey (format = "PEM", pkcs = 8))
521
Para descifrar el archivo, podemos usar nuestra nueva clave privada con OpenSSL:
$ openssl rsautl -inkey key_NEW.pem -in secret.txt -decrypt
flag{gk83h280fwlo2}
Resulta que perdí mucho tiempo intentando usar pyasn1 cuando en realidad podría haber usado openssl sin problema…
Después de todos esos callejones sin salida y pensar demasiado, obtenemos nuestra flag:
flag{gk83h280fwlo2}
ACTUALIZACIÓN: Ya está disponible la segunda parte con el resto de los retos.
-
Desgraciadamente, sólo saqué unos 2900-3000 puntos; mientras que el corte se hizo alrededor de los 3100 puntos… :( ↩
-
Es muy útil clonar parcialmente un repo, en lugar de descargarse cientos de archivos que puede que nunca vayas a usar. ↩
-
Esta es sólo una explicación rápida. Si quieres más información, puedes visitar esta página, (en inglés) o simplemente leer la entrada de la Wikipedia y tirar del hilo. ↩
-
Técnicamente podríamos usar Python, usando
pow (msg, priv_exp, mod)
oRSA.decrypt ()
; pero, por alguna razón, no para de sacar basura. Supongo que OpenSSL añade algo al archivo y por eso no podemos descifrarlo… ↩