모의해킹 스터디 복습

모의해킹 스터디 8주차(1): SQL Injection 포인트 찾기

whydontyoushovel 2024. 12. 11. 12:06

SQL Injection

서버에서 준비한 쿼리문에 악의적인 의도를 가진 SQL구문을 주입하는 해킹 기법.

 


 

 

1. Union SQL Injection

  SQL 질의 결과가 사이트 화면에 출력될 때 

 

2. Error-Based SQL Injection

  사이트에 질의문의 결과는 안 나와도 SQL 에러문이 출력될 때

 

3. Blind SQL Injection

  질의문 결과나 에러문이 화면에 출력되지 않지만 쿼리의 참/거짓 결과를 구분할 수 있을 때   

 

 


 

 

핵심

 

1) SQL Injection의 기본 조건

SQL 쿼리가 사용되는 곳을 중심으로 찾아야 함.

 -> 서버에서 쿼리가 사용 중인 곳이어야 SQL구문을 주입할 수 있음.

 

__ ___ __

 

2) SQL Injection 가능 범위

SQL 쿼리가 사용되는 곳이라면 사용자 입력창 뿐만 아니라 필터/정렬 기준, 쿠키나 user-agent 등의 요청 헤더에서도 일어날 수 있음.

   -> 이제껏 실습한 where 조건절만이 아니라 날짜나 정렬 기준, 검색 필터 또한 (쿼리에 사용되는 한) SQLi의 여지가 있음.

 

__ ___ __

 

3) 수정할 수 있는 쿼리 데이터 찾기

버프 스위트와 같은 웹 프록시 툴을 이용해 서버로 보내는 파라미터와 헤더 정보를 분석해야함.

   -> 2번과 같은 경우 사이트에 직접 표시되는 데이터가 아닌 패킷으로 전달되는 데이터를 확인해야함. 그 데이터가 단순히 웹 페이지에 고정된 데이터인지 DB를 이용하는지 알아본 결과 DB를 이용한다고 생각되면 SQLi를 시도.

 

__ ___ __

 

4) 서버의 쿼리문 예상하기

포인트를 찾고 참(1=1), 거짓(1=2) 결과를 확인하기에 앞서 서버에서 해당 포인트에 어떤 쿼리를 사용하고 있을지 고민하는 습관을 들여야함. 

   -> 고민하면서 시도해야 서버의 쿼리에 대한 그림을 구체적으로 그릴 수 있음. 페이로드 짤 때 필요. 마구잡이로 넣다간 방화벽에 차단당할 수도 있음. 

 

 

 

 


 

SQLi 포인트 찾기

 

  • 예제1: 쿠키
  • 예제2: 컬럼명
  • 예제3: order by 절
  • 예제4: 참/거짓 결과 구분 안됨 + DB에러 확인

 

 

예제_1 

- 서버에서 쿼리에 쿠키 데이터를 사용하는 경우

 

문제 사이트 메인

 

회원가입한 계정으로 로그인

여기서는 sql injection이 일어나지 않는다.

 

로그인한 뒤 index.php 페이지. 로그인 시 sql injection이 가능했다면 test라는 문구에 db 정보를 추출했을 것이다.

 

로그인 데이터를 post방식으로 넘기는 요청 패킷과 그에 대한 응답 패킷.

 

로그인했더니 응답으로 쿠키값을 두개나 주고 있다.

하나는 세션 아이디

다른 하나는 계정 정보

동시에 index.php로 리다이렉션시키고 있다.

 

index.php를 요청한 데이터. 쿠키를 두개나 받았다. 트릭 오어 트릿.

 

저 welcome test의 test가 쿠키의 test인지 확인하기 위해 요청 패킷을 리피터로 보내고 user=test를 testtest로 수정하여 재요청해본다.

 

만약

1. welcome뒤의 test가 사라진다 .

-> 쿠키값을 받아 db의 아이디를 출력 중인 것 (db에 testtest라는 계정이 없기 때문)

2. testtest가 그대로 출력된다.

-> 쿠키로 할당한 값을 그대로 출력

3. 수정해도 test 그대로 출력된다.

-> 로그인 시 받은 아이디로 db의 아이디를 뽑아 세션 변수에 저장하는 방식일 수 있음. (쿠키 사용 X)

 

 

헛다리!!!!!

 

사이트를 돌아댕겨본다.

마이페이지에 가보자.

 

 

여기도 test라는 데이터를 받고 있다. 더하여 계정의 다른 정보(nothing here)도 화면에 출력 중이므로 db 데이터를 끌어와서 쓰는 것으로 보인다.

 

mypage.php에 대한 요청 패킷을 리피터로 보내 쿠키를 수정해본다.

 

만약

1. 폼에 표시되는 정보에 변화가 생긴다.

-> 쿠키에 있는 값으로 db데이터 긁어오는 중

2. 아무 변화도 없다.

-> 헛다리!!!!

3. testtest로 바뀌는데 nothing here에 변화가 없다.

-> testtest라는 계정이 이미 있거나 아이디는 쿠키로 받지만 db데이터는 세션 데이터를 이용해서 가져올 수도 있음.

4. test는 변화가 없는데 nothing here에 변화가 있다.

-> 아이디는 세션 데이터로 받는데 db데이터는 쿠키로 가져옴.

 

 

쿠키를 testtest로 바꿔 요청을 보낸 결과

아이디 표시 부분은 testtest로 바뀌었고

nothing here는 사라졌다.

아이디 정보는 쿠키를 통해 가져왔지만 그에 해당하는 db 정보가 없기 때문에 데이터가 표시되지 않고 있는 듯 하다.

즉, 마이페이지에서 쿠키로 db데이터를 가져오는 중일 수 있다는 뜻.

 

 

아마 서버의 쿼리는 이런 식일 것이다.

select __어쩌구컬럼__ from __어쩌구테이블__ where id = '$_cookie['user']';

 

 

SQL Injection이 가능한지 테스트하기 위해 예상 쿼리문에 맞춰 아래 데이터를 넣어본다.

test' and '1'='1
test' and '1'='2

 

첫번째 입력값의 결과가 user=test일 때와 같고, 두번째 입력값의 결과가 아이디에는 test' and '1'=2를 표시하나 nothing here에 아무것도 표시되지 않는다면 여기서 SQL Injection이 일어날 수 있다는 뜻이다.

 

test' and '1'='1을 쿠키값에 입력한 결과
test' and '1'='2를 쿠키값에 입력한 결과

 

해당 포인트에서 SQL Injection이 일어날 수 있음을 확인하였고

nothing here부분에 db데이터가 표시되는 것을 보아

Union SQL Injection이 가능할 것으로 예상된다.

 

또한 게시판과 달리 결과 데이터가 한 행만 표시되는 형태이기 때문에 페이로드에 limit이나 order by를 적절히 응용해야 원하는 데이터를 볼 수 있을 것 같다. 

 

....아 그냥 페이로드의 test를 생략하면 limit이나 order by 없이도 DB데이터를 추출할 수 있다.

 

증거 1

 

증거 2

 

 

______< 맨 처음 실습할 당시에 >_______

 

지금이야 쿠키로 db데이터 가져오고 있다는 걸 알아서 금방 썼지만

맨 처음에 풀 때는 마이페이지에서 쿠키를 수정하면 뭔가 달라진다는 걸 알아내도 SQLi 포인트로 생각하지 못햇다..

엉뚱하게도 초점이 select가 아닌 update에 맞춰져서 그랬던 것 같다.

 

그때 쓴 글을 보면 왜 이렇게 했지 싶은데 아마 쿠키를 수정한 뒤에 update에 성공하면 쿠키로 db에 접근한다는 증거로 생각한 것 같다. 지금 생각해보면 오히려 반대인데 말이다. 쿠키를 수정한 상태에서 update에 성공했다면 서버에서 쿠키값이 아니라 세션데이터를 식별 데이터로 사용 중이라는 뜻이 된다. 

update를 시도한 결과는 당연히 실패였다. (수정됐다는 alert는 떴지만 로그인할 수 없었다.)

근데 test로 들어가도 아이디 수정이 안되는 걸 보면 그냥 업데이트 기능을 구현하지 않았는지도 모르겠다....

 

두 가지를 간과했다고 생각한다.

1. 쿠키를 수정한 뒤에 계정 정보를 업데이트할 수 없다는 건 수정한 쿠키값이 db에 없다는 뜻이고 쿠키를 이용해 db에 접근하고 있다는 뜻이 된다. 

2. 마이페이지에서 사용되는 쿼리는 update문만이 아니라 select문도 있다.

 

해당 페이지에서 사용될 여러 쿼리문을 먼저 떠올려보자.. 

 

 

 

개인적인 의문 (문제 사이트의 세션과 쿠키)

더보기

(접은글 하단에 정리본 있음.)

 

1) 의문

test로 로그인한 상태에서 로그아웃하지 않고 로그인페이지로 이동하면 새 세션아이디를 받을 수 있는데, 이때 다른 계정으로 회원가입하면 회원가입이 완료되었다는 알림과 함께 welcome test!!라는 문구가 있는 index.php페이지로 리다이렉션 된다.

쿠키에는 user=test가 아직 남아있다.

나는 왜 아직 test계정으로 유지되는 걸까?

 

2) 쿠키 작동 방식

intercept로 패킷을 잡고 user=test를 지워도 intercept를 해제하면 그대로 남아있다. 아니 걍 지울 수가 없다.

로그아웃하고도 user=test가 남아있길래 mypage.php로 접근해봤더니 잘못된 접근이라고 뜬다. 쿠키가 로그인을 유지시키는 건 아니다.

로그아웃하고 다른 계정으로 로그인했더니 set-cookie로 그 계정 아이디가 들어왔다.

변하지 않은 건 쿠키 데이터 뿐이지만 쿠키 때문은 아닌 것 같다. 얜 로그인 과정에 관여하지 않는 듯 하니까.

 

3) 소스 코드 추측

찾아보니 세션 아이디를 재발급하면서도 로그인을 유지할 수 있는 방법이 있다. 로그인 페이지에 진입할 때마다 세션 아이디를 재발급하는 거다. 아마 login.php 상단에 session_regenerate_id 함수가 있지 않을까 싶다.

그리고 쿠키에 대해서는 로그아웃해도 다른 계정으로 로그인하기 전까지 유지되는 걸로 보아서 만료가 없고 덮어쓰기만 있는 거 아닐까 싶다. 

 

4) 의문에 대한 나름의 해결

test로 로그인한 상태에서 로그아웃하지 않고 로그인페이지로 이동하면 새 세션아이디를 받을 수 있는데, 이때 다른 계정으로 회원가입하면 회원가입이 완료되었다는 알림과 함께 welcome test!!라는 문구가 있는 index.php페이지로 리다이렉션 된다.

 

=> 로그인 페이지에 session_regenerate_id가 있으므로 페이지에 접근하면 새 세션아이디를 받을 수 있음. 

=> test로 로그인이 유지되고 있는 상황에서 다른 계정으로 회원가입 함.

=> test계정이 유지되면서 index.php페이지로 접근함.

=> 회원가입한 계정으로는 자동으로 로그인 처리되지 않고 그 계정으로 로그인해야함. 

 

쿠키에는 user=test가 아직 남아있다.

 

=> 쿠키 만료 코드가 없다면 다른 계정으로 로그인해야 쿠키 데이터를 수정할 수 있는데 다른 계정으로 로그인하지 않았으니 test로 유지됨.

 

나는 왜 아직 test계정으로 유지되는 걸까?

 

=> 다른 계정으로 로그인해라. 

 

 

그리고 마이페이지에서 intercept로 잡아서 user=testtest로 바꿨을 때 폼의 id부분에 testtest로 표시되고 http history에 찍힌 편집된 요청 데이터의 헤더에도 user=testtest로 되어있다. 근데 새로고침하니까 그냥 user=test되고 폼의 id부분에도 test가 뜬다. 

 

와씨 납득이 갈만한 이유를 찾았다. 

내가 수정한 요청 헤더의 쿠키는 브라우저가 이미 만든 걸 잡아채서 수정해서 서버에 보내는 거다.

브라우저가 어떻게 쿠키를 세팅할지 그 자체를 수정한 게 아닌 것이다. 

그러니 새로고침하면 원래의 값인 user=test로 돌아오게 되는 것!!!!!! 우왁!

 

 

5) 정리

 

- 문제 사이트는 login.php 상단에 session_regenerate_id()가 있을 확률이 높다.

- 문제 사이트는 쿠키를 만료시키지 않고 있다. 대신 로그인하면 쿠키를 덮어쓰는 방식일 확률이 높다.

- 문제 사이트는 회원가입을 완료해도 자동으로 해당 계정으로 로그인시켜주지 않는다.

- 버프 스위트로 수정한 요청 헤더는 지나가는 패킷 잡아서 수정한 것일 뿐, 브라우저의 요청값 자체를 수정하는 것이 아니다.

 

 

 

 

 

 

▣ 예제_2

- where 조건절의 컬럼명을 동적으로 할당하는 경우

 

앞선 예제와 다른 사이트임.

 

가입된 계정으로 로그인한다.

로그인 페이지에서 진입하며 새로운 세션아이디가 발급되고 있다.

그 외의 쿠키 데이터는 사용되지 않는다.

 

 

마이페이지에 들어가본다.

DB에서 가져오는 계정 정보가 출력되고 있다. 

업데이트 기능은 여전히 작동하지 않는다.

클라이언트측에서 DB의 select문에 접근할 방법이 없다.

 

폼 제출시 이미 db에 있는 아이디(test)를 입력하면 alert로 다른 경고문을 띄울까 싶어 해봤으나 뭘 입력하든 수정되었다고 뜬다. 만약 test를 입력하고 폼을 제출했을 때 뭐 이미 그렇게 저장됐다는 안내가 뜨고, testtest를 입력했을 때 차이를 이용해 blind를 할 수 있을지도 모른다. (test' and '1'='1이 먹힌다면...)

 

이미 있는 아이디라는 경고창을 내가 어디서 봤는데 어디서 봤지...하다가 회원가입 페이지에서 봤다는 것을 깨달았다.

실제로 test의 계정 정보를 입력하니 이미 있는 아이디라는 경고창이 떴다. 

하지만 test' and '1'='1이라는 데이터는 쿼리로 인식하지 못하고 단순히 계정 정보로 인식되었다...

증거 화면..

저 null 문자는 이스케이프처리한 결과인가???

우회해보려고 작은 따옴표 앞에 %a1을 추가해서 로그인해봤는데 로그인이 안되었다.

이 우회법은 서버에서 문자열을 인코딩 (mb_convert_encoding())할 경우 가능하다니, 서버에서 저 인코딩 함수를 사용하지 않거나 prepared statement를사용 중인 지도 모르겠다.

 

 

real_escape_string 우회에 대한 자료는 여기서 보았다.

https://rootable.tistory.com/144

 

addslashes(), mysql_real_escape_string() 우회

0. 주제 선정 SQL Injection에 대해 공부하는데 방어 기법으로 addslashes()와 mysql_real_escape_string()이 등장하였습니다. 그런데 책에서는 이 두가지 함수로 SQL Injection을 모두 방어할 수 있는 것처럼 나와

rootable.tistory.com

 

mb_convert_encoding함수가 사용되는 몇 가지 경우

더보기

1. 웹 앱에서 다국어를 지원할 경우.

2. 시스템 내부 오오래된 데이터의 인코딩방식이 현재와 다를 경우(ex. 옛날엔 비표준 문자셋, 현재는 utf-8)

3. 외부 api가 특정 문자셋을 요구할 경우

4. CSV, XML, JSON 등과 같은 파일이 특정 문자셋으로 인코딩되어 있을 경우

 

등등...

 

 

 

아무래도 다른 페이지를 탐색해야할 것 같다.

 

게시판

게시판으로 왔다.

다른 계정이 작성한 글은 보이지 않는 것을 보니 현재 로그인한 계정이 작성한 글만 보이도록 해둔 것 같다.

아무튼 여기서도 DB데이터를 긁어오고 있다는 뜻.

 

우선 익숙한 검색창을 이용해 sql injection을 시도해본다.

 

like구문을 사용 중인지 확인하기 위해 t만 검색했더니 동일한 검색결과가 나왔다.

이제 t%' and '1%'='1 을 검색해본다.

t만 검색했을 때와 같은 결과가 나올 경우 현재 검색창에서 sqli가 가능할 것이고, 아무 결과도 안 나올 경우 검색창에서는 sqli를 사용할 수 없다. (입력값을 쿼리로 인식하지 않는다는 뜻이기 때문.) 

 

 

확인을 눌렀더니 화면에 아무 게시글도 표시되지 않는다. 입력값을 쿼리로 인식하지 않아 t%' and '1%'='1 이라는 제목의 게시글을 그대로 찾으려고 한 것이다.

 

마이페이지에서 그랬듯이 버프스위트로 가 클라이언트와 서버가 주고 받은 데이터를 확인해본다.

t%' and '1%'='1 검색을 요청한 데이터

 

t%' and '1%'='1 을 검색할 때 내가 POST방식으로 보낸 파라미터가 보인다.

- option_val=username
- board_result=t%25%27+and+%271%25%27%3D%271
- board_search=%F0%9F%94%8D
- date_from=
- date_to=

 

확인된 파라미터는 총 5가지이고 값을 가진 것 중에 board_search는 검색창 옆 아이콘에 대한 데이터이다.

그럼 여기서 쓸만한 데이터는 이제 option_val와 board_result만 남는다.

 

퍼센트가 난무하는 board_result는 내가 입력한 검색값이 url인코딩된 데이터이다.

그럼 남은 option_val은 어떤 데이터일까.

 

option_val의 정체(두둥)

검색창 옆에 있는 검색 필터링 데이터이다.

나는 아까 작성자를 기준으로 검색했다. 이번엔 제목을 기준으로 t를 검색한 요청 데이터를 확인해보자.

option_val의 값이 title로 바뀌었다.

 

현재 페이지에서 사용 중인 쿼리는 대략 이런 형태일 것이다.

select userid, title, view, date
from notice
where $_POST['option_val'] LIKE '%____%'

 

option_val의 값이 조건절의 컬럼을 결정하고 있는 것이다.

 

검색어 입력값인 board_result에서는 입력창에 t%' and '1%'='1을 입력하여 sql injection이 일어나지 않았던 것을 확인했다.

컬럼 이름에서 sql injection이 가능한지 확인해볼 차례이다.

 

 

where title like '%t%'

위와 같은 쿼리에서 컬럼이름에 어떤 값을 입력해야 sql injection이 가능한지 판단할 수 있을까?

(주석은 최대한 넣지 않는 것으로 한다. 어차피 실무에선 주석이 잘 안 먹힌다고 한다.)

 

일단 like '%t%'가 작동해야하기 때문에 title은 like와 떨어뜨릴 수 없다.

그럼 공략해야하는 위치는 title의 앞부분이 될 것이다.

컬럼명의 앞부분에 참(1=1)과 거짓(1=2)을 넣어 요청에 대한 결과를 확인해본다.

 

where title and 1=1 like '%t%' 가 안되는 이유

더보기

and 연산자를 사용할 땐 and 좌우에 있는 조건이 모두 참이어야 이게 실행이 된다.

 

좌: where title

우: 1=1 like '%t%'

 

좌는 그렇다고 쳐도 오른쪽에 있는 구문은 문제가 있다. 1=1이라는 논리값(참)에서 t가 포함됐는지 문자열을 찾아내고 있기 때문이다. 논리값에서 문자열을 찾을 수 없다. 이는 오류를 유발한다.

그래서 참인지 거짓인지 판별하기 전에 오류가 나버리는 문장이라 안되는 것 같다.

 

그니까 실행 순서가

and좌/우의 참거짓 판단

-> (에러가 없다면) and 연산자 처리(두 조건 모두 참인 행 색출)

이런 식이라 에러에서 부터 막힌 것 같다.

 

사실 이것도 수업 들을 때 그러려니 했는데 스터디원 분이 질문을 올리셔서 찾아보았다. 굿.

 

 

where '1'='1' and title like '%t%'

'1'='1' and title의 결과

 

where '1'='2' and title like '%t%'

'1'='2' and title의 결과

 

(참고로 %'와 같이 맞춰야할 쿼리문의 서식(?)이 없는 것 같아서 따옴표를 써야하나 고민했는데 안 써도 똑같이 작동한다.)

 

참의 결과는 t를 검색한 결과와 같은 데이터를 출력하고 있고 거짓의 결과는 아무것도 출력하고 있지 않은 것을 보아 사용자가 입력한 쿼리문이 서버에서 쿼리문의 일부로 해석되고 있다는 것을 알 수 있다.

컬럼명에서 SQL Injection이 일어날 가능성이 있다.

 

그럼 여기서 어떻게 DB의 데이터를 추출할 수 있을까? 

기징 빠르게 떠올릴 수 있는 방법은 blind sql injection이겠지만 이건 최후의 수단으로 남겨두자.

 

where 1=1 and title like '%t%'

 

첫번째로 1=1 union select를 하는 경우이다.

이 경우엔 조건절이 무효화되는 거나 마찬가지이기 때문에 컬럼명 이후를 주석처리 해야한다.

 

라~~~고 생각했는데 이게 웬걸???? 주석처리를 하지 않아도 결과가 나왔다.

 

where 1=1 union select 1,2,3,4,5,6,7,8,9,10 order by 1 like '%t%'

 

1=1로 where절을 무효화시켜 모든 게시글을 불러오고 그 밑에 union select로 행을 추가한다. 그렇게 만들어진 테이블에 order by로 정렬을 적용시키는데 이때 like '%t%'를 order by 절에 포함시키게 되어 order by 1 like '%t%'가 실행된다. 근데 1 like '%t%'가 정렬 시 아무 의미 없는 조건이라 order by 절은 없는 거나 마찬가지인 상황이 만들어진다.

 

***order by 1 like '%t%' 

더보기

=> 1 like '%t%'의 참/거짓을 먼저 평가함. -> (이때 1은 컬럼 인덱스가 아님) -> 1은 문자열로 바꿔도 1임 -> 1에는 t가 포함되지 않음 -> 1에 t 포함되었느냐? ㄴㄴ. 결과 거짓 -> 거짓이라는 결과가 모든 행에 할당 -> 우선순위 없음. 정렬 적용 안됨. ->

 

order by 뒤에 붙는 숫자를 컬럼 인덱스로 쓸 거면 order by 1와 같이 명확히 써야하는 듯.

아래에서 다룰 case when 구문에서도 다시 언급할 예정이다.

 

++order by title like '%t%'

=> title like '%t%'의 참/거짓을 먼저 평가함. -> title컬럼의 데이터가 t를 포함하고 있는 행은 참(1) -> 포함하지 않은 행은 거짓(0) -> 포함하고 있는 행이 우선해서 정렬됨. 

 

하지만 이렇게 하면 union select의 값이 보이지 않는다.

 

union select의 값을 화면에 출력시키기 위해서 where절의 조건을 이용해 union select의 데이터를 제외한 어떤 게시물도 출력되지 않게 해야한다.

예를 들면 이런 식이다.

where username='' union select 1,2,3,4,5,6,7,8,9,10 order by 1 like '%t%'

 

1) where 조건절에서 username 컬럼을 명시하지만 값을 주지 않아 아무 게시글도 추출하지 않게 만들었다.

2) 그 결과 밑에 union select로 행을 이어붙였다.

3) order by 1로 like '%t%'를 무효화시켰다.

 

이렇게 해서 union select의 데이터를 통해 화면에 표시되는 컬럼이 서버 쿼리의 몇 번째 컬럼에 있는지 알아낼 수 있다.

공격 포맷을 만들었으니 이 이후는 지난주차의 실습과정과 동일하다.

 

 

여기까지 왔다면 이런 의문이 들 것이다.

컬럼 개수는 어캐 앎?

 

그건 여기서 알아낸 게 아니다... 게시물 읽는 페이지랑 수정하는 페이지에서 알아낸 것이다.

거기서는 url에 sql injection을 적용했다. 

 

게시물 읽는 페이지. 버프스위트 때문에 css가 안 먹는다.

 

url에서 볼 수 있듯이 이 페이지에서는 게시물 id를 기준으로 데이터를 긁어온다.

그니까 이 페이지의 쿼리는 대충 이런 식일 것이다.

select ________ from board where id = '____'

 

이 형태는 나에게 아주 익숙하다. id 입력하는 곳에 sql injection 절차를 그대로 밟으면 되기 때문이다.

대신 url 형식을 지켜야한다.

 

사용 중인 컬럼 개수를 알기 위해 /findSQLi_2/notice_read.php/?id=60+order+by+1 부터 order+by+11까지 확인해본 결과, 11에서 데이터가 나오지 않는 것을 확인했다. 

 

오 컬럼 10개 쓰고 있군.

~수업 듣는 중~

뭬? 컬럼 이름에서 sql injection이 가능해?! 당장 해봐야지

~실습해보는 중, 근데 이제 주석 달아서..~

여기서도 컬럼을 10개 쓰고 있구만.

 

이런 흐름으로 알게된 것이다.

 

게시글 리스트 페이지에서 컬럼 개수를 알아내고 싶다면

where username='' union select 1,2,3,4,5,6,7,8,9,10 order by 1 like '%t%'

위 공격 포맷의 union select에서 1에서 11까지 넣어보면 될 것이다. (해보니까 됨.)

 

 

 

풀리지 않는 궁금증 한 가지.

like '%___%'에서는 sqli가 안되는데 컬럼명에서만 일어나게 할 수가 있나????

결과를 보아하니 가능한 것 같은데 그럼 쿼리문에 넣기 전에 어떤 처리를 했다는 뜻인가? 어캐 한 거지????

 

 

 

 

 

▣ 예제_3

- order by절의 정렬 기준을 동적으로 할당하는 경우

 

위 예제에서 사용된 사이트와 다른 사이트임.

 

order by 왔다. (얘때문에 이번주 내내 머리속에 에델바이스 재생됨.오ㄹ더바이스..오ㄹ더바이스...)

 

이전처럼 이 페이지의 검색창에 t를 검색한 뒤 요청 패킷을 분석해본다.

 

t를 검색한 요청 패킷

post 방식으로 파라미터를 전달하고 있다.

전달된 데이터는 아래와 같다.

option_val=username
board_result=t
board_search=%F0%9F%94%8D
date_from=
date_to=
sort=title

 

option_val와 board_result를 보았을 때 서버에서 사용하는 쿼리는 예제 2번과 같을 것이다.

select ______ from board where [option_val] like '%[board_result]%'; 

 

이때 전에는 못 보던 파라미터가 생겼다.

sort=title 값이다.

 

sort가 무슨 뜻인가. 정렬이라는 의미를 가지고 있는 영단어이다.

정렬에 title을 파라미터로 가지고 간다? order by에 사용되고 있을지도! 라는 추측을 할 수 있다.

 

추측이 사실인지 확인하기 위해 sort의 파라미터인 title대신 1과 2, 9999999를 입력해본다.

이 숫자는 컬럼 인덱스로 쓰여 sort의 파라미터가 order by에 사용되는 것이 맞다면, 99999를 입력했을 때 서버에서 사용 중인 컬럼의 개수를 초과하여 화면에 데이터가 출력되지 않을 것이다.

 

sort=1 요청 결과. 데이터가 정상 출력되고 있다.

 

sort=999 요청 결과. 데이터가 출력되지 않고 있다.

 

각 입력 결과를 봤을 때 서버의 쿼리는 아래와 같이 추측할 수 있다.

select ______ from board where [option_val] like '%[board_result]%' order by [sort];

 

option_val 컬럼 데이터에 board_result의 값이 포함된 행을 뽑아 sort 컬럼 기준으로 오름차순 정렬하여 테이블을 추출하라는 쿼리문이다.

 

이 사이트도 컬럼이름으로 sql injection이 가능할 것 같지만 여기서 다룰 포인트는 order by 절이다.

order by 절은 참(1=1)/거짓(1=2) 데이터를 어떻게 넣어야 할까??

order by 1=1? ㄴㄴ

 

여기서 활용할 수 있는 게 case when 문법이다.

 

 

case when

case when (조건) then (참일 때 결과) else (거짓일 때 결과) end

case when 구문의 기본 문법이다.

 

예를 들어, case when (1=1) then 1 else 2 end 라는 구문이 있을 때

항상 참이므로 이 case when 구문은 1이라는 값을 반환할 것이다.

(1=2) 를 대신 써넣으면 2라는 값을 반환한다.

 

 

order by 절에서 SQL Injection을 시도하려면 case when이 필요하다.

사용 예시는 두 가지 경우로 나눌 수 있다.

 

1) 서버에서 사용 중인 컬럼 이름을 알고 있을 경우

2) 서버에서 사용 중인 컬럼 이름을 모를 경우 

 

 

컬럼 이름

 

만약 서버에서 사용 중인 컬럼 이름이 파라미터 값과 같다거나 해서 내가 이미 컬럼 이름을 알고 있을 경우이다.

이때는 참일 때 title컬럼을 기준으로 정렬시키고, 거짓일 때 username 컬럼을 기준으로 정렬시키는 방식으로 DB 데이터를 알아낼 수 있다.

CASE WHEN (조건) THEN title ELSE username END

 

이제 조건의 괄호 안에 필요한 페이로드를 넣는 것이다.

 

예를 들어 blind sql injection으로 db명을 알아내겠다 하면

case when (ascii(substr((select database()), 1, 1)) > 79) then title else username end 

 

와 같은 데이터를 넣어 db명의 첫글자 아스키코드가 79 이상이면 title컬럼을 기준으로 정렬되고 79보다 작으면 username을 기준으로 정렬되는 식이다.

 

위의 페이로드를 sort에 입력한 결과. title을 기준으로 정렬되었다.

근데 저거 뭔가 정렬이 이상함

더보기

내 생각에 title을 기준으로 정렬됐으면 te, test, 문, 테스트 순서로 정렬될 것 같은데 문, 테스트, te, test로 정렬됨.

왜지?!

 

1) 79 대신에 0을 넣어서 시도

=> 결과 그대로 (문, 테스트, te, test)

2) blind sql injection없이 단순히 title을 기준으로 정렬했을 때(sort=title)

=> 결과 그대로 (문, 테스트, te, test)

3) username으로 정렬했을때(sort=username)

=> 정렬 순서가 바뀌어 업로드 날짜가 가장 오래된 게시글부터 정렬됐다.

(게시글의 사용자명은 test로 똑같기 때문에 다른 컬럼으로 확인)

 

blind sql injection의 결과와 sort=title을 넣은 결과를 비교했을 때, 같은 결과가 나왔기 때문에 실행 결과는 참(서버 DB명의 첫글자가 아스키코드 79보다 크기 때문에 title 기준 정렬)이 맞는 것 같긴 하다.

 

챗지피티는 영문이 먼저가 아니라 한글이 우선 정렬된 이유로 utf8mb4_general_ci 등의 DB 데이터정렬방식의 문제일 수 있다고 하는데 내 db에서 실험했을 땐 저런식으로 안나왔다!!!!!

 

그래도 sort=binary title로 하니까 te, test, 문, 테스트 순서로 정렬되긴했다...

binary title은 문자열을 바이트 순서로 정렬해서 영어 먼저 오게 되어있다고 한다.

뭔가 정렬 순서가 생각과 다르다 싶으면 컬럼 이름 앞에 binary를 붙여보는 것도 방법일 것 같다.

 

하지만 서버에서 사용되는 컬럼명을 항상 알 수 있는 것은 아니다.

 

 

 

컬럼 이름 모름

 

그럴 때를 대비해 노말틱님께서 추천해주신 방법이 있다.

참이면 title 컬럼을 기준으로 정렬하고 거짓이면 username 컬럼을 기준으로 정렬하는 방식과 달리,

 

조건 결과가 참일 경우 그대로 게시물을 출력하고, 조건이 거짓일 경우 SQL 에러를 유발해 화면에 아무것도 출력되지 않게 하는 방법이다.

 

 

기본 형태는 아래와 같다.

case when (1=1) then 1 else (에러유발코드) end

 

이 쿼리는 항상 참이기 때문에 1을 반환할 것이다.

이제 (1=2)일 때 에러를 어떻게 유발할 것인지가 문제이다.

 

 

  • SQL에러를 유발하는 방법

1. 사용 컬럼 개수를 초과하는 999999를 넣을까?

 

ㄴㄴ 이 방법으론 에러를 유발할 수 없다.

order by case when (1=2) then 1 else 9999999 end;

위 쿼리에서 999999가 컬럼 인덱스라고 생각하기 쉽지만 SQL은 이것을 단순 문자열로 취급하기 때문이다.

 

(1=2)라는 코드에 따라 모든 행은 거짓이라고 판단되어 999999라는 정렬 기준값을 받을 것이고 모든 행의 정렬 기준값이 999999로 동일하기 때문에 정렬은 불가능하다.

그래서 예제 2번에서 봤던대로 에러가 일어나는 것이 아니라 order by 가 작동하지 않게 하는 효과를 만든다.

 

에러를 유발하기 위해선 SQL이 이러지도 저러지도 못하게 만들어야한다.

예를 들면 매트릭스를 발생시키는 것이다.

 

 

2. 매트릭스 발생시키기

 

***매트릭스란?

다중 행/열을 가지고 있는 테이블 데이터.

 

case when 구문은 다중 행/열을 결과값으로 처리하지 못해 에러를 유발한다.

쿼리로 표현하자면 이런 형태이다.

case when (1=2) then 1 else (select 1 union select 2) end

 

case when 구문은 결과로 단일 행, 단일 열의 단일 값을 요구한다.

하지만 밑줄 친 부분은 1이라는 데이터 행 밑에 2라는 데이터 행을 이어붙이고 있으므로

1
2

 

이때 SQL 엔진은 이 값 중 뭘 선택해야할지 알 수 없어 오류를 일으키게 된다.

 

문제 사이트에서 실습해보자.

sort=case when (ascii(substr((select database()),1,1))>79) then 1 else (select 1 union select 2) end

 

이 결과가 참이면 화면에 게시글이 표시되고 거짓이면 화면에 게시글이 표시되지 않을 것이다.

 

이게 (ascii(substr((select database()),1,1))>79)를 실행한 결과이다.

정상적으로 게시글이 표시되고 있다.

 

그리고 이게 (ascii(substr((select database()),1,1))>126) 를 실행한 결과이다.

db이름의 첫 글자가 아스키코드 126보다 크지 않아서 case when구문의 거짓 결과를 표시하고 있다.

case when구문의 거짓 결과는 (select 1 union select 2)로 매트릭스를 발생시켜 에러를 유발하고 있다.

 

 

서버에서 사용하는 컬럼의 이름을 알 경우 

case when (조건) then title else username end

처럼 조건의 결과에 따라 정렬 기준 컬럼을 지정하는 방식을 사용할 수 있고

 

서버에서 사용하는 컬럼의 이름을 모를 경우

case when (조건) then 1 else (select 1 union select 2) end

처럼 조건 결과가 거짓일 때 쿼리 에러를 유발하여 참, 거짓 결과를 구분하는 방법을 사용할 수 있다. 

 

 

그런데 참/거짓 결과의 차이가 없을 경우도 있다고 한다.

이럴 때는 어떻게 처리해야할까?

 

 

 

 

▣ 예제_4

- 참/거짓 결과의 차이가 없는데 에러에 대한 반응은 있는 경우

 

 

예제 1,2,3 사이트와 다른 사이트의 마이페이지 화면이다.

이번 실습은 예제 1번과 비슷한 환경이다.

마이페이지에서 DB 데이터에 접근할 때 쿠키를 사용하고 있기 때문이다.

 

버프 스위트를 통해 쿠키 데이터를 수정했더니 마이페이지의 아이디에 수정된 쿠키 데이터가 표시되고 있다.

 

마이페이지에서 사용되는 select문이

select _____ from member where id = '$_COOKIE['username']';

 

이런 형식이라면 쿠키를 DB에 없는 id로 수정했을 경우 가져올 수 있는 데이터가 없기 때문에 no text 등의 행에 아무 글자도 표시되지 않아야 한다.

 

예제 2번의 경우도 이 현상을 이용하여 sql injection 가능 여부를 판단했지만

이번 문제 사이트에서는 

  • test로 변조
  • testmany로 변조
  • test' and '1'='1로 변조
  • test' and '1'='2로 변조

쿠키를 어떻게 변조하든 아이디 표시부분만 바뀔 뿐 다른 차이가 표시되지 않았다.

 

차이가 없는 이유 추측

더보기

1) 쿠키로 DB에 접근하는 게 아닐 경우

2) no text가 DB에서 긁어온 데이터가 아니고 html에 찍혀나오는 경우

3) select문 결과가 나오지 않을 경우에도 no text가 나오도록 처리

 

아마 2,3번 짬뽕되지 않았을까 싶다. 삼항식 쓰면 될 거 같은데. 아무튼 1번은 아님.

1번이 아닌 이유는 아래 내용 참고

 

그러다 드디어 차이를 보였는데!!!

  • test' 로 변조

했을 경우 아래와 같은 응답 페이지를 표시하였다.

cookie:user=test'

이로써 쿠키로 DB에 접근하고 있음은 확실해졌다.

이젠 이걸 이용하기만 하면 된다.

 

 

사이트가 응답한 이유는 쿼리에 에러가 발생했기 때문이다.

그럼 일부러 에러를 유발하면 될 일이다.

 

앞서 SQL에 에러를 유발하기 위해 매트릭스를 발생시킨 바가 있다.

select 1 union select 2

 

이것을 활용해보자.

 

 

1)  test' and (select 1 union select 2) and '1'='1

 

-> 매트릭스 데이터가 and로 묶여 무조건 에러나는 코드

-> 참/거짓 활용 못함.

-> 참을 만들기 위해서는 select 1만 뽑아야 함.

-> union select 2를 없앨 조건 필요

 

union select 2를 어캐 없앤담!!??

where 조건절을 추가하여 거짓값을 갖게 하면 union select는 아무 값도 반환하지 않음.

 

 

2) test' and (select 1 union select 2 where (1=2)) and '1'='1

 

-> select 1 / union select 2 where 1=2

-> (1=2)는 거짓

-> where조건절은 참인 행만 반환

-> 항상 거짓이면 아무 행도 반환하지 않음

-> select 1의 결과만 반환

-> select ___ from member where id=' test' and 1 and '1'='1 ' 실행

 

 

3) test' and (select 1 union select 2 where (1=1)) and '1'='1

 

-> select 1 / union select 2 where 1=1

-> (1=1)은 항상 참

-> where조건절은 참인 행만 반환

-> 항상 참이면 모든 행 반환

-> select 1 union select 2 라는 2행 테이블 반환

-> 매트릭스 발생

-> DB Error... 출력

 

 

test' and (select 1 union select 2 where (1=2)) and '1'='1 의 결과

 

 

test' and (select 1 union select 2 where (1=1)) and '1'='1 의 결과

 

 

저 쿼리를 넣을 경우 서버에서 실행될 쿼리는 이런 형태이다.

select ____ from member
where id ='test' and (select 1 union select 2 where ____) and '1'='1'

 

 

where 조건절을 추가하여

1) 참/거짓 데이터를 넣을 공간( where (____) )을 마련하고 

2) 참/거짓에 대한 결과 차이를 눈으로 확인할 수 있게 되었다.

 

 

이제 이 공격 포맷으로 Blind SQL Injection을 시도해보자.

 

test' and (select 1 union select 2 where (ascii(substr((select database()),1,1))>79)) and '1'='1

 

where 조건절에

서버의 DB명이 아스키코드 79보다 큰지 묻는 쿼리를 만들었다.

79보다 크면(참) 매트릭스가 발생하여 화면에 DB Error...가 출력될 것이고 79보다 크지 않으면(거짓) 화면에 마이페이지가 출력될 것이다.

 

 

실행 결과 DB Error...가 출력되고 있다. 서버의 DB명 첫글자가 아스키코드 79보다 큰 글자라는 뜻이다.

이를 통해 DB 이름 첫 글자의 아스키코드는 79이상, 126이하라는 사실을 알아냈다.

 

 

79와 126의 중간 값인 102.5의 정수형 102를 기준으로 잡아보자.

test' and (select 1 union select 2 where (ascii(substr((select database()),1,1))>102)) and '1'='1

 

실행 결과 DB Error...가 출력되고 있다. 참이라는 뜻이다. 아스키코드 범위는 102이상 126이하로 좁혀진다.

 

102와 126의 중간 값인 114도 같은 결과를 출력하고 있다.

아스키코드 범위는 114이상 126이하로 좁혀진다.

 

 

114와 126의 중간값인 120으로 실행해보았다.

 

test' and (select 1 union select 2 where (ascii(substr((select database()),1,1))>120)) and '1'='1

페이지가 정상출력되고 있으므로 결과는 거짓이다.

아스키코드 범위는 114이상 120이하로 좁혀진다.

 

 

 

이런 식으로 참/거짓 결과를 구분할 수 없을 때 DB 에러가 표시될 경우 select 1 union select 2에 where 조건절을 추가시켜 참/거짓에 대한 에러를 유발시킬 수 있다.

 

이건 빙산의 일각이고 에러를 유발시키는 방법은 어어어엄청 많다고 하니 그때그때 사이트의 특성에 맞게 고민해야할 것 같다!

 

 

 

SQL Injection은 쿼리가 실행되는 곳에서 일어날 수 있다.

이건 단순히 사용자 입력창에서만 일어나는 것이 아니다.

 

이번 주차에서는 사용자 입력창을 제외한 여러 SQLi 포인트를 실습을 통해 배워보았다.

1) 쿠키

2) 동적으로 할당되는 컬럼명

3) order by 절

 

대부분의 경우에 order by절에서 일어난다고 한다. 그 이유는 prepared statement와 관련이 있는데 자세한 내용은 SQLi 대응 방안에 대한 포스팅에서 설명.