Un blog de Seguridad Informática, desde un punto de vista tecnológico

CTF

Welcome to FristiLeaks – Walkthrough

Introducción

Siguiendo con la serie de máquinas de Vulnhub que tienen un nivel parecido al laboratorio del OSCP, ahora es el turno de ver cómo se soluciona «FristiLeaks» (https://www.vulnhub.com/entry/fristileaks-13,133/).

En el post anterior «Welcome to kioptrix vm 2014» está explicada la solución de Kioptrix, y también se muestra con más detalle algunas de las prácticas que se utilizan aquí.

Information gathering

Recordamos una de las reglas más importantes a seguir para realizar un pentest: Obtener Información, la información es la clave.

Lo primero es obtener la dirección IP de la máquina objetivo: 192.168.8.140 

Una vez ya se sabe la dirección IP el siguiente paso es realizar una enumeración de puertos e identificación de servicios. Es necesario conocer cómo la máquina se comunica con el exterior, ya que estas comunicaciones serán los potenciales vectores de entrada.

1-Se realiza un scan de los 100 puertos más importantes: nmap  -T4 -Pn -F –open –reason $Ip

Se han obtenido los siguiente resultados:

a) Puerto 80 con un servicio http

2- Obtener más información de los servicios detectados en el paso 1. El comando es: nmap -T4 -Pn -sV -O -p 80, –open –reason $IP 

Se han obtenido los siguiente resultados:

a) Hay un Apache httpd 2.2.15

b) Hay un PHP 5.3.3

c) El sistema operativo es, probablemente, un Linux CentOS 2.6.32-3.10

3- Realizar un scan de todos los puertos. Tal y como se comentó en el post de Kioptrix hay diferentes opciones:

a) Utilizar el script «One-two punch» de superkojiman

b) Escanear todos los puertos TCP con nmap y utilizar el parámetro -A

c) Escanear todos los puertos UDP con la tool «unicorn» o el auxiliary scan UDP de Metasploit

 

Al finalizar todos los scans, se realiza una recopilación de toda la información obtenida para su análisis y definición de los posibles vectores de ataque y su priorización.

En base al análisis de los resultados , los vectores de ataque definidos son:

1- Puerto 80 abierto con un servicio HTTP (web)

2- Software Apache 2.2.15

3- PHP 5.3.8

4- Sistema operativo Linux CentOS 2.6.32 – 3.10

Análisis de vulnerabilidades

En este punto, la atención se va a centrar en cada uno de los vectores descritos intentando obtener información más específica de cada uno de ellos y así determinar las probabilidades de explotación.

A) Puerto 80 con un servicio HTTP (web)

La batería de pruebas que se ha de realizar siempre a un servicio web es la siguiente:

1- Navegar por la web y el fichero robots.txt

2- Análisis de las cabeceras

3- Análisis del código fuente

4- Enumeración de directorios

5- Nikto

Los resultados son los siguientes:

1- La navegación por la web

curl -i -L -s 192.168.8.140

b) El fichero robots.txt:

El fichero robots.txt muestra que hay 3 recursos a considerar:

a) cola

b) sisi

c) beer

Al ver el nombre de estos directorios, es fácil comprobar que los nombres de «cola» y «beer» son bebidas. ¿Pero sisi? ¿No era una emperatriz? Pues resulta que SiSi es el nombre de una especie de «fanta» originaria de Holanda. Esto nos da una idea que los directorios de la aplicación son nombres de bebidas.

c) Si se utiliza otras opciones de curl es posible obtener algo más de información. En este caso no hay información novedosa.

2- Se utiliza la herramienta gobuster para realizar la enumeración de directorios :

No hay resultados que valgan la pena.

3- Con la tool Nikto tampoco se ha obtenido informacion relevante.

B) Software obsoleto

Los pasos a seguir siempre que se trate de software desactualizado son:

1- Identificación de la versión

2- Búsqueda de vulnerabilidades y exploits

3- Analizar si el exploit aplica o no en el escenario y a los objetivos

En esta ocasión se va a proceder a buscar exploits utilizando Google y searchsploit

a) Apache 2.2.15

El único exploit que se ha encontrado no aplica para conseguir acceso a la máquina.

b) PHP 5.3.3

En este caso se puede observar como hay 2 exploits de denegación de servicio y uno con múltiples vulnerabilidades (18370.txt). Las vulnerabilidades detalladas en este fichero tampoco proporcionan un acceso a la máquina.

Resumen del análisis de los vectores de ataque

Una vez se ha realizado un análisis preliminar de los vectores de ataque, vale la pena valorar toda la información en su conjunto.

1- Hay un servicio web en el puerto 80 y disponemos de los siguientes recursos:

a) index.html

b) sisi

c) cola

d) beer

2- No hay exploits útiles del Apache 2.2.15 que nos permita acceder a la máquina

3- No hay exploits útiles de PHP 5.3.3 que nos permita acceder a la máquina

Por lo tanto, el único vector de ataque «factible» es el servicio http en el puerto 80 y se descartan los vectores 2 y 3.

Explotación del servicio HTTP – Puerto 80

Bien, la primera acción es visitar los recursos encontrados. En los 3 casos (sisi, beer y cola) lo que se encuentra es la siguiente imagen:

Parece que no se ha tenido éxito. Se revisa el código fuente y los metadatos de la imagen y el resultado es el mismo: No hay nada. El siguiente paso es visitar la página principal, y se obtiene el siguiente resultado:

¿Cómo? ¿Otra imagen? De nuevo, se analiza el código fuente, los metadatos de la imagen esperando encontrar algo….pero el resultado es que no hay nada. En este punto, existe la tentación de empezar a dar vueltas y más vueltas y acabar probando cosas altamente improbables. La recomendación en estos casos es simple: parar y analizar.

Se ha de analizar todo lo que ya se ha encontrado, los vectores de ataque, repasar que no se haya descuidado algún detalle. Si se ha sido metódico, este proceso es sencillo. Entonces, ¿qué es lo que sabemos y tenemos?

Tenemos que habían 4 vectores de ataque y sabemos que 3 de ellos no eran posibles. Por lo tanto, si o si, el vector del puerto 80 y concretamente en esa imagen ha de haber algo. Uhmmm «Drink Fristi», antes se ha comentado que el nombre de los directorios eran bebidas. Podría ser una pista, o simplemente una idea feliz chorra.

Gracias a Google se encuentra que Fristi es una bebida de yogur desnatado con frutos rojos original de Holanda. Todo hace indicar que podría ser un directorio «oculto». Se accede a él y si, por fin, hay un login:

Cuando se ve un login las primeras acciones suelen ser comprobar si hay una SQLi o es vulnerable a brute-force (probando el clásico admin/admin). Recordemos, la información es la clave. Es mejor, antes de nada, analizar el código fuente, intentar identificar si es un CMS, comentarios etc…en resumen, «conocer» el asset.

En este caso, en el código fuente se encuentra la siguiente información:

En un comentario hay escrita una pequeña anotación con lo que parece ser el nombre de un usuario: eezeepz.

Además, más abajo, en otro comentario hay lo que parece ser código en base64 (es fácil de identificar por su estructura o utilizando el Decoder de Burp):

Para decodificar este código es posible utilizar varios mecanismos.

a) Desde consola, copiando el código a un fichero y utilizando comandos:

Y la imagen que se obtiene es:

b) Utilizando Burp, la consola de Firefox, modificando en local el archivo .html…

En ambos casos se obtiene la misma imagen. Ya se dispone de un nombre de usuario (eezeepz) y un password (keKkeKKeKKeKkEkkEk). Prego! login exitoso!

En la zona autenticada se encuentra un «file upload». En este caso, la acción a realizar es intenta subir una webshell/reverse shell al servidor y así tener acceso al mismo. Con todos los datos recopilados es automático saber que la webshell ha de estar en código php. En el primer intento de subir un archivo .php, se produce un fallo porque la extensión no es la correcta:

Creación y acceso con una webshell

Hay múltiples opciones de crear una webshell en php. Desde las que ya incorpora Kali, o la php-reverse-shell de pentestmonkey (link) hasta la creación con msfvenom. Para esta ocasión, se muestra la creación de dos shells diferentes:

a) Con Meterpreter:

b) Sin meterpreter, únicamente una reverse shell:

Las 2 opciones son válidas. Hay que detallar que dado que la funcionalidad del «File Upload» indica que la extensión ha de ser de un tipo en concreto, el nombre de las shells creadas finalizan con *.php.png

Con el objetivo de ver más comandos y funciones de Metasploit se ha preferido utilizar la shell simple (opción b, reverse shell sin meterpreter) y mostrar cómo es posible, una vez ejecutada la shell, hacer un update de la misma a meterpreter. En Google es muy fácil encontrar información sobre qué ventajas conlleva esta técnica.

1- Así pues, se procede a subir la reverse shell esperando que únicamente con el truco de indicar una extensión autorizada (.png) sea posible evadir el mini check de seguridad (siempre que se pueda, lo primero a probar son las cosas más simples).

2- Perfecto, el upload ha sido un éxito. El siguiente paso es configurar el handler:

3- Ahora, se ha de acceder al archivo y que se ejecute correctamente:

4- Una vez ya se ha establecido la conexión, se realiza el update a Meterpreter:

5- Ya se dispone de una sesión en Fristileaks. 🙂

Elevación de privilegios

Una vez ya se está dentro del sistema se ha de volver a iniciar todo el proceso de investigación para determinar qué vector de ataque es el más idóneo para alcanzar root. En esta fase es muy importante el orden e ir de menos a más, de lo más simple a lo más complicado.

Information gathering

Una vez dentro del sistema se ha de recopilar el máximo posible de información con el objetivo de poder responder a la mayoría de estas preguntas (siempre priorizando):

1- ¿Qué información sabemos ya?

2- ¿Qué sistema operativo es?

3- Si es obsoleto, ¿Tiene vulnerabilidades conocidas y hay exploits públicos?

4- ¿Quién soy?

5- ¿Qué usuarios hay?

6- ¿Qué se puede encontrar en /home?

7- ¿Se puede ejecutar el comando sudo?¿Cómo está configurado?

8- ¿Hay algún history disponible?

9-  ¿Hay algún SetUID o SetGID?

10- ¿Cómo está configurado el PATH?

11- ¿Qué lenguajes están soportados?

12- ¿Qué comandos están permitidos para establecer comunicaciones al exterior?

13- ¿Qué interfaces de red tiene?

14- Si hay una aplicación web con un login ¿Hay algún fichero de configuración/conexión con credenciales a la base de datos?

15- ¿Qué procesos están corriendo en la máquina?¿Alguno como root?

16- ¿Hay algo en el Cron?

17- ¿Hay información útil en los logs de máquina?

18- ¿Qué ficheros/directorios puedo leer/escribir?

Mediante el análisis de las respuestas se podrán empezar a pensar y diseñar los vectores de ataques (ordenados de más simple a más complejos) con más posibilidades para realizar la escalada de privilegios.

En el caso concreto de Fristileaks, se muestra alguna de la información obtenida más relevante:

a) ¿Quién soy? ¿Qué kernel es? (y spawn con python):

– Usuario Apache

– Kernel 2.6.32

b) Sistema operativo: CentOS release 6.7

c) ¿Qué usuarios hay, además de root?

– eezeepz

– admin

– fristigod

– fristi

d) Fichero de conexión a la BD, con credenciales:

Lamentablemente, estas credenciales no funcionan para cambiar de usuario en Fristi.

e) Home de cada user:

admin:

eezeepz:

En el Home de eezeepz hay lo que parece ser un listado de comandos y un fichero «notes.txt«. Interesante

d) Además, analizando el contenido de la carpeta /var se encontró otro fichero «notes.txt»:

Es otro detalle más que hace sospechar que en el homedir del usuario eezeepz pueda haber algo interesante. Además, se obtiene el nombre (jerry) del que podría ser el usuario admin.

Análisis de vulnerabilidades

Las primeras vulnerabilidades que se han de probar son:

1- Comprobar el funcionamiento de sudo, y que el password del usuario no sea trivial.

No funciona

2- Que el usuario «root» no tenga como password…»root»

No funciona

3- Si el sistema operativo está desactualizado, ¿Hay exploit directo de elevación de privilegios?.

Este podría ser una opción: Linux Kernel 2.6.32 < 3.x.x (CentOS) – ‘PERF_EVENTS’ Privilege Escalation (1)

Sin embargo, las referencias continuas al homedir del usuario «eezeepz» se hacen merecedoras de ser estudiadas. Además, si bien esto es un juego, en el mundo real un usuario que codifica su password en base 64….hace pinta de ser un pelín desastre. Seguro que algo interesante hay.

Explotación

El contenido del fichero «notes.txt» del homedir del usuario eezeepz es el siguiente:

 Vaya con Jerry….tampoco es un genio no. Visto lo visto, vale la pena seguir por este vector.

Explotación del Runthis

El procedimiento es el siguiente:

1- Conseguir acceder a la homedir del usuario admin

2- Dentro de esta home se hallan ficheros que contienen lo que parecen ser credenciales cifradas así como el script responsable del cifrado:

3- Se busca algo de información sobre la función «codecs.encode» para ver cómo se puede hacer el proceso reversible y obtener los datos en claro:

4- Se realiza un script cutre para proceder al descifrado:

5- Se accede al usuario fristigod con las credenciales obtenidas:

6- Bien, pero el usuario «fristigod» no es lo mismo que root. Tal y como se ha comentado varias veces, de nuevo, la información es esencial. En este punto, es necesario conocer todo lo posible acerca de este usuario, como su «history»:

O la configuración de «sudo»:

Con esta información en la mano, está claro que el usuario «fristigod» realiza acciones con el usuario «fristi» e interaccionando con el fichero «doCOM» . Algo más de información sobre este fichero se puede obtener con el comando «file»:

7-  Solo resta por hacer una prueba más y esperar los resultados:

8- Et voilà ! Root y acceso al flag.

Disfrutad de la máquina 😉

 

 

 

 


DockerMaze challenge write-up

UPDATE 23/11/2015: new info thanks to @nibble_ds, one of the challenge authors, inline the post 🙂

Last November 16-17th the Dockercon eu 2015 was held in Barcelona, and the Schibsted team published the DockerMaze challenge, a labyrinth escape game like those we used to play in the 90s. In the game you wake up alone in the middle of a labyrinth and you have to escape from it, and through a console you can use commands to interact with the environment.

In this post I will show how I had fun solving the challenge! 😀

The «help» command uncovers a set of commands that includes «look», «interact» and «escape». If you do «look front» the game says that there are some signs on the wall, and after executing «inspect wall» some clues are revealed:

Found rooms:
  - schibstedchallenge/dockermaze-weisse:latest
  - schibstedchallenge/dockermaze-stout:latest
  - schibstedchallenge/dockermaze-porter:latest
  - schibstedchallenge/dockermaze-ipa:latest
Found Keys:
  - FollowTheWhiteRabbit
Followed path:
  - Input: https://challenge.schibsted.com/assets/data/ct1.bin
  - Output: ?
More than a year and I'm still here. I'm loosing all hope. Maybe there is another key?

A quick inspection of the binary file doesn’t give much information.

 

Captura de pantalla 2015-11-21 a las 23.05.11

Next step was to download the docker images and to inspect them:

docker pull schibstedchallenge/dockermaze-weisse:latest
docker pull schibstedchallenge/dockermaze-stout:latest
docker pull schibstedchallenge/dockermaze-porter:latest
docker pull schibstedchallenge/dockermaze-ipa:latest

Let’s play with those docker images then 🙂

WEISSE

docker inspect schibstedchallenge/dockermaze-weisse:latest

Relevant info:

"Entrypoint": [
 "/usr/local/bin/start.bash"
 ]
"ExposedPorts": {
 "1954/tcp": {}
 }

Captura de pantalla 2015-11-21 a las 23.39.25

Seems that there’s a ruby application («weisse.rb») listening at the port 1954/tcp, that is executed by the bash script «start.bash». Inside that script additional information is given:

# We use eureka + prana for service discovery.

This clue will be useful later 🙂

Looking at the «weisse.rb» file I saw that the application exposes a REST endpoint («/turing») that runs the received data in an Enigma machine. Furthermore, to set up the enigma machine it tries to get some information making DNS requests to a host with name «porter».

Here are some snippets of it 🙂

...SNIP...
BFBASE = 'aaa'
set :bind, '0.0.0.0'
set :port, 1954
post '/turing' do
 data = request.body.read
 rotors = get_rotors('porter')
plugboard = Hash[*PLUGBOARD.pack('H*').split('')]
 plugboard.merge!(plugboard.invert)
 rotors.map! do |r|
 Hash[[r].pack('H*').split('').zip((0...256).map{|i| i.chr})]
 end
 reflector = Hash[*REFLECTOR.pack('H*').split('')]
 reflector.merge!(reflector.invert)
 enigma(data, plugboard, rotors, reflector)
end
...SNIP...
def get_rotors(nameserver)
 rotors = []
Resolv::DNS.open({:nameserver=>[nameserver]}) do |r|
 ctr = 0
loop do
 begin
 n = r.getresource("walzen-#{ctr}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.to_i
 rescue Resolv::ResolvError
 break
 end
bf = BFBASE.dup
 found_chunks = 0
 rotors[ctr] = ''
while found_chunks < n
 begin
 ck = r.getresource("walzen-#{ctr}-#{bf}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.delete('"')
 rotors[ctr] << ck
 found_chunks += 1
 rescue Resolv::ResolvError
 next
 ensure
 bf.next!
 end
 end
ctr += 1
 end
 end
rotors
end
...SNIP...

STOUT

docker inspect schibstedchallenge/dockermaze-stout

Relevant info:

"Entrypoint": [
 "/usr/local/bin/stout.py"
 ]
"ExposedPorts": {
 "31337/tcp": {}
 }

But an error is raised when trying to run the docker image:

Captura de pantalla 2015-11-22 a las 0.06.51

As the container didn’t start due to the error, I changed the entry point of the container to be able to snoop the «stout.py».

docker run -ti --entrypoint /bin/bash --name stout schibstedchallenge/dockermaze-stout
docker cp stout:/usr/local/bin/stout.py .
#!/usr/bin/env python
import os
import sys
import socket
import base64
from datetime import datetime
from dns import resolver
from flask import Flask, request, make_response
app = Flask('stout')
PORTER_HOST = os.getenv('PORTER_PORT_53_TCP_ADDR')
def xor(data, key):
 return "".join(map(lambda i: chr(ord(data[i]) ^ ord(key[i%len(key)])), xrange(len(data))))
def transform(data):
 s = socket.socket()
s.connect(('ipa', 6060))
 s.sendall(base64.b64encode(data) + "\n")
 ret = s.makefile().readline().decode('base64')
 s.close()
 return ret
@app.route("/gate", methods=['POST'])
def gate():
 t1 = datetime.now()
data = request.stream.read()
dns_resolver = resolver.Resolver()
dns_resolver.nameservers = [PORTER_HOST]
 dns_answer = dns_resolver.query('bitwise.dockermaze', 'TXT')
 secret = dns_answer[0].to_text().strip('"')
ret = transform(xor(data, secret))
t2 = datetime.now()
resp = make_response(ret, 200)
 resp.headers.extend({'X-Dockermaze-Time': t2-t1})
return resp
if __name__ == '__main__':
 if not PORTER_HOST:
 sys.exit('error: cannot get key')
 app.run(host='0.0.0.0', port=31337)

What can be seen is that the script is publishing a REST endpoint that XORes the received data with a secret, obtained through a DNS request to the «porter» host (you have to provide its IP by an envvar), and sends the result to the «ipa» host.

 PORTER

docker inspect schibstedchallenge/dockermaze-porter

Relevant info:

"Entrypoint": [
 "/usr/sbin/named"
 ]
"ExposedPorts": {
 "53/tcp": {}
 }

As suspected providing the info obtained from the «stout» and «weisse» containers, the porter looks like a DNS server.

Captura de pantalla 2015-11-22 a las 13.08.35

Bingo! one of the entries present in the «db.dockermaze» dns zone configuration for bind contains the secret key needed by «stout.py» to work (between other relevant entries we’ll see below).

IPA

docker inspect schibstedchallenge/dockermaze-ipa

Relevant info:

"Entrypoint": [
 "/usr/local/bin/start.bash"
 ]
"ExposedPorts": {
 "6060/tcp": {}
 }
"Env": [
 "PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
 "GOLANG_VERSION=1.5.1",
 "GOLANG_DOWNLOAD_URL=https://golang.org/dl/go1.5.1.linux-amd64.tar.gz",
 "GOLANG_DOWNLOAD_SHA1=46eecd290d8803887dec718c691cc243f2175fe0",
 "GOPATH=/go"
 ]

The golang traces clearly point to @nibble_ds as one of the crime authors 😉

When trying to run the «ipa» image, an error is raised:

2015/11/22 12:20:20 error: envvar AES_KEY not defined

Let’s try to get more info of it:

 

Captura de pantalla 2015-11-22 a las 13.34.52

As can be seen the container also makes use of Prana and Eureka (parts of the Netflix stack), and runs a «ipa» golang binary. In this case, the challenge authors made our life easier giving us the source code too ;). In any case, the bin was not stripped.

$ file ipa
ipa: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped

Snooping the source code of the «ipa» application («ipa.go»), we can see that it listens to the port 6060/tcp, and decodes (base64) and decrypts the received data, using AES-256 CTR mode using a key provided by the environment variable «AES_KEY». The result is sent to the «weisse» REST endpoint and the returned data is base64 encoded and sent back to the caller.

Snippets FTW!

...SNIP...
var AesKey = os.Getenv("AES_KEY")
func main() {
...SNIP...
ln, err := net.Listen("tcp", ":6060")
...SNIP...
func handleConnection(conn net.Conn) {
 defer conn.Close()
br := bufio.NewReader(conn)
 line, err := br.ReadString('\n')
...SNIP...
data, err := base64.StdEncoding.DecodeString(line)
...SNIP...

 decdata, err := decrypt(data, []byte(AesKey))
...SNIP...
transdata, err := transform(decdata)
...SNIP...
ret := base64.StdEncoding.EncodeToString([]byte(transdata))
fmt.Fprintln(conn, ret)
}
...SNIP...
func transform(data []byte) (transdata []byte, err error) {
 c := goprana.NewClient(goprana.DefaultPort)
 resp, err := c.Post("weisse", "/turing", "application/octet-stream", bytes.NewReader(data))
 if err != nil {
 return nil, err
 }
 defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}

 

 

OK so, let’s put all the pieces together:

  1. The «stout» machine expects some data to be received by a POST HTTP method to its REST endpoint, listening at the 31337/tcp port. He gets a secret key from the «porter» machine (via DNS request) and after applying a transform to the received data, sends it base64 encoded to the «ipa» host.
  2. The «ipa» host receives the data sent by «stout», decodes it (base64) and decrypts it using the provided AES_KEY via envvar. Then sends the decrypted data to the «weisse» endpoint.
  3. The «weisse» endpoint applies an enigma decryption to the received data, getting information about the rotors through DNS requests to the «porter» DNS server, and returns the decrypted data to the «ipa».
  4. The «ipa» base64-encodes the response and returns it to «stout».
  5. The «stout» machine decodes the bas64 response and delivers it to the caller.

On the other hand we had the «ct1.bin» file and the «FollowTheRabbit» key, so we can make the below assumptions:

  • The AES_KEY is «FollowTheRabbit».
  • The «ct1.bin» is the encrypted data we want to decrypt using the container chain.

So we need to link the containers in order to let them talk to each others, taking into account that «weisse» and «ipa» use Prana & Eureka to communicate.

Note of the author: Juan told me that would be great to put a diagram in this place to improve the explanation, but… I’ll do it only for a beer. If you want a diagram just make me happy and bring me one!  ;-P

UPDATE 23/11/2015: nibble provided this awesome diagram 🙂

diagram

 

This is what I did:

docker pull netflixoss/eureka:1.1.147
docker run -d --name eureka netflixoss/eureka:1.1.147
docker run -d -P --name porter schibstedchallenge/dockermaze-porter
docker run -d -P --name weisse --link porter:porter --link eureka:eureka schibstedchallenge/dockermaze-weisse
docker run -d -P -e "AES_KEY=FollowTheWhiteRabbit" --name ipa --link weisse:weisse --link eureka:eureka schibstedchallenge/dockermaze-ipa
docker run -d -p 31337:31337 --name stout --link ipa:ipa --link porter:porter schibstedchallenge/dockermaze-stout

And then I waited some minutes before sending the requests to the «stout» endpoint (due to the advice found in the «start.bash» file):

curl -v -X POST --data-binary @ct1.bin http://localhost:31337/gate --header "Content-Type:application/octet-stream"

Captura de pantalla 2015-11-22 a las 19.02.35

OK, I was on the right path but unfortunately I was not attending the Dockercon :(. Fortunately after the con @nibble_ds sent me the key they were giving in the Schibsted booth (thanks sir!).

qrcode

When scanned the QR code a snippet of ruby code appeared:

puts 'z4LufsdfTf{bNsfldpE'.bytes.map { |ch| (ch.ord - 1).chr }.reverse.join

After executing it you get the new key! («DockerMazeSecretK3y»).

We should be close, but when I tried again the same «curl» command but modifying the AES_KEY, the «ct1.bin» didn’t work as the encrypted message. Where could we get a new message?

I remembered the DockerMaze «escape» command that receives a parameter «ip». So I did a DNAT from my public IP to the 31337/tcp port of the «stout» (PublicIP:31337 -> PrivateIP:31337), and executed:

escape x.x.x.x

Where «x.x.x.x» was my public IP, and I got:

Trying to escape... Wait...
Hummm… Everything seems to be okay but you must be faster… 20.040787 seconds is too much

So we need to make something to be faster. After inspecting what part of the chain triggered more, the «weisse» component stood up as highly inefficient. The problem was in this loop when getting the rotors:

...SNIP...

BFBASE = 'aaa'

...SNIP...
def get_rotors(nameserver)
 rotors = []
Resolv::DNS.open({:nameserver=>[nameserver]}) do |r|
 ctr = 0
loop do
 begin
 n = r.getresource("walzen-#{ctr}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.to_i
 rescue Resolv::ResolvError
 break
 end
bf = BFBASE.dup
 found_chunks = 0
 rotors[ctr] = ''
while found_chunks < n
 begin
 ck = r.getresource("walzen-#{ctr}-#{bf}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.delete('"')
 rotors[ctr] << ck
 found_chunks += 1
 rescue Resolv::ResolvError
 next
 ensure
 bf.next!
 end
 end
ctr += 1
 end
 end
rotors
end

Most of the requested DNS entries were like these:

walzen-0 IN TXT "4"
walzen-0-aaa IN TXT "7b57e0a216b65a40534e4c8bcc787a8e5b3722657dcfb0d199950688ef0c718cbf1094bd0ff7d687c69cfba09d42caaa13d4cdb24f8f892877b4a91f596b2615"
walzen-0-aab IN TXT "6f48936c561d66625e31702143c2978ddaf19f60dcfd340e3b3c2b725404a820613ad369ae0a30a5b76de14d08d041337c02ceacbed5e7c3deee67ad7f63f529"
walzen-0-aac IN TXT "f3523e2746b1e524a48451ff1e5c92f6d796b9b89036c43d8ae8f486c7c1bc2ea601499e6eab81e383c0392c2d0514f0e9324af985507efa116a743523cb00fe"
walzen-0-aad IN TXT "1a68df6455c8ec914476fcc5808279f298ed3f5dbba7a3b54b250309d92f17a112b307db75eb1c2af8dd38e473d819afd2e2ea1be6c90b589aba5f470d18459b"
walzen-1 IN TXT "4"
walzen-1-aaa IN TXT "0ec8580062742e72c3d96fc76d4f21bacdf03887256bb7c9d42a27c5cb43e216405163e7a3427a071033ea3944899f88d63f83e41d91ad1a19b39c455c041294"
walzen-1-aab IN TXT "ac8ccfafe184de033afdf13ddcdfd27b8b86989e82d1ffbca99290fbc4a115c2eba05323be80060a30eeaed3689385148aa56e37a6bb4a1bef0db5bd34dbf846"
walzen-1-aac IN TXT "eddd05480c7df9c6d0b69b591eb48f7f20175022f4577170ab7ea78e77b04c5d4e029dbf47fa3e8d49e3d83b4b816999ecb178f561081c292f2b6097544136f7"
walzen-1-aad IN TXT "18a46635e9b9f6d756753cf35f65e0aac1266c7c5ba25231e5e60bce0f2cb82432da675a09132d5e9acafc76a8110155b2c04d1ff2d5fe73cce8966a79286495"

But some of them were not consecutive, leading to a lot of unnecessary failed DNS requests. For example:

walzen-9 IN TXT "4"
walzen-9-aaa IN TXT "3a8a13373496029d73b8d44e23147e947f45d5fd8640073f2ff7953858bb5ce076cfbef68860d8986a7a8fc8ad26d9d3f8fc9fee0e56ed65b14cb0fe84acc724"
walzen-9-aab IN TXT "299cda0f3001505a3caf0b99e2c380f3b532161aa861b2f00675dfa4d08da0ea550dcc53f581692a5bd6d119744272fac0b7db8c6210ffbc8bc9a166bacd9305"
walzen-9-aac IN TXT "a76f638943aae11d925d680948e4672dd252ab54495fc2caf27cf45133b65e7d6ba6820a1225398e214b274dbd00596efbefe6229e18473b20c56de3c135153d"
walzen-9-rzd IN TXT "644fe5f18583ebf9c62e1e1f7bc4ecdd44b4ce70a5086c4ad7579a17a9413e3171bfde11790c877704a2a3e8b32891369bb946e9ae2b2c1bcbe797781c90dc03"

So what I did is to edit the «db.dockermaze» configuration file to make them all consecutive and updated the docker image.

docker cp ./modified-db.dockermaze porter:/etc/bind/db.dockermaze
docker commit porter redsadic/dockermaze-porter:v2
docker run -d -P --name porter redsadic/dockermaze-porter:v2

And when I ran the «escape x.x.x.x» command again… Voilà!

Trying to escape... Wait...
You put the key in the lock and... the door opens! 
Congratulations! You are out of the labyrinth! 
Send an email with the following info to big.ideas+DockerMaze@schibsted.com:
- IP used to escape
- The token 'XXXXXXXXXXXXXXXXXX'
- Short explanation about how you escaped

Wohoooo! challenge solved! 😀

I would want to thank the Schibsted team because I really had a lot of fun with the challenge! Thank you guys! 😀

And that’s all folks!

UPDATE 23/11/2015: some easter eggs from the challenge authors :

  • The hostnames are different kind of beers
  • The STOUT endpoint is /gate because it implements a XOR (a logic gate)
  • The IPA exposed port is :6060 or GOGO 😀
  • The WEISSE (enigma) endpoint port is 1954, the year Alan Turing died
  • Also the endpoint /turing is in honor of him, due to his contribution breaking enigma
  • The DNS records where the rotors are stored are called «walzen-x-yyy». Walzen means rotor in german
  • And…
    • STOUT is the kind of beer that nibble likes less, this is why he did it in python (as a good python hater he is)
    • IPA is one of his favorites beers, and he did it in go 😉
    • WEISSE is other kind of beer he loves, for this reason it is ruby!

PD: looks like the https://challenge.schibsted.com site is down now. Too late if you want to play the challenge now ;(

UPDATE 23/11/2015:

PD: the challenge is now available at http://challenge.schibsted.com. If you wanna play, go for it!!!

 


Write-up VODKA – Final CTF NCN 2014

¡Buenas! Primero de todo me presento: mi nombre es Marc Peña (@p4chul0) y también soy miembro del equipo 0xB33r$. Hoy, continuando con las publicaciones sobre los retos de la final del CTF de la No cON Name, publicamos otro write-up.

Uno de los retos con los que estuve enfrascado durante el CTF fue VODKA (https://github.com/ctfs/write-ups/tree/master/ncn-ctf-2014/Vodka); en el mismo se obtenía un archivo con ese mismo nombre, que tras un primer análisis con file:

root@wheezy-kali:~/ctf_NCN/vodka# file vodka
vodka: bzip2 compressed data, block size = 900k

Parecía que se trataba de un archivo comprimido; tras extraerlo:

root@wheezy-kali:~/ctf_NCN/vodka# bzip2 -d vodka
bzip2: Can't guess original name for vodka -- using vodka.out

Obteníamos un archivo con una captura de tráfico de red:

root@wheezy-kali:~/ctf_NCN/vodka# file vodka.out
vodka.out: pcap-ng capture file - version 1.0

Que pasábamos a analizar con Wireshark:

01_vodka

Donde enseguida veíamos que se trataba de la transferencia de una imagen OpenWRT a través del protocolo TFTP.

Tras buscar como extraer el binario con el propio Wireshark nos encontramos con esto:

Que nos da a entender que la funcionalidad está implementada, pero no en la versión de Wireshark que tenemos instalada en nuestra Kali (1.10.2), por lo que tendríamos que compilar el trunk de Wireshark. Con el siguiente comando:

  • Instalaremos los paquetes necesarios para la compilación.
  • Clonaremos el repositorio de Wireshark.
  • Configuraremos y compilaremos el trunk.

apt-get install autoconf bison flex libtool libgtk2.0-dev libpcap-dev libc-ares-dev libsmi2-dev libgnutls-dev libgcrypt11-dev libkrb5-dev libcap2-bin libgeoip-dev libortp-dev libportaudio-dev qt4-dev-tools && \
apt-get build-dep wireshark && \
cd /opt/ && \
git clone https://code.wireshark.org/review/p/wireshark.git && \
cd /opt/wireshark && \
./autogen.sh && \
./configure --enable-dumpcap --enable-setcap-install --with-dumpcap-group=wireshark && \
make -j 8

Si todo ha ido bien ya podremos abrir el archivo con la última versión de Wireshark:

root@wheezy-kali:~/ctf_NCN/vodka# /opt/wireshark/wireshark vodka.out

Y exportar el archivo (File -> Export Objects -> TFTP):

02_vodka

Ahora tocaba lidiar con la imagen, por lo que instalamos el binwalk (https://github.com/devttys0/binwalk) del gran devttys0 (http://www.devttys0.com/):

apt-get install binwalk

Una vez hecho esto ya se podían observar las características de la imagen:

root@wheezy-kali:~/ctf_NCN/vodka# binwalk openwrt-wrtsl54gs-squashfs.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
32 0x20 TRX firmware header, little endian, header size: 28 bytes, image size: 1323008 bytes, CRC32: 0x6CAC483 flags: 0x0, version: 1
2296 0x8F8 LZMA compressed data, properties: 0x6D, dictionary size: 8388608 bytes, uncompressed size: -1 bytes
517152 0x7E420 Squashfs filesystem, little endian, version 2.1, size: 805671 bytes, 269 inodes, blocksize: 65536 bytes, created: Wed Oct 29 18:53:25 2014

Y extraer el sistema de archivos Squashfs (por defecto en el directorio «_openwrt-wrtsl54gs-squashfs.bin.extracted»):

root@wheezy-kali:~/ctf_NCN/vodka# binwalk -e openwrt-wrtsl54gs-squashfs.bin

Una vez dentro de este ya podíamos pasar a navegar por el sistema de archivos en busca de la flag:

root@wheezy-kali:~/ctf_NCN/vodka/_openwrt-wrtsl54gs-squashfs.bin.extracted/squashfs-root# ls
bin dev etc jffs lib mnt proc rom sbin tmp usr var www

En el directorio que miramos primero (/bin) encontramos un archivo con una fecha de modificación muy cercana a la de creación del sistema Squashfs:

root@wheezy-kali:~/ctf_NCN/vodka/_openwrt-wrtsl54gs-squashfs.bin.extracted/squashfs-root# ls -lisah bin/
total 596K
3670108 4.0K drwxr-xr-x 2 root root 4.0K Oct 29 18:50 .
3670107 4.0K drwxr-xr-x 14 root root 4.0K Jan 30 2007 ..
...
3670142 8.0K -rwxr-xr-x 1 root root 4.2K Oct 29 18:52 nc
...

Por lo que parecía un buen candidato para empezar a buscar. Si abríamos el archivo con un editor de texto veíamos que se trataba de un script en bash que muestra un «Nyan cat» en ASCII Art:

#!/bin/bash
# --------------------------------------------------------------------------
# (c) 2012 BruXy Version: 1.0 http://bruxy.regnet.cz/
# --------------------------------------------------------------------------
# switch off cursor, set blue background, clear screen
echo -e "\E[?25l\E[44m\E[2J"
# Play music
#M=/tmp/cat_orig.mp3
#[ ! -f $M ] && wget http://nyan.cat/music/original.mp3 -O $M >/dev/null 2>&1
#while true; do mplayer $M > /dev/null 2>&1; done &
# easy way to reset terminal and exit subprocesses
trap "reset;killall nc" 2
## Nyan cat ANSI picture data, my own creation :P, size 38 x 11
# Compressed version of picture
D="H4sIAHUNPlAAA22Q2w7CIAyG730rC1IGEs3itoS7cYlLvFGf37YcPMQvo5DxpT+pMYLb/8OVSxNv
Aiq1Db9sSmG1TgKqdhXqktKtsyBW6F08gB/EcgAUG2eAeRxbryqi1hq89yvQgfrFZ6If8E7sVoet
aVqWlNjqgYGlA5ckG5r4EGqvHmgtGeWzlqwLc6dJHD8Sr/IMQadMiTxR0IDrN/RUUjDnLJOghYn9
UgtyzvTcNvvdCw/8WhWjAQAA"

Sabiendo esto vamos podíamos ir a buscar la versión original del mismo:

root@wheezy-kali:~/ctf_NCN/vodka/_openwrt-wrtsl54gs-squashfs.bin.extracted/squashfs-root/bin# wget http://bruxy.regnet.cz/linux/nyan_cat/nyan_cat.sh

Para analizar las diferencias:

root@wheezy-kali:~/ctf_NCN/vodka/_openwrt-wrtsl54gs-squashfs.bin.extracted/squashfs-root/bin# diff nyan_cat.sh nc
9,11c9,11
< M=/tmp/cat_orig.mp3
< [ ! -f $M ] && wget http://nyan.cat/music/original.mp3 -O $M >/dev/null 2>&1
< while true; do mplayer $M > /dev/null 2>&1; done &
---
> #M=/tmp/cat_orig.mp3
> #[ ! -f $M ] && wget http://nyan.cat/music/original.mp3 -O $M >/dev/null 2>&1
> #while true; do mplayer $M > /dev/null 2>&1; done &
14c14
< trap "reset;killall nyan_cat.sh" 2
---
> trap "reset;killall nc" 2
42c42,47
< printf "%*s" $[X*Y] BruXy
---
> file="/etc/banner"
> za="N"
> t1="C"
> drink="$(md5sum $file | grep -o ^[^\ ]*)"
> msg_="${za}${t1}${za}dead${drink}face"
> printf "%*s" $[X*Y] ${msg_}

¡Obteniendo lo que parece el código que genera la flag!

Creamos un nuevo archivo con este código (con la única modificación de la ruta del fichero, para convertirla en relativa):

root@wheezy-kali:~/ctf_NCN/vodka/_openwrt-wrtsl54gs-squashfs.bin.extracted/squashfs-root/bin# cat vodka_flag.txt
#!/bin/bash
file="../etc/banner"
za="N"
t1="C"
drink="$(md5sum $file | grep -o ^[^\ ]*)"
msg_="${za}${t1}${za}dead${drink}face"
printf "%*s" $[X*Y] ${msg_}

¡Y lo ejecutamos  para obtener la flag!:

root@wheezy-kali:~/ctf_NCN/vodka/_openwrt-wrtsl54gs-squashfs.bin.extracted/squashfs-root/bin# chmod +x vodka_flag.txt && ./vodka_flag.txt
NCNdeadb6adec4c77a40c23e04770924d3c5b18face

Aunque tampoco pudimos resolver este reto dentro del CTF. no quiero terminar sin felicitar a los organizadores por unas pruebas de lo más originales.

¡Saludos!


Write Up 5h311 – Final CTF NCN 2014

Buenas,

Para continuar con la saga de write-up’s iniciada en el anterior post con @julianvilas, hoy os pondremos la solución a otro de los retos que tuvimos el placer de testear.

Se trata de un fichero llamado “5h311”, que al ejecutarlo se abre un servicio en local en el Puerto 6969, que la conectarnos muestra lo siguiente:

Ejecución 5h311

Parece ser un acceso por consola que permite una serie de comandos, concretamente “cat”, “echo”, “env”, “exit”, “help”, “ls”, “set” y “unset”.

Si hacemos ls encontramos que el flag se encuentra en el mismo directorio del binario, pero al hacer un “cat flag.txt” podemos observar que no disponemos de privilegios para accede .

Permission Denied

Bueno pues nos ponemos manos a la obra y abrimos el programa con IDA para ver que es lo que nos esconde.

Buscamos en IDA las ocurrencias del comando “cat” con la búsqueda de Strings (ALT+T), con la finalidad de identificar la sección de código donde se están gestionando los comandos:

String put

Como resultado, en la sección .rodata podemos observar el vector que incluye los comandos que son printados por pantalla.

comandos

Pero observando unas direcciones de memoria más abajo, se pueden observar de nuevo los comandos asignados a unos offsets, pero con la peculiaridad que aparece un nuevo comando que no nos ha listado el comando HELP, el comando “Puts”.

puts command

Miramos las Xrefs del offset correspondiente a “aPuts” (x Key):

xrefs puts

Miramos de analizar la primera ocurrencia, que parece ser que se está empleando dentro de una función.

Antes de nada un apunte, observando el programa desde la View Graph, ya se observa algo raro, como si se hubiera ofuscado con algún tipo de programa, debido a que todos los paths van a parar a un mismo punto como se observa en la siguiente imagen:

graph

Procedemos a analizar el bloque de código que emplea el offset de «puts», y observamos que se realizan una serie de comparaciones:

puts code

Concretamente hace una comparación de dos offsets con una llamada a strcmp y verifica mediante el siguiente cmp si son iguales, en ese caso activa el ZF.

Antes de continuar refrequemos que hace el opcode “setz”:

  • The setz sets the byte in the destination operand to 1 if the Zero Flag (ZF) is set, otherwise sets the operand to 0.

Después mediante la instrucción “setz”, se setea el valor de “bl” a 1 si ZF vale 1, es decir bl valdrá 1 si el resultado del strcmp es que son iguales.

Posteriormente mediante la instrucción “test”, se modifica otra vez el ZF en función del resultado del registro bl. (Empezamos a observar algo de redundancia aquí, por lo que la hipótesis de que se ha empleado algún tipo de programa para ofuscar el código es cada vez más firme :P)

Finalmente se mueven de nuevo los dos valores comparados en el strcmp, y se ejecuta la instrucción cmovz (al parecer sin valor alguno debido a que solo se ejecuta si ZF=1, es decir, si ecx y eax ya son iguales).

Como podéis ver, una locura para al final solo acabar realizando una comparación xD Por lo que antes de ingresar en un psiquiárico, procederemos a realizar un análisis dinámico partiendo del bloque de código identificado.

Cuando el proceso recibe una conexión al puerto 6969, éste crea un nuevo proceso hijo mediante la syscall ‘clone’ y le pasa el control al mismo para que continúe su ejecución. Para poder debugear el proceso hijo sin que el padre nos moleste (al cabo de un tiempo envia un SIGALARM y nos mata el proceso), empleamos la instrucción “catch” de GDB para que, una vez cree el proceso hijo, podamos pausar su ejecución, y de esta forma poder debugear el proceso hijo con calma.

 

gdb debug

 

 

Añadimos un breakpoint a la dirección del bloque de código donde hemos observado el offset del ‘puts’, y ejecutamos la instrucción para ver que ocurre. Observamos que no  se observa ningún cambio, por lo que el flujo de ejecución no pasa por ahí. (ouch!) Continuemos con el análisis pues.

putsdoesnotork

Observando más detenidamente el código y continuando con la búsqueda de cadenas de texto interesantes como los comandos encontramos una zona de memoria donde se definen los offsets de las funciones asociadas a cada comando de la shell analizada:

commands

En la imagen se muestran los offsets de las funciones ya renombrados.

Usando la funcionalidad de cross-reference vemos que el primero de estos offsets es llamado desde otro punto del código:

xrefs

 

 

En ese punto se aprecia el acceso a este offset con un desplazamiento dinàmico con el valor almacenado en [eax*8]. Esto permite con el valor de eax recorrer la zona de memoria que contiene los punteros a las funciones de cada comando. Más tarde este offset guardado en el registro eax será llamado con call eax.

Captura de pantalla 2014-10-31 a la(s) 11.40.01

 

Tirando del hilo veremos que el valor de eax viene condicionado supuestamente por el valor del comando introducido por el usuario, mediante el offset «off_804E848».

Captura de pantalla 2014-10-31 a la(s) 11.46.07

Vemos que este valor parece venir de la función _fgets. Aunque de momento, debido a que el flujo del código esta ofuscado con la implementación de una máquina de estados, no podemos estar seguros de todas estas conjeturas. Deberemos por lo tanto ejecutar el análisis de forma dinámica para estar seguros.

Captura de pantalla 2014-10-31 a la(s) 12.08.57

Para analizar todos los valores comentados anteriormente de forma dinámica, utilizaremos gdbserver para debuggar cómodamente  de forma remota y poder indicar los breakpoints directamente desde IDA.

GDBServer

Luego en IDA seleccionamos el Debugger de tipo GDB server y configuramos la ip y el puerto:

IDA GDB Config

 

Ya podemos a priori centrarnos en el comportamiento de los comandos que más nos interesen, en este caso ‘cat’, ‘set’, y el misterioso ‘puts’. Para ello prepararemos el entorno para debuggar y pondremos breakpoints en los puntos más interesantes.

Focalizando en las funciones cat, set, i puts vemos que todas están implementadas ofuscando el flujo del código con una máquina de estados. Por lo tanto buscaremos las llamadas mas interesantes para poner breakpoints. Para verificar que no se nos escapa nada y por donde podemos empezar a mirar, sacamos un grafo de llamadas desde cada función que nos interesa con Graphview.

Captura de pantalla 2014-10-31 a la(s) 12.27.40Captura de pantalla 2014-10-31 a la(s) 12.28.17Captura de pantalla 2014-10-31 a la(s) 12.27.57

 

 

 

 

 

 

 

 

Ponemos los breakpoints en las llamadas empleadas en cada una de las funciones anteriormente mostradas en los callgraphs (strcmp, fopen, fgets, strncpy), con la finalidad de identificar, por ejemplo, las comparaciones realizadas en cada una de las llamadas a “strcmp”,  y empezamos a jugar.

En la función «cat» observamos se emplea una llamada a fopen para abrir el fichero a leer y que el valor de dicho fichero esta hardcodeado a “flag.txt”, por lo que solo se podrá abrir un fichero con dicho nombre.

Captura de pantalla 2014-10-31 a la(s) 12.30.13

Captura de pantalla 2014-10-31 a la(s) 12.31.24

En la función «set» observamos que las variables de entorno se guardan en memoria empezando en el offset dword_804e8a8 sumando un desplazamiento de 0x100 bytes (por lo que resulta en 804e9a8) y que el numero de variables de entorno se guarda en el offset dword_804e8a4.

Captura de pantalla 2014-10-31 a la(s) 12.32.00

 

Podemos ver como en las direcciones de memoria anteriormente comentadas se almacenan las variables de entorno seteadas con el comando «SET», (variable en 0x804e8a8 y valor en un offset de 0x100, concretamente en 0x804e9a8).

EnvVars

 

EnvVars_command

 

En la función “puts” observamos que recorre las variables definidas con set (incluso recorre las que se han “eliminado” con unset pero que aún siguen en la memoria pero sin la primera letra) y compara su valor con la cadena “puts”.

Captura de pantalla 2014-10-31 a la(s) 12.35.58

 

Si una de las variables se llama “puts” entonces compara su valor con “printf”.

Captura de pantalla 2014-10-31 a la(s) 12.36.48

 

Probamos el comportamiento creando una variable de entorno que se llame puts y que su valor sea printf y luego invocamos a puts. Como podemos ver, parece que se hemos conseguido una escalada de privilegios o algo parecido.

Captura de pantalla 2014-10-31 a la(s) 12.37.36

 

Probando ahora el comando cat con un fichero cualquiera del directorio desde donde ejecutamos la shell vemos que nos devuelve “error: permission denied”

Captura de pantalla 2014-10-31 a la(s) 12.38.58

Pero si recordamos que el valor de «flag.txt» está hardcodeado en el binario, miremos de probar pues de modificar el fichero de nuestro entorno local a dicho nombre. Volvemos a provar y ya podemos mostrar el contenido de flag.txt!

Captura de pantalla 2014-10-31 a la(s) 12.51.08

 

En este caso esta prueba no logramos resolverla dentro del tiempo del CTF. Pudimos resolverlo con calma luego desde casa 😛

Solo queda dar gracias al staff de las NCN por la originalidad de los juegos!

 

Post escrito con la colaboración de «Pau Rodriguez», miembro activo de 0xb33r$ 🙂


Solución HIDDENtation – Final CTF NCN 2014

Buenas,

hoy hemos tenido el placer de participar en la final del CTF de las No cON Name por segundo año consecutivo y, antes de explicaros nuestra solución a uno de los retos (que no conseguimos terminar a tiempo ;( ), nos gustaría dar las gracias a la organización; ya que se han currado un montón las pruebas (tanto las de la final como las de las quals) con el único objetivo de hacernos pasar un buen rato. Gracias de verdad, porque ha sido muy divertido! 🙂

El reto que vamos a explicaros se llama HIDDENtation, y el texto que describía el reto decía tal que así: «Dig deep into the file and find the flag.». Este texto venía acompañado por un archivo, que es en el que se centra el juego.

En un primer vistazo al archivo vemos algo tal que así:

hexdump

 

 

Por lo que parece que se trate de una imagen de un volumen cifrado con LUKS (estándar de cifrado de discos duros en Linux). El tema es que el comando «file» no ha identificado el fichero como tal, así que procedemos a analizar si la estructura del archivo es correcta. Para ello utilizamos una plantilla para el «010 Editor» para analizar el format de volúmenes LUKS.

Al ejecutar la plantilla vemos que nos da un error que dice que el archivo no es LUKS, ya que no se corresponde su firma. Así que pasamos a mirarnos el estándar de LUKS para ver cual es su firma, donde nos dice que la cabecera de la partición empieza con el siguiente magic: «{’L’,’U’,’K’,’S’,0xBA,0xBE}», y en nuestro caso en vez de una ‘S’ había una ‘s’ minúscula. Una vez modificada esa letra, volvemos a aplicar el template y vemos como ahora sí lo reconoce:

Cabecera LUKS

 

curioseando un poco el fichero vemos que en la sección de padding de caracteres aparece el siguiente texto: «Try … most common passwd in …», donde los «…» son dos números hexadecimales: 0x19 (25) y 0x07dd (2013). Así que nos vamos a preguntarle a Google y encontramos esto.

Utilizando el siguiente comando

cryptsetup luksOpen hiddentation volume1

 

intentamos descifrar el volumen con los 25 passwords sin éxito :(. Así que parece que tocaba trabajar más en los headers.

Después de pelearnos con el estándar vemos que nos faltaría arreglar las secciones de los key-slots en el header.

Key-slots

 

En este caso vemos :

  1. que los ocho que hay están desactivados («00 00 DE AD»), cuando tendría que haber al menos uno activo (en el que supone que está la clave con la que se cifran los datos protegida con la passphrase correspondiente a uno de los 25 passwords más comunes en 2013),
  2. que los campos donde se indican los offset de los KeyMaterials no parecen estar correctamente calculados. Para estar bien calculados deberían cumplir la fórmula round_up(key_length * stripes / sector_size); en este caso: (32*4000) /512 = 250, por lo que tomamos 256 (0x100).

Así que ya que parece que el key-slot8 es «el bueno», pues marcamos este slot como ENABLED (0x00AC71F3) y retocamos los 8 offsets, quedando algo tal que así:

LUKS OK

 

Ahora nos disponemos a probar otra vez los 25 passwords y tenemos éxito con el número 18: «shadow».

Así que ahora vemos qué pinta tiene el disco:

fdisk

 

 

Y vemos que la tabla de particiones es GPT, y que fdisk no soporta GPT. Procedemos a utilizar parted:

Gparted

 

Y aparecen tres particiones, una XFS, una EXT2 y una FAT32. Haciendo uso de los dispositivos de loop las vamos montando una a una. Por ejemplo:

losetup -o 44040192 /dev/loop2 /dev/mapper/volume1

donde indicamos el offset de la partición, el dispositivo de loop a utilizar y el volumen de disco. Este caso concreto es el de la tercera partición (tras buscar en la primera y la segunda no encontramos el flag).

Y al montar la tercera nos aparece un fichero que se llama «flag.txt». Quedaba un minuto para que se acabase el CTF y con toda el ansia procedemos a abrir el fichero para encontrarnos un «It’s inside this partition, but hidden ;)». Agggggggghhhh xD

Bien, después de analizar la partición con el autopsy, no vimos nada relevante en cuanto a ficheros borrados. Utilizando el comando testdisk vemos que en el mismo volumen hay una partición NTFS borrada previamente.

NTFS

 

La extraemos del siguiente modo:

dd if=/dev/loop2 of=ntfs2 skip=69632 count=34816

la montamos y vemos que hay un «readme.txt». Abrimos y…. «You are very near, but it’s even more hidden!». Mekagüen!! esta gente está loca! ;***, jajaja

De nuevo autopsy, esta vez sobre la partición NTFS y encontramos esto:

ADS

 

Un fichero flag.txt borrado (que no contiene el flag) pero con un Alternate Data Stream que contiene esa secuencia codificada en ROT13. La decodificamos y voilà, ahora sí tenemos flag «NCNd986942b809daa32a6987a7422771a53f59e5a1f02ed700cce43c5196aba749e» (pero el CTF ya ha acabado xD).

Pues parece que si que había que «Dig deeper» para encontrar la bandera ;).

Genial la prueba, muy muy currada. Gracias de nuevo staff de las NcN!! 🙂


Solución reto Australia – Final CTF NcN 2013

A continuación la solución al reto de Australia, que por desgracia no nos dio tiempo a resolver en el mismo CTF, pero que hemos resuelto posteriormente y ahora os exponemos.

Al acceder a la IP del servidor correspondiente al país «Australia», se podía descargar un binario para proceder a su análisis. El binario era un ejecutable de Linux (ELF), que pedía introducir una clave que, principalmente, consistía en el flag que debíamos introducir para obtener la puntuación del juego.

Screen Shot 2013-11-30 at 10.11.14 AM

En el CTF optamos por hacer un reversing estático del programa con IDA, y para ganar tiempo, se obtuvo el código C mediante el empleo de HexRays:

signed int __cdecl main()
{
 signed int result; // eax@2
 int v1; // [sp+1Ch] [bp-4h]@1
print_header();
 v1 = malloc(goodboy_len);
 if ( v1 )
 {
 sub_80481A0();
 if ( fgets(v1, goodboy_len - 1, stdin[0]) )
 {
 if ( check_buffer(v1, goodboy_len - 2) )
 puts("Winner! Post your flag.");
 else
 fwrite("Sorry, that is not correct.\n", 1, 28, stderr);
 result = 0;
 }
 else
 {
 fwrite("Could not read user input.\n", 1, 27, stderr);
 free(v1);
 result = 2;
 }
 }
 else
 {
 fwrite("Could not allocate buffer.\n", 1, 27, stderr);
 result = 1;
 }
 return result;
}
signed int __cdecl check_buffer(int a1, unsigned int a2)
{
 unsigned int i; // [sp+Ch] [bp-4h]@1
for ( i = 0; i < a2; ++i )
 {
 if ( not_flag[i] != (*(_BYTE *)(a1 + i) ^ 0xCC) )
 return 0;
 }
 return 1;
}

Si analizamos el código, se puede observar que el resultado del programa depende del valor de retorno de la función check_buffer(), en la cual el valor del flag XOR’ed con el valor 0xCC debe ser igual al array «not_flag»:

 not_flag[i] != (*(_BYTE *)(a1 + i) ^ 0xCC)

Por lo que para calcular el valor del flag, no hay más que conmutar la función XOR:

0xCC ^ not_flag[i] != (*(_BYTE *)(a1 + i)  

Cabe considerar que la entrada se comprueba BYTE a BYTE, pero eso ya lo analizaremos con detalle más adelante.

Después de varios intentos, pudimos comprobar que el flag no era correcto, por lo que algo fallaba.

Por lo que vamos a utilizar GDB para hacer un análisis dinámico y ver realmente que es lo que se estaba fallando.

Desensamblamos la función check_buffer:

Screen Shot 2013-11-30 at 10.43.11 AM

Analizamos el punto donde se realiza la XOR:

Screen Shot 2013-11-30 at 10.52.05 AM

El BYTE con el cual se realiza una XOR con la entrada del usuario se almacena en la posición -0x7(%ebp).

El siguiente código corresponde al cálculo del valor  que será utilizado para la operación XOR:

0x08048433 <+25>: movl $0xcd000000,-0xc(%ebp) --> -0xc(%ebp) = 0xcd000000
[...]
0x0804843c <+34>: mov -0xc(%ebp),%eax --> EAX = 0xcd000000
0x0804843f <+37>: shr $0x18,%eax --> Shift_Right(EAX,0x18) EAX = 0xCD
0x08048442 <+40>: mov %al,-0x7(%ebp) --> -0x7(%EBP) = 0xCD
0x08048445 <+43>: movzbl -0x7(%ebp),%eax --> %eax = 0xCD
0x08048449 <+47>: and $0xf,%EAX--> %eax = 0xd
0x0804844c <+50>: mov %al,-0x5(%ebp) --> -0x5(%ebp) = 0x0d
0x0804844f <+53>: movzbl -0x7(%ebp),%eax --> %eax = 0xcd
0x08048453 <+57>: and $0xfffffff0,%eax --> %eax = 0xc0
0x08048456 <+60>: mov %al,-0x6(%ebp) --> -0x6(%ebp) = 0xc0
0x08048459 <+63>: movzbl -0x6(%ebp),%eax --> %eax = 0xc0
0x0804845d <+67>: mov %eax,%edx --> %edx = 0xc0
0x0804845f <+69>: shr $0x4,%dl --> %dl (0-7bit) = 0xc
0x08048462 <+72>: movzbl -0x5(%ebp),%eax --> %eax = 0x0d
0x08048466 <+76>: shl $0x4,%eax --> %eax = 0xd0
0x08048469 <+79>: add %edx,%eax --> %eax = 0xdc
0x0804846b <+81>: mov %al,-0x7(%ebp) -->0x7(%ebp) = 0xdc

Con GDB no es necesario realizar todos los cálculos, sino únicamente con insertar un breakpoint en el punto en el que se realiza la XOR y ver qué hay almacenado en el registro %eax:

Screen Shot 2013-11-30 at 11.04.07 AM

Por lo que, en lugar de hacer una XOR con el valor 0xCC como se indicaba en el código, realmente se hace una XOR con el valor 0xDC.

Llegados a este punto, únicamente nos queda definir el formato mediante el cual se ha de introducir el flag. 

Según el código analizado, los valores introducidos por el usuario se almacenan en el registro %eax como resultado de la operación XOR, y los valores son comparados byte a byte mediante el uso del registro %al.

Screen Shot 2013-11-30 at 1.23.41 PM

Screen Shot 2013-11-30 at 11.04.07 AM

Los valores son comparados en formato hexadecimal, en cambio los valores son obtenidos de la entrada del usuario mediante la función fgets, por lo que los datos son obtenidos en formato ASCII. Por este motivo, para obtener los valores del flag hay que hacer una XOR del byte de la variable «not_flag» con el valor 0xDC y hacer una conversión a ASCII, tal y como se muestra a continuación:

Screen Shot 2013-11-30 at 1.35.17 PM

Así que para finalizar, solo hay que obtener los bytes de la variable «not_flag»:

Screen Shot 2013-11-30 at 1.38.48 PM

Y automatizar el proceso para obtener el valor del flag:

#!/usr/bin/ruby
not_flag = [0xeb, 0xe8, 0xbf, 0xe4, 0xea, 0xbe, 0xba, 0xe4, 
 0xe5, 0xea, 0xe8, 0xea, 0xe8, 0xee, 0xe9, 0xba, 
 0xea, 0xe8, 0xeb, 0xba, 0xbf, 0xba ,0xeb, 0xea,
 0xe8, 0xef, 0xbd, 0xba, 0xed, 0xe9, 0xba, 0xee,
 0xe9, 0xed, 0xbe, 0xed, 0xe4, 0xea, 0xbe, 0xba, 
 0xe9, 0xe4, 0xbd, 0xea, 0xb8, 0xe9, 0xb8, 0xbf, 
 0xeb, 0xb9, 0xbe, 0xe4, 0xbe, 0xba, 0xe5, 0xbf, 
 0xba, 0xbf, 0xe5, 0xb8, 0xec, 0xe8, 0xbf, 0xb8]
puts "Solucion: "
not_flag.each { v | 
 p (0xdc ^ v).chr
}
puts

Y finalmente, podremos verificar que el flag introducido es correcto 🙂

Screen Shot 2013-11-30 at 1.45.23 PM

Disfrutamos mucho del CTF. Esperamos ansiosos poder participar en otro bien pronto.

Saludos!


Solución reto China – Final CTF NcN 2013

Hace unos días tuvimos la suerte de poder participar en la final del CTF de las No cON Name 2013, organizado por el Security Staff de Facebook.

La verdad, estuvo genial organizado y realmente nos divertimos mucho. ¡Felicidades Facebook & NcN staff! Enhorabuena también a los otros participantes, y en especial a los ganadores 🙂

En el concurso se publicaron un conjunto de servidores, cada uno de ellos correspondiente con un país.

Cada servidor/país podía ser una base o un flag:

  • Las bases contenían retos que podían ser resueltos una vez por cada equipo.
  • Los flags contenían algún tipo de vulnerabilidad que permitía a los equipos capturarlo (el flag). Los puntos se otorgaban por el tiempo que el equipo tuviese en su poder el flag. Básicamente los flags consistían en escribir en el fichero «/tmp/SCORE_POINTS» del servidor en cuestión, el nombre del equipo.

En este post os vamos a explicar como pudimos capturar el flag de China! 🙂

Al realizar un escaneo de puertos contra el servidor, éste presentaba abiertos los puertos 80 y 12345. El segundo era el utilizado por la organización para monitorizar los flags, así que procedimos a acceder al puerto 80.

Imagen

Y esto era lo que nos encontramos. Lo primero que hicimos, tirar de google translate para traducir el texto en chino. Básicamente era un formulario de login. Al probar a autenticarse, daba un error en el usuario/contraseña, y las pruebas de inyección realizadas no dieron resultado.

Por lo que procedimos a examinar con más detalle las peticiones y respuestas intercambiadas. Para ir al grano, el punto interesante era el proceso de autenticación:

Imagen

Y ¿qué era lo interesante de este caso?

  1. Un token AntiCSRF en el formulario de autenticación nos pareció un poco raro. Este tipo de tokens se utiliza en formularios que permiten hacer algún tipo de modificación
  2. El token AntiCSRF, tiene pinta de algo codificado (desde luego no de un hash)

Procedimos a su decodificación:

Imagen

Lo que nos indicaba que era un archivo binario, nos revelaba un path con un archivo .py, nos mostraba un hash y la palabra secret. Volcamos el contenido en un fichero y ejecutamos el comando file:

python 2.7 byte-compiled

Por lo que procedimos a decompilarlo:

Imagen

Básicamente lo que contenía era una asignación de una variable, siendo el path un metadato del fichero. Por lo que construimos nuestro propio python:

Imagen

Generamos un fichero byte-compiled (.pyc):

Captura de pantalla 2013-11-07 a la(s) 00.19.04

Lo codificamos en base64:

Captura de pantalla 2013-11-07 a la(s) 00.24.28

Y finalmente hacemos tampering de la petición cambiando el token AntiCSRF por el que hemos generado nosotros, y voilà! habíamos capturado la bandera de China 🙂

Esperamos poder subir en los próximos días alguna solución más 🙂

Un saludo!