1. 从“报错”到“盲注”sqli-labs进阶关卡的核心转变如果你已经跟着我的笔记从sqli-labs的第一关一路闯到了第十关那么恭喜你你已经成功解锁了SQL注入的“新手村”对基于报错的注入、联合查询注入这些基础手法有了扎实的体验。从第十一关开始sqli-labs的难度曲线会有一个明显的爬升我们正式进入了“登录框注入”和“盲注”的实战领域。这不再是你输入一个单引号页面就友好地告诉你“这里有SQL语法错误”的简单模式了。从第十一关到第二十关靶场的设计者开始模拟更真实的Web应用场景登录表单、Cookie、HTTP头以及最考验耐心和技巧的盲注。很多朋友在通关前十关后信心满满地来到第十一关输入经典的‘ or ‘1’’1却发现页面要么登录失败要么只是刷新一下没有任何数据库报错信息回显在页面上。这时候就容易卡壳感觉无从下手。这正是本阶段学习的核心价值所在当应用不再将数据库的错误信息直接展示给用户时我们该如何进行渗透测试答案就是盲注。盲注顾名思义就是在“盲”的情况下进行注入我们无法直接看到查询结果或错误详情只能通过应用返回页面的细微差异如登录成功/失败、响应时间长短、页面某个单词的存在与否来一点点“盲猜”出数据库的信息。这个过程更像是在拆解一个没有图纸的密码锁通过听齿轮的声音页面的布尔状态或者感受阻力时间延迟来判断锁芯的结构。本阶段的另一个重点是注入点的转移。前十关几乎都是GET请求下的参数注入而从第十一关起POST请求下的表单注入、Cookie注入、User-Agent注入等纷纷登场。这要求我们不仅要知道Payload怎么写更要清楚该把Payload“放在哪里”。你的Burp Suite、HackBar或者浏览器的开发者工具将成为比输入框更常用的武器。接下来我将带你逐一拆解第十一关到第二十关不仅告诉你通关的Payload更重要的是剖析每一关的设计意图、背后的SQL查询逻辑以及在这个场景下一个经验丰富的测试者会如何思考、如何一步步构造攻击链。我们会遇到布尔盲注、时间盲注、基于报错的盲注甚至需要利用数据库的某些特性来获取信息。准备好了吗让我们开始这段从“明处”走向“暗处”的进阶之旅。2. 第十一关POST请求下的单引号字符型注入与万能密码第十一关是一个典型的登录表单有Username和Password两个输入框。很多新手的第一反应是在两个框里都尝试注入这思路没错但我们需要更系统的方法。首先进行最基本的探测在Username输入一个单引号‘Password随意输入。点击登录后页面弹出了一个数据库报错信息“You have an error in your SQL syntax...”。这是一个非常明确的信号说明注入点存在并且是字符型注入因为单引号破坏了SQL语句的字符串边界。注意在实际测试中遇到登录框优先测试用户名字段因为后端SQL查询语句中用户名字段被引号包裹的概率远高于密码字段密码可能经过哈希处理后才参与查询。根据报错信息我们可以反推后端的SQL查询逻辑大致是SELECT * FROM users WHERE username‘我们输入的用户名’ AND password‘我们输入的密码’我们的目标是将这个条件变为永真让查询能返回结果通常意味着登录成功。最经典的Payload就是在Username输入admin‘ or ‘1’’1Password可以留空或随意输入。但是直接这样输入往往会失败因为拼接后的SQL语句是SELECT * FROM users WHERE username‘admin‘ or ‘1’’1’ AND password‘xxx’这里存在一个逻辑和引号闭合的问题。admin‘之后多了一个单引号与后面的or ‘1’’1组合时语法是混乱的。更标准的做法是使用注释符来注释掉后面的语句。在MySQL中--注意后面有个空格或#可以注释掉后续所有内容。因此一个更可靠的Payload是Username:admin‘ --Password:任意值或不填这样后端的SQL语句会变成SELECT * FROM users WHERE username‘admin‘ -- ’ AND password‘xxx’--之后的所有内容都被注释掉了查询条件简化为username‘admin’。只要数据库中存在用户名为admin的记录就会返回数据导致登录成功。这就是所谓的“万能密码”攻击的一种形式。在第十一关使用这个Payload通常能直接看到登录成功的欢迎信息并伴随着Your Login name:和Your Password:的回显这其实为我们后续的联合查询注入铺平了道路因为成功登录后的页面会执行另一条查询来展示用户信息而那条查询可能也存在注入点且会回显数据。不过这一关更深入的学习点在于理解POST请求的注入测试流程。你不能仅仅在浏览器地址栏操作了。你需要使用Burp Suite拦截登录请求。在HTTP请求体Body中找到uname和passwd参数sqli-labs的参数名。在Burp Suite的Repeater模块中修改这些参数的值发送Payload并观察响应。 这个过程是后续所有POST类型注入的基础。通过Burp Suite你可以清晰地看到原始的POST数据方便地添加各种特殊字符和注释符而不受浏览器URL编码的干扰。3. 第十二关双引号与括号的闭合挑战第十二关的页面和第十一关一模一样但如果你把第十一关的Payloadadmin‘ --原封不动地搬过来会发现登录失败了而且也没有报错信息。这说明注入点的闭合方式发生了变化。我们重新进行探测在Username输入一个双引号“。这次页面报错了错误信息提示语法错误并且从错误信息中我们可以清晰地看到我们输入的双引号被放入了SQL语句中。因此我们推断第十二关的后端查询逻辑是SELECT * FROM users WHERE username(“我们输入的用户名”) AND password(“我们输入的密码”)或者可能是username(“我们输入的用户名” and password“我们输入的密码”)的形式。无论是哪种闭合符号从单引号变成了双引号。所以我们的Payload也需要相应改变。将单引号替换为双引号即可。Username:admin“ --Password:任意值这样SQL语句变为SELECT * FROM users WHERE username(“admin“ -- ”) AND password(“xxx”)双引号被正确闭合后面的内容被注释查询条件变为username“admin”从而绕过登录。这一关看似简单但其核心教学意义在于永远不要想当然。在真实的黑盒测试中你无法看到后端代码。你必须通过系统的测试来判断闭合符号依次尝试‘、“、‘)、“)、‘))等等组合观察页面的反应报错、行为异常等。这是一个必不可少的探测步骤。许多自动化SQL注入工具的第一步就是进行这种闭合符的模糊测试。掌握手动判断的方法能让你在工具失效时依然有路可走。4. 第十三关单引号与括号的闭合及报错注入利用第十三关的界面依然是一个登录框但无论成功与否页面都只返回一串“报错”的提示没有成功登录后的数据回显。这引导我们走向另一种注入类型基于报错的注入。我们首先进行闭合符探测。输入单引号‘页面返回了详细的数据库报错信息。这很好说明存在注入且报错信息可见。但当我们尝试admin‘ --时却发现登录不成功也没有报错。这说明闭合方式可能更复杂。尝试输入admin‘) 页面报错信息发生了变化。通过对比报错信息我们可以推断出原始的SQL语句结构可能类似于SELECT * FROM users WHERE username(‘我们输入的用户名’) AND password(‘我们输入的密码’)或者SELECT * FROM users WHERE username‘我们输入的用户名’) AND password‘我们输入的密码‘。为了确定我们可以构造一个能引发语法错误的Payload来验证。例如输入Username:‘) or (‘1’)(‘1Password:任意值如果登录成功则验证了闭合方式为(‘...’)。在这一关这个Payload是有效的。但这一关的挑战在于即使登录“成功”页面也没有数据回显只显示一些固定的提示信息。我们无法使用联合查询UNION SELECT来直接获取数据。这时候就需要用到报错注入。报错注入的原理是利用数据库函数的执行错误将我们想要查询的数据如数据库名、表名通过错误信息带出来。MySQL中常用的报错函数有updatexml()、extractvalue()和floor(rand()*2)等。以updatexml()为例。它的语法是updatexml(XML_document, XPath_string, new_value)。如果XPath_string的格式不符合XPath语法MySQL就会报错并将这个不合法的字符串内容显示在错误信息中。我们可以利用这一点。首先我们需要让整个SQL语句执行起来所以要先闭合前面的语句。一个典型的Payload如下Username:admin‘) and updatexml(1, concat(0x7e, (select database()), 0x7e), 1) --Password:任意值**我们来拆解这个Payloadadmin‘)用于闭合username(‘...’)中的单引号和括号。and连接一个永真条件这里我们假设admin用户存在如果不存在可能需要换成or或者使用一个永真条件如11。updatexml(1, concat(0x7e, (select database()), 0x7e), 1)这是报错注入的核心。concat(0x7e, (select database()), 0x7e)0x7e是波浪号~的十六进制。select database()用于查询当前数据库名。concat函数将它们拼接在一起例如~security~。updatexml(1, ‘~security~‘, 1)第二个参数‘~security~‘不是一个有效的XPath路径因此数据库执行时会报错错误信息类似于“XPATH syntax error: ‘~security~‘”。--注释掉后续的AND password...部分。发送这个Payload后页面不会显示登录成功但会在错误提示区域显示出包含数据库名security的报错信息。通过这种方式我们就可以在无回显的情况下“借道”错误信息通道一步步获取数据库中的信息例如表名、列名和具体数据。这个过程需要反复构造Payload将select database()替换为select group_concat(table_name) from information_schema.tables where table_schemadatabase()来获取表名再进一步获取列名和数据。虽然繁琐但它是应对无回显场景的利器。5. 第十四关双引号闭合下的报错注入实战有了第十三关的经验第十四关就顺理成章了。这一关同样是登录后无数据回显只显示固定信息。我们首先探测闭合符。输入单引号‘没有报错。输入双引号“页面出现了数据库报错信息。因此确定是双引号闭合。我们尝试使用双引号闭合的万能密码Username:admin“ --Password:任意值但你会发现这一关这样做并不能“成功”登录到有数据回显的页面因为设计如此或者没有任何变化。这说明我们需要使用报错注入。构造基于双引号闭合的报错注入PayloadUsername:admin“ and updatexml(1, concat(0x7e, (select database()), 0x7e), 1) --Password:任意值发送后页面同样会返回一个XPATH语法错误其中包含了当前数据库名security。接下来的步骤就和第十三关一样了通过修改updatexml中select语句的内容逐步获取所需信息。例如获取表名admin“ and updatexml(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schemadatabase()), 0x7e), 1) --报错信息可能会因为长度限制被截断updatexml最大显示32位这时可以使用substring()或mid()函数来分片读取。例如admin“ and updatexml(1, concat(0x7e, substr((select group_concat(table_name) from information_schema.tables where table_schemadatabase()), 1, 30), 0x7e), 1) --然后不断调整substr的起始位置来获取完整数据。这一关巩固了我们在不同闭合符号下运用报错注入的能力。关键在于灵活变换Payload开头用于闭合的部分‘、“、‘)、“)等而核心的报错函数构造逻辑是相通的。6. 第十五关与第十六关布尔盲注的耐心博弈从第十五关开始我们进入了真正的“盲”注世界。无论你输入什么页面只有两种状态“存在”或“不存在”。在sqli-labs中通常表现为输入正确的凭证如数据库中存在的用户时页面会显示“You are in...........”而输入错误时则没有任何提示或显示别的内容。没有报错信息没有数据回显我们唯一能依赖的就是这个布尔状态。第十五关是单引号闭合的布尔盲注。经过探测输入‘导致状态从“存在”变为“不存在”我们确认了这一点。布尔盲注的核心思想是通过构造SQL查询条件让页面根据我们猜测的内容是否成立返回不同的状态从而一位一位地“猜”出数据。例如我们想猜解当前数据库名的第一个字母。数据库名是security第一个字母是s其ASCII码是115。我们可以构造这样的SQL语句SELECT * FROM users WHERE username‘admin‘ and ascii(substr(database(),1,1))115 -- ’ AND password‘xxx’这条语句的意思是如果当前数据库名的第一个字符的ASCII码等于115那么and后面的条件为真整个查询可能返回结果假设admin用户存在页面显示“You are in”。如果不等于115条件为假查询可能无结果页面无显示。因此我们的攻击步骤是猜解数据库名长度admin‘ and length(database())8 --。如果页面显示“存在”则长度是8。猜解数据库名每个字符使用substr(database(), N, 1)截取第N个字符用ascii()函数将其转为ASCII码然后与猜测的数字比较。例如猜第一个字符admin‘ and ascii(substr(database(),1,1))100 --如果“存在”说明ASCII码大于100admin‘ and ascii(substr(database(),1,1))120 --如果“存在”说明小于120通过二分法大于/小于或遍历等于最终确定第一个字符的ASCII码是115即 ‘s‘。重复此过程猜解第二个、第三个...字符直到拼出完整的security。这个过程极其繁琐必须借助工具。Sqlmap是自动化完成此过程的不二之选。使用Sqlmap的命令大致如下sqlmap -u “http://靶场地址/sqli-labs/Less-15/” --data“unameadminpasswdtestsubmitSubmit” --level3 --risk2 --techniqueB --dbs这里--data指定POST参数--techniqueB指定使用布尔盲注技术。Sqlmap会自动完成上述所有的猜测和判断。第十六关是双引号加括号闭合(“)) 的布尔盲注。探测方式类似输入“)会导致页面状态变化。确认闭合方式后Payload的构造只需将第十五关的单引号闭合改为“)闭合即可。例如猜解长度的Payload变为admin“) and length(database())8 --后续的字符猜解Payload也做相应变换。同样使用Sqlmap时需要指定正确的闭合方式有时Sqlmap可以自动探测但手动指定更可靠--prefix“\”)“ --suffix”-- “。不过对于sqli-labs通常使用--level和--risk提高检测等级Sqlmap就能自动识别。布尔盲注是对耐心和工具使用能力的双重考验。手动完成几乎不可能但它深刻地揭示了当应用对用户输入进行严格过滤、只返回布尔状态时数据库信息依然可能被缓慢而确定地窃取。7. 第十七关Update语句注入与二次注入的伏笔第十七关是一个“重置密码”的功能页面。它需要你先输入一个存在的用户名然后输入新密码进行重置。后端执行的很可能是一条UPDATE语句例如UPDATE users SET password‘新密码’ WHERE username‘输入的用户名’注入点通常出现在WHERE条件的用户名处。我们尝试输入一个存在的用户名如admin和一个新密码然后用单引号探测。在用户名后添加‘提交后页面果然报错了确认存在注入且是单引号闭合。对于UPDATE语句的注入我们的目标不仅仅是绕过条件有时更希望能修改其他数据或利用报错提取信息。一个简单的测试Payload是Username:admin‘ and 11 --New Password:123如果密码重置成功说明注入生效。但这一关的妙处在于它常常被用来演示基于时间的盲注。因为UPDATE操作成功后页面可能只是简单地提示“Password updated successfully”没有布尔状态差异。这时我们可以利用sleep()函数来构造时间盲注。时间盲注的Payload如下Username:admin‘ and if(ascii(substr(database(),1,1))115, sleep(5), 1) --New Password:123这条语句的意思是如果当前数据库名的第一个字符的ASCII码等于115‘s‘那么执行sleep(5)让数据库睡眠5秒否则返回1。通过观察页面响应时间是否显著延迟大于5秒我们就可以判断猜测是否正确。这个过程比布尔盲注更慢因为每一次猜测都需要等待睡眠时间。使用Sqlmap进行时间盲注扫描sqlmap -u “http://靶场地址/sqli-labs/Less-17/” --data“unameadminpasswd123submitSubmit” --techniqueT --level3 --risk2--techniqueT指定使用时间盲注技术。此外第十七关还隐含着“二次注入”的概念。想象一下如果用户名本身在注册时被存入数据库后来在重置密码时又被取出并拼接到UPDATE语句中如果用户名里包含了恶意SQL代码如admin‘ --那么在重置密码时这段代码就会被执行。虽然这一关没有直接体现注册功能但它模拟了这种“数据存入后再取出执行”的危险场景值得深思。8. 第十八关HTTP头注入之User-Agent第十八关是一个登录后显示用户IP和User-Agent的页面。成功登录后页面会显示 “Your User Agent is: …” 和 “Your IP address is: …”。这提示我们信息可能来自HTTP请求头。HTTP头注入是指将恶意Payload插入到HTTP请求头字段如User-Agent, Referer, X-Forwarded-For, Cookie等中而这些字段被后端代码不加过滤地拼接到SQL查询里。这一关的注入点就在User-Agent头。为了测试我们需要先正常登录可以使用一个已知的正确用户名和密码如admin/admin。登录成功后我们使用Burp Suite拦截这个显示User-Agent页面的请求可能是登录后的跳转请求也可能是刷新页面的请求。在Burp Suite的Repeater中找到User-Agent这个请求头在其原始值后面添加我们的注入Payload。首先探测闭合。将User-Agent修改为Mozilla/5.0 ... ‘如果页面返回SQL语法错误则证明存在注入且很可能是单引号闭合。因为后端代码可能这样写INSERT INTO log_table (user_agent, ip_address, username) VALUES (‘$user_agent‘, ‘$ip‘, ‘$username’)我们的目标是将恶意代码注入到这个INSERT语句中。我们可以使用联合查询注入来获取数据因为成功注入后查询结果可能会回显在页面上例如原本显示User-Agent的地方变成了我们联合查询的结果。一个典型的Payload如下Mozilla/5.0 ‘ union select 1,group_concat(table_name),3 from information_schema.tables where table_schemadatabase() --我们来分析一下开头的‘用于闭合原SQL语句中VALUES列表里User-Agent值的第一个单引号。union select ...是我们注入的查询。我们需要猜测INSERT语句后面有多少列。通常通过order by来试探但这里更简单的方法是观察页面回显位置。原页面显示User-Agent和IP两个信息说明查询结果至少有两列被用到。我们尝试union select 1,2,3看看哪个数字会显示在“Your User Agent is:”或“Your IP address is:”的位置。假设数字2显示在User-Agent处数字3显示在IP处。那么我们将想要获取的数据放在select语句的第二个和第三个位置。例如group_concat(table_name)放在位置2用于获取所有表名并显示在User-Agent处。--用于注释掉原INSERT语句中后面的内容如, ‘$ip‘, ‘$username‘)避免语法错误。发送这个修改后的请求如果一切顺利你将在原本显示浏览器User-Agent的地方看到emails,referers,uagents,users等表名。这就成功地通过HTTP头字段完成了注入。这一关的关键在于拓宽了我们对“输入点”的认识——注入不仅发生在URL参数和表单 body 里任何从客户端发送到服务端且被后端处理的数据都可能成为攻击入口。9. 第十九关HTTP头注入之Referer第十九关与第十八关类似登录后显示的是 “Your Referer is: …”。这意味着注入点转移到了Referer请求头。Referer头表示当前请求是从哪个页面链接过来的。测试流程完全一样使用正确凭证如admin/admin登录。使用Burp Suite拦截登录后显示Referer页面的请求。在Repeater中修改Referer头的值。首先探测在原始Referer值后添加单引号‘观察是否报错。确认注入存在且为单引号闭合后构造联合查询Payload。例如将Referer头修改为http://靶场地址/sqli-labs/‘ union select 1,group_concat(column_name),3 from information_schema.columns where table_name‘users‘ and table_schemadatabase() --这个Payload用于查询users表的所有列名。同样你需要通过union select 1,2,3先确定回显点。假设数字2会显示在“Your Referer is:”的位置那么就把想要查询的数据放在select列表的第二个位置。这一关和第十八关共同强调了应用程序对所有输入进行净化的重要性包括那些看似由浏览器自动生成、用户不可控的HTTP头。在安全开发中必须对从$_SERVER超全局数组中获取的HTTP_USER_AGENT、HTTP_REFERER、REMOTE_ADDR等值进行严格的过滤和转义绝不能因为它们“看起来”安全就直接使用。10. 第二十关Cookie注入与身份维持第二十关模拟了一个基于Cookie的身份验证场景。你使用admin/admin登录后页面会显示你的用户名和密码这本身就不安全并且你的登录状态似乎由Cookie维持。如果你清空Cookie或者修改它就会被“踢出”登录状态。这一关的注入点就在这个Cookie里。具体来说是Cookie中一个名为uname的参数。操作步骤如下正常登录使用Burp Suite拦截任何一个登录后的页面请求例如首页。观察请求头中的Cookie你会发现类似unameadmin这样的键值对。在Burp Suite Repeater中修改uname的值进行注入测试。首先将其改为admin‘发送请求。如果页面返回SQL报错则证明存在Cookie注入且是单引号闭合。后端逻辑可能是这样的登录成功后服务器将用户名存储在Cookie中。后续每次请求服务器从Cookie中读取uname的值并执行类似SELECT * FROM users WHERE username‘$_COOKIE[‘uname’]‘的查询来验证用户身份并获取信息。既然存在注入我们就可以利用联合查询来获取其他数据。Payload构造如下 将Cookie中的uname值修改为admin‘ union select 1,group_concat(username,0x3a,password),3 from users --解释admin‘用于闭合原查询中的单引号。union select ...进行联合查询。我们需要猜测原查询的列数。通过order by试探或者直接观察页面回显。原页面显示了用户名和密码两个字段所以原查询至少有两列。我们用union select 1,2,3测试发现数字2和3的位置分别对应显示的用户名和密码。因此我们将想要的数据放在select列表的第2和第3位。group_concat(username,0x3a,password)将users表中的所有用户名和密码用冒号连接起来0x3a是冒号的十六进制放在位置2。位置3可以随便放个数字或字符串。--注释掉后续可能存在的其他SQL代码。发送请求后你会在页面上看到所有用户的用户名和密码哈希值在sqli-labs中通常是明文从而实现了越权数据访问。Cookie注入的危害极大因为它利用了用户本地存储的、通常被认为相对可信的凭证信息。攻击者可以通过XSS攻击窃取用户的Cookie或者直接对Cookie值进行篡改如果未签名或加密从而实施注入攻击。防御Cookie注入的方法与防御普通注入一样对从Cookie中取出的值进行参数化查询或严格转义同时给Cookie设置HttpOnly和Secure属性降低被XSS窃取的风险。从第十一关到第二十关的旅程我们跨越了从有回显到无回显从GET到POST再到HTTP头注入的多个维度。每一关都像是一个精心设计的谜题考验着我们对SQL语法、HTTP协议和数据库特性的理解。通关不是终点理解每一关背后的漏洞成因、攻击手法和防御思路才是sqli-labs带给我们的真正财富。在实战中情况会更加复杂多变但这些基础而核心的技术点将是你手中最可靠的武器。