티스토리 뷰

최조 작성: 2014-02-01

최종 수정: 2015-01-20


안녕하세요. Hackability 입니다.


이번에 작성할 내용은 PHP 에서 strcmp 취약점을 이용한 인증 우회 기법 입니다.


아래와 같이 간단히 인증을 하는 login.php 가 있다고 가정해봅니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
 
    $ps = 'pass';
 
    if (!isset($_GET['user_id']) || !isset($_GET['password'])) 
        die("parameter error"); 
 
    if (($_GET['user_id'] == 'admin'
        && (strcmp($ps$_GET['password']) == 0))
        die("login succeed..");
    else 
        die("login failed..");
 
?>


3번째 라인에서는 user_id 와 password 파라미터가 세팅 되었는지 체크하고 


6번째 라인에서는 user_id 가 admin 이고 password 가 ps와 비교합니다. (예를 위해서 미리 $ps 에 값을 대입하였습니다.)


일단 기본 동작은 아래와 같습니다.


http://127.0.0.1/login.php?user_id=admin&password=pass

=> login succeed..


http://127.0.0.1/login.php?user_id=admin&password=asdf

=> login failed..


여기서 재미있는 부분은 strcmp 함수에서 발생됩니다. strcmp 의 함수 내용은 다음과 같습니다.


Description ¶

int strcmp ( string $str1 , string $str2 )


Return Values ¶

Returns < 0 if str1 is less than str2; > 0 if str1 is greater than str2, and 0 if they are equal.


strcmp($a, $b) 를 실행 할 때, $a가 작으면 음수, $b가 작으면 양수, 그리고 $a와 $b가 같으면 0 이 반환됩니다. 만약 우리가 password를 몰라도 strcmp 함수에서 0을 리턴할 수 있는 방법이 있다면 암호 비교 구문을 우회 할 수 있을 것 같습니다.


PHP 특정 버전에서는 위와 같이 우회 할 수 있는 방법이 있는데요. 입력 값으로 배열을 넣으면 strcmp 함수가 0을 리턴합니다. (해당 PHP 버전은 아래 설명합니다.)

(검색 키워드 : strcmp() expects parameter 2 to be string, array given in )


아래는 예제 코드 입니다.


1
2
3
4
5
6
7
8
9
10
11
<?php 
 
$a = Array("a");
$b = 'pass';
 
if (strcmp($a$b) == 0)
  echo "Strings are same !";
else
  echo "Strings are different ...";
 
?>



결과는 "Strings are same !" 으로 출력이 됩니다.


이를 통해 알 수 있는 것은 strcmp 함수에 문자열을 인자로 넣어야 하는데 배열을 인자로 넣으면 반환 값이 0 임을 알 수 있습니다.


그러면 다시 login.php로 돌아가 strcmp 구문을 우회 해보도록 하겠습니다. 우회 코드는 아래와 같습니다.


http://127.0.0.1/login.php?user_id=admin&password[]=a 


위와 같은 입력을 통해 strcmp 의 비교 구문에 문자열이 아닌 배열을 넣을 수 있었고 "login succeed" 를 볼 수 있었습니다.


추가 사항으로 위 취약점은 PHP 버전에 따라 다른 형태를 보입니다. 버전별로 strcmp 반환값을 처리 하는 방식이 다른데요.


PHP 버전 5.2 에서는 strcmp(String, Array()) 시에 Integer (1 or -1) 을 반환하고, 

PHP 버전 5.3 에서는 strcmp(String, Array()) 시에 NULL 을 반환합니다.

(PHP 버전 5.2에서는 Array()를 "Array" 라는 문자열로 변환하여 비교를 하게 됩니다.)


현재 문제가 발생하는 버전은 PHP 5.3 버전인데요. 위 내용으로 유추 해보면 strcmp 함수의 인자 값으로 문자열이 들어 오지 않는 경우 NULL 을 반환하는데 이 부분에서 strcmp의 동작 방식과 충돌이 발생되어 생기는 문제임을 생각해볼 수 있습니다.


문제는 아래와 같이 비교 하는 구문인데요. 

if (strcmp(String, Array() == 0)


PHP 5.3 버전에서 strcmp 입력값으로 Array를 주었을 경우, 결과적으로 아래와 같은 행위를 하게 됩니다.


if (NULL == 0)


우리는 미리 결과를 봤기 때문에 NULL == 0 의 결과가 TRUE 라는 것을 알 수 있는데요, 그러면 결과적으로 NULL은 0 인 것일까요?


아래 PHP 자료형 비교표를 보시면 됩니다.


"==" 으로 느슨한 비교를 한 결과


"===" 으로 엄격한 비교를 한 결과


위 두 표를 보시면 아시겠지만 느슨한 비교를 할 시에는 NULL이 숫자 0과 같기 때문에 0 == NULL 이 TRUE를 반환하지만 NULL이 0과 같은 타입이 아니기 때문에 0 === NULL 시에는 FALSE를 얻게 됩니다.


다시 처음으로 돌아가서 login.php 의 코드를 보면, 이 코드를 보강하기 위해서는 아래와 같이 strcmp 비교 구문을 기존에 "==" 에서 "===" 으로 엄격한 비교를 해주면 보강됨을 알 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
 
    $ps = 'pass';
 
    if (!isset($_GET['user_id']) || !isset($_GET['password'])) 
        die("parameter error"); 
 
    if (($_GET['user_id'] == 'admin') && (strcmp($ps$_GET['password']) === 0))
        die("login succeed..");
    else 
        die("login failed..");
 
?>


결론적으로 PHP 5.3+ 버전에서는 strcmp 시 String 값에 Array 타입이 들어 가게되면 NULL이 반환되게 되는데 이를 "==" 처럼 단순비교 했을 시 생기는 코딩 취약점 입니다. 이를 해결하기 위해서는 위에서 언급했던 것 처럼 "===" 와 같이 강력한 비교를 하거나 is_string, is_array와 같이 입력값에 대한 검증을 한 번 거친 뒤 비교를 해주시면 될 것 같습니다.


2015.01.20 추가 내용


좋은 내용이 댓글에 있어서 내용을 추가 합니다. GET의 경우에는 위와 같이 취약하지만 POST의 경우 역시 위에 의해 취약점이 발생되는지에 대한 내용입니다. 


테스트 환경은 php 5.5 버전에서 진행하였으며 위의 테스트 결과 5.5 버전 역시 strcmp 에 취약한 버전으로 결과가 나왔기 때문에 이 버전으로 POST 테스트를 진행하였습니다.


테스트 코드는 다음과 같습니다.



POST로 패스워드를 받고 "password"와 같으면 admin이라고 해주고 그 외에는 admin이 아니라는 표시를 합니다.


POST로 데이터를 전송하기 위해 curl 프로그램을 사용하였습니다. curl 프로그램의 -X 파라미터를 이용하여 POST로 전송을 하였고, -d 옵션을 이용하여 데이터 부분을 채울 수 있었습니다.



테스트 결과 POST 역시 배열에 의해 GET과 동일하게 취약함을 알 수 있었습니다. 좋은 질문을 해주신 질문자님께 감사드립니다. :)

댓글