The Add/On Trap picoCTF 2026 Solution

Published: March 20, 2026

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.

bash
unzip -P picoctf suspicious.zip
bash
ls -la

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Extract the extension
    Observation
    I 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 -p individual 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:
    bash
    unzip -P picoctf suspicious.zip -d suspicious/
    bash
    ls -la suspicious/
    bash
    # Quick triage without extracting (handy on huge archives):
    bash
    unzip -P picoctf -p suspicious.zip manifest.json | python3 -m json.tool
    What didn't work first

    Tried: Run unzip suspicious.zip without 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.zip and then binwalk suspicious.zip looking 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 dd gives 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 .zip already. If you ever need to peel the CRX header off, the data starts at the offset where the bytes PK first appear (standard ZIP magic).

    Extract vs grep-the-archive. unzip -p archive.zip path/to/file streams 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.

  2. Step 2
    Inspect manifest.json
    Observation
    I 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.
    python
    python3 -m json.tool suspicious/manifest.json
    bash
    grep -iE 'picoCTF|flag|secret' suspicious/manifest.json
    What 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.

  3. Step 3
    Search all JS files for the flag and encoded data
    Observation
    I 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:
    bash
    grep -rn 'picoCTF' suspicious/
    bash
    # Base64 candidates: 20+ alphabet chars, optional = padding:
    bash
    grep -rnE '[A-Za-z0-9+/]{20,}={0,2}' suspicious/ --include='*.js'
    bash
    # String.fromCharCode arrays: bracket + comma-separated char codes:
    bash
    grep -rnE '\[\d{2,3}(,\d{2,3})+\]' suspicious/ --include='*.js'
    bash
    # Hex escape sequences inside JS string literals:
    bash
    grep -rnE '\\x[0-9a-f]{2}' suspicious/ --include='*.js'
    bash
    # Exfiltration sinks - the encoded payload is built nearby:
    bash
    grep -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.zip on the zip archive itself to find the flag without extracting.

    The strings output 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 to picoCTF{hello}.
    • Charcode arrays: \[\d{2,3}(,\d{2,3})+\]. Matches [112,105,99,111,67,84,70,123], which decodes to picoCTF{.
    • 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 for picoCTF{ 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.

  4. Step 4
    Decode obfuscated strings
    Observation
    I 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:
    bash
    echo '<base64_string>' | base64 -d
    bash
    # Decode hex array:
    python
    python3 -c "print(bytes.fromhex('<hex_string>').decode())"
    bash
    # Decode charcode array:
    python
    python3 -c "print(''.join(chr(c) for c in [<codes>]))"
    bash
    # Decode XOR array with key:
    python
    python3 -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 -d without 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 with bytes.fromhex(). String.fromCharCode arrays are JavaScript's native way to build strings from character codes - the Python equivalent is chr().

    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-XOR

    Search 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.

Key takeaway

Browser extensions run with elevated browser privileges and can intercept requests, read cookies, and inject scripts into every page the user visits. Permissions declared in manifest.json are the attack surface: a combination of 'all_urls', 'cookies', and 'webRequestBlocking' gives an extension everything it needs to silently exfiltrate session tokens from any website. Malicious extensions routinely obfuscate payloads using base64, charcode arrays, or XOR to evade static text search, so effective analysis means recognizing those encoding signatures and reversing them systematically.

Related reading

Want more picoCTF 2026 writeups?

Useful tools for Reverse Engineering

What to try next