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/sha1 | hash_equals() または === で比較 |
| in_array() | 第3引数にtrueを指定 |
| extract/parse_str | ユーザー入力を直接渡さない |