CTF – Java Script Kiddie
Even though I know some Javascript, this 400 point web-exploitation hacking challenge from PicoCTF proved to be a difficult for me. Thankfully with my forensic background I was well aware of important elements of this challenge, such as hex, bytes and file signatures. Saying that, it still took a few hours from starting the Java Script Kiddie CTF challenge to obtaining the challenge key..
Update for Java Script Kidde 2: Jump to the bottom to read about the 2nd CTF challenge in this mini-series.
So with this Java Script Kiddie challenge you’re really not given much, just a landing page with a basic form.
Whatever you enter into the HTML form gets rendered as a broken PNG object. Makes sense that entering in the key returns the flag or a way to obtain it.
Analysis of the code reveals it also performs a GET request to a remote page, for some additional byte value data:
128 252 182 115 177 211 142 252 189 248 130 93 154 0 68 90 131 255 204 170 239 167 18 51 233 43 0 26 210 72 95 120 227 7 195 126 207 174 208 233 156 187 185 65 228 0 137 128 228 71 159 245 1 0 98 131 46 22 94 71 244 22 147 21 83 155 252 243 90 24 59 73 247 223 127 242 183 251 124 28 245 222 199 248 122 204 230 79 219 147 11 225 202 239 24 132 55 89 221 143 151 137 63 150 79 211 8 16 4 60 63 99 65 0 2 ...
Hmm okay. Its then time to break the JS source code down and work out how to backwards engineer this challenge.
The code:
function assemble_png(u_in){
var LEN = 16;
var key = "0000000000000000";
var shifter;
if(u_in.length == LEN){
key = u_in;
}
var result = [];
for(var i = 0; i < LEN; i++){
shifter = key.charCodeAt(i) - 48;
for(var j = 0; j < (bytes.length / LEN); j ++){
result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
}
}
while(result[result.length-1] == 0){
result = result.slice(0,result.length-1);
}
document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
return false;
}
First off, looks like the key its expecting is 16 characters long. Anything other than that and the user input isn’t actually used.
Looping through the key and reducing it by 48 converts characters from ASCII to text.
Two nested loops and an array being accessed like ‘(X * constant) + Y’ screams 2-dimensional array manipulation to me. 2D array, or pixels… which is likely as we’re dealing with images.
If the key is 16 characters long, and presumably can be any printable character… that’s 16 raised to the power of something substantial. Even if the key is just numerical digits, that’s 16^10… which is just over 1 trillion… I think.
Brute forcing the whole key is off the table.
Reverse engineering the output
If brute forcing the key seems unlikely, can we reverse engineer the output?
What do we know about the output. Well normally PNG files, start with a series of predefined characters, the PNG signature. The PNG spec states these 8 characters as:
137 80 78 71 13 10 26 10
Which in text form is
<PNG???? (? = unprintable)
So we know that the first 8 bytes in the image must have a numeric value of 137 80 78 71 13 10 26 10
Further research brought me to this tool; OnlinePNGTools. This tool confirmed my thinking, whatever PNG you put in, the base64 encoded values at the start must be the same; iVBORw0KGgoAAA…
As a point in the 2D array, aka pixels, is only affected by one-character of the key, that significantly decrease the problem space we need to search.
I downloaded the HTML page, JS and bytes file.
I then modified the local code so the function now looked like:
function decodePNG(key) {
var LEN = 16;
var shifter;
var result = [];
for(var i = 0; i < LEN; i++){
shifter = key.charCodeAt(i) - 48;
for(var j = 0; j < (bytes.length / LEN); j ++){
result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
}
}
while(result[result.length-1] == 0){
result = result.slice(0,result.length-1);
}
var png = result.slice(0,12).join(" ") + "...";
return png;
}
var keyDiscovered = "";
for(var i = 0; i < 10; i++) {
keyToTest = keyDiscovered + String(i).repeat(16);
keyToTest = keyToTest.substring(0,16);
console.log("Testing: " + keyToTest + ". Output: " + decodePNG(keyToTest));
}
I then ran it in console and got:
The first character matches the expected result when the key starts with a 4.
So we plumb that into keyDiscovered and re-run the script…
Now 8 matches, so add that to the key as the 2nd character. Re-run the script again…
This time we hit on the 9, bringing our first part of the key to 489.
We continue running the hacked script, appending to the key until we get the first 8 characters, 4894 7484.
With half the key calculated, the problem search space has decreased from 16^10 (1 trillion) to 8^10 (1 billion). This means we’ve reduced the possibilities by over 99.9%. Impressive.
Hacking the 2nd half of the Key
Every PNG starts with the same 8 bytes, which we’ve been able to use to calculate the first 8 characters of the key.
To calculate the next part of the key, beyond the file signature, I had to do some further research about IHDR and PNG chunks at libpng.org.
After the file header, the ‘content’ of the file begins, which obviously differs per file. But they all start with a IHDR chunk. Can we exploit that consistency somehow?
This picture (from the forensic tool I often use, OpenText’s EnCase), shows bytes 9 to 16 (bytes 0x08 – 0x0F) contain the target hex we need to match:
0x00 0x00 0x00 0x0D 0x49 0x48 0x44 0x52
So lets convert those values into decimal, to match our script format above…
0 0 0 13 73 72 68 82
I modified the script to output the next half of the key, and re-aligned the outputs. The first 8 bytes end at the highlighted ^ character in the expected row:
Then we just repeat our previous manual implementation… looks like character 8 is next.
Then we hit a snag, which I had expected we would have hit earlier TBH. The above output shows that the next character could be a 5 OR a 6; a branch has formed. We’ll have to note both down and run with both.
If we run with the first branch, character 5, we get an additional branch; the next character could be a 1 or a 2.
We choose the first branch, character 1, giving us an experimental key of 4894 7484 851? ????, and we get further branches.
You get the picture.
Thankfully the first branch doesn’t branch too much more and we get a potential key; 4894748485167104.
We try it…
..and get lucky. First time. A QR code! Excellent.
I dropped it into an online QR decoder and got the flag!
picoCTF{7b15cfb95f05286e37a22dda25935e1e}
Final Java Script Kiddie code
Below is the final code I used to calculate the key for this Java Script Kiddie challenge. It isn’t pretty and evolved in a rush, but it did the job.
Also, could be quite easy to develop the main script to add to the discovered key automatically.
var bytes = [];
resp = "128 252 182 115 177 211 142 252 189 ... ... 0 2";
bytes = Array.from(resp.split(" "), x => Number(x));
function decodePNG(key) {
var LEN = 16;
var shifter;
var result = [];
for(var i = 0; i < LEN; i++){
shifter = key.charCodeAt(i) - 48;
for(var j = 0; j < (bytes.length / LEN); j ++){
result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
}
}
while(result[result.length-1] == 0){
result = result.slice(0,result.length-1);
}
var png = result.slice(0,16).join("\t") + "";
return png;
}
// Manually append to this string as the key is calculated
var keyDiscovered = "4894748485167104";
// Crunch the next character in the key
for(var i = 0; i < 10; i++) {
keyToTest = keyDiscovered + String(i).repeat(16);
keyToTest = keyToTest.substring(0,16);
console.log("Testing " + i + "s = " + keyToTest + ". Output: " + decodePNG(keyToTest));
}
console.log("\t\t\t\t\t\t\tExpected: 137\t80\t78\t71\t13\t10\t26\t10^^0\t0\t0\t13\t73\t72\t68\t82");
I even wrote a brute forcing to crack the last bit, which was never used, but may come in use in the future. I see you Java Script Kiddie 2 challenge!
Brute forcing strings is pretty slow, would be much better to keep values as integers or hex.
function bruteForce(base, verbose) {
if (verbose){
console.log("Brute forcing, used BASE of " + base + " (" + base.length + " bytes)");
console.log(" ");
}
if(base.length < 16) {
for( var i = 0; i < 10; i++ ){
var newKey = base + i;
bruteForce(newKey, false)
}
}
if(base.length == 16) {
var decodedPNG = decodePNG(base);
if(decodedPNG == bruteForceTarget) {
console.log("Valid: " + base);;
}
}
return true;
}
var bruteForceTarget = "137\t80\t78\t71\t13\t10\t26\t10\t0\t0\t0\t13\t73\t72\t68\t82";
console.log( "Started: " + new Date().toLocaleTimeString());
bruteForce(keyDiscovered, true);
console.log("Completed: " + new Date().toLocaleTimeString());
Java Script Kiddie 2 challenge
This 2nd version of the CTF challenge is worth 450 points on PicoCTF.
This sounds and looks the same as the first challenge, just with the blurb being:
The image link appears broken… twice as badly…
The PicoCTF info on this CTF challenge
Examination of the code reveals that functionally it is the same, but now with a 32 character key. (The key in Java Script Kiddie was 16 characters)
FYI, 32^10, is written one quadrillion, one hundred and twenty-five trillion, eight hundred and ninety-nine billion, nine hundred million. Useless info.
Carefully examination of the source code (below), reveals a significant flaw in their ‘security’ / design.
function assemble_png(u_in){
var LEN = 16;
var key = "00000000000000000000000000000000";
var shifter;
if(u_in.length == key.length){
key = u_in;
}
var result = [];
for(var i = 0; i < LEN; i++){
shifter = Number(key.slice((i*2),(i*2)+1));
for(var j = 0; j < (bytes.length / LEN); j ++){
result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
}
}
while(result[result.length-1] == 0){
result = result.slice(0,result.length-1);
}
document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
return false;
}
A single line of the above code contains the flaw, below:
shifter = Number(key.slice((i*2),(i*2)+1));
While the key variable may be twice as long, the incremental value i is being doubled to select the next character. This means that every other character in the key is examined, the ones between them are ignored.
With a minor tweak to my previous code, I was able to extract the key in 60 seconds; 70601060007090105000000020008050.
(I used a zero for the ignored characters, however any value could be used)
This key gave the following QR code:
… and the Java Script Kiddie 2 flag of:
picoCTF{227c2d3465a6a4bcc8a1bc599e34f074}