← Back to Writeups
HTBN/AWeb

AquaCrypt

XESXOR8/23/20264 min read
#web#htb#n/a

AquaCrypt

Platform: Undutmaning | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-03-21 | Status: Solved Techniques: robots_txt_enumeration, source_code_leak, python_deobfuscation, rsa_small_key_factorization, pkcs1_signature_forgery

Summary

English summary: A Flask web application for file sharing requires a valid signature to download files. The /files endpoint lists available files including flag and flag.txt. Goal is to bypass the signature verification to download the flag file.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: undutmaning | ID: 20260321_undutmaning_aquacrypt
  • Tags: flask, robots_txt, obfuscation, rsa, source_code_leak, weak_key, pkcs1_v1_5, signature_verification
  • Indicators: robots.txt with admin paths, Flask/Werkzeug server, signature parameter required for download, obfuscated Python code with gzip+base64, 128-bit RSA key
  • Source: 20260321_undutmaning_aquacrypt.md

Foothold

Vulnerability / Misconfiguration

  1. Robots_txt_enumeration
  2. Source_code_leak
  3. Python_deobfuscation
  4. Rsa_small_key_factorization
  5. Pkcs1_signature_forgery
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • robots_txt_enumeration
  • source_code_leak
  • python_deobfuscation
  • rsa_small_key_factorization
  • pkcs1_signature_forgery
  • Tags: flask, robots_txt, obfuscation, rsa, source_code_leak, weak_key, pkcs1_v1_5, signature_verification

Original Writeup

<details><summary>Click to expand original content</summary>

Description

Efter en mängd kreativt omkopplande lyckas de nödställda på CASCADA få tillgång till ytterligare några delar av nätverket. Där hittar Harriet ganska snabbt en webbtjänst som verkar vara till för att ladda ner filer. Vissa av de tillgängliga filerna har intressanta namn, men Harriet behöver hjälp med att komma åt dem.

Kan du hjälpa henne?

Du kan surfa in till tjänsten på: https://undutmaning-aquacrypt.chals.io

English summary: A Flask web application for file sharing requires a valid signature to download files. The /files endpoint lists available files including flag and flag.txt. Goal is to bypass the signature verification to download the flag file. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Analysis

Initial Reconnaissance

The target is a Flask/Werkzeug (3.1.5, Python 3.13.1) web application. The main page shows a "Secure File Sharing" service with a download form requiring a filename and a signature.

robots.txt Discovery

Checking /robots.txt revealed three disallowed paths:

  • /files_old (404)
  • /admin (accessible!)
  • /staging/ (404)

Source Code Leak

The /admin endpoint showed an admin panel with server configuration and a link to /admin/download-app which returned the full Flask application source code. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Obfuscated Verification Function

The source code contained an obfuscated verification function:

a = "H4sIAAAAAAACA+2dO3brMAxE+6zCXZrsKCf730bq96rYJoH5XNeShqLBwWD40ffnY+D3+fUYwZkD..."
import base64, gzip
exec(''.join(map(lambda x: chr(len(x)), eval(gzip.decompress(base64.b64decode(a)).decode('utf-8')))))

The obfuscation technique: a Python list of strings where len(string) maps to the ASCII character code, joined together to form the verification function source.

Weak RSA Key

Deobfuscating the blob revealed a verify(signature, message) function implementing RSA PKCS#1 v1.5 signature verification with an embedded PEM public key: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  • Key size: Only 128 bits (trivially factorable)
  • Public exponent: e = 3
  • Modulus n:REDACTED

Solution

Step 1: Deobfuscate the Verification Function

import base64, gzip

a = "H4sIAAAAAAACA+2dO3brMAxE+6zCXZrsKCf730bq96rYJoH5XNeShqLBwWD40ffnY+D3+fUYwZkD..."
data = gzip.decompress(base64.b64decode(a)).decode('utf-8')
result = ''.join(map(lambda x: chr(len(x)), eval(data)))
print(result)

This reveals the RSA public key embedded in the verification function.

Step 2: Extract and Factor the RSA Key

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

from cryptography.hazmat.primitives.serialization import load_pem_public_key
from sympy import factorint

# Extract public key parameters
# nREDACTED
# e = 3

# Factor the 128-bit modulus (trivial)
factors = factorint(REDACTED)
# {REDACTED: 1,REDACTED: 1}

p = REDACTED
q =REDACTED

Step 3: Compute Private Key

nREDACTED
e = 3
p = REDACTED
q =REDACTED

phi = (p - 1) * (q - 1)
# phiREDACTED882904702279312174776

d = pow(e, -1, phi)
# d = 156362387859240733255269801519541449851

Step 4: Forge PKCS#1 v1.5 Signature

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

import base64

def pkcs1_pad(message: bytes, key_size_bytes: int) -> int:
    """Create PKCS#1 v1.5 signature padding"""
    # Format: 0x00 0x01 [0xFF padding] 0x00 [message]
    padding_len = key_size_bytes - 3 - len(message)
    padded = b'\x00\x01' + (b'\xff' * padding_len) + b'\x00' + message
    return int.from_bytes(padded, 'big')

# Sign "flag" filename
message = b"flag"
nREDACTED
d = 156362387859240733255269801519541449851
key_size_bytes = 16  # 128 bits

padded_int = pkcs1_pad(message, key_size_bytes)
signature_int = pow(padded_int, d, n)
signature_bytes = signature_int.to_bytes(key_size_bytes, 'big')
signature_b64 = base64.b64encode(signature_bytes).decode()
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

print(f"Signature: {signature_b64}")
# Output: pML6OO3r7xqMD8H+1czLKA==

Step 5: Download the Flag

curl 'https://undutmaning-aquacrypt.chals.io/download/flag?signature=pML6OO3r7xqMD8H%2B1czLKA%3D%3D'

Note: flag.txt was a decoy containing just "test". The actual flag was in the file named flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR