接到一个授权的Web测试任务,目标是个老PHP站点。我习惯先把sqlmap跑起来,再用Burp Suite手工跟一遍请求,双线并行效率最高。SQL注入这东西在PHP站点里实在太常见了,尤其是早期没有框架、纯拼SQL字符串的老代码,几乎一打一个准。这篇就当一次完整实操记录,从信息收集、注入检测到数据提取,最后再说清楚PHP侧到底怎么防,适合刚接触sqlmap的测试人员,也适合PHP开发想了解漏洞原理后做加固的参考。
1. 前期准备:先把靶场和目标环境搭起来
1.1 为什么选PHP站点练手
PHP在Web服务端语言里的存量依旧巨大,很多中小站点、老业务系统、CMS二次开发项目都是PHP写的。SQL注入的核心问题不在于语言本身,而在于开发习惯——直接用字符串拼接SQL语句、过滤不严格、错误信息回显给用户,这几点在PHP项目里的出现概率尤其高。
拿这套流程跑一遍真实环境,你会看到SQL注入从检测、确认、利用到最终提取数据的完整闭环。练手的时候我建议用pikachu靶场、DVWA或者sqli-labs,这几套环境都是开箱即用,里面有专门为注入设计的页面,比对着教科书看理论要直观得多。
1.2 环境准备:sqlmap安装与运行验证
sqlmap是Kali Linux自带的,如果本地不是Kali,也可以用pip直接装:
pip install sqlmap sqlmap --version装完之后做个快速验证,拿一个已知存在注入的靶场URL测一下,确保工具本身可用。这里要强调一句:sqlmap是一个攻击性很强的工具,只能在目标授权范围内使用。靶场、CTF平台、自己搭建的测试环境都没问题,拿公网站点尝试就是另外一回事了,这个边界必须分清。
1.3 目标分析思路
拿到一个PHP站点,我通常会先看这几个位置:
- URL参数:
index.php?id=1、list.php?cat_id=2这类GET参数最常见 - 登录表单:POST方式提交用户名密码,登录框如果存在注入,往往就是万能密码的入口
- 搜索框:SQL的LIKE查询拼接,很容易被
%和单引号干扰 - Cookie或Header参数:有些框架会把用户信息存进Cookie,服务端拆出来拼SQL
对这些位置做一个列表,标记参数类型、请求方式、是否需要登录态,后面测的时候按优先级逐个过。信息收集做得越细,sqlmap的命中率就越高,不要一上来就对着首页乱打。
2. 注入点确认:手工判断比自动扫描更靠谱
2.1 先手工探一遍
sqlmap虽然强大,但我不建议一开始就甩给它一个URL硬测。先手工用Burp Suite或浏览器开发者工具观察响应差异,很多细节在自动工具的输出里反而不容易看出来。
最基础的三板斧:
- 在参数后面加单引号
',观察是否报错、是否返回异常内容 - 用
and 1=1和and 1=2对比,正常页面和异常页面的内容差异 - 在参数里加
--注释符,看SQL语法是否被改变
比如目标URL是http://target.com/goods.php?id=1,依次尝试:
http://target.com/goods.php?id=1' http://target.com/goods.php?id=1 and 1=1 http://target.com/goods.php?id=1 and 1=2 http://target.com/goods.php?id=1--+如果1=1时页面正常、1=2时页面空白或内容不同,说明数字型注入存在。如果单引号报错,说明字符型注入存在。这一步能帮你判断注入类型,后面给sqlmap指定参数时心里就有数了。
2.2 用sqlmap做基础检测
手工确认有注入后,再用sqlmap做系统的数据库信息探测:
sqlmap -u "http://target.com/goods.php?id=1" --batch--batch参数让sqlmap全程自动选择默认选项,适合无人值守跑批。这个命令会做以下几件事:
- 测试参数
id是否可注入 - 判断注入类型是布尔盲注、时间盲注、报错注入还是联合查询
- 识别后端数据库类型和版本
输出里看到Parameter: id (GET)和Title: MySQL >= 5.0.12 AND time-based blind之类的内容,就说明注入点已经确定了。如果是MySQL 5.0以上版本,information_schema库会带来极大的信息提取便利。
2.3 盲注场景的确认技巧
有时候页面没有任何报错回显,and 1=1和and 1=2返回的页面看起来完全一样,这时候要靠时间盲注来判断。手工测试时可以在参数后加:
id=1 and sleep(5)如果页面明显卡了5秒,说明条件判断生效,注入存在。sqlmap检测时间盲注的时候也是这个原理,只是它会反复测试多个延迟值来减少网络波动带来的误判。这里有个小技巧:如果目标网络延迟本身不稳定,时间盲注检测容易误报,可以配合--time-sec把延迟时间调大,误报率会降很多。
3. sqlmap实战:从基础检测到数据提取
3.1 获取数据库列表
确认注入之后,第一步拿库名:
sqlmap -u "http://target.com/goods.php?id=1" --dbs这一步会枚举出目标MySQL实例里所有的数据库。很多PHP站点用的是同一个数据库账号,如果你拿到了root权限级别的连接,连phpMyAdmin的配置库、其他业务的库都会一览无余,这也是权限过大的风险所在。作为测试人员,看到这种情况一定要写进报告里,属于高危风险项。
3.2 指定数据库并枚举表
拿到库名后,比如有一个叫shop的库,接着枚举表名:
sqlmap -u "http://target.com/goods.php?id=1" -D shop --tables输出会列出这个库下所有数据表,重点关注名字像user、admin、member、account这类表,通常存放着敏感信息。如果表特别多,sqlmap默认会中断询问是否继续,用--batch模式跑完整个列表。
3.3 枚举字段并导出数据
锁定目标表,比如admin_user,然后看字段:
sqlmap -u "http://target.com/goods.php?id=1" -D shop -T admin_user --columns字段信息里如果看到username和password,直接导出:
sqlmap -u "http://target.com/goods.php?id=1" -D shop -T admin_user --dumpsqlmap会尝试破解MySQL的哈希格式密码,常见的是MD5,破解不了也会把哈希值原样保存到本地文件。整个测试过程的输出默认会存在~/.local/share/sqlmap/output/目录下,每次跑完的结果都会留痕,方便复盘。
3.4 参数调优:level、risk和线程
有的注入点藏得深,比如参数在Cookie里、需要特定请求头才能触发、或者要登录之后才能访问。这时候默认的level 1就不够用了,要加大探测深度:
sqlmap -u "http://target.com/goods.php?id=1" --level=3 --risk=2--level控制测试的范围,level 1只测GET和POST参数,level 2会加入Cookie测试,level 3会测User-Agent、Referer等Header--risk控制测试的payload危险程度,risk 2会加入OR条件的payload,SQL语句报错的概率更高,但也更容易破坏数据
这里提醒一下:--risk=3会使用基于OR的注入payload,在真实业务库里执行有可能会修改数据,测试环境无所谓,生产环境一定要避免。如果你不确定后果,risk保持在1或2就好。
线程数也可以适度调整:
sqlmap -u "http://target.com/goods.php?id=1" --dbs --threads=5--threads默认1,调到5到10能明显加速布尔盲注的检测过程。但线程太高会产生大量并发请求,很容易触发WAF拦截或干扰业务正常访问,务实的选择是5。
3.5 特殊场景:POST和登录态
很多PHP站点最有价值的数据在后台,而后台是需要登录的。sqlmap处理这种情况也很简单,先用Burp抓一个已登录的请求,保存成文件,然后直接用-r指定:
sqlmap -r request.txt --dbs如果是纯POST表单,也可以用--data:
sqlmap -u "http://target.com/login.php" --data="username=admin&password=123456" --dbs登录后的Cookie可以用--cookie传进去:
sqlmap -u "http://target.com/admin/info.php?id=1" --cookie="PHPSESSID=xxxxxxxxxx" --dbs这套组合拳在真实测试里非常常用,很多渗透测试的瓶颈不在技术,而在于怎么进入一个有价值的注入点。
3.6 遇到WAF时的绕过思路
现在不少PHP站点前面会套一层云WAF或者主机层面的防护。sqlmap直接扫会被秒封IP或直接返回403。这时候先别急着上tamper脚本,先确认WAF存在:
- 手工输入单引号看是否弹拦截页面
- 用
--identify-waf让sqlmap尝试识别WAF类型
确认有WAF之后,再考虑绕过。常见的手法有:
- 修改User-Agent,
--random-agent - 用
--tamper加载混淆脚本,比如space2comment把空格替换成注释符 - 双写绕过关键字过滤,比如
union写成ununionion,这和sqlmap的--tamper=between或--tamper=modsecurityversioned的某些逻辑类似 - 用内联注释
/*!50000 UNION*/的方式绕过过滤正则,MySQL解析器会执行注释内的内容
sqlmap -u "http://target.com/goods.php?id=1" --tamper=space2comment --random-agent --dbs说实话,真实生产环境的WAF绕过不是一两个tamper就能解决的,需要结合具体过滤规则手工构造payload。sqlmap的tamper脚本库给了很多思路,但最终还是要手工调。这一块水很深,作为测试人员要清楚:绕WAF不是目的,确认漏洞存在并给出加固建议才是目的。
3.7 从攻击者视角看PHP注入高发位
完整跑完一遍sqlmap后,你大概率能在某个PHP站点的这几个位置命中注入:
- 商品列表和详情页的ID参数
- 文章/公告页的分类ID
- 用户中心里读取个人信息的参数
- 搜索关键词
这些位置的共同点是:参数直接来自前端请求,而后端代码没有做类型校验也没有参数化查询。攻击者不需要理解PHP代码,只需要对着URL猜参数名就能展开探测。换句话说,注入漏洞的暴露面,比你想象的要大得多。
4. 漏洞成因解剖:为什么PHP网站容易中标
4.1 字符串拼接SQL是万恶之源
绝大多数PHP注入漏洞都长一个样,先把用户输入直接拼进SQL语句,再丢给数据库执行。比如登录代码写成这样:
$username = $_POST['username']; $password = md5($_POST['password']); $sql = "SELECT * FROM users WHERE username = '{$username}' AND password = '{$password}'"; $result = mysqli_query($conn, $sql);当用户在用户名框里输入admin' --时,SQL语句变成:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'xxx'--把后面的密码判断注释掉了,整个查询只判断用户名,密码形同虚设。如果再输入' OR '1'='1,条件变成永真,连用户名都不需要知道。这就是所谓的万能密码绕过。
这种写法的致命问题在于:用户输入被当成了SQL代码的一部分,而不是纯数据。数据库引擎不会区分哪段是你写的、哪段是用户传的,它只会老老实实执行整条语句。
4.2 过滤函数为什么靠不住
很多PHP开发者会在拼接之前加一层过滤,比如用addslashes、mysqli_real_escape_string,或者自己写一个正则把'、"、select、union这些关键字替换成空字符串。这种做法看起来合理,但对抗思路是无穷的:
- 编码绕过:URL编码、Unicode编码、HEX编码
- 双写绕过:
ununionion、selselectect,简单的正则替换可以被轻松绕过 - 大小写绕过:
SeLeCt绕过不区分大小写但只匹配小写的正则 - 注释符拆分:
UN/**/ION,MySQL允许关键字中间夹注释符
之前热词里有人搜sqlmap双写绕过怎么用,实际上就是这类场景。过滤是基于黑名单的,而攻击者只需要找到一个漏网的关键字符组合即可。
4.3 PHP类型松散带来的边界问题
PHP是弱类型语言,比较运算符==在类型不一致时会进行自动转换。比如用户输入1e0,PHP会把它当作科学计数法的1,这带来了一些SQL注入以外的逻辑绕过风险,比如:
if ($user['role'] == $_POST['role']) { // 权限判断 }如果数据库里的role是整数0,攻击者构造一个字符串让它和0相等,就可以绕过权限判断。这类问题和SQL注入不直接相关,但属于同一类思维:对用户输入过于信任。安全测试的时候,弱类型比较边界也值得顺手验证一下。
4.4 报错信息泄露加剧了漏洞被利用的速度
PHP报错回显是sqlmap快速定位注入点的重要辅助。You have an error in your SQL syntax这类MySQL错误信息如果直接输出到页面,攻击者连盲注都不需要打,直接把payload填进报错函数就能看到结果。比如updatexml报错注入:
id=1 AND updatexml(1, concat(0x7e, (SELECT user())), 1)页面会直接显示出当前数据库用户名。一条请求就能拿到信息,效率比时间盲注高太多。所以生产环境关闭display_errors并配置好日志记录,是PHP安全加固里成本最低、收益最明显的一步。
5. 安全防范:测试的最终目的是让站点更硬
5.1 第一道防线:PDO预处理参数化查询
PHP侧最有效的修复方案,就是用PDO预处理语句替代字符串拼接。PDO的prepare会把SQL结构和数据分开传输,数据库引擎在执行前就知道哪个位置是占位符,用户输入永远只是数据,不会变成代码逻辑。
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?"); $stmt->execute([$username, $password]);就这么几行,整条SQL语句的结构就被固定住了。攻击者传入' OR '1'='1,PDO会把它当作用户名字符串去匹配,而不是当作SQL代码执行。无论输入什么,数据库都只会把它放在参数位置。
mysqli同样支持预处理语法,老的mysql_*函数已经在PHP 7里彻底移除了,如果你还在维护老项目,升级和改造是迟早的事。
5.2 第二道防线:输入校验和类型约束
参数化查询解决了注入,但输入校验依然是必要的纵深防御。每个参数都应该有明确预期:
- ID参数必须是非负整数,用
filter_var($_GET['id'], FILTER_VALIDATE_INT)强制转换 - 邮箱、手机号、日期都必须有对应的格式校验
- 枚举类型(如状态码)用白名单校验,只允许在预设范围内取值
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT); if ($id === false || $id <= 0) { http_response_code(400); exit('Invalid parameter'); }这里有个常见误区:很多教程喜欢用htmlspecialchars来防SQL注入,但这是错的。htmlspecialchars是用来防XSS的,它对单引号实体化后确实会影响SQL注入的构造,但完全不能替代参数化查询。两者要同时用,各管各的领域。
5.3 第三道防线:数据库权限收敛
拿sqlmap打下来之后,我最怕看到的就是PHP站点连接的数据库账号是root。一旦注入点存在,攻击者可以直接读mysql.user表、尝试INTO OUTFILE写webshell,甚至--os-shell直接拿服务器权限。
正确的做法是权限最小化:
- 给应用创建一个独立账号,只授权应用库的
SELECT、INSERT、UPDATE、DELETE - 绝对不授予
FILE、SUPER、GRANT权限 - 多个业务使用多个独立账号,互不交叉
SQL注入的破坏力有多大,很大程度上取决于数据库账号的权限有多高。你把SQL语句堵死了,但权限管理没跟上,风险依然存在。
5.4 第四道防线:框架和WAF兜底
现代PHP框架(Laravel、ThinkPHP、Symfony)默认使用ORM和查询构造器,内部已经处理了参数绑定。用框架开发的站点,注入漏洞概率会低很多,但也别掉以轻心——有些开发者会用DB::raw()或whereRaw()拼接自定义SQL,一不留神又回到了老路上。
WAF是最后的兜底手段,通常部署在Web服务器之前,用正则规则拦截注入特征。它的优势是无需改代码就能挡住大部分自动化攻击,缺点也很明显:规则滞后、容易误杀、绕过空间大。WAF应该作为临时缓解措施,而不是长期替代代码修复的方案。
5.5 上线前的自查清单
把一次完整的测试经验总结成清单,很有用,每次发布前对照过一遍:
- 所有SQL语句是否都使用了PDO预处理或框架查询构造器
- 是否有裸拼接SQL的地方,全局搜索
mysqli_query、$pdo->query和字符串拼接符号 display_errors是否关闭,日志级别是否配置合理- 生产环境是否保留了phpinfo页面、后台默认密码
- 数据库账号权限是否最小化
- 管理后台是否开启双因素认证
- 敏感日志(登录日志、操作日志)是否记录完整
照着清单过一遍,能挡住九成常见的注入攻击。
6. 踩坑记录与排查技巧实录
6.1 目标站响应慢导致误判
时间盲注检测经常出现误报,尤其是目标跨地域或服务器负载高的时候。sqlmap基于响应延迟判断,网络抖动会把正常请求变成“延迟响应”,容易误判出注入点。解决办法:
- 用
--time-sec 10把延迟阈值加大 - 多次检测同一个注入点,验证结果是否稳定
- 在非高峰时段测试
6.2 目标有WAF时,先用手工确认再跑工具
某些云WAF会把sqlmap的特征识别得很准,一旦检测到就直接封IP。遇到这种情况,可以先手工用绝对路径、注释符、编码变形构造几个payload试试,确认WAF的过滤规则后,再有针对性地配tamper。一上来就全速跑sqlmap,大概率是把自己封出去。
6.3 数据库配置了安全模式
MySQL的secure_file_priv如果非空,INTO OUTFILE和LOAD_FILE都会受限。sqlmap拿到高权限后想写webshell,经常会遇到The MySQL server is running with the --secure-file-priv option这个错误。这说明底层做了安全加固,是好事。遇到这种报错别硬绕,回到业务层面找其他漏洞,或者承认这个点打不穿。
6.4 PHP环境常见的兼容坑
测试老项目时,遇到过目标服务器PHP版本过高导致老CMS直接白屏的情况。比如热词里有人搜fatal error: directive 'track_errors' is no longer available in php,这是PHP版本升级后旧配置项失效导致的问题。排查的时候可以先看PHP错误日志,确认是应用代码兼容问题还是真的被攻击了,别一看到报错就往漏洞上想。
如果你在用Mac的M系列芯片跑phpStudy,也遇到过找不到适合的PHP版本之类的问题,可以先确认本地PHP二进制和扩展版本是否匹配,再决定是换版本还是编译安装。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 处理方式 |
|---|---|---|
| sqlmap检测不到注入点 | 目标对参数做了过滤,或注入点在非默认参数位置 | 提高--level,加入Cookie和Header测试 |
| 扫到一半连接被重置 | WAF封IP或连接超时 | 调低线程、加--random-agent、更换出口IP后重试 |
| 扫描很慢 | 网络延迟高,目标使用时间盲注响应慢 | 合并请求、调大--time-sec、使用--throttle控制请求频率 |
| 报了注入但dump不出数据 | 数据库用户权限不足 | 确认数据库连接账号是否有读写目标表权限 |
| 拿到密码哈希解不开 | 使用了强哈希算法或加盐 | 更换更大的密码词典,或直接提交给离线破解环境处理 |
| PHP项目升级后数据库连不上 | PHP版本不兼容旧MySQL扩展 | 替换为PDO或mysqli驱动,升级相关依赖 |
最后再分享一点我的体会
做了这么多年SQL注入测试,最深的感受是:漏洞本身不可怕,可怕的是不知道漏洞为什么存在。sqlmap帮你找到入口,但想彻底看懂一个注入点,还是得回到代码层面、回到SQL语句本身去分析。这也是我为什么每次都强调,工具是放大器,不是替代品——手工验证和代码审计的基本功,才是安全测试的核心竞争力。
如果你正在学sqlmap,建议从靶场起步,把它和PHP代码审计结合起来练。每打穿一个注入点,就翻翻对应的源码,想清楚是哪一行代码出了问题。这样反复几轮,比纯刷工具对技术的理解要深得多。后面再遇到老代码,光看URL就能猜个八九不离十,测试效率自然就上来了。