📚 网络安全 Kali 学习系列
模块 0 基础阶段 ✅ 已完成
01-18 Linux系统 → 计算机网络 → 密码学 → Python/Bash/PHP/JS → Docker
模块 1 信息收集与侦察 ✅ 已完成
19 nmap → 20 masscan → 21 指纹识别 → 22 漏洞扫描与 Web 基础
模块 2 Web 应用渗透 ✅ 已完成
23 sqlmap → 24 XSS → 25 CSRF/上传 → 26 命令执行 → 27 文件包含/SSRF → 28 反序列化 → 29 XXE → 30 越权 → 31 WAF
模块 3 代码审计(白盒分析阶段)
✓ 32 代码审计入门:从黑盒到白盒的思维转换
✓ 33 Node.js 原型链污染:原理与利用
34 PHP 代码审计实战:从危险函数到利用链(当前篇)
35 Java 代码审计:SpEL 与 JNDI 注入(预告)
模块 4-5 后续持续更新
导读
第 32 篇建立了代码审计的方法论框架,第 33 篇深入了 Node.js 的原型链污染。本篇把白盒审计的镜头对准 PHP——这门语言贡献了 Web 安全史上最多的漏洞类型。SQL 注入、文件上传、命令执行、反序列化,每一种你都已在模块 2 中从黑盒视角利用过,现在换个方向:打开源码,从代码层面找到这些漏洞的根源。
读完本篇你将能:搭建 PHP 代码审计环境并掌握从 grep 到 Semgrep 的扫描流程、识别 PHP 项目中的 SQL 注入审计特征(二次注入与 ORDER BY 注入)、从白盒角度审计文件上传漏洞(白名单绕过与条件竞争)、追踪 escapeshellarg() 的安全边界与绕过方式、定位 unserialize() 的调用点并追踪 POP 链起点。
目录
1. 审计环境搭建与流程总览
2. 危险函数搜索实战:grep + Semgrep 扫描
3. SQL 注入审计:二次注入与 ORDER BY 注入
4. 文件上传审计:白名单绕过与条件竞争
5. 命令执行审计:escapeshellarg 的安全边界
6. unserialize 审计:POP 链起点追踪
7. 分级练习
1. 审计环境搭建与流程总览
PHP 代码审计不需要复杂的工具链——一个能高亮 PHP 语法的编辑器加命令行工具就能开始。但为了让审计过程更高效,建议搭建以下三层环境。
| 层级 |
工具 |
用途 |
| 扫描层 |
grep / ripgrep + Semgrep |
批量定位危险函数和漏洞模式 |
| 阅读层 |
VS Code + PHP 插件 / Vim + tagbar |
跳转定义、追踪函数调用链 |
| 验证层 |
Docker + DVWA / Vulhub |
复现漏洞确认可利用性 |
审计流程分四步走:扫描定位 → 人工确认 → 利用验证 → 修复建议。第一步用 grep 或 Semgrep 扫出所有危险函数调用点;第二步逐个人工检查参数来源是否可控;第三步在靶场环境中构造 Payload 验证漏洞是否真实可利用;第四步给出修复方案。本篇按这个流程逐步展开。
Shell · 审计环境快速搭建
# 1. 安装 ripgrep(比 grep 快 10 倍)
sudo apt install ripgrep
# 2. 安装 Semgrep(pip 安装最简单)
pip3 install semgrep
# 3. 拉取 DVWA 靶场用于验证
docker run -d -p 8080:80 --name dvwa vulnerables/web-dvwa
# 4. 获取待审计的 PHP 源码
git clone https://github.com/cispay/test-audit-php.git audit-target
环境就绪后,下一章开始实战扫描。
2. 危险函数搜索实战:grep + Semgrep 扫描
PHP 危险函数按漏洞类型分为五大类。审计的第一步不是逐行阅读源码,而是用搜索工具把所有危险函数调用点一次性扫出来,建立"待确认清单"。
| 漏洞类型 |
危险函数 |
grep 模式 |
| 代码执行 |
eval assert system exec |
rg "eval\|assert\|system\|exec" |
| 命令执行 |
passthru shell_exec popen |
rg "passthru\|shell_exec\|popen" |
| 反序列化 |
unserialize |
rg "unserialize" |
| 文件包含 |
include require include_once |
rg "include\|require" --php |
| SQL 拼接 |
mysql_query mysqli_query |
rg "query.*\\\$_(GET\|POST)" |
ripgrep 一键扫描
ripgrep(命令名 rg)比传统 grep 快数倍,默认递归、自动跳过 .git 目录和二进制文件。以下命令一次性扫出所有高危函数调用点,并附带行号和文件名。
Shell · ripgrep 一键扫描全部危险函数
|
1
2
3
4
5
6
7
8
|
# 一行扫出所有高危函数,-n 显示行号,-i 忽略大小写
rg -ni '\b(eval|assert|system|exec|passthru|shell_exec|popen|unserialize)\s*\(' .
# 输出示例:
config.php:12: system($cmd);
upload.php:45: eval($_POST['code']);
db.php:78: unserialize($cookieData);
utils.php:103: exec('convert ' . $filename);
|
扫描结果中每一行都是一个待确认点。但不是所有危险函数调用都有漏洞——如果参数是硬编码字符串(如 system('date')),就是安全的。只有参数来自用户输入($_GET、$_POST、$_COOKIE)或间接来自用户输入时,才需要深入分析。
💡 小贴士
PHP 中还有一类"间接危险函数"容易漏扫:call_user_func()、array_map()、usort()——它们的回调参数如果来自用户输入,等同于代码执行。扫描时加一条:rg -ni 'call_user_func|array_map|usort\|array_filter' .
3. SQL 注入审计:二次注入与 ORDER BY 注入
第 32 篇展示了"预处理语句误用"——拼接后送进 prepare() 的 SQL 注入。本篇深入两种更隐蔽的 SQL 注入模式,它们在黑盒测试中极难发现,但在白盒审计中一目了然。
二次注入:存储后才引爆
二次注入的核心特征是:用户输入第一次入库时被正确转义了(用了预处理语句),但第二次从数据库取出后直接拼入另一条 SQL 语句,没有再次转义。黑盒测试要构造两步 Payload 才能触发,而代码审计只需追踪数据从"入库"到"出库"到"二次拼接"的完整路径。
PHP · 二次注入:注册 + 修改密码
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 第一步:注册时用户名安全入库(预处理)
$stmt = $pdo->prepare("INSERT INTO users(name) VALUES(?)");
$stmt->execute([$_POST['name']]);
// 攻击者注册用户名:admin'--
// 第二步:修改密码时取出用户名,直接拼接
$name = $_SESSION['name']; // "admin'--" 从数据库取出
// ⚠️ 直接拼入 SQL,未用预处理
$sql = "UPDATE users SET password='$newpass'";
$sql .= " WHERE name='$name'";
// 实际执行的 SQL:
// UPDATE users SET password='new' WHERE name='admin'--'
$pdo->query($sql);
// 结果:admin 的密码被改了,-- 后面的被注释
// 修复:所有 SQL 均使用预处理,包括取出的数据
|
审计要点:第 1-3 行入库用了预处理(安全),但第 7-10 行从数据库取出 $name 后直接拼接到 UPDATE 语句中。这种"入库安全但出库不安全"的模式是二次注入的典型特征。审计时搜索所有 query() 调用(非 prepare()),追踪其参数是否来自数据库查询结果。
ORDER BY 注入:预处理管不了的字段
预处理语句的 ? 占位符只能用于值的位置(WHERE 条件值、INSERT 值等),不能用于列名、表名或排序方向。当代码把用户输入直接放到 ORDER BY 后面时,预处理无法保护。
PHP · ORDER BY 注入
// 用户控制排序列和方向
$sort = $_GET['sort'] ?? 'id';
$order = $_GET['order'] ?? 'ASC';
// ⚠️ 预处理无法保护列名和方向
$sql = "SELECT * FROM products ORDER BY $sort $order";
$pdo->query($sql);
// 修复:用白名单严格限制可选项
$allowed = ['id', 'price', 'name'];
if (!in_array($sort, $allowed)) { $sort = 'id'; }
$order = ($order === 'DESC') ? 'DESC' : 'ASC';
⚠️ 常见错误
用 ORDER BY ? 加预处理来防注入
正确做法:列名和排序方向不能用预处理占位符,必须用白名单。对列名用 in_array($input, $allowed_columns) 严格校验,方向只允许 ASC 或 DESC 两个硬编码值。
第四章 文件上传审计:白名单绕过与条件竞争
文件上传是 Web 应用最危险的功能之一。一个上传校验不严格的接口,可以让攻击者直接获取 WebShell。审计文件上传功能时,关注点不是"有没有校验",而是"校验在哪一层、能不能被绕过"。
4.1 白名单绕过的五条路径
白名单本身比黑名单安全,但实现方式不对,白名单也能绕过。以下是审计中常见到的五种绕过路径:
路径1MIME 类型伪造 — 拦截 Content-Type 头判断类型,攻击者用 curl 伪造
路径2扩展名双写 — shell.phtml.php 被截取为 .php
路径3空字节截断 — shell.php%00.jpg,PHP < 5.3.4 截断后文件名为 .php
路径4.htaccess 绕过 — 上传 .htaccess 让 .jpg 文件被当做 PHP 解析
路径5大小写绕过 — shell.PhP 在大小写不敏感的系统上等效于 .php
先看一段典型的不安全上传代码,它在 MIME 类型和扩展名上都存在绕过可能:
PHP · 不安全的文件上传校验
vuln_upload.php
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 |
// 仅检查 MIME 类型 — 可被 curl 伪造
$allowed_types = ['image/jpeg', 'image/png'];
if (!in_array($_FILES['file']['type'], $allowed_types)) {
die('文件类型不允许');
}
// 扩展名白名单 — 但用了 strtolower 后又末尾截取
$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
$ext = strtolower($ext);
if (!in_array($ext, ['jpg', 'png'])) {
die('扩展名不允许');
}
// 问题:未检查文件内容,也未重命名
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);
|
这段代码有两个致命缺陷。第一,$_FILES['file']['type'] 完全由客户端控制,用 curl -H "Content-Type: image/jpeg" 即可伪造。第二,文件名直接拼接保存,攻击者可上传 shell.php.jpg 然后利用服务器配置或 .htaccess 让其以 PHP 解析。
审计时需要问三个问题:MIME 类型校验是否依赖客户端可控数据?扩展名校验是否考虑了双扩展名和空字节?上传文件是否被重命名为随机名称?
💡 小贴士
审计文件上传时,搜索 move_uploaded_file 和 $_FILES 定位上传逻辑后,沿数据流回溯:文件名从哪来 → 经过了什么校验 → 最终保存路径是否可控。这条链路上任何一环可控都是漏洞。
4.2 条件竞争上传(TOCTOU)
条件竞争上传是文件审计中最容易被忽略的漏洞类型。它的核心逻辑是:先保存文件,再检查内容,最后删除恶意文件。在"保存"和"删除"之间的窗口期内,攻击者通过高并发请求访问该文件触发执行。
PHP · 条件竞争:先存后删
race_upload.php
|
1
2
3
4
5
6
7
8
9
10
11
12
13 |
// 步骤1:先保存文件到可访问目录
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);
// 步骤2:检查文件内容
$content = file_get_contents('uploads/' . $_FILES['file']['name']);
if (strpos($content, '<?php') !== false) {
// 步骤3:发现恶意内容,删除
unlink('uploads/' . $_FILES['file']['name']);
die('恶意文件已删除');
}
// 窗口期:步骤1到步骤3之间,文件存在于可访问目录
// 攻击者用脚本并发访问 uploads/shell.php 触发执行
|
攻击者上传一个写入 WebShell 的 PHP 文件,内容为 <?php fputs(fopen('shell.php','w'),'<?php eval($_POST[cmd]);'); ?>。文件被保存的瞬间,攻击者用 Python 多线程脚本并发访问该文件,触发 PHP 执行写入持久化 WebShell,随后即使原文件被删除,WebShell 已经落地。
⚠️ 常见错误
上传后检查内容、发现恶意就删除,这样足够安全
正确做法:检查必须在保存前完成。文件先存到不可访问的临时目录,通过 getimagesize() 或 exif_imagetype() 验证文件头,校验通过后重命名为随机文件名并移动到可访问目录。不存在"先存后删"的窗口期。
4.3 安全上传的审计检查清单
审计文件上传功能时,按以下清单逐项核查:
1MIME 类型不依赖 $_FILES['type'],改用 finfo_file() 检测实际文件头
2扩展名白名单只允许 jpg/png/gif 等图片类型,禁止 php/php3/php5/phtml/pht
3上传文件重命名为 bin2hex(random_bytes(16)) 生成的随机名,不保留原始文件名
4校验在临时目录完成,通过后才移动到可访问目录,消除竞态窗口
5上传目录配置 Nginx/Apache 禁止执行 PHP,作为纵深防御
第五章 命令执行审计:从 escapeshellarg 到 RCE
PHP 中调用系统命令的函数有六个:exec、system、passthru、shell_exec、popen 和反引号操作符。审计时用 ripgrep 一次性搜索全部危险函数:
Shell · 搜索全部命令执行函数
|
1
2
3
4
5
6
|
# 在项目根目录搜索命令执行函数
rg '(exec|system|passthru|shell_exec|popen|proc_open)\s*\(' \
--php --type-add'backtick:*.php' \
-n --no-heading
# 输出示例:utils.php:42: exec("convert " . $filename . " out.png", $output);
|
5.1 命令注入 vs 参数注入
审计命令执行漏洞时,要区分两种不同的攻击方式。命令注入是攻击者在命令行中插入管道符或分号,执行全新命令;参数注入是攻击者在合法命令的参数位置注入额外选项,改变命令行为。
PHP · 命令注入与参数注入对比
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 17 |
// 命令注入:用户输入 ; 或 | 接续新命令
$ip = $_GET['ip']; // 8.8.8.8; cat /etc/passwd
system("ping -c 4 " . $ip);
// 实际执行:ping -c 4 8.8.8.8; cat /etc/passwd
// 参数注入:用户输入额外选项改变命令行为
$file = $_GET['name']; // --output=/tmp/shell.php
system("tar czf archive.tar.gz " . $file);
// escapeshellarg 防命令注入但不防参数注入
$ip = escapeshellarg($_GET['ip']);
system("ping -c 4 " . $ip);
// 输入 8.8.8.8; cat /etc/passwd → 被转义为 '8.8.8.8; cat /etc/passwd'
// 整体被当做一个参数传给 ping,命令注入失效
// 但 --output=/tmp/shell.php 仍被当做一个"参数"传给 tar
// escapeshellarg 不阻止以 - 开头的参数注入
|
关键结论:escapeshellarg() 能阻止管道符注入,但无法阻止参数注入。如果命令本身有危险参数(如 tar 的 --to-command 或 find 的 -exec),即使转义了输入仍可被利用。
5.2 escapeshellarg 的安全边界
审计中遇到 escapeshellarg() 不能直接判定安全。该函数在单引号外包裹输入,并转义内部单引号。但有两个已知边界问题:
边界1:多字节字符注入
在 LC_ALL=zh_CN.UTF-8 环境下,精心构造的多字节输入可"吃掉"转义单引号,PHP < 8.1 的旧版本受影响
边界2:escapeshellarg + escapeshellcmd 混用
两个函数同时使用反而会引入漏洞:escapeshellcmd 会破坏 escapeshellarg 的引号保护,导致注入
⚠️ 常见错误
用 escapeshellarg() 转义后拼接到命令中,再套一层 escapeshellcmd() 双重保险
正确做法:二选一,不要混用。escapeshellarg() 用于整个参数是用户输入的场景;escapeshellcmd() 用于用户输入只是命令的一部分时。最佳方案是用 proc_open() 传数组参数,完全绕过 shell 解析。
5.3 proc_open:最安全的命令调用方式
当业务必须调用系统命令时,proc_open() 以数组方式传参,不经过 shell 解析,从根源消除注入:
PHP · proc_open 数组传参
safe_exec.php
|
1
2
3
4
5
6
7
8
9
10
11
12
|
// 用户输入作为数组元素,不经 shell 解析
$ip = $_GET['ip'];
$desc = [
['pipe', 'r'],
['pipe', 'w'],
];
$proc = proc_open(
['ping', '-c', '4', $ip], // 数组,不经 shell
$desc, $pipes
);
// 即使 $ip = "8.8.8.8; rm -rf /" 也只被当做 ping 的参数
|
💡 小贴士
命令执行审计的核心判断:用户输入是否出现在命令字符串中?如果是,检查是否用了 escapeshellarg;如果用了,再检查是否同时用了 escapeshellcmd;如果同时用了,标记为漏洞。最优解是搜索是否存在 proc_open 并建议迁移。
第六章 unserialize 审计:POP 链起点追踪
PHP 反序列化漏洞的核心不是 unserialize 函数本身,而是它触发的魔术方法调用链。审计时首先要定位 unserialize 的调用点,然后追踪输入是否可控,最后沿魔术方法链找到可利用的"终点"。
6.1 三大魔术方法与 POP 链构造
POP(Property Oriented Programming)链的构造依赖于 PHP 的魔术方法自动调用机制。审计时重点关注三个起点方法:
__wakeup()
unserialize() 时自动调用,最直接的入口。如果 __wakeup 中调用了危险方法或设置了属性,可作为链首
__destruct()
对象销毁时自动调用。即使 unserialize 时 __wakeup 被绕过,脚本结束时 __destruct 仍会执行,是最常用的触发点
__toString()
对象被当做字符串使用时调用。常作为中间环节,在 __destruct 中将属性对象当做字符串处理时触发
下面是一段典型的含 POP 链的代码,审计时需要从终点回溯到起点:
PHP · POP 链:__destruct → __toString → eval
pop_chain.php
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 24 |
// 中间环节:Show 类有 __toString
class Show {
public $cmd;
public function __toString() {
eval($this->cmd); // 终点:代码执行
return '';
}
}
// 起点:Destroyer 类有 __destruct
class Destroyer {
public $obj;
public function __destruct() {
// 将属性当做字符串 → 触发 __toString
echo $this->obj;
}
}
// 漏洞入口:用户可控数据传入 unserialize
$data = $_COOKIE['session'];
$obj = unserialize($data);
// 脚本结束时 __destruct 自动调用 → echo $this->obj
// 若 obj 是 Show 对象 → 触发 __toString → eval($cmd)
|
攻击者构造的 payload 是一个序列化的 Destroyer 对象,其 obj 属性设为 Show 对象,Show 的 cmd 属性设为要执行的 PHP 代码。整个链路:unserialize 创建对象 → 脚本结束触发 __destruct → echo $this->obj 触发 __toString → eval 执行代码。
审计这类型漏洞的步骤是逆向的:先用 grep 搜索 eval|system|exec|include 等终点函数,确认它们在魔术方法中,然后回溯查找哪些类的 __destruct 或 __wakeup 会调用到这些方法,最后确认 unserialize 的输入是否可控。
6.2 phar 反序列化:不调用 unserialize 的漏洞
phar 反序列化是 PHP 中最隐蔽的漏洞之一。当 phar.readonly = Off 时,攻击者可以构造恶意 phar 文件,通过文件操作函数(如 file_exists、is_file、filesize 等)触发反序列化,无需代码中显式调用 unserialize。
PHP · phar 反序列化触发
|
1
2
3
4
5
6
7
8
9
10
11
12 |
// 看似无害的文件存在检查
$path = $_GET['file']; // phar://evil.phar
if (file_exists($path)) {
echo '文件存在';
}
// file_exists 处理 phar:// 协议时
// 自动解析 phar 的 manifest 元数据
// manifest 中的序列化数据被 unserialize
// 无需代码中显式调用 unserialize()
// 审计要点:搜索所有文件操作函数 + phar 协议
|
审计 phar 反序列化时,搜索所有文件系统函数(file_exists、is_dir、filesize、file_get_contents、fopen 等共 30+ 个)是否接受用户输入,并检查输入是否可能以 phar:// 开头。修复方案是对路径输入做 realpath() 校验和协议白名单过滤。
💡 小贴士
PHP 8.0 中 __wakeup 的绕过已被修复(不再可通过设置属性数量不一致来跳过),但 __destruct 和 phar 反序列化仍然有效。审计 PHP 8 项目时,重点搜索 __destruct 而非 __wakeup。
6.3 Semgrep 自动化反序列化规则
手动审计大型项目时,用 Semgrep 自定义规则批量搜索 unserialize + 可控输入的模式:
YAML · Semgrep 反序列化检测规则
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
rules:
- id: php-unserialize-user-input
pattern: unserialize($X)
pattern-inside: |
$INPUT = $_REQUEST[...];
...
unserialize($INPUT);
message: "用户可控数据传入 unserialize,存在反序列化风险"
languages: [php]
severity: ERROR
- id: php-phar-deserialization
pattern: file_exists($_GET[...])
message: "file_exists 接受用户输入,可能触发 phar 反序列化"
languages: [php]
severity: WARNING
|
运行 semgrep --config rules.yml --php ./src/ 即可自动标记所有用户可控输入流入 unserialize 和文件操作函数的位置,大幅减少人工搜索工作量。
第七章 分级练习
以下练习按难度递进,建议在本地搭建 DVWA 或自行编写漏洞代码进行实操。每个练习标注了涉及的知识点和审计思路。
🟢 基础练习:危险函数定位
练习 1:用 ripgrep 扫描项目中的全部危险函数
任务:在任意 PHP 项目(可用 DVWA 源码)中,编写一条 ripgrep 命令,一次性搜索以下六类危险函数:
① 命令执行:exec、system、passthru、shell_exec、popen
② 代码执行:eval、assert、create_function
③ 文件包含:include、require(带变量参数的)
④ 文件操作:move_uploaded_file、file_get_contents(带变量参数的)
⑤ 反序列化:unserialize
⑥ SQL 拼接:mysqli_query、$pdo->query(带字符串拼接的)
验证标准:每条结果都带行号和文件名,手动检查前 5 条命中,判断是否真正可利用。
审计思路:先广度搜索定位,再逐个深度分析。不是所有危险函数调用都是漏洞,关键看参数是否可控。
🟡 进阶练习:构造 POP 链
练习 2:手工构造反序列化 payload
任务:根据以下代码片段,构造一个可执行 phpinfo() 的序列化 payload:
代码结构:Class A 有 __destruct() 调用 $this->b->run();Class B 有 __call() 魔术方法转发到 eval($this->code);入口是 unserialize($_POST['data'])
步骤:
1. 从终点 eval 回溯:需要触发 B 的 __call
2. __call 由"调用不存在的方法"触发,A 的 __destruct 调用 $this->b->run(),若 b 是 B 的实例且 B 没有 run 方法 → 触发 __call
3. 构造:A 对象的 b 属性 = B 对象,B 的 code 属性 = "phpinfo()"
4. 用 php -r "echo serialize(...);" 生成 payload
验证标准:payload 提交后页面输出 phpinfo 信息。
🔴 高级练习:编写 Semgrep 自定义规则
练习 3:编写 Semgrep 规则检测二次注入
任务:编写一条 Semgrep 规则,检测"从数据库取出数据后直接拼入 SQL"的二次注入模式。
提示:
① 模式匹配 $X = $pdo->query("SELECT ...")->fetch() 后接 $pdo->query("... $X ...")
② 使用 pattern-inside 和 metavariable-relationship 关联两个语句
③ message 字段说明风险:数据库取出的数据可能包含 SQL 注入 payload
验证标准:在含二次注入漏洞的测试代码上运行,规则能命中且不误报安全代码。思考:为什么预处理不能防止二次注入?
知识回顾
危险函数六大类
ripgrep 搜索定位
Semgrep 自定义规则
二次注入原理
ORDER BY 白名单
文件上传五条绕过
条件竞争 TOCTOU
escapeshellarg 边界
proc_open 数组传参
POP 链构造
phar 反序列化
魔术方法回溯审计
下一篇预告
📚 网络安全 Kali 学习系列 · 第 35 篇
Java 代码审计:SpEL 注入与 Fastjson 反序列化
从 PHP 审计过渡到 Java 生态,聚焦 Spring Boot 项目中最常见的两类漏洞:SpEL 表达式注入和 Fastjson 反序列化链,对比两种语言的审计思路差异。
网络安全 Kali 学习系列 · 模块 3 代码审计 · 第 34 篇
学安全,为守护 · 用安全,为建设