Java Script Kiddie 2 picoCTF 2019 Solution

Published: April 2, 2026

Description

A harder key-based image challenge. The key setup is more complex than part 1.

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    View and analyze the JavaScript
    Observation
    I noticed the challenge is client-side and explicitly references a 'key-based' scheme, which meant all scrambling logic would be visible in the page source and understanding the key-to-column-shift mapping was the essential first step before writing any solver.
    View page source on the challenge. The JavaScript has a more complex key combination scheme than part 1. Carefully read how each pair of key digits maps to a row-shift amount for one of the 16 columns of ciphertext.
    Learn more

    In part 2, the key is 32 characters (16 two-digit values), each two-digit value specifying how many rows to shift in the corresponding column via modulo arithmetic. Read the JS logic precisely before writing your solver.

    Tools like Chrome DevTools' Sources panel let you set breakpoints in the JavaScript and step through it, making it easier to understand the key-to-column-shift mapping.

  2. Step 2
    Extract and replicate the column-rotation logic
    Observation
    I noticed the JS encodes each of the 16 column shifters as a two-digit decimal pair, giving a brute-force space that is far smaller than exhaustive search if I use the fixed 8-byte PNG magic header as a known-plaintext oracle to gate each candidate key.
    Transcribe the JavaScript column-rotation logic into Python. For each of the 16 columns i, the key provides a two-digit shifter that determines which source row feeds each output row: result[(j * 16) + i] = bytes[(((j + shifter) % num_blocks) * 16) + i]. The output must start with valid PNG magic bytes (89 50 4E 47 0D 0A 1A 0A).
    python
    python3 << 'EOF'
    hex_str = 'PASTE_HEX_FROM_JS'
    ciphertext = bytes.fromhex(hex_str)
    png_magic = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
    
    # Replicate JS key logic here
    # Then brute-force key and check bytes
    EOF
    What didn't work first

    Tried: Reuse the part 1 solver directly by treating the key as 16 single-digit values instead of 16 two-digit pairs.

    Part 2 encodes each shifter as two decimal digits, giving a per-column modulus range of 0-99 rather than 0-9. A part-1-style solver iterates only 10^16 single-digit keys and maps each digit directly to a shift, so the column-rotation formula is wrong from the start and no candidate will ever reproduce the PNG magic bytes.

    Tried: Check only the first two bytes (0x89 0x50) of the decoded output as the PNG oracle instead of all 8 magic bytes.

    The 2-byte oracle is far too loose - many wrong keys will produce those two bytes by coincidence, flooding the brute-force with false positives. Checking all 8 PNG magic bytes (89 50 4E 47 0D 0A 1A 0A) is still fast and reduces false positives to essentially zero, making the correct key unambiguous.

    Learn more

    If the key is longer or has more combinations to test, consider constraining the search by checking all PNG structural requirements: magic bytes at offset 0, IHDR chunk at offset 8, and valid chunk CRCs. Each additional constraint dramatically reduces the valid key space.

  3. Step 3
    Render the decoded image
    Observation
    I noticed that once a candidate key produced the correct PNG magic bytes, the fully unscrambled output would form a valid image file, and the challenge's QR-code format meant a scanner tool like zbarimg was needed rather than visual inspection to extract the flag string.
    Once the correct key is found, write the rotated/decoded bytes to a .png file and open it to read the flag. The image is a QR code. Scan it with a QR reader (e.g., zbarimg flag.png) to extract the flag string.
    python
    python3 -c "
    with open('flag.png', 'wb') as f:
        f.write(decoded_bytes)
    "
    bash
    xdg-open flag.png
    What didn't work first

    Tried: Read the flag directly from the rendered PNG by looking at the image visually instead of scanning it as a QR code.

    The decoded image is a QR code, not a human-readable text image, so there is no plaintext flag to read visually. The flag string is encoded in the QR symbol and must be decoded with a tool like zbarimg or a phone QR scanner - opening and inspecting the image manually will only show you a black-and-white grid.

    Tried: Run zbarimg on the still-scrambled ciphertext hex dump written directly to a .png file before finding the correct key.

    The ciphertext bytes are not a valid PNG and do not form a recognizable QR pattern, so zbarimg will exit with 'scanned 0 barcode symbols from 1 image'. The column-rotation must be fully reversed with the correct key before writing the file - zbarimg has no ability to undo the scrambling.

    Learn more

    If the image does not open, verify the bytes are correct by running pngcheck flag.png or examining the first bytes with xxd flag.png | head. A valid PNG must have correct chunk lengths and CRC32 values throughout.

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

Replicate the JS column-rotation logic in Python, brute-force the 16 two-digit shift values using PNG magic bytes as an oracle, and render the image (which is a QR code containing the flag).

Key takeaway

Client-side cryptography is inherently transparent because the scrambling algorithm and any hardcoded data are fully visible to the attacker. When a file format imposes a fixed magic-byte header, those known bytes serve as a crisp oracle: each candidate key either reproduces the expected bytes or it does not. This known-plaintext oracle pattern appears in many real-world attacks, including WEP keystream recovery, CBC padding-oracle attacks, and any situation where predictable plaintext structure leaks information about the key.

Related reading

Want more picoCTF 2019 writeups?

Useful tools for Web Exploitation

What to try next