CertCTF 2025

I had the pleasure of participating in CertCTF, organized as part of a bachelor’s thesis in digital forensics at Halmstad University in collaboration with MSB and CERT-SE. As someone who had never “competed” in a CTF before but had worked through various Hack The Box, TryHackMe, and OverTheWire challenges, CertCTF was incredibly fun and educational.

I also took the opportunity to document the flags I managed to collect. Below is a quick and braindump I compiled during the event. Unfortunately, I didn’t capture all the flags I had hoped for, but I’m satisfied with placing 11th out of 61 participants. Not bad for a first time! šŸ˜‰

When the CTF started, we received a zip file containing:

  • A network dump from the attack
  • Windows logs from a compromised server
  • A memory dump from the compromised server

Also included was the scenario for the CTF, detailed below.

Intro

The original message was recieved in Swedish, I’ve transelated it into english.

Gentle Dental’s IT manager, Micke, had just finished his 45-minute coffee break, during which he and the clinic’s head dentist had been discussing expensive cybersecurity solutions that the boss deemed unnecessary. As Micke sat down at his desk, the dentist, Per, mentioned that the printer was acting strangely.

Upon investigating, Micke noticed a small computer plugged into the Ethernet port that the printer normally used. Panicking, he realized something terrible might have happened. He called you, an expert in IT attacks, to help since Gentle Dental has some important, high-profile clients.

You received all network traffic that occurred during Micke’s coffee break on the company’s network, along with event logs and a memory dump from their file server. Your mission: investigate the IT attack and determine what happened.

Attacker’s IPv4 Address

What IPv4 address did the attacker initially use in the attack?

Answer format: 111.222.333.444

I noticed an unusual number of ARP requests from a specific address, clearly scanning the network for responsive hosts. Flag 1 Flag: 192.168.177.141

Domain Controller’s IPv4 Address

What IPv4 address belonged to the domain controller?

Answer format: 111.222.333.444

Domain controllers often communicate via LDAP. Searching the network dump for LDAP traffic, I found that 192.168.177.129 was talking over port 389 and was most likely the domain controller.

Flag 2 Flag: 192.168.177.129

FTP Server’s IPv4 Address

What IPv4 address belonged to the FTP server?

Answer format: 111.222.333.444

FTP typically uses port 21. Searching the network dump for traffic on port 21, I saw that the attacker’s server tried to communicate with several IP addresses, but only 192.168.177.155 responded.

Flag 3 Flag: 192.168.177.155

Attacker’s Hostname

What was the attacker’s hostname?

This was trickier but still straightforward. Filtering the network dump for the attacker’s server revealed a DHCP request, which included the hostname under Option 12.

Flag 4 Flagga: Kali

Timestamp of Last Packet

What was the local Swedish time when the last packet in the network traffic recording was received? Round down to the nearest whole second.

Answer format: HH:MM:SS

In Wireshark, I set the timestamp to local time and scrolled to the end of the network dump.

Flag 5 Flag: 09:42:01

Windows Defender Modification

Which MpPreference parameter did the attacker modify for Windows Defender Antivirus?

Answer format: ParameterName

Flag 6

Looking through the Windows Defender logs, I found that the registry value:

HKLM\SOFTWARE\Microsoft\Windows Defender\Exclusions

was altered, which corresponds to modifying:

Get-MpPreference | Select-Object ExclusionPath

Flag: ExclusionPath

Unauthorized Login

The attacker managed to log into one of the machines. What was the Logon ID for this unauthorized login?

Answer format: 0x123AB

Checking the Security logs for the attacker’s local IP (192.168.177.141), I found a successful login event—jackpot!

Flag 7 Flag: 0xFFCAB

Attacker’s Password

What password did the attacker use to log in during the "Unauthorized Login" challenge?

Answer format: Password123

Based on the log in Unauthorized Login we know the time of login and that the authentication was done with NTLM V2. NTLM V2 is known to be weak, so we should be able to crack it. We also have access to the network dump of the authentication.

To crack NTLMV2 you need to know: User, Domain, Challenge, NTLMV2Response and HMAC-MD5.

Challenge can be found in Session Setup Response for NTLM authentication. NTLM Challenge Challenge: NTLM Server Challenge: 19c218f2f9a912c1

HMAC-MD5 and NTLMv2Response can be found in the authentication. NTLMV2Response & HMAC-MD5 NTProofStr is the same as HMAC-MD5

HMAC-MD5: 7db9780888f4832bd162cf679cbffc0e

If you scrolldown you can also observe user and domain. UserDomain

NTLMv2Response: 010100000000000080bf6de99591db01f7bccb719a3b1d400000000002001800470045004e0054004c004500440045004e00540041004c0001001400460054005000530045005200560049004300450004002400670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c0003003a0046005400500053006500720076006900630065002e00670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c00070008001829aae99591db010000000000000000

Add all this together to a format that hascaat can use.

FTPService::gentledental.local:19c218f2f9a912c1:7db9780888f4832bd162cf679cbffc0e:010100000000000080bf6de99591db01f7bccb719a3b1d400000000002001800470045004e0054004c004500440045004e00540041004c0001001400460054005000530045005200560049004300450004002400670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c0003003a0046005400500053006500720076006900630065002e00670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c00070008001829aae99591db010000000000000000

Use wordlist of your choice, i used rockyou

Using Hashcat:

hashcat.exe -a0 -m5600 hashcatcrack.txt rockyou.txt
  • a0 specifies straight mode (wordlist attack).

  • m5600 specifies the NetNTLMv2 hash type.

  • hashcatcrack.txt is our compiled hash.

  • rockyou.txt is our wordlist.

Success!

Session..........: hashcat
Status...........: Cracked

FTPSERVICE::gentledental.local:19c218f2f9a912c1:7db9780888f4832bd162cf679cbffc0e:010100000000000080bf6de99591db01f7bccb719a3b1d400000000002001800470045004e0054004c004500440045004e00540041004c0001001400460054005000530045005200560049004300450004002400670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c0003003a0046005400500053006500720076006900630065002e00670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c00070008001829aae99591db010000000000000000:DentalSurgery528

Password can be found at the end of the string.

Flag: DentalSurgery528

Attacker’s Server

What was the attacker’s own server’s IPv4 address?

Answer format: 111.222.333.444

PowerShell logs showed a script modifying DNS DoH settings, which, combined with suspicious DNS traffic, suggested data exfiltration via DNS.

$script:ClassName = 'ROOT/StandardCimv2/MSFT_DNSClientDohServerAddress'

Unusual traffic from the FTP server to an unknown IP led me to suspect it was the attacker’s server. Flag 9

Flag: 10.245.122.37

Ransomware

Apart from the “regular” challenges, there was also a sub-challenge related to ransomware. The scenario is described below.

Your colleague, who was responsible for disk forensics, found an executable file and three other text files. One file containing sensitive data now appears to contain some form of encrypted data. Your colleague is fairly certain that the sensitive file can be decrypted and comes to you for help, as you are an expert in areas such as reverse engineering and cryptography.

What is the key to decrypt the file?

Along with the introduction, the following files were provided:

  • ransom_note.txt
  • pubkey.txt
  • sensative_data.encrypted
  • enc.exe

The file enc.exe was used to encrypt the data.

pubkey.txt contained a public key: 506395958

sensative_data.encrypted contained encrypted data in the format:

(540488803, 187826906) (512275051, 36345508)...

If you open enc.exe in Ghidra and dig around a bit, you can find the function below, which is likely used to generate the public key.

gen_pub_k

After a lot of Googling and a bit of AI chatting, I got the following answer, which seemed reasonable:

void gen_pub_k(uint param_1)
{
  mod_exp(2, param_1, 0x40000017);
  return;
}

mod_exp(base, exponent, modulus): This function is likely performing modular exponentiation:

result = (2^{\text{param_1}}) \mod 0x40000017 The value 0x40000017 in decimal is 1073741847, which is close to 2^30 , possibly chosen for cryptographic reasons.

param_1 is the exponent (probably a secret key or part of the key derivation process).

Just a few clicks away in Ghidra, you can find the mod_exp function.

mod_exp

Exactly how this works is a mystery to me—I probably should have taken some cryptography courses in college… But ChatGPT had an answer!

ChatGPT

That being said, it also helped generate a Python script to compute the private key. Some modifications were needed to make it work.

import math

def brute_force_discrete_log(base, modulus, public_key, max_attempts=2**24):
    """Brute-force method to find x in base^x ≔ public_key (mod modulus)"""
    value = 1  # Start with base^0 = 1
    for x in range(max_attempts):
        if value == public_key:
            return x  # Found private key!
        value = (value * base) % modulus  # Compute next power
    return None  # Not found within the attempt limit

def baby_step_giant_step(base, modulus, public_key):
    """Baby-step Giant-step algorithm for discrete logarithm"""
    m = math.isqrt(modulus) + 1  # Step size
    table = {}

    # Baby-step: Compute base^j % modulus for j in [0, m]
    value = 1
    for j in range(m):
        table[value] = j
        value = (value * base) % modulus

    # Compute base^(-m) mod modulus (modular inverse of base^m)
    base_inv_m = mod_inverse(pow(base, m, modulus), modulus)

    # Giant-step: Compute public_key * base^(-i*m) % modulus
    value = public_key
    for i in range(m):
        if value in table:
            return i * m + table[value]  # Solution found
        value = (value * base_inv_m) % modulus

    return None  # No solution found

# Given values
base = 2
modulus = 1073741847  # 0x40000017
public_key = 506395958  # Example public key (replace with actual value)

# Try brute-force first (for small keys)
private_key = brute_force_discrete_log(base, modulus, public_key)
if private_key is None:
    print("Brute-force failed, trying Baby-Step Giant-Step...")
    private_key = baby_step_giant_step(base, modulus, public_key)

if private_key is not None:
    print(f"Recovered private key: {private_key}")
else:
    print("Failed to recover the private key.")

Running this results in retrieving the private key used in the encryption: 177370085.

In Ghidra, you can investigate further and find the following function, which is used for encrypting the data:

void eg_enc(int param_1,uint param_2,uint *param_3,uint *param_4)

{
  int iVar1;
  uint uVar2;
  uint uVar3;
  
  iVar1 = rand();
  uVar3 = iVar1 % 0x40000016 + 1;
  uVar2 = mod_exp(2,uVar3,0x40000017);
  *param_3 = uVar2;
  uVar3 = mod_exp(param_2,uVar3,0x40000017);
  *param_4 = (uVar3 * param_1) % 0x40000017;
  return;
}

With a bit more AI help, you can get the following solution:

EncryptionSolution

Again, AI helped generate a script to decrypt the files using the private key.

from sympy import mod_inverse

def elgamal_decrypt(private_key, p, c1, c2):
    """Decrypts a single ElGamal encrypted pair (c1, c2)."""
    s = pow(c1, private_key, p)  # Compute shared secret: s = c1^private_key mod p
    s_inv = mod_inverse(s, p)  # Compute modular inverse of s
    M = (c2 * s_inv) % p  # Recover plaintext
    return M

# Given values
p = 1073741847  # Prime modulus
private_key = 177370085  # Recovered private key

# Encrypted data from file
encrypted_pairs = [
    (540488803, 187826906), (512275051, 37345508), (231599851, 823437630),
    (631399654, 628252041), (1035050165, 1006835836), (896417195, 206453254),
    (14141576, 263001145), (204987116, 186642158), (606374554, 598224082),
    (547306682, 128791588), (587262377, 531917810), (982294489, 538345953),
    (471862643, 967908048), (634400714, 763656344), (375171454, 544183238),
    (1055094817, 793021731), (39117044, 951544349), (361114958, 4360479),
    (327139844, 501552997), (467622466, 316485080), (36903752, 556784675),
    (44658571, 501318067), (443001524, 1031119903), (555206443, 620324572),
    (499990172, 366058919)
]

# Decrypt all pairs
decrypted_messages = [elgamal_decrypt(private_key, p, c1, c2) for c1, c2 in encrypted_pairs]

print("Decrypted messages:", decrypted_messages)

Running this results in a bunch of ASCII values:

Decrypted messages: [mpz(80), mpz(101), mpz(108), mpz(108), mpz(101), mpz(95), mpz(83), mpz(118), mpz(97), mpz(110), mpz(115), mpz(108), mpz(111), mpz(115), mpz(95), mpz(72), mpz(97), mpz(114), mpz(95), mpz(83), mpz(118), mpz(97), mpz(110), mpz(115), mpz(10)]

Which can be decoded into text:

decrypted_ascii = [80, 101, 108, 108, 101, 95, 83, 118, 97, 110, 115, 108, 111, 115, 95, 72, 97, 114, 95, 83, 118, 97, 110, 115, 10]
decrypted_text = "".join(chr(c) for c in decrypted_ascii)
print(decrypted_text)

Result?

Flag 1: 177370085

Flag 2: Pelle_Svanslos_Har_Svans

Outro

I absolutely can’t say that I understand everything that was done, especially not the underlying mathematics, but I still learned something new. Decompiling binary files and finding functions in them was actually really fun!

One cannot sweep under the rug that AI is a useful tool, especially when one has a rough idea of what they want to achieve. Without ChatGPT and Deepseek’s developing reasoning, I would have easily gotten stuck on certain parts. It’s simply about using them in the right way; one should see them as an extension of one’s own abilities.

If I had a bit more time over the weekend, I’m sure I could have captured a few more flags, especially the ones related to authentication.

A full write-up of all the CTF questions is available at the organizers Github: https://github.com/heltseriost/CertCTF2025-Writeup

I read through all of them, and it’s a bit frustrating when you realize that you were so close to solving a flag but didn’t quite get there. One big mistake I made was not checking the memory dump—I got completely stuck in event logs and decompiling.

For the next CTF, which isn’t too far away, I will take this with me: not getting too caught up in the details, as well as trying to form a picture of the entire sequence of events rather than just each individual flag.

All in all, it was a fun CTF, especially for a beginner. The ransomware part was tough but still doable with the help of Google and some AI. Definitely happy with my 11th place.

Scoreboard

And to end it all…

https://www.youtube.com/watch?v=bLlj_GeKniA

See you at LAN, or maybe on UNDUTMANINGEN 2025 šŸ¤