SqliLabs刷到Less18和Less19,很多人的第一反应是:这关怎么不按套路出牌?登录框试了半天的单引号、双引号、or 1=1,页面岿然不动;用户名密码输错了会报错,输对了就正常跳转,看起来完全不像有注入点的样子。其实这两关的精髓在于把战场从“参数”转移到了“HTTP头部”。Less18的注入点藏在User-Agent,Less19藏在Referer,登录成功后服务端会把这两个头部的原值拼接进SQL语句,而我们能利用这个点,通过报错注入把整个数据库一步步拖出来。
这两关放在一起刷特别有意思,因为它们的源码逻辑几乎一样,唯一的区别就是注入点所在的请求头不同。如果你只跟着网上的payload机械复制,可能一分钟就通关了,但下次换个场景照样不会。我尽量把背后的闭合原理、报错函数的用法、数据提取链路,以及我实际踩过的坑都写清楚,给正在刷靶场的人一个可以直接“抄作业”又可以自己延伸的完整参考。
1. Less18-19整体认知:题目到底在考什么
1.1 两关的题面与隐藏逻辑
先看Less18的题面,就是一个普通的登录页,用户名和密码两个字段。尝试SQL注入会失败,因为后端对uname和passwd这两个参数做了转义处理,经典的or 1=1根本糊弄不过去。
但如果我用正确的账号密码登录——本地靶场默认就是admin、admin——页面会显示类似这样的内容:
Your IP ADDRESS is: 127.0.0.1 Your User Agent is: Mozilla/5.0 ...这行User Agent回显就是突破口。服务端把请求头User-Agent的内容拼接进了一条INSERT语句,写入了数据库的某张表,用来记录访问日志。关键点在于:这个头部字段没有经过任何过滤和转义,和参数处理完全两个标准。
Less19换成了Referer头。页面把Referer直接查出来回显在页面上,同样拼接进SQL语句。所以只要我们在登录成功的状态下,把头部值改成SQL注入payload,就能让服务端执行我们构造的SQL语句。
1.2 为什么把这两关放一起研究
这两关本质是同一个漏洞模型:非参数位置的输入点被拼接进SQL。大多数SQL注入教程都在教GET参数和POST参数的注入,但实际业务里,凡是能被服务端读取并输出或存储的请求数据,都可能成为注入点。
User-Agent、Referer、Cookie、X-Forwarded-For、X-Client-IP,这些都属于“请求头型注入”。Less18和Less19是理解这类问题最好的入门样本,因为它们没有加任何WAF,没有过滤,单纯演示了漏洞的形成过程。
同时,这两关的难度梯度设计也很合理。Less18需要你理解“注入点前面还有一段SQL”,要用单引号去闭合它;Less19在此基础上又增加了“Referer为空就不执行”的坑,考验你对SQL语句拼接结构的判断能力。把两关连在一起刷完,你对POST型头部注入的理解会完整很多。
1.3 前置技术栈:你需要会什么
刷这两关前,建议先掌握:
- SQL基础:会select、where、group_concat的基本语法
- MySQL函数基础:updatexml、extractvalue这些报错函数的用法
- HTTP协议基础:清楚User-Agent、Referer在请求中的位置
- 抓包工具基础:Burp Suite的Proxy抓包、改包、重放操作
如果你对这几个点还有模糊的地方,不用急,下面每一节都会把对应的操作和原理拆开细讲,你跟着实操一遍自然就通了。
2. 环境准备与核心思路:先把工具和原理理顺
2.1 本地靶场部署与工具清单
刷SqliLabs必须要用本地环境,千万不要对公网上的真实站点做任何注入测试,这是安全底线。我自己的环境是PHP集成环境加源码目录一套搞定:
- PHP集成环境,我用的是PHP 5.6版本,SqliLabs对高版本PHP会有一些接口兼容问题,如果页面报错或者函数失效,优先排查PHP版本
- SqliLabs源码放进Web根目录,按照说明创建数据库并导入SQL文件
- Burp Suite作为抓包改包主力,本机浏览器配上代理
- 备用方案是浏览器开发者工具配合修改请求头的插件
装完之后用admin/admin登录一下,确认能正常跳转和显示IP、UA等信息,就可以开始操作了。
2.2 头部注入的形成原理:一句话说透
我们来看Less18服务端大概做了什么操作。你登录成功后,后端会拿到你的用户名、IP地址、User-Agent,然后构造一条SQL语句插入日志表。
简化后的逻辑是这样的:
INSERT INTO uagents (uagent, ip_address, username) VALUES ('$uagent', '$ip', '$uname');其中$uagent就是请求头User-Agent的原值。如果我把它构造成:
xxx' , '127.0.0.1', 'admin')-- a那拼接出来的SQL就变成了:
INSERT INTO uagents (uagent, ip_address, username) VALUES ('xxx' , '127.0.0.1', 'admin')-- a', '127.0.0.1', 'admin');两个单引号被我们闭合,后面的注释符把原本剩余的部分直接吞掉。这样一来,整条SQL语句的结构就被我们控制住了。
Less19的Referer注入同理,只是拼接位置变成了Referer字段。理解了这一层,后面所有payload都只是在这个闭合基础上做数据提取而已。
注意:Less18的User-Agent如果留空,很多环境容易出现逻辑异常,导致注入过程不顺畅。建议先保证UA、Referer这些头部都有正常值,然后再开始注入测试。
2.3 为什么要用报错注入而不是联合查询
很多刚刷完前面关卡的人会习惯性地先试union select,但在Less18里这样很难出结果,因为页面不回显查询结果,只回显插入的字段数据。也就是说,联合查询的结果没有地方显示给你看。
报错注入的思路完全不同:它利用的是MySQL在遇到语法错误时会把错误信息回显到页面上这个特性。我们构造的payload会故意触发一个报错,并且把想要的数据拼在报错信息里。页面虽然报错,但报错文字里携带了数据库信息,这比联合查询更适配这种“无结果回显”的场景。
Less18和Less19的核心玩法都一样:闭合前面的SQL拼接,让updatexml或extractvalue报错,然后把数据带出来。
3. Less18完整通关:从登录到拖库的每一小步
3.1 第一步:登录并抓包确认回显点
打开浏览器访问靶场,输入admin/admin登录。按正常流程走,登录成功后会看到欢迎页面,上面有你的IP和User Agent信息。
这时打开Burp Suite,开启拦截,刷新一次页面,把当前请求抓下来。我们会看到这样一个POST请求:
POST /sqli-labs/Less-18/ HTTP/1.1 Host: 127.0.0.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Referer: http://127.0.0.1/sqli-labs/Less-18/ Content-Type: application/x-www-form-urlencoded Cookie: PHPSESSID=xxxxxx Content-Length: 25 uname=admin&passwd=admin登录后的请求,Even without Burp你也可以直接用浏览器插件改UA重放,但在Burp里改包更干净。关键操作:把原始User-Agent那一段替换成测试payload。
3.2 第二步:确认注入点存在
先把User-Agent的值改成非常规内容,比如末尾加个单引号:
Mozilla/5.0'发送请求,如果页面出现SQL语法报错,或者输出内容和正常时不一样,说明单引号确实破坏了原有SQL结构。
接下来做一个经典的闭合测试。把UA改成:
Mozilla/5.0' AND '1'='1发送后观察,如果页面正常显示UA相关的内容,说明恒真条件生效,SQL执行成功。再把UA改成:
Mozilla/5.0' AND '1'='2发送后如果页面异常或者不显示UA信息,说明恒假条件导致SQL执行失败。
这一真一假的对比,就证明了我们完全控制了SQL语句的判断逻辑。到这里,可以确认注入成立。
3.3 第三步:用updatexml构造报错注入
报错注入的常用函数是updatexml和extractvalue。updatexml的语法结构是:
UPDATEXML(xml_document, xpath_expr, new_value)第二个参数需要是一个合法的XPath表达式,如果传入不合法,MySQL会直接报错,并且把传入值回显在错误信息里。利用这个特性,我们把要查的数据拼进XPath位置,数据就被报错信息带出来了。
Less18里我常用的payload结构是这样的:
Mozilla/5.0' AND updatexml(1, concat(0x7e, database(), 0x7e), 1) AND '1'='1拆解一下:
- 开头的
Mozilla/5.0'是为了闭合掉SQL中已有的单引号 AND updatexml(...)让整体SQL在满足前面条件的同时,必然触发报错concat(0x7e, database(), 0x7e)把波浪号和数据库名拼在一起,确保报错信息里能看到完整的返回内容- 结尾的
AND '1'='1是重新闭合后半段的单引号,保证语法完整
发送后页面回显的报错信息里就会出现波浪号包裹的数据库名。
3.4 第四步:拖出表、字段、数据的完整套路
拿到数据库名之后,依次换掉函数里的查询语句就行。
获取所有表名的payload:
Mozilla/5.0' AND updatexml(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema=database()), 0x7e), 1) AND '1'='1获取某张表的字段名,把users替换成目标表名:
Mozilla/5.0' AND updatexml(1, concat(0x7e, (select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'), 0x7e), 1) AND '1'='1获取一条用户数据:
Mozilla/5.0' AND updatexml(1, concat(0x7e, (select concat_ws(0x3a, username, password) from users limit 0,1), 0x7e), 1) AND '1'='1concat_ws(0x3a, username, password)的意思是:把username和password用冒号连接在一起,0x3a就是冒号的十六进制写法。limit 0,1控制取第几条,想取后续数据就改成limit 1,1、limit 2,1。
注意:updatexml的报错信息有32字节显示上限,超过部分会被截断。所以如果表名很长,或者多个字段名拼接,经常只能看到前半段。解决办法是分多次查询,用
substr函数截取后半段,比如把database()改成substr(database(), 25),从第25个字符开始显示。
3.5 Less18实操中的几个细节
- 每次修改UA后都要重新发送登录后的请求吗?不一定,看后端逻辑。如果会话保持有效,可以直接改UA重放请求观察回显;如果会话失效,就重新登录一次再改包。
- 浏览器默认UA很长,替换时建议把完整UA换掉而不是在后面追加。因为默认UA末尾没有单引号,直接追加会破坏闭合逻辑。
- 如果报错信息里出现
Near ''xxxx''这样的提示,说明我们的引号数量没配对,返回去检查payload里的引号和注释符。
4. Less19完整通关:Referer注入的差异与坑
4.1 从UA到Referer:变化的是什么
Less19登录后页面会显示:
Your Referer is: http://127.0.0.1/sqli-labs/Less-19/服务端把Referer头原样回显并存入数据库。和Less18的区别听起来只是“换了个字段”,实际操作时会有几个完全不同的体验。
首先,Referer头的内容默认是一个合法的URL,里面通常不包含单引号,这反而让闭合逻辑更清晰。其次,Referer头为空时,后端的SQL拼接逻辑可能根本不执行,这会导致注入失败。
所以刷Less19的第一步建议是:先正常登录,用抓包工具确认当前请求里的Referer字段是什么,然后在这个基础上做修改。
4.2 Less19的注入姿势对比
参考Less18的思路,Less19的核心payload可以这样写:
http://127.0.0.1/sqli-labs/Less-19/' AND updatexml(1, concat(0x7e, database(), 0x7e), 1) AND '1'='1闭合思路完全一致:URL末尾单引号闭合前面的值,最后补一个AND '1'='1闭合整个语句。发送后报错信息里就能看到数据库名。
后面的数据提取链路,从表名到字段名到数据,和Less18的写法完全一样,只需要替换外层头部值即可。
4.3 最容易翻车的几个坑位
第一,Referer头如果被某些插件屏蔽或留空,SQL语句里就会出现两个单引号紧挨着的情况,注入通常以失败告终。我在第一次刷这关时,因为浏览器插件把Referer置空,调试了快二十分钟才反应过来。如果你发现payload怎么改都没反应,先去看请求头的实际值。
第二,Referer的值里可能带了URL参数,例如原始Referer就是http://127.0.0.1/sqli-labs/Less-19/?id=1这样的,如果直接在后面加单引号,闭合逻辑会变得混乱。建议先把Referer整体替换成简洁的http://xxx再加payload,路径越短越容易控制。
第三,Less19同样有32字符显示截断问题。如果要读取的数据很长,比如要查的密码字段是多行拼接,必须配合substr分段读取,否则结果不完整。实际操作时我习惯用limit 0,1先只取一条数据,确认字段格式后再批量拉取。
5. 源码审计与防护视角:为什么这两关值得细品
5.1 从源码看漏洞成因
SqliLabs的源码非常适合用来对比“转义与不转义的区别”。在这两关里,用户名和密码字段都经过了转义处理,直接输入引号会被加反斜杠,无法改变SQL结构。但User-Agent和Referer这两个字段没有任何处理,直接拼进SQL字符串里。
这种“部分防护”在真实业务里太常见了。开发知道要对用户输入的参数做防注入,但忽略了HTTP头部、Cookie这些也能被客户端完全控制的地方。很多系统里,请求头字段为了日志记录或风控需求被存储,一旦拼接进SQL,就成了和参数注入一样严重的漏洞。
用生活化的比喻:你家大门装了防盗锁,但后院的小门常年敞着。攻击者不一定要撬正门,绕着走就行。
5.2 从攻击视角反推防御
理解Less18和Less19的攻击过程,其实是在帮我们建立防御直觉。
- 第一层:永远不要用字符串拼接SQL,参数化查询是解决这类问题的根本手段
- 第二层:请求头字段如果需要存储,宁可先规范化处理,比如只保留白名单字符,或者对引号做转义
- 第三层:数据库的错误信息不要直接输出到页面,这是报错注入能成立的前提
- 第四层:日志型数据表不要执行复杂查询,插入操作尽量降低权限
这些防御手段单独拎出来都不复杂,但组合起来就是一套有效的纵深防御。只看攻击不看防御,技术视野会少一半。
5.3 真实场景里头部注入更隐蔽
Less18和Less19看起来很玩具,但真实环境里的头部注入往往出现在API网关日志、统计系统、风控系统这些地方。攻击者不需要直接打业务主站,只需要在一些边缘系统埋点,等管理员查看日志时触发payload,或者通过批量请求把注入口刷起来。
很多扫描器只会测GET和POST参数,不会自动测UA和Referer,所以这类问题在一些老旧系统里存活周期很长。这也是我建议新手认认真真把这两关源码读一遍的原因,理解漏洞在哪个位置、为什么存在,比记住payload更有价值。
6. 常见问题与排查技巧实录
6.1 登录失败或登录后没有IP、UA回显
大部分情况是PHP版本问题。SqliLabs是老项目,高版本PHP下部分函数行为变化会导致SQL语句执行异常。我自己遇到最典型的是:登录能成功但页面不显示“Your User Agent”那几行字。这种时候先把PHP版本切回5.6试试,问题一般能解决。
也有可能是数据库初始化不完整,SqliLabs的说明文档里要求导入固定的SQL文件,如果漏了某张表,也会影响回显。
6.2 payload发送了但页面完全没反应
首先确认请求头真的带上了。Burp里改完UA或Referer后,要重新点一次“Forward”把请求发出去,别改了包却忘了放行。其次确认改的是当前生效请求的那一行,有些请求会多次发送,你可能改了第一次,但页面回显的是第二次的结果。
最后,检查payload里的引号闭合。Less18末尾的AND '1'='1和前面UA的'必须配对,少一个引号整条SQL就报语法错误,自然看不到数据。
6.3 报错信息出现了,但数据总是不完整
这是updatexml的32字符显示限制。我的解决套路是combination:先拉一个总长度的依据,比如用length(database())确认长度,再用substr分段读取。读取数据时优先用单条数据,别用group_concat把几十条记录拼一起,否则截断后你永远只能看到开头的字段名。
6.4 表名有下划线或者其他特殊字符怎么办
表名里有下划线不影响SQL执行,但如果表名很长,报错回显会截断。关键字眼能看一半就行,配合substr分段拉取,或者按位置推算完整名字。真实环境中,拿到表名的一部分往往已经能推断出表用途了,剩下的可以结合数据字典继续猜。
6.5 为什么有些教程用的payload和我写的不一样
常见变体有extractvalue用法、floor(rand(0)*2)双查询报错、name_const报错,这些都能实现类似效果。Less18和Less19对payload类型没有限制,只要能触发MySQL报错并把数据带出来就行。选updatexml是因为它语法短、可控性高,个人用起来最顺手。
用我的话说,Less18和Less19最大的价值不是让你多拿一个通关记录,而是逼你跳出“参数注入”的思维定式。我在刷完这两关后,再看到任何把请求头拼入日志表的代码,第一反应就是这里能不能闭合、能不能报错带数据。这种条件反射,光看文档是练不出来的。
后面如果你把整个SqliLabs系列刷完,再回头对比Less18和Less19,会发现它俩正好是一个分水岭:前面的关卡大多在教你怎么往“参数”里塞东西,这两关却在教你怎么往“请求结构”里塞东西。实战中后者往往更隐蔽,也更考验对SQL拼接逻辑的理解。先把这两关啃透了,后面遇到Cookie注入、JSON参数注入、文件上传场景里的SQL注入,思路都会顺畅很多。