DVWA--SQL注入漏洞分析(原理及知识拓展)
Low问题1为什么要先用单引号英文来测试一下注入类型1. 验证是否存在注入点打破平衡正常的输入比如输入1通常会被程序正常处理页面显示正常你无法判断后台到底有没有把用户的输入当代码执行。输入1后台 SQL 可能是SELECT * FROM users WHERE id 1。页面正常显示你看不出任何破绽。输入1你故意输入一个单引号破坏了 SQL 语句的语法平衡。后台 SQL 变成了SELECT * FROM users WHERE id 1注意最后多了一个单引号。结果数据库看不懂这个语法直接报错如You have an error in your SQL syntax...。结论只要页面报错就说明你的输入被原封不动地带入数据库执行了这就是漏洞存在的铁证。2. 判断注入类型字符型 vs 数字型单引号测试还能告诉你后台代码是怎么处理你的输入的。字符型注入输入1报错输入1正常。说明后台代码用了引号包裹你的输入如id $input。DVWA Low 级别就是典型的字符型。数字型注入输入1不报错或者被自动过滤但输入1 and 11正常1 and 12异常。说明后台代码直接拼接数字如id $input。3. 判断引号闭合方式虽然大多数情况是用单引号但有些数据库或代码可能使用双引号。如果输入1没反应但输入1报错了那就说明后台是用双引号包裹的。在 DVWA Low 级别中输入1报错而1正常这直接告诉我们要用单引号来闭合后面的语句。总结这就好比你要撬开一把锁输入1就像是用钥匙正常开锁锁开了你也不知道锁芯结构。输入1就像是往锁眼里塞一根火柴如果锁坏了报错说明这个锁芯结构是暴露的、脆弱的你才有机会用铁丝SQL 注入语句去撬开它。问题2为什么第二步要猜解字段数简单来说这是因为 SQL 语法有一条铁律联合查询UNION的两个结果集必须拥有相同数量的列。如果不猜解出正确的列数你的注入语句就会因为“格式不匹配”而报错无法执行。以下是详细的原因分析和操作逻辑1. 核心原因SQL 的“门当户对”原则UNION操作符的作用是将两个SELECT语句的查询结果合并在一起显示。数据库要求这两个结果集必须“结构一致”。比喻这就好比你要把两列火车的车厢连在一起。如果前面的火车有2节车厢后面的火车也必须有2节车厢才能连上。如果你强行拿3节车厢去连2节车厢连接就会失败报错。2. 实战演示报错与成功在 DVWA Low 级别中后台原本的查询语句是1SELECT first_name, last_name FROM users WHERE user_id ...;你看不到这段代码但你知道它查出了first_name和last_name也就是2列。❌ 错误示范列数不匹配如果你不知道列数直接猜是 1 列你的输入1 UNION SELECT 1#后台拼接后的 SQL1SELECT first_name, last_name FROM users WHERE user_id 1 2UNION 3SELECT 1;结果数据库报错The used SELECT statements have a different number of columns使用的 SELECT 语句列数不同。注入失败。✅ 正确示范列数匹配你通过ORDER BY猜到了是 2 列你的输入1 UNION SELECT 1, 2#后台拼接后的 SQL1SELECT first_name, last_name FROM users WHERE user_id 1 2UNION 3SELECT 1, 2;结果2 列对 2 列匹配成功页面会显示出1和2注入成功。3. 为什么要用 ORDER BY 来猜因为我们在黑盒测试看不到代码不知道后台到底查了几列。ORDER BY是用来探测列数最稳妥的方法ORDER BY 1按第 1 列排序 - 正常说明至少有 1 列ORDER BY 2按第 2 列排序 - 正常说明至少有 2 列ORDER BY 3按第 3 列排序 -报错说明没有第 3 列结论既然第 3 列不存在那总列数就是2。总结猜解字段数是为了构造一个语法正确的 UNION 语句。只有知道了列数你才能把SELECT 1, 2, 3...中的数字个数填对进而让数据库把数据“吐”出来。问题3我在获取数据库信息users表的数据时出现了下图这种问题是数据库的编码格式Collation不兼容导致的。为什么会报这个错简单来说这是数据库的一个“洁癖”问题原本的查询DVWA 的first_name,last_name使用的是一种编码格式比如latin1_swedish_ci。注入的查询查询information_schema系统库使用的是另一种编码格式通常是utf8_general_ci。当用UNION把这两部分强行拼在一起时数据库发现两边“语言不通”编码不一致于是报错Illegal mix of collations非法的排序规则混合。怎么解决3种方法不需要重写整个语句只需要在原来的基础上加一点点东西强制让它们“语言统一”即可。方法一使用CONVERT函数最推荐成功率高把查询出来的表名强制转换成和原页面一样的编码通常是latin1。输入以下代码11 union select 1, CONVERT(group_concat(table_name) USING latin1) from information_schema.tables where table_schemadatabase()#解释CONVERT(... USING latin1)的意思就是“不管原本是什么编码请强制把它当成 latin1 给我显示”这样就能骗过数据库的检查。方法二使用CAST函数这也是一个强制转换的方法把它转成二进制或者字符集。输入以下代码11 union select 1, CAST(group_concat(table_name) AS CHAR CHARACTER SET latin1) from information_schema.tables where table_schemadatabase()#方法三不使用UNION改用报错注入进阶玩法如果UNION实在太麻烦可以换一种思路利用updatexml函数让数据库在报错时把数据带出来DVWA Low 级别通常支持这种。输入以下代码11 and updatexml(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schemadatabase()), 0x7e), 1)#注意这种方法页面会显示XPATH syntax error: ~guestbook,users~虽然报错但要的数据就在报错信息里。总结思路完全正确只是遇到了编码冲突。请使用方法一加上CONVERT...USING latin1再次尝试应该就能看到guestbook和users这两个表名了。问题4为什么要看服务端代码看这段代码能帮你彻底理解为什么之前的注入能成功看到$id $_REQUEST[id];你就知道程序完全信任你的输入没有过滤单引号。看到$query SELECT ... $id;你就知道变量是被单引号包裹的所以注入时必须用来闭合它。看到or die(...)你就知道为什么输入错误语法时网页会直接打印出 MySQL 的报错信息这就是所谓的“报错注入”的基础。Medium问题1如果改成1’or 11 #会出错吗在 Medium 级别mysqli_real_escape_string会转义单引号。所以1变成了1\。SQL 变成WHERE user_id 1\。这只是一个普通的查询查询 ID 为1\的用户。如果你发现1 or 11 #不出错那说明可能你的 DVWA 版本或者配置并没有正确开启转义或者你实际上是在 Low 级别测试。建议你再次确认页面上是否显示 Security: Medium查看源码是否有mysqli_real_escape_string如果确实是 Medium且1 or 11 #不出错那只能说明 MySQL 的宽容度很高把1\ or 11解析为了真。总结在 Medium 级别1 or 11 #理论上应该失效只能查到 ID 1但不会报错。如果你想看到所有用户你需要用UNION SELECT并且必须绕过单引号的转义。最推荐的尝试数字型 UNION其实 Medium 级别很难直接用 UNION因为单引号被转义了。通常 Medium 级别的解法是利用报错注入或者盲注。但 DVWA 的 Medium 级别其实是一个“数字型注入”的陷阱。你应该尝试11 UNION SELECT user, password FROM users不带单引号不带注释SQL 语句SELECT ... WHERE user_id 1 UNION SELECT user, password FROM usersMySQL 会把整个字符串1 UNION ...转成数字1。所以这也查不到数据。最终答案在 DVWA Medium 级别由于mysqli_real_escape_string的存在所有的单引号注入都会失效。你必须使用数字型注入并且利用 MySQL 的类型转换特性。但说实话DVWA 的 Medium 级别 SQLi 设计得有点尴尬它实际上是想让你练习预处理语句但作为攻击者你很难绕过这个转义。High问题1爆第一个表名长度出错了是什么原因后面应该怎么办这报错是因为你把 payload 输错地方了。 错误原因位置不对看截图你把 payload 输入到了浏览器的地址栏或者 DVWA 的Session ID 输入框里。Session 输入框这个框是用来填“会话 ID”的它不是 SQL 注入的入口。地址栏如果你直接修改 URL记得要符合 GET 请求的格式?id...。High 级别的 SQL 注入入口在哪里High 级别的注入点其实是在 URL 的id参数里。✅ 正确操作步骤请按照以下步骤在Burp Suite Repeater里操作这样最稳第一步抓取正确的包在 DVWA 左侧菜单点击“SQL Injection”不是 SQL Injection (Blind)。随便输入一个 ID比如1点击Submit。在Burp Suite的 Proxy - History 里找到这个请求。右键 -Send to Repeater。第二步构造 Payload在 Repeater 里你会看到 URL 类似这样GET /dvwa/vulnerabilities/login.php?id1SubmitSubmit...修改id的值。你刚才尝试的 payload 是1 AND (SELECT length(table_name) FROM information_schema.tables WHERE table_schemadvwa LIMIT 0,1) 5这个 payload 是正确的逻辑但是需要确保URL 编码空格要变成或%20括号等特殊字符最好编码。位置必须替换 URL 中的id参数。修改后的 URL 参数部分应该是这样的在 Repeater 里直接改原始id1修改后猜第一个表名长度是否为 5id1 AND (SELECT LENGTH(table_name) FROM information_schema.tables WHERE table_schemaDATABASE() LIMIT 0,1)5注意我用DATABASE()代替了dvwa这样更通用也不用担心引号问题虽然 High 级别其实支持引号只要不在 Session 里输就行。第三步发送并观察点击Send。观察 Response 的长度Length。如果长度和id1时一样说明猜对了第一个表名长度就是 5。如果长度变了比如变得很小或者显示 User ID not found说明猜错了长度不是 5。 总结 High 级别的核心技巧放弃 UNION因为is_numeric限制了字母。使用子查询AND (SELECT ...) ...。盲注或报错由于无法直接回显数据主要靠判断页面长度布尔盲注或者利用报错信息报错注入。十六进制绕过如果遇到必须写字符串但不能用引号的情况可以用十六进制例如0x61646D696E代表admin。推荐练习顺序先试1 AND (SELECT 1 FROM users LIMIT 1) 1确认子查询可行。再试1 AND extractvalue(1, concat(0x7e, (SELECT database())))看能不能直接报错出数据。如果报错不行就用ascii(substr(...))慢慢猜。ImpossibleDVWA 的Impossible级别是整个模块的精华所在它不再是教你“怎么绕过防御”而是直接展示了“如何正确编写代码以防止攻击”。如果说 Low 到 High 级别是在教你“做贼”那么 Impossible 级别就是在教你“做警察”。以下是你在 Impossible 级别能学到的核心安全开发知识️ 核心防御机制预处理语句这是 Impossible 级别最重要的知识点。Low/Medium 级别的做法试图通过黑名单替换为或过滤函数mysqli_real_escape_string来修补 SQL 语句。这种做法总会有漏洞如宽字节注入。Impossible 级别的做法使用预处理语句。代码逻辑1$data $db-prepare( SELECT first_name, last_name FROM users WHERE user_id :id LIMIT 1; ); 2$data-bindParam( :id, $id, PDO::PARAM_INT ); 3$data-execute();学到的原理预处理语句将SQL 代码和用户数据完全分离了。数据库先编译 SQL 模板SELECT ... WHERE id :id。然后把用户输入的数据$id作为纯文本传进去。结果无论你输入 OR 11还是什么妖魔鬼怪数据库都只会把它当作一个字符串值去寻找 ID而绝不会把它当作 SQL 命令执行。结论这是防御 SQL 注入的终极方案。 输入验证Impossible 级别引入了严格的输入验证。代码逻辑1if( is_numeric( $id ) ) { 2 // 执行查询 3}学到的原理白名单验证永远不要相信用户的输入。如果业务逻辑要求 ID 必须是数字那就强制检查它是不是数字。如果不是数字直接拒绝执行连数据库都不碰。这大大减少了攻击面。 最小权限原则虽然代码里看不出来但配合 Impossible 级别的配置通常会强调数据库用户的权限控制。学到的原理运行 Web 应用的数据库账号应该只拥有最小必要权限。比如这个查询只需要SELECT权限那么数据库账号就不应该有DROP TABLE或UPDATE的权限。即使真的被注入了虽然用了预处理很难黑客也删不掉库。️ 其他安全增强Impossible 级别通常还包含以下防御措施这也是 Web 安全开发的标配Anti-CSRF Token代码中会检查checkToken( $_REQUEST[ user_token ]...。学到的防止跨站请求伪造。所有敏感操作如修改密码、提交数据都必须携带服务器下发的随机令牌。限制输出/错误信息代码中通常不会把数据库报错直接回显给用户LIMIT 1也是为了防止信息泄露过多。学到的永远不要把原始错误信息如mysql_error()直接展示给前端用户这会泄露数据库结构。 总结你应该怎么做在 Impossible 级别你不需要也无法进行 SQL 注入。你应该做的是阅读源代码。对比Medium和Impossible的代码。看看mysqli_real_escape_string和PDO::prepare的区别。记住这个模式以后你自己写代码PHP/Java/Python永远使用预处理语句PreparedStatement / Parameterized Queries。永远不要试图自己拼接 SQL 字符串。这就是 Impossible 级别的意义它展示了正确的代码写法。