본문 바로가기

Web Hacking/Dreamhack

[Dreamhack] blind sql injection advanced write up

 
이 문제는 비밀번호에 한글이 포함된다. 한글은 인코딩 시 유니코드로 표현되기 때문에 1바이트에서 4바이트 사이의 가변적인 바이트 값을 가지게 된다. 이점에 유의하며 풀어야 한다.
 

 
문제를 보면 쿼리 문과 입력 폼이 하나 존재 한다. 코드를 보면,
 
 

import os
from flask import Flask, request, render_template_string
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', 'user_db')
mysql = MySQL(app)

template ='''
<pre style="font-size:200%">SELECT * FROM users WHERE uid='{{uid}}';</pre><hr/>
<form>
    <input tyupe='text' name='uid' placeholder='uid'>
    <input type='submit' value='submit'>
</form>
{% if nrows == 1%}
    <pre style="font-size:150%">user "{{uid}}" exists.</pre>
{% endif %}
'''

@app.route('/', methods=['GET'])
def index():
    uid = request.args.get('uid', '')
    nrows = 0

    if uid:
        cur = mysql.connection.cursor()
        nrows = cur.execute(f"SELECT * FROM users WHERE uid='{uid}';")

    return render_template_string(template, uid=uid, nrows=nrows)


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

 
사용자 입력에 대해 DB에 쿼리를 날리고 있다. DB 초기 설정 파일을 보자.
 

CREATE DATABASE user_db CHARACTER SET utf8;
GRANT ALL PRIVILEGES ON user_db.* TO 'dbuser'@'localhost' IDENTIFIED BY 'dbpass';

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

INSERT INTO users (uid, upw) values ('admin', 'DH{**FLAG**}');
INSERT INTO users (uid, upw) values ('guest', 'guest');
INSERT INTO users (uid, upw) values ('test', 'test');
FLUSH PRIVILEGES;

 
user_db라는 테이블이 생성되고 utf8로 char-set을 지정해놓았다. DB에 존재하는 데이터를 보면 admin, guest, test라는 세 유저가 존재하며 admin의 비밀번호가 플래그 형식으로 되어 있다. 우리는 admin의 비밀번호를 구하면 된다.
 

취약점 분석


 

사용자의 입력값을 그대로 가져와서 db에 쿼리문으로 날린다. 따라서 싱글쿼터 탈출을 해서 sql injection 공격을 할수가 있다. 

 
또한 문제의 위 템플릿 부분을 살펴보면, 쿼리문에서 반환된 행 갯수를 토대로 html로 화면에 뿌려지는 문자가 다른걸 알 수 있다. 행이 정확히 한 개일 경우 user [유저 id] exists 라는 문자열이 나오게 된다.
 
테스트 해보자.

 
admin입력 결과 다음과 같이 user "admin" exists라는 문자열이 나오게 된다. 쿼리문이 false일 경우 

 
쿼리 문만 변할 뿐 따로 문자열이 나오지는 않는다. 이 점을 이용해서 admin의 비밀번호를 구해야 한다.
 
 

admin 비밀번호 길이


admin의 비밀번호를 구하려면 char_length라는 함수를 이용해야 한다. 비밀번호가 아스키 문자로만 이루어져 있을 경우에는 length 함수를 이용해도 별 문제 없지만, 유니코드가 포함되어 있을 경우 예상과 다른 글자수가 반환될 수 있다.
(legnth 함수는 문자열을 바이트로 표현했을 때의 길이를 리턴하기 때문에 정확함을 위해 char_length를 사용해야 한다.)
 

 
사용한 코드는 위와 같다. pwd_legnth라는 변수를 선언 후 for문을 돌면서 get요청을 보내며 pwd_length를 증가시킨다. 이때 참일 경우 for문을 탈출한다.
 

 
 
 

admin의 비밀번호별 비트 수


admin의 비밀번호를 구하기 위해서 이전에는 string 모듈로 모든 아스키 값을 생성해서 비교하는 방식을 이용했다. 그러나 이번에는 비밀번호에 "한글"이 포함되기 때문에 일일이 다 비교가 불가능하다. 따라서 각 문자들의 비트를 구해서 문자로 변환하는 방법을 이용해야 한다.
 
그러기 위해 각 문자의 비트 수를 구해야 한다. (한글이 포함되기 때문에 각 비밀번호의 비트 수는 모두 같지 않다.)
 

 
위 그림을 보면 bit_legnth라는 변수를 초기화 시켜놓고 for문을 돌며 안에서 무한루프를 돌게된다. 각 비밀번호의 비트수를 모르기 때문에 무한루프를 돌때마다 bit_length를 1씩 증가 시킨다. 이때 쿼리가 true가 되면 반복문을 탈출한다.
 
쿼리문을 보자
 

query = "admin' and length(bin(ord(substr(upw, {i}, 1))))={idx}#"

 
먼저 upw의 첫 번째 비밀번호부터 아까구한 13번째 비밀번호까지 차례대로 가져온다. 차례대로 10진수의 아스키 코드 값으로 변환 후 2진수로 변환한다. 2진수로 변환된 upw를 한 글자 씩 bit_length와 비교한다. 

 
 

admin 비밀번호의 비트 값


이제 구한 비트 수를 토대로 다시 반복문을 돌며 비트의 값들을 알아내야 한다.
 

 
위 사진을 보면 각 비밀번호의 비트에 대해 1인지 비교하고 있다. 참일 경우 비트의 값은 1이되며, 거짓일 경우 0이 되기 때문에 맞는 갑을 bit에 넣어주고 10진수로 변환 후 출력해준다.

 

UTF 인코딩


이제 구한 각각의 비밀번호의 10진수 값을 UTF-8로 인코딩을 해야한다.

 
int.to_bytes함수를 이용할 수 있다. 첫 번째 인자에는 구한 10진수 값을, 2번 째 인자에는 변환 시킬 바이트 수를 적어준다. 각 문자는 가변적인 바이트 값을 가지고 있으므로 비트수에 7을 더해 8로 나눠 준다.
 
마지막으로 바이트 배열로 인코딩된 값을 utf-8로 디코딩 진행해준다.
 
 

exploit


 
 


알게된 것들?
 
- 한글의 경우 최대 3바이트로 표현될 수 있다
- utf-8 인코딩 방식은 모든 나라의 언어를 표현하며 대부분의 언어는 2바이트 표현이 된다. 그러나 영문자의 경우 1바이트로 표현이 되기 때문에 utf-8은 1바이트에서 4바이트까지 모든 문자를 가변적인 길이로 문자를 인코딩한다.
- 유니코드 표준에 빨리 등록된 문자일 수록 적은 바이트로 표현이 가능하다. (영어가 1바이트인 이유)
- 한글은 3바이트 구간에 존재하기 때문에 한글로 작성된 파일은 용량이 늘어난다.
- 유니코드 인코딩 방식은 여러가지가 있지만 utf-8이 ascii와 가장 잘 호환되는 방식이며, 용량도 가장 적게 차지하여 주로 사용된다.
- 유니코드의 기본 바이트 순서는 빅엔디언이다
- 아스키코드는 7비트로 모두 표현 되지만 확장 아스키의 8비트를 모두 사용하는 개념도 있다