아 ~ 오랫만이네요 ... :)
역시 2019년의 그 약속은 지켜지지 않았습니다. 꾸준히 블로깅하려고 했는데 ...
2020년도 되었고, 작년에 지키지 못한 약속도 있고해서 ... 오늘은 PHP 취약점 하나를 분석하고, 그 내용을 블로깅하려고 합니다.
뭐 대단한 내용은 아니니... 잘 아시는 분들은 여기서 뒤로가기를 ...
CVE-2019-11045 취약점은 "Ubuntu Securify Notices"(https://usn.ubuntu.com/)를 모니터링하다가 발견한 취약점입니다. "Ubuntu Security Notices", 줄여서 USN은 꾸준하게 모니터링하는 사이트는 아닌데 ... 혹시 놓친 취약점이 있을까봐 틈틈히 모니터링하고 있습니다.
USN에 있는 취약점 링크는 https://usn.ubuntu.com/4239-1/ 입니다. 총 4개의 취약점이 패치되었습니다. 그 중에서 개인적으로 CVE-2019-11045 취약점이 대상으로 적당할 것 같아서 취약점과 관련된 이슈를 트래킹하고, 분석 및 테스트하는 방법에 대해서 블로깅하려고 합니다.
USN에는 취약점에 대한 간략한 정보와 CVE ID 정도가 공개되기 때문에 빠르게 정보를 습득하고, 관심있는 취약점을 추린 다음, Debian/Redhat/Ubuntu 등의 배포판에서 관리하는 이슈 트래커에서 CVE ID로 검색해서 대상 프로젝트의 이슈 트래커나 git 커밋 로그 등을 확인합니다. CVE-2019-11045에 대한 내용은 아래 링크에서 확인할 수 있습니다.
배포판에서 관리하는 이슈 트래커에서 프로젝트에서 관리하는 이슈 트래커나 git의 커밋 로그 같은 중요 정보를 얻을 수 있습니다.
PHP의 이슈 트래커에 들어가서 취약점과 관련된 내용을 확인해보면 아래와 같이 "취약한 소스 코드", "PoC", "패치"가 잘 공개되어 있는 것을 확인할 수 있습니다.
PoC와 패치된 코드가 아주 간단합니다. PoC를 보면 DirectoryIterator에 전달되는 문자열 중간에 \x00(null character)가 포함되어 있습니다. 취약점 이슈 제목이 "DirectoryIterator class silently truncates after a null byte"이니, 대충 "../../ryat\x00/php"를 전달했는데 PHP가 허락도 없이 마음대로 \x00 이후의 문자열을 제거하고, "../../ryat"만 사용하는 것으로 예상할 수 있습니다.
PoC를 테스트하기 위해 github에서 PHP 프로젝트의 소스를 체크아웃하고, 취약한 버전으로 체크아웃 한 뒤 빌드합니다.
$ git clone https://github.com/php/php-src.git
$ cd php-src
$ git checkout php-7.2.25
$ ./buildconf --force
$ ./configure && make
제가 테스트한 코드와 디렉토리 구조는 아래와 같습니다. 테스트 결과에 보이는 것처럼 \x00 이후의 문자열이 제거되어 ./ddd 디렉토리까지만 파싱되는 것을 확인할 수 있습니다.
그렇다면 패치된 코드는 어떤 역할을 할까요 ? zend_parse_parameters에 들어가는 2번째 파라미터에서 's' 문자가 'p'로 변경되었습니다. PHP 공식 사이트에서 확인해보면 zend_parse_parameters는 사용자가 전달한 파라미터를 파싱하는 역할을 하는데, 이중에서 두번째 파라미터는 printf 같은 함수의 포멧스트링과 똑같다고 생각하면 됩니다. 지금 파싱하는 파라미터가 어떤 타입인지 정의하는 역할을 하는거죠. s는 string, 즉 파싱하려는 파라미터가 문자열일 때 사용하고, p는 파싱하려는 파리미터가 경로명일 때 사용합니다.
그럼 zend_parse_parameters 함수는 두번째 파라미터를 변경하면, 어떤 동작을 하길래 취약점이 제거되는 것일까요 ? 답은 PHP의 Zend/zend_API.c 파일을 분석하면 찾을 수 있습니다. zend_parse_parameters 함수에 전달된 파리미터는 zend_parse_va_args -> zend_parse_arg -> zend_parse_arg_impl 함수에 전달됩니다. zend_parse_arg_impl 함수는 아래 그림에 보이는 것처럼 두번째 파라미터 값에 따라 분기하는 코드가 포함되어 있습니다. 두번째 파라미터에 'p' 문자가 있으면 zend_parse_arg_path 함수가 호출됩니다.
zend_parse_arg_path 함수는 파라미터에 전달된 값이 경로명인지 검증하는 함수인데, 이 취약점의 PoC에 사용된 것처럼 \x00 문자 사용해서 문자열을 임의로 자르는 행위를 방지하기 위해 CHECK_NULL_PATH 매크로를 사용해서 경로명에 \x00 문자가 포함되어 있는지 확인합니다.
CHECK_NULL_PATH 매크로는 strlen() 함수를 사용해서 구한 문자열 길이와 PHP 소스코드에서 전달한 문자열의 길이를 비교해서 두 값이 같으면 \x00 문자가 포함되지 않은 것이고, 두 값이 다를 경우 \x00 문자가 포함되어 있는 것으로 확인하는 매크로입니다. 즉, zend_parse_parameters 함수의 2번째 파라미터에 'p' 문자를 전달하면 경로명에 \x00 문자가 포함되어 있는지 확인하고, 예외처리하기 때문에 이 간단한 패치만으로 취약점이 해결되는 것입니다.
패치 후 동일한 코드를 테스트하면 아래와 같이 예외 처리 코드가 실행되는 것을 확인할 수 있습니다.
PHP에 새로운 기능이 추가되거나, 기존 코드가 변경될 때 zend_parse_parameters 함수의 두번째 파라미터가 잘 설정되었는지 확인해야겠네요 :)
두서 없지만... 또 이렇게 새로운 CVE 하나를 분석했습니다.