Censored Country Walkthrough — Smuggling Past a JA4 TLS Fingerprint Firewall
Platform: CyberTalents · Category: Web/Crypto · Difficulty: Hard
Goal: Get past a zero-trust gateway that only accepts one exact TLS ClientHello fingerprint.
Challenge
“Behind the Great Firewall…” — a gateway that TLS-fingerprints every client with JA4 and only lets one mysterious fingerprint through. The public instance is 54.153.42.171 (only ports 22 and 443 open).
Recon: the server leaks its own secret
The first HTTPS request — with any normal fingerprint — gets a 403 Forbidden. But look at the response headers:
X-Expected-JA4: t13i1013h2_646c4f7e2ebe_77cf8e4d6e9f
X-Expected-JA4-R: t13i1013h2_1301,1302,1303,1304,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0015,0023,002b,002d,0033,4469,ff01_0403,0804
That’s the entire recipe, served on a platter. JA4-R is the raw fingerprint:
- TLS 1.3 (
t13) - no SNI — the
iint13i...means the ClientHello targets an IP with no SNI extension - 10 cipher suites
- 13 extensions
- ALPN
h2 - signature algorithms
0403, 0804
Understanding JA4
JA4 is a TLS client fingerprint. The full JA4 string is:
t13i1013h2_<b>_<c>
where the two hashed components are computed as:
b = sha256(ciphers sorted, comma-delimited hex)[:12]
c = sha256(extensions sorted (SNI/ALPN excluded), comma-delimited hex + "_" + sigalgs in order)[:12]
We verified both hashes against the leak: 646c4f7e2ebe and 77cf8e4d6e9f reproduce exactly. So the task is: send a ClientHello that produces this exact JA4.
The forge: uTLS
Normal TLS stacks won’t let you craft a ClientHello freely — Go’s crypto/tls and OpenSSL decide extensions for you. uTLS exists precisely to defeat fingerprinting: it lets you build a custom ClientHelloSpec.
The spec must contain, in the right order for hashing (order doesn’t matter for JA4 — it sorts — but it matters for real-world parsing):
- 10 ciphers:
1301,1302,1303,1304,c02b,c02c,c02f,c030,cca8,cca9 - 13 extensions, NO SNI: ALPN
h2(0x0010), status_request (0x0005), supported_groups x25519 (0x000a), ec_point_formats (0x000b), signature_algorithms[0403,0804](0x000d), unknown 0x0012, padding→512 (0x0015), session_ticket (0x0023), supported_versions [1.3] (0x002b), psk_kex_modes dhe (0x002d), key_share x25519 (0x0033), unknown 0x4469, renegotiation_info (0xff01)
The two unknown extension types (0x0012, 0x4469) are emitted with GenericExtension — JA4 only hashes extension types, so their payloads don’t matter.
The pitfall that cost an hour
uTLS’s BoringPaddingStyle omits the padding extension entirely for ClientHellos under 256 bytes. Our first short hello came out with 12 extensions instead of 13 — and the fingerprint didn’t match. The fix:
&utls.UtlsPaddingExtension{GetPaddingLen: utls.AlwaysPadToLen(512)}
Force the hello to exactly 512 bytes so the padding extension (0x0015) is always present.
Result
handshake OK, proto: h2
HTTP/1.0 200 OK
FLAG{h4ndcr4ft3d_h3ll0_sl1ps_thr0ugh}
Why this worked (the real lesson)
- TLS fingerprints are not authentication. JA4 (and its predecessor JA3) describe how a client speaks TLS — they can be impersonated with a custom stack like uTLS. Using a fingerprint as an access-control gate is security by obscurity with a public spec.
- The server leaked the expected fingerprint in an error header. That turned the challenge into a hash-reproduction puzzle — fun, but a reminder that verbose 403s are free reconnaissance.
- The padding pitfall is generic: fingerprint-based rules that depend on extension presence are sensitive to ClientHello length heuristics in the forging library.
Defense takeaways:
- Never use client fingerprints alone for authorization; pair with real credentials or mTLS.
- Don’t echo expected fingerprint values in error responses.
- If you must fingerprint, treat it as one low-trust signal among many.
Files
forge/main.go— the uTLS forger (working exploit)ja4calc.py— pcap ClientHello parser / JA4 verifier
Writeup of a solved CyberTalents web/crypto challenge. All attack surface described here is part of the challenge’s intended vulnerable application.