picoCTF 2019 - JS Kiddie writeup

@radekk
@radekk
Oct 24 · 6 min read

The writeup for Script Kiddie 1 and the Script Kiddie 2 challenges

Photo by Kevin Horvat on Unsplash

The picoCTF 2019 had a bunch of engaging challenges. I want to give you a brief overview on how I solved two of the web challenges —JS Kiddie 1 (400 points) and JS Kiddie 2 (450 points). What you might find interesting is that you could solve two challenges with the same code.

This challenge seemed to be a problematic one since not as many people solved it as did for other problems. In comparison, the easiest one was solved by over 21k people, whereas ~570 and 460 players solved JS Kiddie 1 and 2. It gives us less than 3% of all players that solved it.

Java Script Kiddie 1

The challenge web site contains two elements — the text input and the button. The source code of this page revealed that we were expected to provide the key to decrypt an image.

If you provide the invalid key, it will result in a corrupted image that will not render correctly in the browser. Whenever we give the valid key, we should see a correct PNG image.

Understanding the source code

To solve this challenge, you need to understand the source code first and find a way to either brute-force a key or build it up based on available data.

JS Kiddie 1 challenge — source code
Raw response from the “/bytes” endpoint
  • Lines 5–8: It sends a query to /bytes endpoint which returns numbers separated with space character. Then it’s being split up to create an array. The bytes variable stores bytes representing a (malformed) PNG image that you need to decrypt.
  • Lines 10–29: The assemble_png(u_in) function takes a single argument which is the key (16 characters long) and then decrypts bytes array to restore an image and renders it on the page.
  • Lines 18–23: The first loop iterates over every character from the key (left to right) and is setting up so called shiftervalue. The key.charCodeAt(i) — 48 code converts string into integer, i.e. "5" would be 53decimal as defined in the ASCII Table, 53 — 48 = 5. It’s an old trick that was common in the assembly language. As a result it would convert string “123” into an array of [1 2 3] instead of [49 50 51]characters.
  • Lines 20–22: The second loop is decrypting an image by splitting up bytesvalues into blocks of 16 bytes (key length). That would result in 720 bytes that gives 45 blocks (720 /16 = 45).
  • Line 21: The output of decrypted image is stored in the result variable.

What do we know so far?

  • The first loop — 16 iterations over columns, the second loop — 45 iterations over rows
  • The total numbers of combinations for the 16 bytes key is 16¹⁰ — which is not really feasible for the brute-force attack

To better understand and visualize the decryption process I created the visualization that should help you understand how bytes are decrypted (in this challenge the decryption process is based on re-ordering by adding the “shifter” value to a row number — different for each column). The first number in the key is our shifter value for the first column, then it takes the second number for the second column and so on.

The decryption process for the key of “4020000000000000” — using only first 288 bytes for demo purpose

Reducing the number of combinations

Instead of testing all of the 10¹⁶ combinations which doesn’t make much sense, I concentrated on decreasing the number of keys as much as possible. That would help to easily run through the defined set of combinations instead of blindly guessing they key.

The PNG file format (first 8 bytes)

One of the most important clues was hidden in the data:image/png fragment. It describes the PNG image where single bytes were encoded with base64.

document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));

The first 8 bytes of the PNG file header were already known for this file format and it was: 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A (the PNG file header bytes — constant).

Guessing the next 8 bytes

The all provided bytes could be verified by using hexdump command in the terminal. Here is the example of printing out the PNG file bytes:

$ hexdump -C test.png | head -n 2
00000000 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52
00000010 00 00 0c 14 00 00 08 de 08 06 00 00 00 04 0f fc

It seems that the rest of missing bytes consists of 0x00 0x00 0x00 0x0D 0x49 0x48 0x44 0x52. I wrote the bash one-liner to verify if those bytes are actually constant or at least the most common ones for the PNG file format. It scans through 100 different PNG files and extracts the first 16 bytes.

$ for i in `seq 0 15`; do _png_byte=$(for file in *.png; do dd if="$file" skip=$i bs=1 count=1 2> /dev/null | hexdump -C | head -n 1 | awk '{print $2}'; done;); echo -n "Position [${i}]: "; echo -n "${_png_byte}" | sort | uniq -c | sort -rn | awk '{print "0x"$2}' | head -n 1; done;
Position [0]: 0x89
Position [1]: 0x50
Position [2]: 0x4e
Position [3]: 0x47
Position [4]: 0x0d
Position [5]: 0x0a
Position [6]: 0x1a
Position [7]: 0x0a
Position [8]: 0x00
Position [9]: 0x00
Position [10]: 0x00
Position [11]: 0x0d
Position [12]: 0x49
Position [13]: 0x48
Position [14]: 0x44
Position [15]: 0x52

What is next after decrypting the first 16 bytes of PNG file?

The next step is to get each byte from the PNG file header (16 bytes) and locate it inside of the bytes variable. That would provide the shifter value for each byte. As a result you should’ve 16 shifter values which equals to the decryption key. The biggest challenge here is that there might be multiple positions for the same byte — it requires to test all of them if there is more than one.

Brute-forcing bytes with the multiple valid positions

I wrote the brute-force script to find and test valid positions for each byte from the PNG header. It occurs there are 4 bytes at offsets 2, 8, 9 and 10 where there is more than one valid position for a byte.

More than one valid block for: 78 at 2 with count of 2 
More than one valid block for: 0 at 8 with count of 2
More than one valid block for: 0 at 9 with count of 3
More than one valid block for: 0 at 10 with count of 2

Instead of manually testing for these values you can easily solve it by calculating the Cartesian product to get all possible keys.

I ended up reducing the number of combinations from 10¹⁶ to only 24 possible keys and here is the result:

Images generated by brute-force script, only one valid PNG file (QR code) — 24 images

To decode the QR Code image you can use online qr-decoder.

key: 4549618526012495
flag: picoCTF{cfbdafe5a65de4f32cce2e81e8c14a39}
The result of visualization when using the valid key for decryption process

You can find the source code of my brute-force script here:

The brute-force script — source code

JavaScript Kiddie 2

The JS Kiddie 2 challenge doesn’t differ much in regards to the first one. The same brute-force script will solve it. There are two major changes to the code of the second challenge:

The length of key variable is not 16 but 32 characters:

var key = "00000000000000000000000000000000";

The shifter value formula is different than in the first one:

shifter = Number(key.slice((i*2),(i*2)+1));

What it does it uses every second byte from the key as actual key. It means that even though we have 32 characters, only 16 are being used.

Images generated by brute-force script, only one valid PNG file (QR code) —9 images
key: 3738193605318569
flag:
picoCTF{3aa9bd64cb6883210ee0224baec2cbb4}

Thank you for reading and I hope you enjoyed it. Let me know in the comments if you have any questions or topics you would like me to write about.

Don’t forget to follow me on Twitter: https://twitter.com/radekk

Thanks!

@radekk

Written by

@radekk

Application Security Engineer, Pentester and Bug Bounty hunter. I love solving puzzles and DIY projects. My Twitter handle is: @radekk

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade