提起Web安全入门,绕不开的就是SQL注入;绕不开SQL注入练习,自然就绕不开BWAPP这个靶场。我刚入行那会儿,白天看了一堆SQL注入的原理文章,晚上回去就想找个地方练手。DVWA的界面太老,Pikachu的题目量少,最后真正让我把SQL注入从“看着会”练到“上手能打”的,还是BWAPP。这是一个把各种常见漏洞场景打包好的PHP练习环境,里面光SQL注入模块就有近十个入口,既有GET型、POST型,也有JSON、登录表单和SQLite场景,Level还分了Low、Medium、High三档。今天这篇不聊虚的,就聊聊我在BWAPP上把SQL注入模块从Low刷到High的完整通关过程,以及那些踩过之后才知道的坑,给准备刷这个靶场的朋友一份能直接照着操作的流水账。
1. 认识BWAPP靶场与SQL注入演练环境
1.1 BWAPP到底是什么,和其他靶场有什么不一样
BWAPP全称是Buggy Web Application,翻译过来就是“故意做得漏洞百出的Web应用”,是一个开源的PHP+MySQL靶机项目。和DVWA这类单页面应用不同,BWAPP等于把几十种主流Web漏洞场景全部拆开做成了一套独立的模块化应用,你登录进去之后会看到一个类似“漏洞目录”的列表,点哪个就进入哪个漏洞场景,每个场景都有独立的参数、逻辑和后台代码。
对我这种刷题型选手来说,BWAPP最大的优点就是“分类清楚、题型足够多”。拿SQL注入来举例,DVWA基本就是User ID查询那一个页面,最多就是SQL Injection和SQL Injection (Blind)两个入口;而BWAPP单独把SQL注入列成了一个分类,下面有GET搜索、POST搜索、JSON请求、登录表单、SQLite等多个入口,每个入口的服务端写法都不一样。这意味着你在同一个靶场里,能同时练习“在URL传参注入”“在POST表单注入”“在JSON数据结构里注入”这三种常见场景,而不是换一个靶场就要重新熟悉一遍界面。
另外还有一点我很喜欢:BWAPP每个漏洞模块都提供了PHP源码查看入口。当你注入不进去、想不明白为什么过滤规则这么奇怪的时候,点开源码看一行就知道服务端到底做了什么处理。这对新手来说极其友好,因为学习漏洞本质上是学习“代码为什么会写成这样”,而不是单纯背几个payload。
1.2 为什么练习SQL注入优先推荐BWAPP
我就直接给结论:如果你是零基础或刚入门,想通过刷靶场把SQL注入彻底吃透,BWAPP的优先级应该排在DVWA和Pikachu前面。原因有三点。
第一,题目梯度设计合理。BWAPP每个模块分三个安全级别,Low级别几乎没有任何过滤,适合理解注入原理;Medium级别加了一些过滤函数,适合练习“绕过滤”;High级别要么用了预处理查询,要么做了严格的字符过滤,不适合继续硬打,反而适合用来理解“怎么修代码才能防注入”。这一条龙下来,等于把攻击和防御都过了一遍。
第二,场景覆盖足够广。前面说了,GET、POST、JSON、登录表单、SQLite都有。实际上做Web安全的都清楚,同一个SQL注入漏洞,换一个传参位置,手感和坑都不一样。比如GET注入可以直接改URL,但POST注入就要借助Burp Suite抓包改数据;JSON注入如果格式化不当,payload很容易被转义掉。这些场景差异只有在同一个靶场里连续对比才能印象深刻。
第三,它给了你“高级别打不动”的真实正反馈。很多靶场到高等级仍然用字符串拼接SQL,只是加个过滤函数,所以还能靠技巧绕;但BWAPP的High级别直接改成预处理查询(Prepared Statement),这是根儿上的修复方案。我在刷到高级别时会碰壁,这反而让我明白了一件事:现实中遇到规范开发的系统,SQL注入之所以难打,不是因为你不会绕过,而是因为人家压根没用拼接SQL。
简单对比一下资源:
| 对比项 | BWAPP | DVWA | Pikachu |
|---|---|---|---|
| 漏洞模块数量 | 多,按分类展示 | 少,固定几个页面 | 中等,适合专项练习 |
| SQL注入入口种类 | GET、POST、JSON、登录、SQLite | 主要是GET和Cookies | GET、POST为主 |
| 安全级别 | Low、Medium、High | Low、Medium、High、Impossible | 无等级概念,按题目固定 |
| 源码查看 | 内置入口,方便对照 | 需到官方下载源码文件 | 部分页面有提示 |
| 难度曲线 | 平缓,适合入门 | 比较陡,Impossible很难 | 偏基础,题型套路固定 |
2. 环境搭建与登录准备:先把靶场跑起来
2.1 本地部署的两种方式与版本坑
BWAPP的搭建方式网上有好几种,我推荐两种最省事的方法,一种是直接用Docker,另一种是用小皮面板或者phpstudy这类集成环境。
用Docker是最省心的,找到官方镜像或社区维护的镜像,一条命令拉下来启动即可。但我更推荐本地装PHP集成环境,因为BWAPP很多时候需要你查看源码、调php.ini配置,装在本地方便折腾。我当时的搭建步骤大概是这样:下载源码放进小皮面板的网站根目录,启动Apache和MySQL,浏览器访问http://127.0.0.1/bwapp/,第一步会跳转到安装页面。安装页会让你选择数据库类型,默认填MySQL,数据库名填bwapp,账号密码按本机MySQL的实际情况填,之后点“Create Database”基本就完成了。
这里有一个大坑,很多新手卡在这里:BWAPP的源码比较老,对PHP 5.x版本兼容最好,如果用PHP 7.x或更高版本运行,安装页面能打开,但进入某个漏洞模块时可能白屏或直接报错。实际上它主要的问题是老的mysql函数和PHP 7后废弃的语法。解决办法有两个方向:一是把PHP版本切到5.6或7.2,二是如果坚持用新版PHP,遇到白屏模块就去改对应文件里的写法。我个人实测下来,下载一个PHP 5.6版本的小皮面板,跑BWAPP基本不会遇到兼容性问题。如果你就是想在新版PHP下强上,那至少要开启php.ini里的display_errors,不然报错被吞掉,你连从哪儿下手都不知道。
2.2 初始化数据库与安全级别选择
安装完以后,进入主界面之前你需要登录,默认账号密码一般是bee和bug,这是BWAPP自己的预制账号。登录界面上还有一个security level的选项,这就是我刚才说的难度分级。我建议新人先把级别设为Low,跑通一遍SQL注入流程后再切成Medium去练绕过。
这里特别提醒一句:每次切换安全级别后,最好重新登录一次,否则部分页面可能加载的还是上次级别对应的会话状态。这不是逻辑错误,而是会话和级别绑定在小细节上的问题,我第一次刷的时候没注意,切到Medium后页面还是没过滤,差点以为是靶场坏了,折腾了半天才发现是会话没刷新。
在正式进入模块之前,建议先在浏览器里按F12打开开发者工具,把Network面板开出来。后面很多注入操作需要你看请求参数、响应内容,甚至手动改包。我之前就是懒得开开发者工具,在浏览器地址栏里直接改URL参数,结果遇到POST型注入的时候完全无从下手,后来老老实实配好了Burp Suite才顺畅起来。
2.3 定位SQL注入模块:它有哪些入口
登录后主页面会显示一个漏洞分类列表,点进“SQL Injection”分类,你会看到类似这样的模块列表:
- SQL Injection (GET/Search)
- SQL Injection (POST/Search)
- SQL Injection (GET/JSON)
- SQL Injection (Login Form/Hero)
- SQL Injection (SQLite)
- SQL Injection (Blind)
- SQL Injection (Stored)
这里我建议按顺序刷,先从GET/Search开始,因为它的请求参数直接暴露在URL里,最适合理解注入流程。后面的POST/Search和JSON涉及请求体修改,需要抓包工具配合;Login Form是登录绕过场景,考验的是万能密码逻辑;Blind对应盲注,主要练布尔盲注和时间盲注;SQLite则是基于SQLite数据库的注入,语法上略有差异;Stored属于存储型注入,虽然也是SQL注入,但重点是写入和后台展示,不是我们今天说的查询型注入主线。
刷的顺序我个人的建议是:GET/Search先打通,搞定联合查询和报错注入;然后去Login Form练万能密码;再回头做POST/Search和JSON;最后再碰Blind和SQLite。这样难度爬坡最平缓,思路也能接得上。
3. 联合查询注入:从判断注入点到拖出整张表
3.1 三步判断注入点:报错、恒真、恒假
进入SQL Injection (GET/Search)模块,页面是一个搜索框,输入一个电影名称,比如iron man,点击搜索后,URL会变成类似sqli_1.php?title=iron+man&action=search。先观察正常请求和响应,确认参数title把值传到了后端SQL查询语句中。那么怎么判断这里能不能注入呢?我的固定打法分三步。
第一步,输入一个单引号',观察页面是否报错。如果后端是直接把字符串拼进SQL语句,那么title=''这种结构就会因为多出一个单引号造成语法错误,页面要么直接报MySQL错误,要么返回一个无记录的结果页。在BWAPP的Low级别下,报错信息是直接展示在页面上的,所以这一步非常明显。
第二步,输入恒真条件,比如iron man' and '1'='1,因为'1'='1'永远成立,所以查询结果应该和正常搜索一致。第三步,输入恒假条件,比如iron man' and '1'='2,因为'1'='2'永远不成立,查询结果应该变成空白。如果两步的返回结果有明显差异,那就说明单引号确实参与到了SQL语句的字符串闭合里,存在字符型注入。
这套“报错+恒真+恒假”的判断方法不只是针对BWAPP,遇到任何SQL注入点都可以先套一遍。很多新手上来就union select,结果注入点都还没确认,自然什么都回显不出来。我见过不少朋友在真实授权测试里也是这个毛病,判断注入点的耐心是没有的,一条payload梭哈,打不出来就喊着不存在漏洞。判断注入点其实花不了多少时间,却是整个注入流程里最不能省的一步。
3.2 order by确定字段数和union select定位回显点
确认了注入点,接下来要回答两个问题:当前SQL查询查了几列?页面回显了哪一列?答案需要用到order by和union select。
先确定字段数,payload长这样:
iron man' order by 1-- iron man' order by 2-- iron man' order by 3----是SQL注释符,作用是把后面的内容全部注释掉,这样我们注入的片段后面的原始SQL就不会干扰我们的语句。在URL里直接传这串字符时,注意注释符后面的空格会被浏览器丢掉,要么用%20代替空格,要么换用#注释符。我一般习惯用Burp Suite的Repeater来发请求,这样不会遇到浏览器自动编码的困扰。
order by后面的数字代表按照第几列排序。如果数字大于实际查询列数,就会报错“Unknown column”。所以在BWAPP的GET/Search场景里,从1开始逐个递增,当order by 7报错而order by 6正常时,就说明查询语句取了6列。接下来用union select判断回显位:
iron man' union select 1,2,3,4,5,6--union的作用是合并两次查询的结果,前面查询不到数据时,后一组数据就会显示出来。页面如果正常回显,你会看到一排数字1到6,但页面上通常只显示其中某几个数字。此时页面上出现数字的位置,就是联合查询的可回显列。比如页面只显示3和4,说明第3和第4列的位置可以输出我们想要的数据。记下这个位置,后续获取数据库名、表名的时候,只要把对应位置的数字替换成我们要读取的函数或表达式就行。
3.3 利用information_schema一步步拿库、表、字段、数据
拿到回显位以后,剩下的就是套模板了。先爆数据库和版本信息:
iron man' union select 1,database(),version(),user(),5,6--在BWAPP里执行完,页面会显示当前数据库的名字,一般默认是bwapp,同时会显示MySQL版本和当前用户。看到这些信息说明整条链路已经打通了。接下来获取该库下所有表名:
iron man' union select 1,group_concat(table_name),3,4,5,6 from information_schema.tables where table_schema=database()--information_schema是MySQL自带的“数据库的数据库”,里面保存了整个实例的元数据,tables表存放了所有库名和表名的对应关系。group_concat函数则可以把查询到的多行结果合并成一行,便于在单个回显位输出。BWAPP里通常有一张users表,看名字就知道是存放用户账号的表。继续查这张表的字段:
iron man' union select 1,group_concat(column_name),3,4,5,6 from information_schema.columns where table_name='users'--这里注意,table_name的值如果直接写'users',引号需要和前面的字符串闭合配合好。如果觉得引号总被过滤,可以改成十六进制形式:where table_name=0x7573657273,0x开头后面的部分就是字符串users的十六进制表示。这个技巧在Medium级别过滤单引号的时候特别有用,建议先记下来。
查到users表里有login和password字段后,直接查数据:
iron man' union select 1,group_concat(login,0x3a,password),3,4,5,6 from users--0x3a是冒号:的十六进制写法,用来把login和password在输出时拼接成admin:密码的格式,方便一眼看明白对应关系。到这一步,一次完整的联合查询注入就结束了。整个链路总结下来就四个词:判断注入点、order by数列、union找回显、information_schema查元数据。这套流程适用于绝大多数MySQL环境下的查询型注入。
4. 盲注与过滤绕过:没有回显怎么继续打
4.1 布尔盲注原理与手工二分法
联合查询虽好,但前提是页面会把查询结果展示出来。BWAPP的SQL Injection (Blind)模块就专门设计成了不显示数据内容,只显示“存在记录”和“不存在记录”两种状态。这种情况下,我们就要用盲注。
布尔盲注的核心思路:构造一个条件判断,让SQL语句根据条件的真假返回不同的结果,然后通过观察页面差异判断条件是否成立。比如:
iron man' and ascii(substr(database(),1,1))>97--这条语句的意思是:先截取当前数据库名字的第一个字符,转成ASCII码,判断它是否大于97(字母a的ASCII码)。如果数据库名第一个字符的ASCII码大于97,条件成立,查询结果和正常无差异;否则查询结果为空。
那怎么从“大于97”变成具体的字符呢?用二分法。比如判断到大写还是小写,如果ASCII大于97,说明第一个字符在字母a到z之间;接着判断是否大于109,也就是字母m;如果大于109,范围缩小到n到z;再判断是否大于115……如此反复,每次把范围缩小一半,一个字符最多猜7次就能确定下来。手工盲注虽然慢,但当你一步步把数据库名、表名、字段名全部猜出来的时候,对SQL执行逻辑的理解会深刻非常多。
我建议在BWAPP练习盲注时,配合Burp Suite的Repeater做半自动测试,因为页面返回的布尔差异要在响应长度上观察。Burp里右键“Send to Repeater”改payload,看响应包的“Length”值,有记录时是300多字节,无记录时是200多字节,差异一目了然。等盲注练熟了,再考虑写脚本来自动化,否则直接上脚本很容易变成“跑payload的机器”,盲注的原理反而不清楚。
如果觉得手注太慢,也可以直接用Python写一个简单的布尔盲注脚本,用requests库发请求,通过响应内容判断页面状态:先返回正常搜索时页面里的某个唯一字符串做基准,再拼上注入条件,逐字提取。脚本核心逻辑大概长这样:
import requests import string url = "http://127.0.0.1/bwapp/sqli_4.php" cookies = {"PHPSESSID": "你自己的会话ID"} charset = string.printable def get_payload(pos): # 判断database()在第pos个字符的ASCII码 return f"iron man' and ascii(substr(database(),{pos},1))>{mid}-- " # 对每一位字符做二分逼近,这里省略了请求构造细节,核心是多轮请求收敛范围写脚本的时候要注意设置延时,不然请求太快,本地MySQL跟得上,但如果是模拟线上靶场,容易被软性限频。而且脚本跑起来之前一定要先手动确认两次不同条件的响应确实有稳定的差异,否则脚本等于在瞎猜。
4.2 文本过滤后的绕过技巧
把安全级别切换到Medium之后,你再回GET/Search模块,用之前的payload会发现自己输入的某些字符被过滤掉了。这时页面可能直接提示Hacking attempt,或者把空格和注释符给替换成了空字符串。我切到Medium级别第一次被打蒙时,第一反应是去翻源码。打开sqli_1.php一看,果然有一段过滤函数,它会过滤空格、and、or等关键字。这时就要用“等价替代”的思路来绕。
空格被过滤,可以用注释符/**/代替。SQL语句中/**/和空格在大多数MySQL场景下是等价的,所以order by 3可以写成order/**/by/**/3。关键字被过滤,可以用大小写变形,MySQL默认对关键字大小写不敏感,UNION、Union、uNiOn效果一样。如果select这种纯关键字被过滤,还可以用内联注释/*!50000select*/这种MySQL特性写法,不过实测下来BWAPP的Medium级别没到这么狠的程度,大小写和注释符交替用就够了。
常见过滤方式与绕过对应关系我整理成了表:
| 过滤内容 | 绕过方法 | 示例 |
|---|---|---|
| 空格 | 使用/**/代替 | order/**/by/**/3 |
and/or | 用&&和||,或大小写变形 | 1' && '1'='1 |
| 单引号 | 使用十六进制绕过 | table_name=0x7573657273 |
| 逗号 | 使用join或from结构替代 | limit 1 offset 0 |
| 注释符 | 使用url编码或换行符 | --%20或# |
select | 内联注释或大小写 | /*!select*/ |
这里要特别提醒,切换Medium级别后之前已经登录的会话可能还残留,如果发现过滤规则没生效,先退出重新登录,确认当前级别确实是Medium。BWAPP在这块的逻辑是页面加载时读取会话里的级别设置,一旦判断条件读到的还是Low,过滤代码就不会执行,这是很多人刷题时容易忽略的干扰因素。
4.3 万能密码与登录表单注入场景
登录表单注入是BWAPP里非常经典的一个模块,入口是SQL Injection (Login Form/Hero)。场景本身就是一个登录框,要求输入账号密码。它的SQL语句大概长这样:
SELECT * FROM users WHERE login='$user' AND password='$password'如果后端直接用用户输入拼接SQL,我们就可以在用户名框输入一个“万能密码”来绕过登录:
admin' or '1'='1'--把它代入SQL语句中,字符串闭合之后就变成了:
SELECT * FROM users WHERE login='admin' or '1'='1'-- ' AND password='anything'因为'1'='1'恒成立,login='admin' or '1'='1'这个条件整体为真,注释符又把后半段密码判断全部注释掉了,所以查询直接返回了第一条记录,登录被成功绕过。这就是网上说的“SQL注入万能密码”的底层原理,不是什么神奇的魔法,就是字符串闭合和逻辑运算的组合。
在BWAPP里刷这个模块时,我建议同时打开源码看它Low、Medium、High三个级别分别怎么处理的。Low级别直接拼接,漏洞百出;Medium级别可能用了mysqli_real_escape_string,单引号被转义,万能密码就失效了;High级别基本使用了预处理查询,完全免疫。看懂这三层变化后,你就理解了为什么现代的开发规范强调要用参数化查询,而不是靠输入过滤来防注入。任何黑名单式的过滤都有可能被绕过,只有参数化和预编译才是根治方案。
5. 常见问题与排查技巧实录
5.1 白屏、报错、乱码:环境问题优先排
刷BWAPP时遇到的最多问题不是注入不进去,而是环境本身出问题。我遇到的第一个白屏是在进入某个POST注入模块时,整页空白什么都没显示。当时第一反应是漏洞模块写坏了,折腾半天才发现是因为PHP版本太高,这个页面用到了一个老函数,新版PHP已经把它移除了。后来我在php.ini里打开display_errors和error_reporting(E_ALL),刷新页面看到具体报错信息,才知道问题出在哪。
所以刷靶场之前,先把PHP错误显示打开,这一步能省掉很多无意义的排查时间。如果你用的是小皮面板的PHP 5.6,默认配置一般没问题;如果是PHP 7.x,建议先把版本切回5.6,或者找社区修复过的BWAPP版本。另一个常见问题是数据库初始化失败,安装页面提示“Database doesn't exist”。这种情况多半是数据库账号权限不够,或者数据库名填的不是bwapp。直接进MySQL命令行手动创建一个bwapp库,再给当前用户赋上这个库的所有权限,重新执行安装脚本就行。
还有乱码问题,页面中文或特殊字符显示成问号。这通常是数据库连接字符集和页面编码不一致造成的。BWAPP默认大多用latin1,而浏览器用的是UTF-8,在查看中文数据时就会乱码。你可以把PHP代码里的连接字符集改为utf8,或者在MySQL配置文件里设置character_set_server=utf8,改完重启MySQL再刷新页面就正常了。
5.2 语句不生效:编码、注释符、请求方式三连查
注入payload明明看起来没问题,但页面就是不回显,或者直接返回500,这事我在Medium级别刷过滤的时候经常碰见。后来总结了一套排查顺序,按三步走基本能定位问题。
第一步查URL编码。在浏览器地址栏直接输入'和#等特殊字符时,浏览器可能不把它们作为URL的一部分,甚至直接丢弃。比如#在URL里是锚点的意思,后面内容根本不会发送给服务器。所以我后来在浏览器里验证payload时,尽量用Burp Suite的Repeater,或者用curl命令行,避免浏览器自作主张做编码转换。
第二步查注释符。SQL注入里的--注释符必须带空格才能生效,但如果是在URL里传,末尾的空格又会变成%20,有时后端会把%20解码成空格,问题不大;但有时浏览器或后端框架会把末尾空格去掉,导致--变成--,而--这个写法在MySQL里后面必须有空格,否则不视为注释。遇到这种情况,要么用#注释符,要么用--%20显式编码空格。
第三步查请求方式。GET注入可以改URL,POST注入就要改请求体。很多人把GET/Search里的payload原样搬到POST/Search里,结果发现参数名都变了,自然打不通。POST注入的排查思路是先看请求体里有哪些字段,比如title=xxx&action=search,然后把注入点放在title=xxx的值里,而不是放在URL里。如果一开始就用Burp Suite抓包看请求体,这种低级错误完全能避免。
5.3 从Low到High:学会看懂源码修复逻辑
刷完Low级别和Medium级别后,如果还卡在High级别打不进去,我的建议是别硬扛。打开模块附带的源码入口看一下High级别的处理代码。通常你会看到类似这样的逻辑:先用mysqli_real_escape_string对用户输入做转义,更高一级则直接改用预处理语句:
$stmt = $link->prepare("SELECT * FROM movies WHERE title LIKE ?"); $stmt->bind_param("s", $title); $stmt->execute();看到这一行,你就该明白High级别已经不存在SQL注入了。这不是“过滤没绕过去”,而是后端从根本上改变了SQL执行方式。参数占位符?会把用户输入当作纯数据处理,而不是SQL代码的一部分,任何注入payload都无法改变SQL语句本身的结构。
所以刷到High级别打不动时,正确的收获不是“我技不如人”,而是“我知道了怎么修代码”。PWAPP这个靶场最有意思的地方,就是它通过保护级别把攻击和防御两条线放在同一个场景里。当你把Low、Medium、High全部对照源码看完,其实一套完整的漏洞分析报告就构成了:漏洞成因、攻击路径、过滤失效分析、修复建议,全齐了。
我个人在刷完整个SQL注入模块后最大的感触是,SQL注入难的不是背payload,而是理解SQL语句是怎么被拼接出来的。只要你会看SQL的执行上下文,知道引号和注释符到底闭合了什么,就算把BWAPP的题目全忘记,换到真实环境里也一样能顺着思路拿下来。靶场永远只是起点,但它给了你一个怎么折腾都不会出事的安全环境,请务必在授权和合法范围内练习。下一步你可以继续往后刷报错注入、堆叠注入和SQLite场景,那些又是另一片新天地。