보안 스터디/웹 해킹

[드림핵/워게임] simple_sqli -1(웹해킹)

성밍쟁 2024. 1. 8. 11:37
728x90
반응형

문제

https://dreamhack.io/wargame/challenges/24/

 

simple_sqli

로그인 서비스입니다. SQL INJECTION 취약점을 통해 플래그를 획득하세요. 플래그는 flag.txt, FLAG 변수에 있습니다. Reference Server-side Basic

dreamhack.io

 

문제코드

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

 

 

 

코드 분석

우선 url부분부터 살펴보자

@app.route('/')
def index():
    return render_template('index.html')

'/' 로 가는 이 부분은 그냥 index.html 템플릿으로 가는 부분이고

핵심은 아래 /login 부분인 것 같다.

@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)

GET 요청이면 login.html 템플릿으로 가져오는 것이다.

POST 요청일시..

폼에서부터 userid, userpassword 를 입력받고(아이디와 비밀번호), res 변수에 userid와 userpassword를 넣고 존재하는지 확인하여, admin이면 flag 출력하는 코드가 작성되어 있다

 

query_db는 어떻게 쓰이는 건지 한 번 찾아보면

def query_db(query, args=(), one=False):
    cur = g.db.execute(query, args)
    rv = [dict((cur.description[idx][0], value)
               for idx, value in enumerate(row)) for row in cur.fetchall()]
    return (rv[0] if rv else None) if one else rv

대충 쿼리 구문 작성하는 것 같다. 실제 코드에서도 저렇게 나와있고..

for user in query_db('select * from users'):
    print user['username'], 'has the id', user['user_id']

이것들은

https://flask-docs-kr.readthedocs.io/ko/stable/patterns/sqlite3.html

 

Using SQLite 3 with Flask — Flask 0.9dev documentation

Using SQLite 3 with Flask In Flask you can implement the opening of database connections at the beginning of the request and closing at the end with the before_request() and teardown_request() decorators in combination with the special g object. So here is

flask-docs-kr.readthedocs.io

여기서 확인하였다.

 

아무튼 login 까지 알아봤고 다음으로는 위쪽코드를 살펴보면

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

DATABASE 는 "database.db"이고

저 DATABASE에 파일이 존재하는지 확인하고, 데잍어가 존재하지 않는다면, sqlite3와 연결한 변수를 db로 두고 생성해주는 것이다. 기존에 사용할 때는

users {
	'guest' : 'guest'
    	'admin' : FLAG
}

이런식으로 사용되었었는데, 이걸 데이터베이스화 시킨 것 같다.

 

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

query_db 부분에서는 아까 

def query_db(query, args=(), one=False):
    cur = g.db.execute(query, args)
    rv = [dict((cur.description[idx][0], value)
               for idx, value in enumerate(row)) for row in cur.fetchall()]
    return (rv[0] if rv else None) if one else rv

여기서 args=() 부분이 빠진 걸로 오버라이딩 한 것 같다.

get_db로 query문을 이용하여 데이터를 가져오는 것이 query_db 함수의 역할이다.

 

마지막으로 처음보는 형식인 코드는

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

이 부분인데, 

@app.teardown_appcontext 는 app에 연결된 특정 컨텍스트가 해제될때 실행되는 함수 등록으로써, 컨텍스트가 종료될 때 등록된 함수가 호출되는 것이다.

무엇이 실행되는 함수인가는 close_connection으로, 데이터베이스를 닫는 역할을 한다.

데이터베이스의 연결이 존재하면 닫는 역할이다. 이 문제를 푸는데 필요없는 코드인 것 같다.

 

 

 

풀이 전략

sql의 주석처리는 -- 아니면 #이다.

핵심 부분은 

userid = request.form.get('userid')
        userpassword = request.form.get('userpassword')
        res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')

 

이 부분이다. 폼에서부터 id와 비밀번호를 받는데, admin으로 접속할 때만 flag 가 나오므로, admin 아이디로 로그인해야한다.

그래서 아이디에 

admin"--

를 입력하고 비밀번호에 아무거나 입력하고 로그인 하면 flag 가 출력이 되지 않을까 싶다

 

 

실행

메인페이지에서 Login 버튼을 눌러서 로그인창으로 넘어가고

아이디에는 admin" --, 비밀번호는 아무거나 입력을 해서 Login 을 했더니

플래그가 정상적으로 나왔다.

 

플래그는 DH{c1126c8d35d8deaa39c5dd6fc8855ed0} 이다.

 

 

에필로그

처음으로 풀이 하나도 안 보고 내 스스로 생각해서 풀은 문제이다.좀 뿌듯하지만 그만큼 난이도가 쉽다고 생각한다.SQL과 관련해서 공부할려고 MySQL 책도 샀는데, 한 번 공부해야겠다.

 

 

+ 추가 내용

동적으로 쿼리문 생성한 뒤 query_db 함수에서 SQLite 질의 방식 : RawQuery

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

 

728x90
반응형