Cheese
Reproducing a lockdown browser's integrity headers from its config file alone, without ever installing it.
NNS{5aY_cHe353_4Nd_sm113_F0R_7H3_l0CkdoWN_bRoWs3R_anD_h0P3_tHat_yoU_6e7_7he_CorReC7_4n5w3R_50meHow} Challenge
The instance serves a small page:
This exam runs in Safe Exam Browser only.
Launch exam -> sebs://
Safe Exam Browser (SEB) is a real lockdown/kiosk browser used for online
exams. GET /exam returns 403 {“detail”:“Safe Exam Browser required”}
unless the request looks like it came from a real, correctly configured SEB
client. Rather than installing SEB (a kiosk app that takes over the screen -
exactly what the challenge note warns against) and going through a real
exam session, the goal is to “cheese” it: replicate SEB’s client-side
integrity headers by hand from the config file alone.
Step 1: decrypt exam.seb
exam.seb is:
outer gzip
-> “pswd” + RNCryptor-like encrypted blob (password-based)
-> AES-256-CBC plaintext, itself gzip-compressed
-> Apple XML plist (the actual SEB settings)
Cloned the open-source SEB Windows client
(github.com/SafeExamBrowser/seb-win-refactoring) to get the exact byte
layout and crypto parameters from
SafeExamBrowser.Configuration/Cryptography/PasswordEncryption.cs:
version(1) | options(1) | encryptionSalt(8) | authSalt(8) | IV(16) | ciphertext | HMAC-SHA256(32)
encryptionKey = PBKDF2-HMAC-SHA1(password, encryptionSalt, 10000 iters, 32 bytes) authKey = PBKDF2-HMAC-SHA1(password, authSalt, 10000 iters, 32 bytes) HMAC = HMAC-SHA256(authKey, header||ciphertext) (must match trailing 32 bytes) plaintext = AES-256-CBC-decrypt(ciphertext, encryptionKey, IV), PKCS7-unpadded
The key gotcha: .NET’s Rfc2898DeriveBytes(password, salt, iterations)
3-argument constructor defaults to HMAC-SHA1 as its PRF (not SHA-256 as
generic RNCryptor docs suggest) - using SHA-256 for the KDF made the HMAC
check fail every time. Switching to SHA1 for both PBKDF2 calls made the
computed HMAC match immediately, and AES-CBC decryption then yields a
gzip-compressed plist that decompresses to the full SEB configuration
(~95 settings), including startURL = https://.../exam and
sendBrowserExamKey = true.
Step 2: compute the SEB “Config Key” header
SEB authenticates requests to the exam URL with a header computed purely from the (decrypted) configuration file and the request URL - no secret beyond the config itself is required:
ConfigurationKey = SHA256( canonical_json(full_raw_settings_dict) ) -> hex
ConfigKeyHash = SHA256( url_without_fragment + ConfigurationKey ) -> hex
header: X-SafeExamBrowser-ConfigKeyHash:
The “canonical JSON” serializer is intentionally idiosyncratic (from
SafeExamBrowser.Configuration/ConfigurationData/Json.cs), not standard
JSON:
- dict keys sorted by .NET
StringComparer.InvariantCulture(approximated in Python withsorted(keys, key=lambda s: (s.lower(), s))- critical, since a naive ordinal/codepoint sort puts all-uppercase keys like “URLFilterEnable” before any lowercase key, which is wrong; culture-aware sort interleaves by letter, e.g. “URLFilterEnable” sorts next to “urlFilterRegex”, not at the very top) - the “originatorVersion” key is dropped
- empty
<dict>values are dropped (empty arrays are kept) - no whitespace, no character escaping at all (not valid JSON if a string contains a quote - doesn’t matter here)
- bool -> “true”/“false” lowercase, byte data -> base64, no trailing comma
Re-implemented this exactly in Python using plistlib (which preserves
int/real/date/data distinctions from the plist) to parse exam.plist, then
serialized with the matching rules and hashed with SHA-256 twice as above.
Step 3: request the flag
Sent a plain curl/HTTP GET to /exam with just the computed
X-SafeExamBrowser-ConfigKeyHash header (no SEB installation, no
X-SafeExamBrowser-RequestHash/Browser-Exam-Key needed - the server only
checks the Config Key hash) and got back HTML containing the flag directly
in an inline <script> block (a client-side JS string comparison “exam
question” that never actually needed answering - the flag was already
sitting in the page source once past the SEB gate).
Flag
NNS{5aY_cHe353_4Nd_sm113_F0R_7H3_l0CkdoWN_bRoWs3R_anD_h0P3_tHat_yoU_6e7_7he_CorReC7_4n5w3R_50meHow}