Description
What kind of information can an Add/On reach? Is it possible to exfiltrate them without you noticing? Download the browser extension suspicious.zip (password: picoctf) and inspect it to uncover the hidden flag.
Download suspicious.zip and extract it using the password 'picoctf'.
Inspect all the extension files for hidden data.
unzip -P picoctf suspicious.zipls -laSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Extract the extensionObservationI noticed the challenge provided a password-protected ZIP containing a browser extension, which meant extraction was the mandatory first step before any file-level analysis was possible; having the full directory tree on disk lets you inspect the manifest, trace script imports, and run grep across all files at once.Extract first. You *can*unzip -pindividual files for grep-only triage, but for an unknown extension you want all files on disk so you can inspect the manifest, follow imports, and see the full directory layout.bash# Extract everything to a working directory:bashunzip -P picoctf suspicious.zip -d suspicious/bashls -la suspicious/bash# Quick triage without extracting (handy on huge archives):bashunzip -P picoctf -p suspicious.zip manifest.json | python3 -m json.toolWhat didn't work first
Tried: Run
unzip suspicious.zipwithout the -P flag to extract without a password.unzip rejects the archive with 'unsupported compression method' or prompts for a password and then fails, producing zero output files. The archive was encrypted with AES-256 using 'picoctf' as the password; you must pass -P picoctf for extraction to succeed (omitting -P causes unzip to prompt interactively for the password, but that prompt will still fail unless you type 'picoctf').
Tried: Try
file suspicious.zipand thenbinwalk suspicious.ziplooking for hidden nested archives before extracting.binwalk reports the outer ZIP and any embedded ZIPs inside it, but none of the embedded entries are a standalone archive - they are just the normal compressed members. Following binwalk's offsets and carving raw bytes with
ddgives you corrupted partial files instead of the actual extension directory tree. Extract with unzip so file paths and directory structure are preserved intact.Learn more
Extension formats are all ZIP archives under the hood. Chrome
.crx= ZIP + a small CRX3 signature header prepended; Firefox.xpi= ZIP, no header; this challenge ships a plain.zipalready. If you ever need to peel the CRX header off, the data starts at the offset where the bytesPKfirst appear (standard ZIP magic).Extract vs grep-the-archive.
unzip -p archive.zip path/to/filestreams a single file to stdout without writing anything to disk - good for one-shot extraction during triage. For real analysis (manifest review, cross-file string search, deobfuscation), extract everything: tools and editors work better against a real directory tree.Every extension contains:
manifest.json(configuration + permissions), background scripts / service workers, content scripts (injected into web pages), and optional popup HTML/CSS/JS. Malicious extensions slip past the Chrome Web Store / Firefox AMO review by being benign on submission and pulling malicious payloads later, or by hiding behavior behind obfuscation.Step 2
Inspect manifest.jsonObservationI noticed that every browser extension must have a manifest.json declaring permissions and all entry-point scripts, which suggested reading it first to map the full attack surface and identify which JS files run in which context before digging into code.Read the manifest. It tells you exactly what the extension can do, which JS files run in which context, and where the flag is most likely embedded.pythonpython3 -m json.tool suspicious/manifest.jsonbashgrep -iE 'picoCTF|flag|secret' suspicious/manifest.jsonWhat didn't work first
Tried: Install the extension directly in Chrome (via chrome://extensions/ in Developer Mode) and look for the flag in the DevTools console or network tab at runtime.
Installing an unknown malicious extension in your real browser exposes your actual session cookies and browsing history to any exfiltration logic in the extension. Even sandboxed, the extension may phone home to an external server. Static file inspection is always safer and sufficient for CTF challenges - the flag is embedded in the JS source, not produced at runtime.
Tried: Search only manifest.json for the flag string, since the challenge says to 'inspect the extension'.
manifest.json is a configuration file listing permissions and entry points; it does not contain code or embedded data beyond metadata. The grep returns no match. The flag is hidden inside one of the JavaScript files referenced by manifest.json, so you must follow the 'background_scripts' or 'content_scripts' entries to the actual JS files.
Learn more
What each suspicious permission actually grants:
"<all_urls>"- the extension can inject content scripts into every website you visit and make cross-origin HTTP requests to any server. Effectively converts the extension into a universal man-in-the-browser."webRequestBlocking"- lets the extension synchronously intercept, modify, redirect, or cancel any outgoing HTTP request before it leaves the browser. Adblockers use this; so do credential-stealing exfiltration scripts."cookies"- read and write cookies for any domain (paired with host permissions). Game over for session cookies on every site you're logged into."nativeMessaging"- communicate with a co-installed native binary outside the browser sandbox. Real malware uses this to hand control to a persistence-installing helper."tabs"- read URLs and titles of every open tab."storage"- on its own, benign. Combined with the above, it's where exfiltrated data is staged.
A legitimate extension needs a small subset of these. Asking for several of them at once - particularly
<all_urls>+cookies+webRequestBlocking- is the classic credential-stealer permission stack.Step 3
Search all JS files for the flag and encoded dataObservationI noticed that after inspecting the manifest the flag was not in plaintext there, which suggested it was hidden inside the JavaScript files referenced by background_scripts or content_scripts; a layered grep sweep covering the raw picoCTF prefix and common obfuscation signatures (base64 blobs, charcode arrays, hex escapes, exfiltration sinks) was the systematic way to locate it.Grep raw first (cheap, often just works), then sweep for each obfuscation form by signature. Also flag any exfiltration call (fetch/XHR/sendBeacon) - those point at where the encoded payload is built.bash# Raw plaintext first - sometimes the flag is just sitting there:bashgrep -rn 'picoCTF' suspicious/bash# Base64 candidates: 20+ alphabet chars, optional = padding:bashgrep -rnE '[A-Za-z0-9+/]{20,}={0,2}' suspicious/ --include='*.js'bash# String.fromCharCode arrays: bracket + comma-separated char codes:bashgrep -rnE '\[\d{2,3}(,\d{2,3})+\]' suspicious/ --include='*.js'bash# Hex escape sequences inside JS string literals:bashgrep -rnE '\\x[0-9a-f]{2}' suspicious/ --include='*.js'bash# Exfiltration sinks - the encoded payload is built nearby:bashgrep -rnE '(fetch|XMLHttpRequest|sendBeacon|navigator\.sendBeacon)' suspicious/ --include='*.js'Expected output
suspicious/background.js:47: const flag = "picoCTF{4dd_0n_tr4p_...}";What didn't work first
Tried: Run
strings suspicious.zipon the zip archive itself to find the flag without extracting.The
stringsoutput from a compressed zip is mostly garbage bytes from deflate-compressed data; compressed content is not printable ASCII. Even uncompressed stored entries appear in raw form without file-boundary context, so you cannot tell which file a hit belongs to. Extracting and then grepping the real JS files gives accurate per-file line numbers and readable context.Tried: Limit the base64 grep to the manifest.json and popup HTML files, skipping background and content scripts.
The flag is embedded in the background service worker or content script JS, not in the manifest or popup HTML. Those files are where extension logic lives. Skipping
--include='*.js'or limiting to non-JS files means the grep never touches the file that actually contains the encoded flag.Learn more
Concrete signatures by obfuscation type:
- Base64:
[A-Za-z0-9+/]{20,}={0,2}. Real base64 strings of meaningful length plus optional=padding.cGljb0NURntoZWxsb30=decodes topicoCTF{hello}. - Charcode arrays:
\[\d{2,3}(,\d{2,3})+\]. Matches[112,105,99,111,67,84,70,123], which decodes topicoCTF{. - Hex escapes:
\\x[0-9a-f]{2}inside JS string literals."\x70\x69\x63\x6f"=pico. - Unicode escapes:
\\u[0-9a-f]{4}. Same idea, longer form:"pico"=pico.
Decoder strategy: try cheapest first. Pipe each candidate through base64 decode; print outputs that contain printable ASCII. Fall back to charcode-array decode (Python:
chr()mapped over the list). Then hex/unicode escapes (codecs.decode(s, 'unicode_escape')). Print all candidate decodes; eyeball forpicoCTF{or anything that reads as English.Exfiltration sinks are useful as triangulation. The flag is rarely a static string; it's usually built into the data the extension would send out. Find the
fetch()/XMLHttpRequest/navigator.sendBeacon()call, walk back through the variables that build its body argument, and the encoded form is right there. See the CTF Encodings guide for the full decoder ladder, and the Web Challenges guide for adjacent extension-and-DOM patterns.- Base64:
Step 4
Decode obfuscated stringsObservationI noticed that the grep sweep returned encoded-looking candidates (base64 blobs, charcode arrays, or hex escape sequences) instead of a plaintext flag, which suggested the extension author had obfuscated the payload and that reversing the cheapest encoding first (base64, then charcode, then hex/XOR) would recover the hidden flag.Decode any encoded strings found. Common techniques in malicious extensions: base64, hex escape sequences, charcode arrays, and XOR with a hardcoded key.bash# Decode base64:bashecho '<base64_string>' | base64 -dbash# Decode hex array:pythonpython3 -c "print(bytes.fromhex('<hex_string>').decode())"bash# Decode charcode array:pythonpython3 -c "print(''.join(chr(c) for c in [<codes>]))"bash# Decode XOR array with key:pythonpython3 -c "key=b'<key>'; data=[<bytes>]; print(''.join(chr(b^key[i%len(key)]) for i,b in enumerate(data)))"What didn't work first
Tried: Pipe the base64 candidate directly to
base64 -dwithout stripping surrounding quotes or whitespace from the JS source.If you copy the string including the surrounding JS quotes ('...') or any trailing whitespace, base64 -d exits with 'invalid input' or produces garbage bytes. You must extract the raw base64 alphabet characters only (strip delimiters, newlines, and whitespace) before decoding. A safe approach is: grep out the match, pass through tr -d "'\"\n " to strip noise, then pipe to base64 -d.
Tried: Assume the XOR key is a single byte and brute-force 256 values looking for 'picoCTF{' in the output.
Single-byte XOR brute force works only when the key is exactly one byte. If the actual key is a multi-byte string (e.g. 'secretKey42'), every candidate single-byte key produces garbled output because the key cycles at a longer period. Check the script for a nearby const declared as a string - multi-character string keys are much more common in obfuscated extension code. Use the multi-byte XOR one-liner with the full key once you locate it.
Learn more
Each obfuscation technique has a straightforward reversal. Base64 is a reversible encoding that represents binary data as printable ASCII using 64 characters (A-Z, a-z, 0-9, +, /). It expands data by 33% (3 bytes become 4 characters) and is identifiable by its character set and optional
=padding at the end. Hex encoding represents each byte as two hex digits - easily reversed withbytes.fromhex(). String.fromCharCode arrays are JavaScript's native way to build strings from character codes - the Python equivalent ischr().XOR obfuscation with a hardcoded key is trivial to reverse once the key is found. The key is almost always near the data, in one of two layouts:
// Layout 1: key as a sibling const in the same IIFE (function() { const k = "secretKey42"; const enc = [0x13, 0x0c, 0x07, 0x06, ...]; // ... decode loop using k and enc })(); // Layout 2: key string immediately above the encoded blob const KEY = "hunter2"; const PAYLOAD = "AwAEAB0F..." // base64-then-XORSearch for short string constants (5-32 chars) declared near any large numeric array or base64 blob. XOR is symmetric - applying the key again decodes - and rolling XOR cycles the key index modulo the key length. If the key is genuinely missing, frequency analysis on the ciphertext recovers the key length (Kasiski/IC), then per-position single-byte XOR brute force recovers each key byte.
For heavily obfuscated JavaScript, tools like de4js, jsnice.org, or running the code in a controlled Node.js environment (with network calls mocked out) can automatically deobfuscate it. The goal is always to determine what data is being collected, where it is being sent, and what the actual malicious behavior is - which is exactly the process of real malware reverse engineering applied to browser extensions.
Interactive tools
- Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
- Base64 & Base32 DecoderDecode Base64 and Base32 strings with auto-detection. Multi-layer mode unwraps nested encodings automatically.
- Recipe ChainStack decoders into a pipeline: Base64, hex, ROT, XOR, Morse, URL, Atbash, Vigenère, and more. Magic mode auto-discovers the chain. Bookmark the URL to save it.
Flag
Reveal flag
picoCTF{4dd_0n_tr4p_...}
Extract the .zip extension, search all JS files for picoCTF directly, then look for encoded forms: base64 strings, \x hex escapes, String.fromCharCode arrays, or XOR-encoded byte arrays. The flag is hidden in the extension's JavaScript source.