1. 理解这个项目:bWAPP登录表单为什么值得研究
先说结论:在本地靶场里把登录表单的SQL注入绕过玩明白,是理解Web安全攻防对抗最直接的一条路。bWAPP是一个专门用来做安全训练的漏洞靶场项目,里面有几十种真实存在的Web漏洞场景,登录表单的SQL注入就是其中的入门必修课。它不是为了教人攻击真实业务,而是让安全测试人员、开发者和学生在自己的机器上,用合法的环境理解漏洞是怎么产生、怎么被利用、又该怎么修复。
我为什么挑这个标题来写?因为大多数讲SQL注入的文章都在说“万能密码”那三板斧,但实际工作中遇到的过滤规则、输入校验、参数化查询兜底,会让基础写法全部失效。bWAPP的登录表单恰好提供了一个相对干净的实验场,可以练习各种绕过思路。你在靶场里把思路理清之后,再看真实项目里的注入点,就不容易一头雾水。这篇文章适合刚接触Web安全的人、写后端接口的开发者,也适合正在准备渗透测试相关认证的同行。
bWAPP登录表单的核心逻辑其实非常简单:它接收用户名和密码两个输入,直接拼进SQL查询语句里,查询成功就放行登录。这种代码写法在十年前的业务系统里非常常见,现在一些内部系统里依然能看到。靶场代码形如:
SELECT * FROM users WHERE login='$login' AND password='$password'两个输入都被单引号包裹,直接嵌入查询。问题在于,如果用户填写的值里含有单引号、注释符、逻辑运算符,这段SQL的语义就会被完全改写。这不是探测出来的技巧,而是代码结构决定的必然结果。
这个训练场景的价值不只是“能绕过”,而是逼着你思考:如果开发者在前面加了过滤函数,后端又怎么处理?如果过滤了空格怎么办?如果过滤了关键字怎么办?这些问题的答案,就是绕过姿势的核心。
2. 个中原理:登录验证的SQL拼接逻辑
2.1 认证查询是怎么被改写的
登录表单的本质是让用户提交一个身份标记(用户名)和一个凭证(密码),程序拿这两个值与数据库里的记录比对。最原始的实现方式就是字符串拼接。假设用户在用户名框输入了admin,密码框输入了123456,拼出来的查询就是:
SELECT * FROM users WHERE login='admin' AND password='123456'数据库在 users 表里找 login 等于 admin 且 password 等于 123456 的记录,找到就说明凭证正确。这个逻辑本身没有问题,问题在于:如果有人输入的 login 值是admin' OR 1=1 --,拼接后的查询就变成了:
SELECT * FROM users WHERE login='admin' OR 1=1 -- ' AND password='xxx'单引号闭合了 login 字段的引号,OR 把判断条件扩展成恒真,两个短横线注释掉了后面的密码校验。数据库执行这条语句时,条件永远为真,第一条记录就被返回出来,程序误判为认证成功。
很多人把这段学完之后就以为自己会SQL注入了,其实这只是个开始。真正的难点在于,当服务器端引入过滤机制后,上面的写法会被直接拦截。你需要在脑子里建立一个模型:注入的本质是“闭合前文 + 构造逻辑 + 注释后文”,无论过滤规则怎么变,你都要想办法完成这三个动作。
2.2 为什么登录框是练手的好场景
登录框天然适合学习SQL注入,因为它的返回结果非常直观:成功或者失败。这意味着你可以利用这个二元反馈来推断后台SQL语句的执行情况,验证你的假设。相比那些无回显的接口,登录框给了你一个清晰的“信号灯”。
bWAPP的靶场还有一个特点:它同时提供多种数据库后端(MySQL、SQLite等),你可以对比不同数据库对注入语法的影响。比如MySQL的注释符是#,而SQLite同样支持--注释。Oracle的字符串拼接用||,MySQL默认则可以用CONCAT()函数。这种差异在实际渗透测试里经常成为关键突破口。
2.3 靶场练习的边界意识
在开始任何操作之前,必须把边界说清楚:bWAPP是安装在你自己环境里的漏洞靶场,你对它有完全合法的测试授权。本文所讲的所有技术,只适用于这类靶场环境,以及你拥有明确授权的测试项目。把同样的话术用在未经授权的系统上,是违法的。理解漏洞原理,最终是为了写出防御代码,这一点千万别跑偏。
3. 分步实操:非常规绕过姿势逐个拆解
3.1 第一层:闭合与万能密码的变体
先做基础探测。在用户名框输入一个正常值之外的内容,观察页面反应。最经典的验证法是输入admin'加一个单引号,如果页面报错或者行为异常,基本可以确定单引号是被拼进SQL语句的。正常的程序此时应该做参数化查询或者转义处理,如果直接报数据库错误,注入点基本就实锤了。
基础绕过用' OR 1=1 --就可以实现。注意--后面有一个空格,在MySQL的注释语法里面,--注释符必须后跟一个空格或控制字符,否则不生效。这是很多新手第一次踩坑的地方。如果你用的数据库是SQLite,也可以尝试#注释符。
万能密码的进阶变体包括:
' OR '1'='1' -- ') OR ('1'='1 ' OR 1=1 /*第一行是最常见的,第二行用于括号包裹输入的场景,第三行用/*开头,配合注释内容实现同样效果。实际测试时,如果页面没有报错但也没有登录成功,可以尝试把--换成#,或者把OR改成||(在某些数据库里||是逻辑或,比如Oracle,而MySQL默认把它当字符串连接符,所以这个要分后端来试)。
3.2 第二层:空格被过滤时的替代思路
bWAPP登录框的防护强度是可以调整的。当你把靶场安全等级调高后,会发现输入里含空格直接被拦。空格被过滤是绕过题目里最常见的一道坎,因为SQL语法的基本单位就是空格:SELECT、FROM、WHERE这种关键字之间必须有分隔。空格被吃掉之后,SQL语句会变成SELECT*FROM,数据库无法解析。
替代空格的常见方案是使用内联注释/**/。MySQL支持在SQL语句中用注释替代空格,并且注释内容会被忽略。所以' OR 1=1 --可以改写成:
'/**/OR/**/1=1/**/--/**/注意最后的--后面跟了/**/,这个注释必须继续到行尾,用来确保后面的密码字段被彻底注释掉。这个写法在 MySQL 后端下可以直接执行。
另外还有一个思路是用制表符和换行符来充当分隔符。URL编码下的%0a、%0b、%09在部分数据库后端可以被识别为空白字符。不过在实际登录表单中,POST 请求体里的这些字符是否被解析,取决于后端框架,不能一概而论。这种方法的优势是很多过滤规则只检查了空格,不会检查控制字符。
空调替代方案里还有一个容易忽略的点:括号。SQL查询中某些子句是可以用括号包裹的,比如OR (1=1),括号本身就可以作为语句结构的分隔。当你构造复杂注入时,可以利用括号减少空格的依赖。
3.3 第三层:关键字被过滤时的双写与等价转换
过滤规则最常见的做法是黑名单,把OR、AND、SELECT、UNION这些关键字全部直接删除或者替换为空。这里有两种绕法:双写和等价替换。
双写针对的是“删除后拼接”这种实现。如果过滤代码是str_replace("OR", "", $input),它会把所有 OR 替换为空,但是替换完成后剩余的内容会重新拼在一起。你输入OORR,替换后变成OR,正好恢复。于是上面的万能密码变成:
' OORR 1=1 --针对SELECT关键字被删除的情况,就像SELSELECTECT,过滤完中间的 SELECT 后,剩下的还是完整的 SELECT。这种绕法只对简单字符串替换的黑名单有效,如果过滤规则做了递归过滤直到不再变化,那这招就失效了。
等价替换的思路则是找同义语法。比如把OR换成||,把AND换成&&,把=换成LIKE,把SUBSTR()换成MID()或SUBSTRING(),把CONCAT()换成CONCAT_WS()。不同的数据库对这些替代语法的支持程度不一样,这也是为什么要先识别后端数据库类型。
以判断用户名字段为例,布尔盲注里常用的一条语句是:
admin' AND SUBSTRING(login,1,1)='a' --如果SUBSTRING被禁用,可以换MID(login,1,1),或者用LEFT(login,1),再不行就用LIKE 'a%'来做前缀匹配。这些函数在 MySQL 里效果几乎一样,但是过滤规则不太可能把所有函数都拉黑。
3.4 第四层:联合查询获取真实数据
绕过登录只是第一步。进入系统之后,你还需要理解联合查询注入(UNION SELECT)的用法,因为它可以直接把数据库里的其他数据带出来,是获取信息最多的注入方式。
联合查询的核心要求是:UNION 前面的查询和后面的查询必须返回相同数量的列。由于登录查询通常是SELECT * FROM users,列数不固定,你需要先测出这个表有几列。最常用的探测方法是ORDER BY:
admin' ORDER BY 1 -- admin' ORDER BY 2 -- admin' ORDER BY 3 --如果 ORDER BY 3 报错,说明前面的查询只返回了两列。ORDER BY 数字的边界就是列数的上限。在 bWAPP 的默认 users 表里,通常有 login 和 password 两个字段,所以ORDER BY 2正常,ORDER BY 3大概率报错。
确定列数以后,就可以用 UNION SELECT 直接构造数据。假设前面查询返回两列,注入如下:
' UNION SELECT 'hacked','hacked' --登录逻辑只关心查询是否返回记录,不关心记录内容是否真实。这样做可以让系统直接把数据库里不存在的用户名密码当成合法凭证放行。当然这只是演示,真正获取数据时,你会把第二列的位置替换成@@version(数据库版本)、database()(当前数据库名)这些函数,把信息直接打印在页面上。
另一种情况是在登录后发现页面展示用户信息。比如登录成功后页面显示当前用户名,那就说明上面查询选出来的某一列被直接输出到了页面上,这是回显注入。联合查询需要去查询真实存在的用户数据,此时注入点在用户名处,我们利用 UNION 将数据与查询结果合并,在页面回显出来。这时的 payload 形如:
' UNION SELECT login, password FROM users --如果把密码列直接显示在页面上,你只需要登录一次,就能把所有用户的密码散列值全部拉出来。实际测试中,页面是否会有多行返回取决于程序如何处理查询结果。很多业务代码只取第一行,这时要在前面加一个不存在的用户名来保证你 UNION 的那行是第一条,比如:
noexist' UNION SELECT login, password FROM users --3.5 第五层:布尔盲注与时间盲注的姿势
当页面没有任何回显,既看不到报错也看不到数据,仅仅表现为登录成功或失败时,就进入了盲注环节。bWAPP 登录表单完全可以通过页面提示来区分两种状态,这为布尔盲注创造了条件。
布尔盲注的核心是让注入条件为真和假时返回不同结果。比如:
admin' AND 1=1 -- admin' AND 1=2 --第一行返回登录成功,第二行失败,说明你能控制判断条件。接下来就是逐字符地猜解数据库内容。利用 SUBSTRING 函数结合 ASCII 编码,一条经典的猜解语句是:
admin' AND ASCII(SUBSTRING(database(),1,1))>100 --这条语句的意思是:当前数据库名的第一个字符的 ASCII 码是否大于100。如果登录成功,说明确实是,那么继续用二分法逼近,最终确定第一个字符。接着把第二个参数改为2,继续猜第二个字符,直到把整个数据库名拼出来。
时间盲注是在布尔盲注无效时(比如页面永远显示同样的内容)使用的方案。MySQL 里可用SLEEP(5)让查询强制延迟5秒。判断逻辑是:如果条件成立就睡觉,不成立就立即返回,通过响应时间差来判断真假。
admin' AND IF(ASCII(SUBSTRING(database(),1,1))>100, SLEEP(3), 0) --如果页面加载时间大约延迟了3秒,说明条件为真。时间盲注的速度比布尔盲注更慢,但它在极端环境下是最可靠的手段。实际操作中我会先确认当前后端确实支持 SLEEP 函数,否则要把 IF 和条件判断换成等价的函数组合。
3.6 参数构造过程中的计算思维
上面那些 payload 看似随手写出来的,实际上每一步都有明确的推理依据。我把这个过程拆解成一套可复用的思维模板:
第一,观察输入点被包裹的位置。如果是login='$login',那么你要做的是在值里输入一个单引号,把前面的引号闭合掉,然后再接你的逻辑代码。第二,判断是否有其他过滤。如果输入里的空格被删了,就改用/**/或制表符。如果关键字被删了,就考虑双写或者同义词。第三,观察回显特征。页面有没有报错、有没有数据回显、有没有延迟,这决定了你使用哪一种注入变体。
整个过程就像拼图:你手里能用的输入字符是拼图碎片,页面的响应是提示信息,后端代码的写法是拼图底板。只有三者匹配,注入才能成功。新手常见的问题是拿着一个通用 payload 到处试,不分析为什么失败,这是最影响进步的习惯。每次失败,都要先问一句:后端到底把我输入的哪个字符吃掉了?是引号、空格、还是关键字?
4. 防护视角:这些绕过姿势背后对应的防御手段
学了攻击姿势,如果不谈防御,这类技术文章的意义就少了一半。把上面的绕过姿势逐一对照到防御措施上,你会发现其实防御方案相当集中。
参数化查询(PreparedStatement)是解决SQL注入的根本手段。它把SQL语句的结构和参数数据分离,数据库先编译语句结构,再把参数当作纯数据传入,无论参数里写了什么,都无法改变SQL语义。如果你接管的是新项目,这是唯一推荐的做法。如果是对存量系统做改造,至少要把所有用户可控的输入改为参数化查询,而不是继续在字符串拼接上缝缝补补。
输入校验作为第二道防线也很有必要。把上面提到的黑名单逻辑反过来,改成白名单:只允许字母、数字、下划线等安全字符,其他一律拒绝。对于登录表单来说,用户名一般就是字母和数字的组合,密码允许范围稍宽但也可以限制长度和字符集。这种策略直接让注入代码无法进入SQL语句。
过滤规则的设计也有值得反思的地方。很多系统只做了单次替换,被双写轻松绕过。如果确实要保留过滤逻辑,应在过滤后端准备日志和告警,每当有人输入带注释符、单引号、SQL关键字的组合,就直接记录并告警。这些输入在正常业务场景中几乎没有出现概率,一旦出现基本可以判定为扫描或攻击行为,不必在过滤逻辑里死磕,而是让防线升级为态势感知。
数据库权限管理同样关键。业务系统连接数据库的账号应该遵循最小权限原则:一个登录功能只需要执行 SELECT 的权限,就不应该给它 DELETE 或 DROP 权限。很多真实事故的破坏力之所以大,不是因为注入本身,而是因为数据库账号权限过高。
最后是响应层面。生产环境不应返回数据库原始报错信息,统一返回友好错误页。报错信息会暴露数据库类型、表名、SQL语句的片段,大幅降低攻击门槛。把错误细节记录到服务端日志里,供安全人员分析,而不是把细节直接扔给浏览器。
5. 常见问题与排查技巧实录
5.1 单引号报错但万能密码无效
这是我被问得最多的问题。输入admin'后页面确实报错,说明注入点存在,但输入' OR 1=1 --却登录失败。这种矛盾通常有两个原因。
第一个原因是注释符后面少了个空格。MySQL 的--注释符行尾必须有空格,如果你在--后直接回车或输入了其他字符,这个注释就失效。你可以把--改成#,因为#不需要尾随空格。第二个原因是后端不是简单地删除或忽略异常输入,而是做了转义处理,它会把你输入的单引号前加上反斜杠,让引号变成字符串的一部分而不是SQL语法结构。这种情况下,万能密码不会生效,你需要观察报错内容来确认具体是哪种情况。
5.2 过滤了空格后还是被拦
空格被过滤后换上了/**/,结果还是被拦。这时要检查拦截点:如果拦截发生在Web应用层(比如WAF),检测的是整个HTTP请求,那么你输入的/**/可能也在规则列表里。如果拦截发生在应用代码里,多半是代码先把/和*都过滤了。后者更常见,因为很多开发者采用笨办法,把/,*,-,#,'这几个字符一起过滤。遇到这种情况,尝试用制表符和换行符这类空白控制字符,它们的绕过思路不一样。
5.3 盲注猜解总是超时
时间盲注里 SLEEP 没生效,先确认数据库版本和当前连接的账号权限。有些数据库配置里SLEEP函数不可用,或者当前用户没有权限调用。可以用BENCHMARK(10000000, SHA1('test'))这种CPU密集函数代替,它也能产生可测量的延迟。不过 BENCHMARK 在高并发数据库上可能影响其他业务的查询性能,靶场里无所谓,生产环境测试时千万控制好循环次数。
5.4 页面显示内容太少,联合查询不知道选哪些列
用 ORDER BY 探测列数时,注意报错和不报错的分界线。先从 1 开始,每次加1,直到报错,最后一个不报错的数字就是列数。如果页面还限制了查询结果的显示行数(比如只取第一条),优先保证 UNION 注入产生的行出现在第一条。前一节提到前面加一个不存在的用户名就是干这个用的。这个方法看似不起眼,但在实战里能救你一命,否则你辛辛苦苦 UNION 出来的数据永远排在第二位,页面根本不会显示。
5.5 关于工具的取舍
我平时练习这类靶场,交互式工具和手工请求都会用。手工构造 payload 能帮你把语法细节焊死在脑子里,工具则适合在时间紧、需要批量验证的时候用来提高效率。但有一条必须提醒你:不要一上来就对着靶场跑扫描器,至少要先把基础 payload 在手工环境里过一遍,知道每一步为什么生效或失败。否则扫描器跑出来的结果你是看不懂的,一旦成功了你也不知道成在哪,失败了更是一脸黑。
5.6 一条经验留给新同学
把每次成功的绕过都记录下来,包括靶场环境、后端数据库、过滤规则、payload、响应特征。积累到几十条之后,你会发现所谓的“花式绕过”背后其实是同一套骨架:闭合、构造、注释。招式再多,核心就这个。理解了原理之后,你在真实授权测试里遇到再奇怪的过滤,也能静下心来一步一步推理出可行方案。技术这种东西,熟练度和理解深度永远比背payload重要。