개념정리
PHP Wrapper를 이용한 LFI 취약점을 실습하는 문제다.
○ LFI
LFI(Local File Inclusion) 취약점은 웹 애플리케이션에서 발생할 수 있으며, 공격자가 악의적인 목적으로 웹 서버의 파일 시스템에 접근하여 중요한 파일을 읽거나 실행할 수 있는 취약점이다.
LFI는 주로 웹 애플리케이션에서 파일을 동적으로 로드할 때 발생한다. 이때, 애플리케이션은 파일 이름을 사용자 입력값으로 받아들이는데, 이 입력값에 대한 검증이 제대로 이루어지지 않으면 공격자가 파일 시스템의 다른 파일에 접근할 수 있다.
공격자는 LFI 취약점을 이용하여 애플리케이션의 설정 파일, 암호화된 비밀번호, 데이터베이스 정보 등 중요한 정보를 탈취할 수 있다. 또한 공격자는 원격 코드 실행(RCE) 공격을 수행하여 웹 서버를 제어할 수도 있다.
○ PHP Wrapper
PHP wrapper란 PHP에서 파일을 다루는 함수에 대한 인터페이스 역할을 하는 것으로 fopen()
, copy()
, file_exists()
및 filesize()
등과 같은 파일 시스템 함수와 함께 사용하기 위한 다양한 URL 스타일 프로토콜을 위한 많은 내장 래퍼를 제공하는 것을 말한다. 내장 래퍼를 통해 파일 입출력을 수행할 수 있다.
○ PHP Wrapper 종류
file://
wrapper:- 가장 기본적인 파일 시스템에 대한 Wrapper로, 파일을 읽고 쓸 수 있다.
http://
wrapper:- HTTP 프로토콜을 이용하여 원격 서버의 파일을 가져올 수 있는 Wrapper다.
ftp://
wrapper:- FTP 프로토콜을 이용하여 원격 FTP 서버의 파일을 다운로드하거나 업로드할 수 있는 Wrapper다.
zip://
wrapper:- Zip 아카이브 파일의 내용을 가져올 수 있는 Wrapper다.
data://
wrapper:- 문자열을 파일처럼 다룰 수 있는 Wrapper다. Base64 인코딩된 문자열 데이터를 처리할 수 있습니다.
phar://
wrapper:- PHP Archive(PHAR) 파일의 내용을 다룰 수 있는 Wrapper다. PHAR 파일은 PHP 코드와 리소스를 포함한 아카이브 파일이다.
PHP wrapper는 파일입출력을 간편하게 하기 위해 제공된 기능이다.
파일입출력을 수행할 수 있기 때문에 PHP Wrapper를 이용해서 LFI를 수행하면 서버 내부의 파일을 쉽게 읽을 수 있다. 따라서 공격자가 악용하기 쉽기 때문에 PHP Wrapper을 악용 할 수 없도록 검증을 수행해야한다.
문제분석 & 풀이
List를 클릭하면 [그림]와 같이 flag.php, hello.json 파일에 접근할 수 있도록 하이퍼링크가 걸려있다.
hello.json을 클릭하면 [그림]과 같이 ../uploads/hello.json
에 접속하게 되고 hello.json 파일의 내용이 출력된다.
힌트에 따르면 /var/www/uploads/flag.php
에 접속하면 flag가 있다고한다.
flag.php에 접근해보자.
권한이 없다고 한다.
아마도 검증 로직이 존재하는 것 같다. 검증 로직을 파악하기 위해서 코드를 분석해보자.
<h2>View</h2>
<pre><?php
$file = $_GET['file']?$_GET['file']:'';
if(preg_match('/flag|:/i', $file)){
exit('Permission denied');
}
echo file_get_contents($file);
?>
</pre>
view.php를 보면 file 파라미터로 flag
, :
를 검증한다. 따라서 ../uploads/flag.php
에 접근했을 때, flag가 확인되면서 exit('Permission denied')
가 실행된 것이다.
그렇다면, flag라는 키워드를 우회해서 flag.php를 요청하고 읽어야한다는 뜻이다.
고민 끝에 file 파라미터를 우회해서 flag.php를 읽는 방법은 없다고 판단했다.
<div class="container">
<?php
include $_GET['page']?$_GET['page'].'.php':'main.php';
?>
</div>
index.php를 보자.
[그림]과 같이 page 파라미터를 이용해 php 파일을 include하는 것을 확인할 수 있었다.
여기서 취약점이 발생하는 이유는 page 파라미터에 대한 필터링이 존재하지 않기 때문이다.
즉, page 파라미터는 LFI가 발생하고 해당 파라미터를 이용하면 flag.php에 접근할 수 있다.
page 파라미터에 /var/www/uploads/flag
값을 입력하고 전송하면 [그림]처럼 can you see $flag?
라는 문구가 출력된다.
아마도 flag는 flag.php에 $flag
변수에 정의된 것으로 추측된다. 즉, 목적은 flag.php를 실행하는 것이 아니라 코드를 leak 해야한다.
코드를 leak 하는 방법은 php wrapper를 이용하면 된다.
php에서는 php wrapper라는 파일 시스템 함수를 사용할 수 있는 내장 래퍼를 제공 URL 스타일로 제공한다.
즉, LFI를 수행할 수 있는 환경이라면 php wrapper 프로토콜을 이용해서 내부 파일을 읽을 수 있다.
php://filter/convert.base64-encode/resource=/var/www/uploads/flag
php filter wrapper를 이용해서 /var/www/uploads/flag.php
에 접근하도록 payload를 작성했다.
resource로 지정한 경로의 파일을 base64 형태로 로드하는 payload다.
payload가 정상적으로 실행된다면 flag.php 코드가 base64 인코딩 값으로 출력될 것이다.
[그림]와 같이 예상대로 출력되는 것을 확인할 수 있다.
base64 디코딩해보자.
대응방안
○ 입력값 검증
사용자로부터 입력받은 파일 경로나 파일명 등의 값에 대해 필터링과 검증을 철저히 수행한다. 입력값을 사용하기 전에는 절대 경로와 상대 경로를 구분하고, 접두어를 검사하여 file://
, http://
, ftp://
등의 Wrapper를 사용하는지 확인한다.
○ Whitelist 검증
파일 경로나 파일명 등의 입력값을 처리할 때는 Whitelist를 사용하여 허용된 값만 처리하도록 해야한다. 허용된 디렉터리나 파일 리스트를 미리 정해두고, 그 외의 값을 거부하는 방식을 적용한다.
○ Wrapper 사용 제한
file://
, http://
, ftp://
등의 Wrapper를 사용할 때는 제한된 범위 내에서만 사용하도록 설정한다. 특정 디렉터리나 파일을 제외한 나머지는 Wrapper를 사용하지 못하도록 제한한다.
○ open_basedir 설정
open_basedir = /var/www/html/
PHP의 php.ini 파일을 수정하여 open_basedir 설정을 사용함으로써 파일 시스템에 접근 가능한 범위를 제한한다. open_basedir 설정을 통해 특정 디렉터리 이하의 파일만 읽을 수 있도록 제한한다. 위 예제는 /var/www/html
디렉토리에서만 파일 시스템 접근을 허용하는 예제다.
○ secure function
realpath 함수는 상대 경로나 가상 경로 등을 절대 경로로 변환해주는 함수다. 따라서 상대경로를 조작하는 LFI 취약점을 방지할 수 있다.
$path = realpath($_GET['path']);
if (strpos($path, '/var/www/html/') === 0) {
include $path;
} else {
die('Access denied');
}
pathinfo 함수는 파일 경로를 파싱하고 파일명, 디렉터리명, 확장자 등의 정보를 반환하는 함수다. 예제와 같이 확장자를 파싱해서 검증을 수행할 수 있다.
$path = $_GET['path'];
$ext = pathinfo($path, PATHINFO_EXTENSION);
if (in_array($ext, ['php', 'inc'])) {
include $path;
} else {
die('Access denied');
}
basename 함수는 파일 경로에서 파일명만 추출하는 함수다. 이 함수를 사용하여 입력을 검증하면 .jpg.php
와 같은 확장자 우회 방법들을 방지할 수 있다.
$path = $_GET['path'];
if (strpos($path, '/') !== false && basename($path) === 'config.php') {
include $path;
} else {
die('Access denied');
}
reference
https://www.php.net/manual/en/wrappers.php