
1. 项目概述为什么从DVWA开始你的手工注入之旅如果你刚接触网络安全尤其是Web安全那么“SQL注入”这个词你一定不陌生。它就像一把古老的万能钥匙虽然技术原理不复杂但时至今日依然是渗透测试中最常见、也最致命的漏洞之一。很多新手一上来就想复现各种炫酷的CVE结果往往卡在第一步——环境搭建或者被复杂的应用逻辑绕晕最终挫败感满满。这就是为什么我强烈建议你的第一个手工注入实战一定要从DVWADamn Vulnerable Web Application开始。DVWA不是一个真实的、有业务逻辑的复杂系统而是一个专门为安全学习而设计的、充满漏洞的PHP/MySQL应用。它把环境配置简化到了极致提供了一个纯净的、可控的“实验室”。在这里你可以毫无心理负担地“搞破坏”专注于理解漏洞本身而不是和环境斗智斗勇。本次复现的核心就是彻底掌握在DVWA的“SQL Injection”模块中如何不借助任何自动化工具如sqlmap仅凭浏览器和你的大脑一步步手工完成从漏洞探测、信息获取到最终拖库的全过程。这个过程会让你真正理解SQL语句是如何被拼接、执行以及攻击载荷是如何起作用的。这不仅是CTF比赛的基础更是你未来进行代码审计、漏洞挖掘时不可或缺的底层思维。2. 环境准备与靶场搭建打造你的专属安全实验室工欲善其事必先利其器。一个稳定、隔离的测试环境是安全研究的底线。我推荐使用虚拟机方案这能保证你的操作不会影响宿主机也方便随时快照和回滚。2.1 基础环境选择与配置我个人的习惯是在VMware或VirtualBox里安装一个Kali Linux。Kali天生集成了海量的安全工具但对我们本次实验而言它的价值在于提供了一个干净的Linux基础。当然你完全可以使用Ubuntu、CentOS等任何你熟悉的Linux发行版甚至用Windows下的PHPStudy、XAMPP等集成环境来搭建DVWA原理是相通的。在Kali中我们需要确保LAMPLinux, Apache, MySQL, PHP栈正常运行。通常Kali已经预装了这些服务但可能需要手动启动。# 更新软件包列表 sudo apt update # 安装Apache2、MySQL、PHP及必要扩展如果尚未安装 sudo apt install apache2 mysql-server php php-mysql libapache2-mod-php -y # 启动Apache和MySQL服务并设置开机自启 sudo systemctl start apache2 sudo systemctl start mysql sudo systemctl enable apache2 sudo systemctl enable mysql安装完成后可以在浏览器访问http://你的Kali_IP如果看到Apache的默认页面说明Web服务正常。2.2 DVWA的部署与初始化接下来是部署DVWA。整个过程就像把一个PHP网站放到服务器目录下一样简单。下载DVWA从DVWA的官方GitHub仓库https://github.com/digininja/DVWA下载最新源码。可以直接用git克隆或者下载ZIP包。cd /var/www/html sudo git clone https://github.com/digininja/DVWA.git这会在/var/www/html目录下创建一个DVWA文件夹。你的访问地址就是http://你的Kali_IP/DVWA。配置文件准备DVWA提供了一个配置模板。cd /var/www/html/DVWA/config sudo cp config.inc.php.dist config.inc.php sudo nano config.inc.php关键配置项是数据库连接信息$_DVWA[ db_server ] 127.0.0.1; $_DVWA[ db_database ] dvwa; $_DVWA[ db_user ] dvwa; $_DVWA[ db_password ] pssw0rd;默认的数据库密码是pssw0rd你可以按需修改。切记这个密码仅用于本地测试环境绝对不要用于任何生产或外部可访问的系统数据库与权限设置我们需要创建一个对应的数据库和用户。sudo mysql -u root在MySQL命令行中执行CREATE DATABASE IF NOT EXISTS dvwa; CREATE USER dvwalocalhost IDENTIFIED BY pssw0rd; GRANT ALL PRIVILEGES ON dvwa.* TO dvwalocalhost; FLUSH PRIVILEGES; EXIT;文件权限调整PHP需要写入权限来创建配置文件和管理会话。sudo chown -R www-data:www-data /var/www/html/DVWA sudo chmod -R 755 /var/www/html/DVWA注意www-data是Apache服务在Debian/Ubuntu/Kali系统中的默认运行用户。权限设置是Web安全的重要一环这里为了方便实验临时放宽在实际生产环境中必须遵循最小权限原则。访问与初始化在浏览器打开http://你的Kali_IP/DVWA/setup.php。点击页面底部的“Create / Reset Database”按钮。DVWA会自动创建所需的数据表并插入初始数据。如果一切顺利页面会提示成功并自动跳转到登录页http://你的Kali_IP/DVWA/login.php。默认登录凭证是用户名admin密码password。2.3 关键安全等级设置登录后在左侧菜单找到“DVWA Security”。这里的安全等级Security Level决定了DVWA的防护强度直接影响到我们注入的难度。Low完全没有防护。代码直接拼接用户输入是学习原理的最佳难度。Medium使用了mysql_real_escape_string()函数进行转义并尝试将输入转换为数字。存在绕过可能。High使用了预处理语句的雏形通过分离用户输入与SQL逻辑但实现上仍有瑕疵需要更高级的技巧。Impossible真正使用了参数化查询Prepared Statements从根源上杜绝了SQL注入。对于本次从零开始的手工注入请务必先将安全等级设置为 “Low”。我们的目标是先理解最本质的漏洞原理之后再挑战更高难度。3. SQL注入核心原理与手工注入方法论在动手之前我们必须把脑子里的“武器原理”搞清楚。很多人知道输入 or 11能绕过登录但不知道为什么换一个场景就懵了。手工注入的魅力就在于你需要像侦探一样通过反馈的信息一步步推理出后端SQL语句的原貌。3.1 漏洞产生的根源字符串拼接假设DVWALow级别中处理用户查询用户ID的PHP代码是这样的$id $_GET[id]; $query SELECT first_name, last_name FROM users WHERE user_id $id; $result mysqli_query($connection, $query);当用户输入1时SQL语句是SELECT ... WHERE user_id 1这没问题。 但当用户输入1 or 11时语句变成了SELECT first_name, last_name FROM users WHERE user_id 1 or 1111这个条件永远为真True。于是WHERE子句的逻辑变成了user_id 1 OR True这会导致查询返回users表中的所有记录而不仅仅是ID为1的用户。核心漏洞程序信任了用户的输入并将其直接拼接到SQL命令字符串中没有经过任何检查或处理使得用户输入可以“突破”数据区域篡改SQL命令的逻辑结构。3.2 手工注入的通用流程侦察、试探、利用、拓展手工注入可以归纳为一个四步循环我称之为“注入四部曲”侦察与探测Reconnaissance找到可能存在注入的点参数并判断注入类型数字型字符型。使用、、\等字符试探观察页面回显、错误信息或响应时间的差异。信息收集Information Gathering如果存在注入利用UNION SELECT语句设法让数据库“告诉”我们当前查询的列数、数据库名、表名、列名等信息。这一步是后续操作的基础。数据提取Data Exfiltration在知道了数据结构后编写Payload直接查询敏感数据如用户名、密码哈希、个人信息等。权限提升与拓展Privilege Escalation Expansion尝试利用数据库特性如读写文件、执行系统命令来获取服务器权限。这在MySQL的特定配置下是可能的。接下来我们就严格按照这个流程在DVWALow的SQL注入模块中走一遍。4. DVWA Low级别手工注入全流程实录将DVWA安全等级设为Low然后访问“SQL Injection”页面。你会看到一个简单的输入框提示你输入User ID。4.1 第一步侦察与漏洞确认我们的目标是探测id这个参数。正常输入输入1点击Submit。页面显示了ID为1的用户信息Admin...。这说明这个参数是有效的。注入试探字符型输入1数字1加一个单引号。点击提交。理想情况页面返回了数据库错误信息例如You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version...。这几乎就是漏洞存在的“铁证”它说明我们输入的单引号破坏了SQL语句的语法且错误被直接显示了出来这叫“错误回显注入”是最容易利用的一种。实际情况在DVWA Low级别你确实会看到类似错误。这立刻告诉我们两件事A) 存在SQL注入漏洞B) 注入点是字符型因为需要用单引号闭合。注释后续语句为了构造出合法的SQL语句我们需要处理原查询中我们输入之后的代码。输入1 --注意--后面有个空格这是MySQL的单行注释符。提交。页面正常显示了ID为1的用户信息。为什么因为此时的SQL语句是SELECT ... WHERE user_id 1 -- 。--之后的所有内容都被当作注释忽略掉了语句合法。实操心得#也是MySQL的注释符但在URL中#通常被当作锚点所以更多使用--杠杠空格或/**/多行注释。在浏览器地址栏或表单提交时空格可能会被编码有时需要尝试1%20--%20或1--在URL中常代表空格。4.2 第二步信息收集——判断列数ORDER BY在利用UNION合并查询之前我们必须知道当前查询语句返回了多少列字段。使用ORDER BY子句进行猜测。ORDER BY n表示按第n列排序如果n超过了实际列数数据库就会报错。输入1 order by 1 --页面正常说明查询结果至少有1列。输入1 order by 2 --页面正常说明至少有2列。输入1 order by 3 --页面正常说明至少有3列。输入1 order by 4 --页面报错或返回空/异常。这说明实际列数小于4。结论当前查询语句返回的列数为3。为什么是ORDER BY因为UNION操作要求前后两个SELECT语句的列数必须相同。ORDER BY是我们探测列数最精准、兼容性最好的方法。4.3 第三步信息收集——确定回显点UNION SELECT知道有3列后我们用UNION SELECT来“替换”掉原查询结果并看看哪些列的内容会显示在页面上这些位置就是我们的“回显点”可以用来输出我们想查询的信息。输入1 union select 1,2,3 --Payload解析1是为了闭合原语句的单引号并使原查询user_id1大概率返回空因为可能没有ID为1的用户或者我们后面用union覆盖了它。union select 1,2,3是我们构造的查询它固定返回一行三列数据第一列是数字1第二列是2第三列是3。观察结果提交后页面很可能不再显示“Admin”而是显示了数字2和3或者1,2,3中的某几个。这说明页面的第二列和第三列的内容被展示出来了。第一列可能是user_id可能没有显示。记录在我的测试中2和3的位置被显示在了“First name”和“Surname”对应的位置。这意味着我们可以把2和3替换成任何我们想让数据库执行的SQL函数或查询语句其结果就会显示在页面上。4.4 第四步信息收集——获取数据库信息现在我们把回显点2和3替换成数据库函数。获取当前数据库名输入1 union select 1, database(), 3 --database()是MySQL内置函数返回当前连接的数据库名称。提交后在“First name”的位置你应该会看到dvwa。成功获取MySQL版本和当前用户输入1 union select 1, version(), user() --version()返回MySQL服务器版本。user()返回当前执行查询的数据库用户。提交后你可能会看到类似8.0.36的版本和dvwalocalhost的用户信息。了解版本有助于查找已知的版本特定漏洞或利用函数。获取所有数据库名非必要但有助于了解环境输入1 union select 1, group_concat(schema_name), 3 from information_schema.schemata --information_schema.schemata是MySQL的系统表存储了所有数据库的信息。group_concat()函数将多行结果合并成一个字符串用逗号分隔便于在单个回显点查看。提交后你会看到一个包含information_schema, mysql, performance_schema, dvwa等的长字符串。4.5 第五步数据提取——获取表名和列名在知道了数据库名dvwa后我们需要找出里面有哪些表特别是存放用户凭证的表。获取dvwa数据库中的所有表名输入1 union select 1, group_concat(table_name), 3 from information_schema.tables where table_schemadvwa --information_schema.tables存储所有表的信息。where table_schemadvwa限定只查询dvwa数据库下的表。提交后你可能会看到guestbook,users等。users表显然是我们最感兴趣的。获取users表的所有列名输入1 union select 1, group_concat(column_name), 3 from information_schema.columns where table_schemadvwa and table_nameusers --information_schema.columns存储所有列的信息。提交后你会得到一个类似user_id,first_name,last_name,user,password,avatar,last_login,failed_login的字符串。太好了我们看到了user和password列。4.6 第六步数据提取——拖取核心数据用户名与密码现在万事俱备我们可以直接查询users表里的敏感信息了。输入1 union select 1, group_concat(user, :, password), 3 from users --这个Payload从users表中查询user和password列并用冒号:将它们连接起来然后用group_concat把所有行合并成一个字符串输出在第二个回显点。点击提交。你会在页面上看到类似这样的结果admin:5f4dcc3b5aa765d61d8327deb882cf99, gordonb:e99a18c428cb38d5f260853678922e03, 1337:8d3533d75ae2c3966d7e0d4fcc69216b, pablo:0d107d09f5bbe40cade3de5c71e9e9b7, smithy:5f4dcc3b5aa765d61d8327deb882cf99大功告成你已经成功手工获取了DVWA中所有用户的用户名和密码哈希值。其中admin的密码哈希5f4dcc3b5aa765d61d8327deb882cf99对应的明文就是passwordMD5哈希。5. 中高级难度挑战与绕过技巧在Low级别我们像是在打固定靶。而Medium和High级别则设置了简单的障碍。理解如何绕过它们能让你对防御机制有更深的认识。5.1 Medium级别转义与数字型注入将DVWA安全等级调至Medium。你会发现输入框变成了下拉菜单只能选择数字ID。这意味着前端限制了输入但真正的考验在后端。查看源码或想象Medium级别的处理代码可能类似$id $_POST[id]; $id mysqli_real_escape_string($connection, $id); // 转义特殊字符 if (is_numeric($id)) { // 检查是否为数字 $query SELECT first_name, last_name FROM users WHERE user_id $id; // ... }绕过思路抓包改参既然前端限制我们就绕过前端。使用Burp Suite或浏览器开发者工具F12 - Network在提交时拦截HTTP请求将id1直接修改为id1 or 11。类型判断注意Medium级别的SQL语句变成了WHERE user_id $id没有单引号这是一个数字型注入。因此我们的Payload里不需要考虑闭合引号。直接注入逻辑即可例如1 or 11。注释使用因为去掉了单引号原语句末尾可能没有需要注释掉的内容但有时为了稳妥可以加上#或--注释掉可能存在的后续字符。例如1 or 11 #。实操过程用Burp Suite拦截提交请求将id1修改为id1 or 11发送。页面返回了所有用户信息说明注入成功。后续的union select等操作与Low级别类似只是无需处理单引号闭合。例如判断列数id1 order by 3 #。5.2 High级别分离输入与逻辑但仍有蹊跷High级别采用了不同的界面输入在一个单独的弹窗页面/vulnerabilities/sqli/session-input.php进行看起来像是用了会话Session或二次处理来隔离输入。其核心防御思想是模拟“预处理语句”先将用户输入存入$_SESSION然后在另一个页面从$_SESSION中读取并使用。代码可能类似// 第一页session-input.php $_SESSION[id] $_GET[id]; // 第二页sqli.php $id $_SESSION[id]; $query SELECT first_name, last_name FROM users WHERE user_id $id LIMIT 1;;关键点它仍然使用了字符串拼接$id只是输入来源变了。并且它加上了LIMIT 1这限制了每次查询只返回一行结果给UNION注入制造了麻烦。绕过思路利用注释绕过LIMIT 1LIMIT 1在SQL语句的末尾。我们可以用注释符#或--将其注释掉这样UNION查询的结果就能完整返回。Payload构造在输入框输入1 union select 1,2,3 #。注意这里用#注释掉了后面的 LIMIT 1;中的单引号和LIMIT子句。后续步骤一旦UNION生效后续的信息收集和数据提取步骤与Low级别完全一致。重要心得High级别的设计告诉我们即使采用了输入与逻辑分离的架构如果最终拼接SQL语句时没有使用参数化查询Prepared Statements并且存在像LIMIT 1这样可以被注释掉的“尾巴”漏洞依然存在。这强调了根本解决方案是参数化查询而非各种过滤和转义。6. 常见问题、排查技巧与防御思考在实际手工注入过程中你肯定会遇到各种意外情况。这里记录一些我踩过的坑和解决方法。6.1 常见问题速查表问题现象可能原因排查与解决思路输入后页面空白或报500错误1. 魔法引号magic_quotes_gpc开启现代PHP已弃用。2. 后端有自定义的过滤或WAF。1. 检查PHP配置。在DVWA中可忽略。2. 尝试双写绕过或使用编码%27。ORDER BY测试时数字很大了还不报错1. 原查询返回的列数确实很多。2. 注入点位置不在SELECT后可能在子查询等位置。1. 继续增大数字测试直到报错。2. 尝试使用union select null,null,...来试探null可适配任何数据类型。UNION SELECT后页面显示原查询结果不显示我们的数字1. 原查询WHERE条件如id1有结果UNION后它排在第一行被显示。2. 回显点判断错误。1. 使原查询无结果id-1 union ...或id999 union ...。2. 尝试在UNION后每个位置都放置一个独特字符串如a,b观察哪个出现。知道列名但查询时提示“Unknown column”1. 列名记错或写错大小写。2. 当前数据库上下文Database Context不对。1. 重新用information_schema.columns查询确认列名。2. 确保你的FROM子句指定了正确的数据库和表名如dvwa.users。使用group_concat返回结果被截断group_concat有长度限制默认1024字节。1. 先查询group_concat_max_len。2. 在会话中设置更大值1; SET SESSION group_concat_max_len 1000000; --需堆叠查询支持。3. 使用limit分批次查询。6.2 从攻击者视角看防御经历了完整的注入过程你现在应该能深刻理解防御的重要性。最有效的防御手段按优先级排序使用参数化查询Prepared Statements这是根除SQL注入的银弹。让SQL语句的结构和传入的数据完全分离数据库不会将数据当作代码执行。DVWA的Impossible级别就采用了这种方式。对输入进行严格的校验在白名单允许的范围内校验如ID只能是数字比黑名单过滤过滤,等更可靠。Medium级别的is_numeric()就是一种白名单校验虽然被我们绕过了前端但后端校验本身是有效的思路。最小权限原则为Web应用连接数据库的用户分配最小的必要权限。比如只授予SELECT权限不授予DROP,FILE,EXECUTE等。这样即使被注入损失也有限。避免详细的错误回显像DVWA Low级别那样将数据库错误直接展示给用户是极大的帮助。生产环境应使用自定义错误页面记录错误日志到后端而非前端。使用Web应用防火墙WAF作为辅助手段可以拦截一些已知的、特征明显的攻击Payload。手工注入的练习目的绝不是为了攻击。它是一把钥匙帮你打开理解Web安全底层逻辑的大门。通过亲手构造每一个Payload观察每一次响应你会对“数据即代码”这一安全核心问题有肌肉记忆般的理解。当你未来自己编写代码时这种理解会自然而然地提醒你永远不要信任用户输入。