资讯详情

DVWA字符型SQL注入实战:从构造闭合到预编译防御全解析

📅 2026/10/22 15:58:49 | 华诺云谱 👁 阅读
DVWA字符型SQL注入实战:从构造闭合到预编译防御全解析
最近整理2月底那次DVWA字符型SQL注入的完整测试记录发现还是有很多值得写下来的细节。网上讲SQL注入的文章不少但大多是直接扔一串payload让你照着打很少有人把“为什么这个字符能闭合”“为什么要用1 and 11这条语句”讲清楚。这次我把DVWA从Low到High三个级别的字符型注入完整走了一遍从判断注入点到拿数据每一步都记录了当时的实际反馈和绕坑过程写下来给正好在练Web安全基础的朋友做个参考。这篇内容适合三类人刚入门Web安全、正在刷DVWA的初学者写代码时想确认自己有没有踩SQL注入坑的开发同学以及准备面试时想快速梳理注入思路的求职者。字符型注入是所有SQL注入类型里最容易上手的一类搞清楚它后面数字型、搜索型、报错型、盲注其实都是一通百通。1. 字符型注入的核心思路和初判方法1.1 数字型和字符型到底差在哪很多新手一上来就卡在“字符型”这三个字上。要理解它先看后台查询语句长什么样。数字型注入的查询大概是SELECT first_name, last_name FROM users WHERE user_id $id;注意这里的$id是直接拼接进去的没有引号包裹所以传1和传1 or 11在语法层面是等价的都落在数字表达式的位置上。而字符型注入的查询长这样SELECT first_name, last_name FROM users WHERE user_id $id;$id被单引号包住了这是最典型的情况。用户传入的内容对后台来说是一个字符串字面量不是可执行的逻辑片段。所以字符型注入的核心动作就是想办法闭合掉前面的单引号把自己构造的逻辑从字符串里“逃逸”出来同时还要处理掉后面那个你控制不到的单引号否则SQL语法会报错。这个“逃逸”的过程跟JavaScript里用引号闭合字符串然后注入事件是很像的——本质都是输入突破了数据边界变成了代码的一部分。1.2 判断注入点时的两条黄金探测语句第一步永远是先确认参数存不存在注入而不是直接上order by或者union select。DVWA Low级别的输入框我依次输入了下面两组内容测试一1 and 11测试二1 and 12反馈非常直观第一条正常返回了First name: admin第二条直接空白什么都没有。这个现象说明两个关键问题。第一输入确实拼进了SQL语句而且是字符型拼接第二Query能识别我构造的闭合逻辑证明这里不是硬编码的字符串比较而是真实执行了拼接后的SQL。更直白地说如果后台改成参数化查询这两条输入会原样当作字符串去匹配不会出现“第二条查不到”的结果差异。注意很多文章喜欢用1 or 11来探测注入点这条语句在DVWA里也能生效但它有一个坏处——它会让SQL恒真导致返回所有数据。在真实测试环境里数据量大的情况下会把页面刷爆而且无差别返回所有行不利于区分到底哪一列是回显位。先探测再攻击顺序很重要。2. Low级别完整注入流程与语句构造2.1 环境说明与数据回显分析DVWA Low级别的输入框对应的是查看用户详情功能输入用户ID返回对应的姓名信息。默认数据库里有8个用户ID从1到8其中有一个用户是我们注入进去的默认密码admin的那个。页面回显两列First name和Surname这直接暴露了查询列数至少是2。当时我最关心的一个问题为什么页面只显示了名字没有把user_id本身显示出来翻源码就明白了$getid SELECT first_name, last_name FROM users WHERE user_id $id;;查询只取了first_name和last_name两列user_id确实没查出来。这意味着联合注入时要特别留意不能用user_id占列位而是要把想查的数据放进这两列对应位置。2.2 用 order by 猜解列数联合查询union select要求前后两个查询的列数一致否则语句直接报错。所以第二步是猜列数。我用的语句1 order by 2 -- -后台SQL变成SELECT first_name, last_name FROM users WHERE user_id 1 order by 2 -- -;order by 2表示按第二列排序查询正常。接着试1 order by 3 -- -这次页面报错“Unknown column 3 in order clause”。有报错回显的好处就在这里一次就能定位。当order by 3报错时说明表里只有2列那么后面union select就只能跟两个字段。这里解释一下-- -的作用--是SQL的注释符后面最好跟一个空格。MySQL的--注释要求后面至少跟一个空白字符直接写--在某些版本里可能不生效。我习惯用-- -两个横杠一个空格或者#都能达到“注释掉我构造内容之外的残余语句”的效果。2.3 联合注入逐步拿库先确认回显位置在哪一列1 union select 1,2 -- -页面显示First name: 1Surname: 2。两个位置都能回显这样后续查询就自由了。接着查当前数据库名和版本号1 union select database(), version() -- -回显dvwa和对应的MySQL版本号这套环境跑的是MySQL 5.x系列。查库名是后续一切操作的起点dvwa是我本机靶场的库名如果是真实授权测试这里拿到的就是目标应用的库名。然后从information_schema.schemata把库里所有库名列出来1 union select schema_name, 2 from information_schema.schemata -- -再定位当前库里有哪些表1 union select table_name, 2 from information_schema.tables where table_schemadvwa -- -回显里出现了users、guestbook等表名到这里基本就能确定数据目标了。查users表有哪些列1 union select column_name, 2 from information_schema.columns where table_schemadvwa and table_nameusers -- -列名全部回显出来user_id、first_name、last_name、user、avatar、password、last_login等。这里有个经验之谈既然password以MD5形式存储把user和password两列一起查出来后续如果要还原明文直接扔到在线MD5解密或者本地跑字典就能出结果。最后取数据1 union select user, password from users -- -页面把8个用户的用户名和对应MD5哈希全部显示出来字符型注入流程到这里就走通了。3. Medium和High级别的防御绕过与思路升级3.1 Medium级别的转义绕过Medium级别不是简单的拼接了后台代码变成了$getid mysqli_real_escape_string($conn, $_GET[id]);mysqli_real_escape_string会对单引号、双引号、反斜杠等特殊字符做转义处理其中最核心的是把单引号变成\这样输入里的单引号就无法闭合SQL语句中的引号了看似防住了字符型注入。但实际上Medium级别依旧有突破空间。关键点在于如果id参数本身在SQL语句里没有被引号包裹那么转义单引号就没有意义。在Medium级别代码里查询语句是WHERE user_id $id等于说引号被代码去掉了$id以数字形式拼接。所以直接输入1 union select user, password from users就能绕过转义限制因为在转义函数处理后你传的1 union ...里根本没有单引号需要处理。这是开发者的一个典型失误——用了转义函数却把SQL语句里的引号去掉了防御等于白做。另一种思路是构造不包含单引号的恒真语句比如1 or 11也能直接拉出全部数据同样不需要引号参与。3.2 面对LIMIT限制时怎么处理Medium和High级别有一个共同改动SQL加了LIMIT 1WHERE user_id $id LIMIT 1;这条限制要求查询结果最多只能返回一行。前面直接用union select拿多行数据的思路就卡住了因为即使union出多行受LIMIT限制也只能显示一行。解决办法很简单把LIMIT 1注释掉。语句变成1 union select user, password from users -- -或者1 union select table_name, 2 from information_schema.tables where table_schemadatabase() -- -注释掉LIMIT后多行数据正常回显。这算是Medium级别另一个容易踩的坑不知道加注释的人会在这一步卡很久反复怀疑是不是转义把union吞了。3.3 High级别前端黑名单与后端边界判断High级别的前端用JavaScript限制输入只能是数字if (isNaN(document.getElementById(id).value)) { ... }这层限制属于“防君子不防小人”。只要绕过前端直接构造请求或者用本地代理工具拦截请求改包把id1 union select user, password from users-- -直接发给后端前端限制完全不生效。真正需要关注的是High级别的后端改动$id $_GET[id]; $data $db-prepare(SELECT first_name, last_name FROM users WHERE user_id ? LIMIT 1);这里已经换成了预处理语句prepared statement用占位符?传递参数数据与SQL语句在语法层面完全分离。到了这个级别传统字符型注入是过不去的因为用户输入不再参与SQL语句拼接。对High级别我的建议很直白不用死磕单引号闭合那一套了能打进去多半是其他配合漏洞比如后台还存在一个数字型注入点、搜索框存在宽字节注入等而不是字符型注入本身的问题。学习时把重点放在“为什么预编译能防注入”上比折腾绕过更有价值。4. 常见问题与排查技巧实录整个测试过程中遇到几个典型的坑整理成表格方便你对照排查现象可能原因排查方向输入1 and 11返回空参数后端可能有额外的字符处理或拼接位置不是唯一换个方式判断注入点试试直接1看报错信息order by 3没报错MySQL版本对order by的容错处理或者表里确实有3列继续order by 4观察别在3就停union 查询无回显前面查询为空union前的查询结果影响回显把前查询条件改成1 and 12强制为空确保union结果直接显示加注释后语法仍然报错注释符后缺空格-- -必须带空格推荐用#更稳浏览器输入中文或特殊字符异常前端编码问题测试环境里尽量用ASCII字符构造payload不要用中文变量名拿不到表名/列名当前数据库用户权限不足查一下当前用户权限确认是否有information_schema的读权限DVWA里是root权限没问题另一个很实际的经验每执行一步payload先用一个能确认状态的语句验证比如先跑1 order by 2 -- -看是否正常回显再跑1 order by 3 -- -触发报错确认“能正常回显”与“触发报错”这两个状态本身是区分开的。很多新手把payload一次性堆上去报错时无法判断是语法错、闭合错还是权限错排查难度直接翻倍。注意事项在真实授权测试中information_schema相关的查询请求量比较大大库环境下不要太频繁跑语句容易被日志系统记录出异常访问模式。练习环境无所谓生产环境测试要有节奏感先想清楚再发请求。还有一个关于工具使用的提醒以上所有手工注入过程用浏览器地址栏手动改参数都可以完成但我不建议在地址栏直接输入空格加号混乱的payload。更推荐的方法是用拦截代理工具先把请求抓到再修改参数重放这样能精确控制每一次请求也方便看返回包的实际内容。抓包工具方面适合练习的常见选择有浏览器自带的开发者工具面板、Burp Suite社区版等选一个顺手的即可。5. 为什么防御必须依赖预编译5.1 过滤和转义的边界在哪里从DVWA三个级别可以看到不同的防御方式对应不同的信任级别。前端过滤不靠谱因为请求可以完全绕过前端。后端转义不彻底因为开发者的拼接方式可能抵消转义的作用。注释符可以破坏LIMIT限制单引号逃逸可以突破字符串边界。这背后的核心问题是你永远无法预测攻击者用什么方式绕过你的过滤规则。今天过滤单引号明天攻击者可能用URL编码绕过今天过滤空格明天可能用注释符替代空格今天过滤union明天可能用大小写混合或者内联注释。如果安全方案是围绕“不让某些关键字出现”来设计的它本质上是个永无止境的攻防游戏。5.2 参数化查询解决问题的本质预编译语句参数化查询能一劳永逸解决SQL注入原因是它从根上改变了数据的传递方式。在预编译模式下SQL语句的结构和参数是分开传输的先发送SELECT ... WHERE user_id ?这样的语句模板让数据库完成编译再发送参数1 union select ...作为数据。数据库在执行时只会把参数当作值来处理不会再参与语句解析。生活化的类比是这样你把一张印好的表格交给办事窗口空的地方填内容工作人员看到的是表格内容但内容永远不会变成表格的一部分。而直接拼接SQL语句等于你把一张空白的纸交给窗口让工作人员把你的话手写进表格如果这段话里混着一条“同时给其他人盖章”的指令他也照做。5.3 纵深防御的落地建议在实际项目里防御SQL注入不能只靠一句“用了预编译就完事”。我的建议是至少做四层第一层数据库操作统一走预编译禁止直接拼接SQL。这个不是技术问题而是规范问题需要代码评审卡住。第二层用户输入做类型校验和长度限制。ID参数就校验必须是数字用户名长度限制在合理范围非法输入直接拒绝而不是继续进SQL。第三层数据库账号按最小权限分配。应用连接账号只给它需要的表权限SELECT用只读账号写操作用单独账号这样即使注入成功攻击者能读到的数据面也大幅收窄。第四层操作日志和异常行为监控。短时间大量类似查询、返回数据异常增大、报错信息异常这些都应该有告警。安全不是一个点的事情而是整条链路的每个环节都做对。最后的实操心得我在DVWA上跑完这三个级别最大的感触是字符型注入看起来很简单但它把开发者和攻击者之间的博弈展示得非常典型。开发者以为转义单引号就安全攻击者就利用没有引号的拼接绕过去开发者以为加个LIMIT限制数据量就稳了攻击者用注释符直接把它删掉开发者以为前端限制输入就能挡住不怀好意的人攻击者一个抓包重放就绕过去了。我个人实际测试时有个小习惯每拿到一个payload都会顺手把执行后的SQL语句在脑子里还原一遍强迫自己看清楚到底哪个部分是我构造进去的、哪个部分是后台原有的。这个习惯练久了判断一个注入点是否存在时基本不用试第二条语句就能猜到结果。另外给准备刷完DVWA往进阶走的朋友一个建议字符型注入搞定之后把同样思路迁移到搜索型、POST型、Header头注入上差别主要在位置和数据入口逃逸的本质完全一样。再往后可以尝试用自动化工具跑一遍相同流程对比手工和工具的结果你会对手工注入的价值有更深的体会——工具不是用来替代思考的是用来验证思路、提高效率的。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑