Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions picoctf/web/javascriptkiddie/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CC = gcc

CFLAGS = -g -Wall

jsk_solver: jsk_solver.o

jsk_solver.o: jsk_solver.c

.PHONY: clean
clean:
rm -f *.o a.out core jsk_solver
29 changes: 29 additions & 0 deletions picoctf/web/javascriptkiddie/javascriptkiddie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Original JavaScript script that is found after capturing requests via BurpSuite
*/

var bytes = [];
$.get("bytes", function(resp) {
bytes = Array.from(resp.split(" "), x => Number(x));
});

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;
}
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
91 changes: 91 additions & 0 deletions picoctf/web/javascriptkiddie/jsk_solver.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
Written by Madalina Stoicov
March 27, 2025
PicoCTF: Java Script Kiddie
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int main(int argc, char **argv)
{
// Insert the given bytes array as a string
// This data might differ depending on difference instances of the challenge
char *bytes_as_string = "156 255 80 255 117 10 239 248 152 253 120 232 36 127 116 255 151 235 25 172 215 0 56 102 219 174 30 15 36 188 93 90 249 36 32 45 123 73 191 151 236 241 151 68 144 250 157 130 1 180 20 85 213 2 157 248 68 255 250 13 60 66 249 82 187 157 29 222 29 30 0 252 126 251 95 0 174 72 194 108 29 101 70 21 121 40 26 132 73 119 254 237 73 192 96 219 137 80 89 71 0 145 1 152 0 69 254 71 0 65 68 35 0 0 119 114 13 222 68 119 1 0 78 40 155 111 95 90 164 0 26 2 0 245 186 0 84 0 0 233 145 46 110 49 48 16 78 223 135 64 197 10 0 120 0 243 252 62 144 188 21 61 1 110 148 208 22 114 160 31 156 17 45 59 72 237 74 218 0 8 32 123 136 65 179 150 32 56 206 43 240 9 156 225 69 54 226 158 106 148 62 48 1 232 173 0 239 248 243 206 82 255 241 252 56 55 152 132 108 181 78 254 175 251 60 183 38 231 63 123 204 48 43 13 131 4 113 75 243 215 32 200 144 195 29 233 196 63 3 190 139 207 89 28 107 159 185 101 59 120 121 12 245 116 64 96 250 187 241 234 231 207 213 239 119 191 233 71 205 127 144 40 251 253 173 186 246 10 227 252 202 242 163 74 237 33 75 49 205 74 154 165 126 231 30 231 232 199 118 65 211 98 204 7 250 244 141 155 243 123 82 137 252 35 183 201 132 91 252 37 244 56 188 86 125 103 216 248 215 146 144 149 21 164 233 219 127 127 207 208 30 154 111 203 63 127 141 231 146 5 20 4 81 239 38 36 19 191 63 61 183 223 215 205 210 239 168 135 148 201 39 248 212 191 160 151 116 19 150 99 249 141 111 188 0 225 193 61 73 140 160 56 23 53 48 5 99 100 175 250 125 151 253 12 150 85 41 72 206 97 52 79 88 196 130 26 157 254 185 181 42 146 217 255 24 125 155 88 111 116 167 62 238 36 52 95 57 54 126 233 184 143 46 183 234 73 183 108 163 228 218 233 129 44 169 191 74 0 30 126 245 10 249 245 241 65 191 245 73 209 50 140 26 72 132 223 181 204 200 123 185 186 183 218 175 228 249 75 180 91 229 252 193 203 187 253 52 166 28 117 119 13 238 134 74 227 127 71 251 237 50 191 61 76 230 90 241 178 221 233 202 254 211 228 156 60 202 241 71 49 24 90 187 3 245 247 159 124 157 250 227 18 150 50 49 101 86 235 162 234 57 124 108 116 245 226 190 28 43 129 220 86 245 85 107 38 215 223 119 242 72 140 213 103 209 194 70 30 96 111 204 128 234 55 184 247 205 49 227 5 220 101 80 171 155 217 87 33 26 173 127 187 128 253 215 111 203 54 210 243 29 237 148 204 235 202 131 191 191 211 157 54 147 104 188 87 4 251 25 17 185 219 247 124 135 228 176 223 135 196 157 130 215 206 124 122 136 248 28 23 175 56 104 209 253 47 161 236 61 252 147 140 86 102 185 82 110 231 91 251 245 216 243 254 236 176 127 134 31 135 152 251 90 0 216 127 102 56 99 56 64 204 61 95";

int png_bytes[16] = {137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82};

int i;
int count = 0;

// copy of initial string for counting number of bytes
char *bytes_as_string_counting = strdup(bytes_as_string);

char *token = strtok(bytes_as_string_counting, " ");

// count the number of bytes
for (i = 0; token != NULL; i++)
{
count++;
token = strtok(NULL, " ");
}

// official bytes int array with count number of slots
int bytes[count];

// make copy for adding numbers to bytes array
char *bytes_as_string_copy = strdup(bytes_as_string);

// reassign token
token = strtok(bytes_as_string_copy, " ");

// iterate again and add numbers to the bytes array
for (i = 0; token != NULL; i++)
{
bytes[i] = atoi(token);
token = strtok(NULL, " ");
}

// Now for the solving!

int key[16];
int shifters[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int curr_shifter;
int index;
int j;

// for each space in the key array
for (i = 0; i < 16; i++)
{

// for each possible shifter
for (j = 0; j < sizeof(shifters); j++)
{
curr_shifter = shifters[j];

/*
Calculate the index of bytes based off the original script
Original from JS script: bytes[(((j + shifter) * 16) % bytes.length) + i]
We do not add anything to shifter because we're just iterating through possible shifters
*/
index = ((curr_shifter * 16) % count) + i;

// if the bytes in the PNG array match our calculated bytes,
// add key shifter to our key array
if (png_bytes[i] == bytes[index])
{
key[i] = curr_shifter;
break;
}
}
}

// print the key
for (i = 0; i < 16; i++)
{
printf("%d", key[i]);
}
printf("\n");
}
139 changes: 139 additions & 0 deletions picoctf/web/javascriptkiddie/writeup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# PicoCTF: Java Script Kiddie

## Context

We are provided with a website that prompts you to enter some text. We are not given information on what the text should be. Upon submitting that text, we get a broken image link. We are not provided with any site source code.

## Background Information: PNG images

Every PNG image contains the same first 8 bytes, known as the "PNG signature": 89 50 4E 47 0D 0A 1A 0A.

The next chunk of a PNG is the image header (IHDR), which contains information about the image's width, depth, and color information. The image below displays what the IHDR could look like for a basic image. Note that the first 8 bytes of the IHDR are the size of the IHDR chunk and the fact that the chunk is an IHDR. Therefore, these bytes will be consistent throughout all PNGs: 00 00 00 0D 49 48 44 52.

![Alt text](jsk-images/basic-png-format-ex.png)

## Vulnerability

As we are given no source code for this challenge, we may try to get more information by capturing requests via BurpSuite.

We see that upon sending text in the website's input area, two requests are sent. We capture both of them and send each to the BurpSuite Repeater for further analyzing.

Upon hitting "Send" in the BurpSuite Repeater for the first request, we see that the HTML contains a JavaScript script. See `javascriptkiddie.js` for the script. It gathers information from /bytes (see next paragraph), calls a function assemble_png, and then pieces together an image.

![Alt text](jsk-images/repeater-js.png)

Upon hitting "Send" in the BurpSuite Repeater for the second request, we see that we are directed to /bytes and we get a series of bytes in our response. See the top of `jsk_solver.c` (char* bytes_as_string) for a possible version of the bytes (every instance of the challenge will have a slightly different set of bytes).

![Alt text](jsk-images/repeater-bytes.png)

Putting these pieces of information together we gather that the assemble_png function does the following:
1. Set a default key of 0000000000000000 (16 0s)
2. If the inputted key from the user is 16 digits long, keep the default key. Else, make the key the user's inputted key.
3. For every digit in the key, grab 1/16 of the bytes and calculate a new positition for each byte in that chunk depending on the value of the digit in the key.
4. Eventually, this shuffling will rearrange all of the bytes into a result array.

We realize that we must input the key that will rearrange the bytes to resemble a PNG.

## Exploitation

Let's write a script that will find the key we need to use. Please reference jsk_solver.c.

First we must acknowledge that the bytes sent by the site do not at all resemble a PNG. Recall that the first 8 bytes must be 89 50 4E 47 0D 0A 1A 0A (in decimal: 137 80 78 71 13 10 26 10). The next 8 bytes of any PNG will be 00 00 00 0D 49 48 44 52 (in decimal: 0 0 0 13 73 72 68 82). These are the only parts of the target PNG we know. Note that assemble_png relied on 16-byte chunks, signalling that we only need to know the first 16 bytes of the PNG to be able to make a 16-long key to reassemble our PNG.

Therefore we can declare that the first 16 bytes of our target PNG are:

`int png_bytes[16] = {137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82};`

I first parsed the bytes to see what length our resulting array would need to be:

```c
int i;
int count = 0;

// copy of initial string for counting number of bytes
char *bytes_as_string_counting = strdup(bytes_as_string);

char *token = strtok(bytes_as_string_counting, " ");

// count the number of bytes
for (i = 0; token != NULL; i++)
{
count++;
token = strtok(NULL, " ");
}

// official bytes int array with count number of slots
int bytes[count];
```
I then made an int array for our bytes:

```c
// make copy for adding numbers to bytes array
char *bytes_as_string_copy = strdup(bytes_as_string);

// reassign token
token = strtok(bytes_as_string_copy, " ");

// iterate again and add numbers to the bytes array
for (i = 0; token != NULL; i++)
{
bytes[i] = atoi(token);
token = strtok(NULL, " ");
}
```
To construct the key, I iterated once for each position in the key, and for each iteration iterated through all possible digits. Note that the JS had `shifter = key.charCodeAt(i) - 48;`, letting us know that only digits 0-9 are possible key digits. Through these iterations, if the target bytes in `png_bytes` matched the calculated byte in, `bytes` we added that shifter to the key and proceeded to the next digit in the key. The calculation for which byte to get in `bytes` is based off `assemble_png`'s calculations:

`result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]`

In my script, there is no inner "j" loop because we are manually going through the possible shifters, so my calculations are a bit simpler:

`index = ((curr_shifter * 16) % count) + i;`

This is what this whole portion of the script looks like:

```c
// Now for the solving!

int key[16];
int shifters[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int curr_shifter;
int index;
int j;

// for each space in the key array
for (i = 0; i < 16; i++)
{

// for each possible shifter
for (j = 0; j < sizeof(shifters); j++)
{
curr_shifter = shifters[j];

/*
Calculate the index of bytes based off the original script
Original from JS script: bytes[(((j + shifter) * 16) % bytes.length) + i]
We do not add anything to shifter because we're just iterating through possible shifters
*/
index = ((curr_shifter * 16) % count) + i;

// if the bytes in the PNG array match our calculated bytes,
// add key shifter to our key array
if (png_bytes[i] == bytes[index])
{
key[i] = curr_shifter;
break;
}
}
}
```
From this script we are able to calculate the key needed for the specific set of bytes we were given! Once entering the key into the website, we are given a QR code. I just scanned it to get the flag, but because we are security-minded people, you should not scan random QR codes and instead use QR code decoder to see where it gets you.

## Remediation

This kind of challenge is not something you would find naturally as an exploit, but it shows how easy it can be to reassemble a PNG if all we need are the first 16 bytes. To make this reassembling more difficult, the key could be longer (so we would need to iterate through possible values of width and length, etc.), the unscrambling algorithm could be more complex, or the key could include both letters and numbers (so we would have to iterate through more possible values for the key).

# Sources/Credits

Written by Madalina Stoicov

- https://en.wikipedia.org/wiki/PNG
11 changes: 11 additions & 0 deletions picoctf/web/javascriptkiddie2/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CC = gcc

CFLAGS = -g -Wall

jsk2_solver: jsk2_solver.o

jsk2_solver.o: jsk2_solver.c

.PHONY: clean
clean:
rm -f *.o a.out core jsk2_solver
25 changes: 25 additions & 0 deletions picoctf/web/javascriptkiddie2/jsk2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
var bytes = [];
$.get("bytes", function(resp) {
bytes = Array.from(resp.split(" "), x => Number(x));
});

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;
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading