주제 : Webhacking Study - Query sniff
날짜 : 2018. 04. 06
작성자 Sakuya Izayoi


1. 개요 및 목적

Secuinside 2017에서 발표된 SQL - MITM Attack(Query sniff)에 대해 알아보고 그 쿼리에 대해 연구한 내용을 정리한다. 


2. 내용 

이번 문서도 하나의 발 에서 시작된다. 발표의 첫 시작은 간단한 SQL injection을 먼저 소개하고, Query sniff에 대해 소개하는 내용이었다.
발표를 들을 당시에는 "음.음. 그렇군" 하고 넘어갔던 부분이 막상 기억이 제대로 나지 않아서 다시한번 확실히 잡고 가기로 하는 취지에서 정리하게 되었다.

발표 첫 페이지부터 웹 분야를 연구하는 사람들의 기를 살려주는 강렬한 문구를 접할 수 있다.
간단한 SQL injection의 설명은 검색하면 쉽게 구할수 있는데다, 본 발표에서도 간략하게 설명하고 있으니 여기서는 생략하도록 한다.

아래는 본 연구에 사용한 작업환경이다.

MYSQL : 5.7.21-0ubuntu0.16.04.1
OS : Ubuntu 16.04 

우선 문서에 적힌 복잡한 쿼리를 이해하기 전에 필요한 사전 지식에 대해서 확실히 짚고 넘어가도록 하자.

3.Base Knowledge

웹 분야의 공격 기술에 대해 조금이라도 학습을 해 본 사람이라면 SQL injection이 상당히 친근할 것이고, 특히나 각종 해킹방어대회 등에 출전하는, 혹은 워게임 사이트 등을 취미로 하는 사람들이라면 Mysql이 상당히 친숙할 것이다. 이 Mysql에서는 5.0 이상 버전부터 Information_schema라고 하는 특수한 DB를 제공한다.

이 DB에는 여러가지 유용한 테이블들이 있는데, 공격자의 관점에서 바라볼 때 자주 언급되는 테이블은 TABLES, COLUMNS, SCHEMA, PROCESSLIST 정도가 아닐까 싶다.
TABLES 테이블은 임시테이블을 제외한 모든 테이블의 정보가, COLUMNS는 모든 컬럼에 대한 정보, SCHEMA는 DB에 대한 정보가 들어있으며 PROCESSLIST는 현재 실행중인 쿼리에 대한 정보가 들어있다. 이번시간에 중점을 두는것은 PROCESSLIST 테이블이다.


3.1 PROCESSLIST

조금만 화제를 돌려서 펜테스트(모의해킹)를 진행하는 사람이 되었다고 생각해보자. 단지 OWASP TOP 10이나, 국내 기업의 사이트를 대상으로 한다면 국정원 8대취약점 등을 리스트로 만들어서 체크하는 그런 기계적인 모의해킹이 아닌, 실제 공격자의 입장이 되어서 진행한다고 생각해보자.
가장 짜릿한 순간은 웹쉘을 올리는데 성공하였거나, DB내의 정보를 획득하는데 성공하여 '공격에 성공했다' 라고 담당자에게 말하고 패치를 하도록 도와주는 부분이 아닐까?

문서의 성격에 맞게 DB내의 정보를 획득하는데 성공했다고 가정하자. 하지만 그 값이 해쉬, 혹은 암호화가 되어서 저장되어 있다면 과연 담당자에게 '공격에 성공하여 데이터를 획득하는데 성공했습니다' 라고 말한다면 돌아오는 담당자의 답변은 무엇일까?
'아, 그럼 당장 패치를 해야겠군요. 방안을 알려주시면 저희가 처리하겠습니다' 와 같은 이상적인 답변은 거의 돌아오지 않는다. 대부분 돌아오는 답변은 '단방향 암호화 되어서 저장하는데, 평문을 획득하신데 성공하신건가요?' 라고 되물을 확률이 상당히 높다. 경험상 평문으로 저장하는곳은 거의 없었으며, 대부분 간단하게라도 해쉬를 채택하고 있었다. 그럼 우리가 되돌려줄 답변은 '아뇨, 하지만 언제든지 취약점을 통해 공격 받을 가능성이 있습니다' 라고 답변하면 패치를 해줄 확률은 반반이다. 사실 이러한 보안적인 업무 말고도 하는 일이 많으니 '취약점이 발견되었으니 얼른 고치세요!' 하고 볶아 챌 수도 없는 노릇이고, 이런 선의에서 비롯된 모의해킹에서 점점 실망하게 되는 이유중 하나가 아닐까 싶다.

이러한 문제를 해결하기 위해 가장 간단한 방법은 평문을 획득하는것이다. 하지만 말처럼 쉽게 해결되는 부분이 아닌것이지만, PROCESSLIST를 통한다면 가능하다.

PROCESSLIST의 테이블 구조를 보면, 어떤 쿼리를 실행중(info)인지, 어떤 유저(user)가 실행했는지, HOST는 어디인지(host) 등등 자세한 정보가 나온다. 여기까지 읽으면 위에 적은 평문을 획득한다는 내용과 바로 연결지어 생각하기 어렵다. 하지만 다음의 PHP코드를 보도록 하자.

모든 자세한 정보가 주어지지 않는 블랙박스 테스트 환경에서는 가정(Guessing)에서 부터 출발한다. 하지만 이번엔 코드를 알고있거나 혹은 필요한 코드만 받아서 진행하는 화이트박스/그레이박스 환경이라고 생각하자.

이 소스코드를 보고 2가지를 알 수 있다.
첫번째는, 해당페이지에서 SQL injection을 막기 위해서 Prestatement를 사용하여 작성했다는것
두번째는, 특별한 암호화를 PHP내에서 거치지 않고 mysql측에 맡기고 있다는것이다.

이러한 사실을 토대로 내릴수 있는 결론은 여러가지 있겠지만, 필자가 내린 결론은 아래와 같다.
1. 해당 페이지에서의 일반적인 SQL injection공격은 막혀있다. 
2. 해당 페이지에서 암호화를 거치지 않는것은 global.php에서 특별한 처리를 해주고 있는것은 아닐까?
3. 우선 해당 페이지는 다른 곳에서 취약점이 있을때 추가적으로 확인해 볼 가치가 있겠다.

사실 이러한것들 말고도 여러가지 떠올릴수 있다. 주어진 것은 소스코드의 단편뿐이고 사람의 생각(망상)은 무한하게 펼쳐질 수 있다. 우선은 global.php에서 암호화를 하지 않고 평범하게 평문을 받아서 mysql Query를 통해 md5 해쉬를 거친 후 DB에 저장, 요청한다고 가정하자.

그렇다면 지금이 바로 PROCESSLIST가 나설때다. 소설과 같은 아래 타임라인을 보도록 하자.

유저가 로그인을 시도하고 해당 쿼리가 끝나기전 공격자가 processlist에 접근하고 그 결과값을 받는다면, 유저의 로그인 쿼리는 끝나지 않았으므로 PROCESSLIST에 남아 있을것이고, 여기에 적힌 md5(평문) 값을 볼 수 있을것이다. 유저와 공격자 간의 PROCESSLIST를 두고 벌이는 경쟁에서 이긴다면, 관리자에게 '평문을 획득했으니 얼른 패치를 하시는게 좋지 않겠습니까?' 라고 당당히 말할 수 있다. 하지만 이런 타임라인을 들이대면서 '이론상 된다구요!' 라고 말해봐야 별 설득력 없이 끝나니, 필요성을 느꼈다면 실제로 어떻게 해야할지 천천히 알아보자.

3.2 benchmark

뜬금없이 benchmark 이야기를 꺼내는것은 성능을 테스트하기 위한것이 아니라, Time-based SQL injection 공격에 사용하는 sleep의 대체용인 그 benchmark이다.
해당 함수는 같은 동작을 여러번 반복하는것으로 그 성능을 체크하기 위해 만들어진 함수인데, 같은 행동을 여러번 한다는 그 특징때문에 복잡한 연산을 시킬경우 시간지연이 발생하며 이를 sleep의 우회방안으로 사용하고 있는것이 공격자의 현 실정이다.

위에서 언급했지만, 같은 동작을 여러번 반복하는 함수이다. 이를 확인하기 위해서는 복잡한 연산을 시켜서 얼마나 긴 시간이 걸렸는지 체크하는 것이 맞으나, 그건 여러분들이 SQL injection에서 많이 해 봤으리라 생각한다. 그럼 이 반복을 확인하기 위해서 다음으로 적합한 것은 변수를 사용하는 것이다.
변수를 사용할 때 하나 재미있는 점은 하나의 Query로 변수의 변경사항을 바로바로 체크할수 있다는것이다.

해당 쿼리를 보면 before 에서는 @a변수를 1로 초기화 하였고, calc에서는 @a값에 +1 을 한것을, result에서는 그 결과를 출력하게 했다.
결과값을 보면 알겠지만, @a가 각각 1,2,2 로 되어 있는것을 볼 수 있다. 계산이 된 결과값을 출력하며, 그 이후로는 그 결과를 계속 가지고 있게된다. 이후 같은 세션이라면 해당 변수의 값은 유지된다. 

그럼 여기서 benchmark를 엮어서 한번 보도록 하자.

@a:=@a+1을 9번 적지 않아도 benchmark 하나로 1을 9번 더해서 10이 된 것을 볼 수 있다. 물론, benchmark는 벤치마킹을 위해 제작된 함수인 만큼 쿼리문을 실행 할 수도 있다.

쿼리가 좀 복잡해졌지만, 잘 보면 @a+1 대신에 concat을 통해 @a에 rand()*10의 값을 이어 붙여준 값을 대입하도록 해놓았다. rand함수는 0~1 사이의 랜덤한 float값을 내놓는데, 이 값에 *10을 한 후 floor를 통해 한자리 정수를 얻을수 있게 제작한 것이다.

결과적으로, 9자리의 숫자가 결과값으로 돌아왔다. 물론 rand함수의 특성상 실행할때마다 결과는 달라진다.
자, 기초적인 내용은 끝이 났다. 여기까지 읽었다면 이를 PROCESSLIST와 이미 엮어본 사람도 이미 있을것이다.

점점 복잡해지는것이 느껴진다. benchmark의 횟수를 2회로 줄이고 확인하면 result에 processlist의 컬럼 중, info의 값을 계속 이어붙이도록 했다. 보기 편하도록 개행과 탭을 넣었지만, 실제 환경에서는 공격자가 알아볼 수만 있으면 되므로 크게 문제 없는 부분이다. 중요한것은, 내가 이 쿼리를 실행중일때는 다른 쿼리문들도 이 변수에 값이 저장되어 확인 할 수 있다는 점에 있다.

사용자가 많고 동시다발적으로 처리되는 대형 포털사이트 같은 경우엔 딱히 지연을 주지 않아도 몇몇개는 운좋게 묻어 나올것이다. 하지만 일반 기업과 같은 곳은 그렇지 못할 확률이 상당히 크다. 그렇다면 의도적으로 지연시킬 수 밖에 없는데, 이때 어떻게 해야 할 것인가에 대해 몇가지 꼼수가 떠오르는 사람이 있을것이다.

1. 일단 benchmark의 횟수를 늘린다. 결과값이 수천만개가 엮여 나오더라도 내가 원하는 정보만 있으면 된다.
2. benchmark의 횟수를 크게 늘리지 않고 sleep(1)을 통해 해결한다.

각각의 장단점이 있다.
1. benchmark횟수를 늘린다면 좋은 시간지연이 되겠지만, 나오는 결과값을 감당할수 없을만큼 값이 거대해진다. 물론 이 경우 where 조건절을 조금만 활용하는 것으로 해결 할 수 있다.

내가 임의의 변수에 나만의 고유문자열(여기서는 Sakuya)를 넣고 info에 고유문자열이 들어가는 쿼리는 제외해 버리면된다.

2. sleep(1)을 줄 경우 경쟁관계에서 불리해 지기만 하니 별로 필요가 없다 -(From rubiya)

3.3 Sniffing

취약한 게시판 소스를 하나 뚝딱뚝딱 하도록 하자.

그리고 이 게시판 소스를 활용해서


uniqid+mt_rand가 포함된 임시테이블의 이름, 컬럼명, 값을 알아 낼수 있는지에 대한 테스트를 진행하자.


위에서 설명한 것을 토대로 Injection Query를 작성하도록 하자.

'),('answer','sakuya',(SELECT a.b FROM (SELECT @dummy, @query:='',@tmp:=0x20, benchmark(500000,(@tmp:= (SELECT Group_concat(info) FROM information_schema.processlist WHERE info not like '%dummy%' or sleep(0)))or(IF((@query not like concat('%',@tmp,'%')),@query:=concat(@query,@tmp,0x0a),0))), @query`b`)`a`))#

IF 이하 구문은 @tmp에 저장된 processlist의 결과를 어떻게 붙여 나갈것인가에 대한 쿼리문으로 이해하면 좋다. 

해당 쿼리문의 결과로 성공적으로 값을 얻어온 것을 볼 수 있다. 이를 입력해주면 문제는 clear 하게 되는것은 덤.


4. 결론 및 팁

이 방법은 prestatement 를 사용하는 SQL의 경우 먹히지 않는다는것을 확인했다. 이러한 경우에 다른 방법이 필요할 것으로 보이며, 해당 Query또한 막상 보면 너무 복잡하고 쓸모없어 보이지만, 모든 지식이 그러하듯 원리를 알면 어디든 응용할수 있지 않을까?



PS. 이건 codegate2018 본선에 사용된 문제의 POC/Writeup 임미다. 참고하실분이 있으련지는 모르겠지만.. 요기있어요

exploit.py

Praise the blanket Writeup.docx


'Web' 카테고리의 다른 글

XSS TEST  (0) 2019.04.15
[TIP?] sql injection을 위한 깔끔한 코드 작성법  (2) 2018.08.21
Webhacking Study - no more BLIND  (6) 2017.04.18
SQLi.py  (0) 2016.11.02
Upload 코드의 흔한 실수  (2) 2016.03.07
Posted by IzayoiSakuya
,