今日は4人しかこなかった… 火曜の8:45からってのがきつすぎるんかな…
31 Kangacha
PHPで書かれた簡単なwebアプリケーションの問題. Gachaというボタンを押せば日本の戦艦の名前がランダムでリストに追加されていく. ソースコードも与えられているので読んでいく.
if (isset($_POST['submit']))
{
// Gacha
if ($_POST['submit'] === 'Gacha')
{
// Yamato is ultra rare
$ship[] = mt_rand(0, count($shipname)-2);
$s = implode(',', $ship);
$sign = hash('sha512', $salt.$s);
setcookie('ship', $s);
setcookie('signature', $sign);
}
// Clear
if ($_POST['submit'] === 'Clear')
{
setcookie('ship', '', 0);
setcookie('signature', '', 0);
}
header("Location: {$_SERVER['REQUEST_URI']}");
exit();
}
このソースから, $_COOKIE['ship']
に戦艦の番号の配列が, $_COOKIE['signature']
にあらかじめ決められた$salt
と戦艦番号のハッシュ値が格納される.
// Check signature and read
if (isset($_COOKIE['ship']) and
isset($_COOKIE['signature']) and
hash('sha512', $salt.$_COOKIE['ship']) === $_COOKIE['signature'])
$ship = explode(',', $_COOKIE['ship']);
else
$ship = array();
ここで$_COOKIE['signature']
のバリデーションを確認され, 不正であれば配列の中身が空になってしまう.
このアプリにはlength-extension attack
に対して脆弱性があるらしい. H( salt || ship)
と “,10” から、 H( salt || ship || ",10" )
が求められるので、クッキーに不正に Yamato を追加しても signature のチェックを誤魔化すことができる.
反復形ハッシュ関数Hについて,H(M||M’)がH(M)とM’のみから計算できるという性質を利用する攻撃である.ここで,M||M’はMとM’との連接を表す
以下のようなスクリプトを書く.
参考
import subprocess
import requests
# セッションを接続するための準備
kangacha_url = "http://ctfq.sweetduet.info:10080/~q31/kangacha.php"
s = requests.Session()
# 一度ポストし、クッキーの情報を得る
r = s.post(kangacha_url, data={"submit": "Gacha"})
data = s.cookies["ship"]
signature = s.cookies["signature"]
# hashpump を subprocess で呼ぶための準備
args = {}
args["data"] = data
args["signature"] = signature
args["key"] = 21
args["append"] = ",10"
cmd = "hashpump -s {signature} -k {key} -d {data} "
cmd += "-a {append}"
cmd = cmd.format(**args)
proc = subprocess.Popen(cmd.strip().split(" "), stdout=subprocess.PIPE)
out, err = proc.communicate()
# 得られた cookie を url エンコードにする
crack_signature, crack_data = out.decode("utf-8").strip().split("\n")
crack_data = crack_data.replace("\\x", "%")
# cookie を変更して再接続
s.cookies.clear()
setargs = {"domain": "ctfq.sweetduet.info", "path": "/~q31"}
s.cookies.set("ship", crack_data, **setargs)
s.cookies.set("signature", crack_signature, **setargs)
r = s.get(kangacha_url)
print(r.text)
hashpump
という the hash length extension attack 用のツールをbrewでインストールする必要がある.
Simple Auth
テキストボックスと送信ボタンがあるシンプルなサイトがある. このサイトのソースコードを読むと, 以下のようにパスワードが判定されている.
if (strcasecmp($_POST['password'], $password) == 0)
echo "Congratulations! The flag is $password";
else
echo "incorrect...";
strcasecmp
が何かを調べると,
strcasecmp — 大文字小文字を区別しないバイナリセーフな文字列比較を行う
ということがわかる. これに配列を渡すと返り値が0になるらしい. 配列を渡すにはname=password[]
とすればいい.
HTTPS is secure.
めちゃめちゃいい問題. SSL/TLS通信がどのように行われているのかがわかる. 詳しくは…
wiresharkでパケットを読んでみると, 192.168.66.129
と192.168.0.39
, 192.168.66.129
と192.168.0.40
の2組がhttpsで通信を行なっている. それぞれの公開鍵を取得して秘密鍵を取り出そうと頑張ってみる. パケットNo. 7とNo. 76のSecure Sockets Layer/TLSv1 Record Layer: Handshake Protocol: Certificate/Handshake Protocol: Certificate/Certificates (1498 bytes)/Certificate/signedCertificate/subjectPublicKeyInfo/subjectPublicKey:/modulus:
が公開鍵である. これから”:”を取り除いて10進数にすると以下の2値が手に入る.
u1 = 20912068408571562329765690555061159289641629285082404210189101064954330953315593257557260077525915152641073106397431556875680393639301995231540409600633056790407217644109479375811025952060540276714119842291972532268686811648476477127818267411283106601195166099848608860814911133056759210847640244371352294577674757844032344743192797680553522630615249481210459669536735468283778508143359159893770374788694416907786510825727199111604249000530550012491935109887922826382346971222271516625157446929215544796309806757863550058676780306722906895167581167203804721314732494889662194466565293268848629536070864750745494338531
u2 = 20810915617344661448636429656557804394262814688853534649734586652859523797380885650024809244693377123486154907319690068259378744245911427062593140588104970879344505836367952513105241451799550533959908906245319537215140226739848280012005678383612764589285444929414256249733809498630880134204967826503346173071037885178145189051140796573786694250069189599080301164473268293037575740360272856085402928759232391893060067823996007021668671352199570084430112300612196486186252109596457909476374557998336186613887204545677563178904634941310201398366965571422359228917354256271527331840144577394174480450746748283277750230727
この1024ビット数を普通に素因数分解すると何年もかかるらしい. しかし, この2つが共通の因数を持っていると仮定すれば最大公約数を求める要領でできる. つまり以下のような式になる.
u1 = p1 * q
u2 = p2 * q
ここで、ユークリッドの互除法を用いてqの値を求める.
def gcd(x, y):
r = modlog(x, y)
while r != 0:
x = y
y = r
r = modlog(x, y)
return y
def modlog(x, y):
r = x % y
return r
def main():
u1 = 20912068408571562329765690555061159289641629285082404210189101064954330953315593257557260077525915152641073106397431556875680393639301995231540409600633056790407217644109479375811025952060540276714119842291972532268686811648476477127818267411283106601195166099848608860814911133056759210847640244371352294577674757844032344743192797680553522630615249481210459669536735468283778508143359159893770374788694416907786510825727199111604249000530550012491935109887922826382346971222271516625157446929215544796309806757863550058676780306722906895167581167203804721314732494889662194466565293268848629536070864750745494338531
u2 = 20810915617344661448636429656557804394262814688853534649734586652859523797380885650024809244693377123486154907319690068259378744245911427062593140588104970879344505836367952513105241451799550533959908906245319537215140226739848280012005678383612764589285444929414256249733809498630880134204967826503346173071037885178145189051140796573786694250069189599080301164473268293037575740360272856085402928759232391893060067823996007021668671352199570084430112300612196486186252109596457909476374557998336186613887204545677563178904634941310201398366965571422359228917354256271527331840144577394174480450746748283277750230727
q = gcd(u1, u2)
print q
if __name__=="__main__":
main()
すると, p1とp2が以下のようにして求まる.
p1 = u1 / q
p2 = u2 / q
次に秘密鍵を構築する. pem形式の秘密鍵は, ASN.1のバイナリデータをBase64でエンコードされたテキストファイルである. そこでの公開指数として655337がよく用いられる.
公開指数eの値として65537もよく使われますが,その理由としては, これが素数であり,しかも65537=2^16+1 が成り立つためです。これを2進表現したものは1を 2つしか含まないので,この値でべき乗するには17回の乗算で済みます。 これは3を指数eとした場合の2回と比べると多いですが, 512ビットを無作為に選択した場合に必要とする平均乗算回数768回と比べると圧倒的に少ないといえます。 また,65537をeとして使う場合,eに3を使う場合に生じる問題をほとんど避けることが可能です。
秘密鍵は以下のようにして求める.
from Crypto.PublicKey import RSA
from Crypto.Util.number import inverse
p = 139446642537534304777628614240154046272434122794892522124374234093313897652592278876204620931659231555942782873768406065030569534203407105601097455479995730772421725109267044663491213232687718387909353507690622331780468229128999879032054673690005684809410661625656125511253714586807242182927209779610158700317
q = 149964660518396798660782215517197000054264985822608779681144262791391323000835825727277636178043097046988857828384650158626906824855399961360412435818827649355003329726451846544435103030378220357694459358803967598155925736581896165952170564324730092516286266118841005062382011803493961966912439338500328959743
e = 65537
n = p*q
d = inverse(e, (p-1)*(q-1))
key = RSA.construct(map(long, (n,e,d)))
print key.exportKey()
上記の出力をprivate<1, 2>.pem
として保存する.
さらに秘密鍵はパスがかかった状態なので, 以下のコマンドを実行してパスを解除する.
python genpem<1, 2>.py > private<1, 2>.pem
openssl rsa -in private<1, 2>.pem -out private<1, 2>.pem.unencrypted
最後にTLS通信を復号する. wiresharkでのCertificateメッセージを右クリックして, プロトコル設定 > RSA keys list を選択する. SSL Decrypt というウィンドウで以下を入力する.
192.168.0.39 443 http /Users/......../private1.pem.unencrypted
192.168.66.129 443 http /Users/......../private2.pem.unencrypted
すると, 今まで見えてなかった通信がHTTPとして見えるようになるので, 画像ファイルをエクスポートすればフラグゲット.
Simple Auth II
今回はユーザー名とパスワードを入れる認証. これもソースコードの認証を担っている部分を見てみる.
if ($_POST['id']!=='' or $_POST['password']!=='')
{
$try = true;
$db = new PDO('sqlite:database.db');
$s = $db->prepare('SELECT * FROM user WHERE id=? AND password=?');
$s->execute(array($_POST['id'], $_POST['password']));
$ok = $s->fetch() !== false;
}
sqliteを使っているが, 同じディレクトリにあるsqliteのデータベース情報から読み込んでいることがわかる. これにブラウザからアクセスするとダウンロードできてしまう. これをsqlite3を使って読み込むとuserテーブルにFLAGが隠されている. ちなみに以下のコマンドを使うとsqliteの表示が綺麗になる.
.headers on
.mode column
Are you ESPer?
数あてゲームが実装されている. 段々難易度が上がっていき20問全てに正解することはほぼ不可能である. この問題に関しては山田さんのqiitaを参考にした.
結論としては, 1行1行アセンブリ言語を読んでいけば解ける. もう少しアセンブリ言語について勉強しないと厳しい. また, UbuntuとMacでのtime()
関数の値が異なるという点と, サーバーに時差があるのでそれを加味しなければならない点でハマるらしい.
感想
HTTPSの解読はとても勉強になった. 共通因数がわかるだけでここまでできてしまうことに驚いた.