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:

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.

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": {}
}

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”.

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.

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).

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:

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.

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.

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.

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.

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”.

The “ipa” base64-encodes the response and returns it to “stout”.

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

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:

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)