Skip to main content

Java Script Kiddie picoCTF 2019 Solution

Reverse JavaScript that decodes a scrambled image and find the key that restores it correctly.

Published: April 2, 2026Updated: August 25, 2026

Description

The image is formed by combining two hex strings. Solve the key to reveal the image.

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1View the page source
    Observation
    The challenge is called 'Java Script Kiddie' and takes a key on a web page. So the scrambling logic and the hardcoded byte data are both sitting in the client-side JavaScript.
    Open the challenge URL and view page source (Ctrl+U). Find the JavaScript that combines a user-supplied key with a hardcoded hex string to produce the bytes of a PNG image.
    Learn more

    The JavaScript performs cyclic block shifting: for each digit position i in the 16-character key, the digit value is used as a row-offset when reassembling PNG bytes from a scrambled byte array fetched from /bytes. The formula is: result[(j*LEN)+i] = bytes[(((j+shifter)*LEN) % bytes.length)+i]. No XOR is involved.

    A PNG always starts with the magic bytes 89 50 4E 47 0D 0A 1A 0A, immediately followed by the first chunk header 00 00 00 0D 49 48 44 52 (length 13 plus IHDR). That is 16 known bytes, one for each of the 16 columns, which is exactly enough known plaintext to pin every key digit.

  2. Step 2Extract the hardcoded hex string
    Observation
    The JavaScript pulls a hex string from /bytes and uses it as the raw material for reconstruction. Copying it locally lets the brute force run offline, with no repeated network requests.
    Copy the hardcoded hex string from the JavaScript source. Each pair of hex digits is one byte of the scrambled PNG byte array.
    Learn more

    The key is 16 digits long (one digit per column of the output). Each of the 16 key digit positions can be solved independently: for each position i, try all 10 digit values (0-9) and pick the one that places the correct byte of the known 16-byte PNG header at position i of the output.

  3. Step 3Brute-force or derive the key
    Observation
    The JS formula maps each key digit independently onto one column of the output, and every PNG starts with the same 16 bytes: the 8-byte magic 89 50 4E 47 0D 0A 1A 0A followed by 00 00 00 0D 49 48 44 52, the IHDR chunk header. Those known bytes act as an oracle, so each of the 16 digit positions can be solved on its own, 10 candidates apiece.
    Write a script to try all possible key values (0-9 for each digit position). For each candidate digit at position i, apply the cyclic block-shift formula and check if the resulting byte matches the expected byte of the 16-byte PNG header at that position. Display the valid image once all 16 digits are found.
    python
    python3 << 'EOF'
    hex_str = 'PASTE_HEX_HERE'
    scrambled = bytes.fromhex(hex_str)
    
    # PNG magic plus the first chunk header (length 0x0d + 'IHDR'):
    # 16 known bytes, exactly one per column of the 16-wide layout.
    known = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
                   0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52])
    
    LEN = 16  # key length from JS source
    key = []
    for i in range(LEN):
        for shifter in range(10):
            # JS formula with j = 0: result[i] = bytes[(((0+shifter)*LEN) % bytes.length)+i]
            if scrambled[((shifter * LEN) % len(scrambled)) + i] == known[i]:
                key.append(str(shifter))
                break
    print('Key:', ''.join(key))
    EOF
    What didn't work first

    Tried: Treating the scrambling as XOR and trying to XOR the hex string against the PNG magic bytes to recover the key.

    The JavaScript does cyclic block-shifting (row reordering), not XOR. XOR-ing the scrambled bytes against the header bytes produces nonsense offsets. The correct approach reads the formula result[(j*LEN)+i] = bytes[(((j+shifter)*LEN) % bytes.length)+i] and interprets shifter as a row index, not a bitmask.

    Tried: Brute-forcing all 16 digits together with a nested loop over 10^16 combinations.

    10^16 iterations would take millions of years. The formula maps each key digit independently to exactly one column of the output, so each position i can be solved in isolation with at most 10 candidates, collapsing the search to 160 checks total.

    Learn more

    Because each digit controls one column of the output independently, you can brute-force each of the 16 positions separately (10 candidates each) instead of trying all 10^16 combinations. Check whether the byte at the target column matches the expected PNG header byte for that position.

  4. Step 4Enter the key and view the image
    Observation
    The page has a key input that triggers the rendering client-side. Submitting the recovered key there makes the JavaScript reconstruct the PNG bytes and display the flag image in the browser.
    Submit the discovered key in the input field on the challenge page. The image rendered will contain the flag.
    Learn more

    Alternatively, use Python Pillow or the browser's Canvas API to render the resulting bytes as an image and save it as a PNG file for reading.

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{...}

Brute-force the 16-digit key by solving each column independently: for each position, find the digit that places the correct byte of the 16-byte PNG header (magic plus IHDR chunk header) at that column via the cyclic block-shift formula.

Key takeaway

Any scrambling scheme written in client-side JavaScript is transparent, because the source is fully readable. When the output format has a known fixed header like PNG magic bytes, those bytes act as a known-plaintext oracle, letting you recover each key component on its own instead of searching the whole key space. That divide-and-conquer approach collapses the search exponentially, and it works anywhere key material maps independently onto disjoint sections of output.

Related reading

Useful tools for Web Exploitation

Where to go next