企业查询网站,大学 英文网站建设,windows server 2003 wordpress,河北黄骅市简介1. 环境准备与靶场搭建#xff1a;你的第一个安全实验室 很多朋友想学网络安全#xff0c;但一上来就被各种复杂的术语和工具吓退了。其实最好的学习方法就是动手实践#xff0c;而DVWA#xff08;Damn Vulnerable Web Application#xff09;就是一个为你量身打造的“漏洞…1. 环境准备与靶场搭建你的第一个安全实验室很多朋友想学网络安全但一上来就被各种复杂的术语和工具吓退了。其实最好的学习方法就是动手实践而DVWADamn Vulnerable Web Application就是一个为你量身打造的“漏洞游乐场”。我刚开始接触安全测试的时候也是从DVWA入手的它把常见的Web漏洞比如我们今天要重点讲的SQL注入都集成在一个环境里而且分成了从易到难的不同级别特别适合新手一步步打怪升级。DVWA本质上是一个用PHP/MySQL写的Web应用但它故意留下了很多安全漏洞。你不需要去网上找那些可能存在法律风险的网站来练手在自己的电脑上搭一个DVWA想怎么测试就怎么测试完全合法且安全。搭建过程其实很简单对于新手我最推荐用Docker的方式几乎是一键完成避开了配置PHP、MySQL环境的那些坑。假设你的电脑上已经装好了Docker和Docker Compose如果没装去官网下载安装步骤很直观。接下来创建一个docker-compose.yml文件内容如下version: 3 services: dvwa: image: vulnerables/web-dvwa ports: - 8080:80 environment: - PHP_ENABLEOn restart: always保存文件后在同一个目录下打开命令行输入docker-compose up -d。等它拉取镜像并运行起来打开浏览器访问http://localhost:8080你就能看到DVWA的登录页面了。默认的账号是admin密码是password。第一次登录会提示你初始化数据库点一下按钮就行整个过程不超过两分钟。看到那个熟悉的紫色界面恭喜你你的个人安全实验室就建好了在开始实战前记得在DVWA左侧菜单栏将安全等级Security Level设置为Low。这个设置会影响后端代码的防护强度我们从最简单的开始才能看清漏洞最原始的样子。好了环境就绪我们直接进入正题看看这个号称“Web安全头号杀手”的SQL注入到底是怎么一回事。2. Low难度实战手工注入的“经典咏流传”把安全级别调到Low后我们点开SQL Injection这个模块。你会看到一个简单的输入框让你输入User ID。这个场景太经典了它模拟了一个根据用户ID查询用户名的功能比如在用户个人资料页。我们输入数字1然后提交页面显示了ID为1的用户名和姓氏。看起来一切正常但魔鬼藏在细节里。第一步探测漏洞存在与闭合方式。这是注入的起点就像开锁前要先知道锁的类型。我们在输入框里不再输入数字而是输入一个单引号然后提交。页面瞬间变脸返回了一串MySQL的错误信息“You have an error in your SQL syntax...”。这个错误是我们的“好朋友”它明确告诉我们我们输入的单引号破坏了后端SQL语句的结构导致语法错误。这说明我们输入的内容被直接“拼接”到了SQL语句里。为了确认我们再试一下双引号发现页面正常返回没有报错。这就基本确定了后端的SQL语句大概是这样的格式SELECT ... FROM ... WHERE user_id $input。我们的输入被放在一对单引号里面所以我们输入的单引号会提前闭合掉前面的引号导致后面多出一个引号从而报错。第二步判断字段数量。知道有漏洞后我们要开始“探索”数据库的结构了。这里要用到ORDER BY子句。它的本意是根据第几列来排序结果我们可以利用它来探测查询结果到底返回多少列字段。我们在输入框输入1 ORDER BY 2 #。这里的#在MySQL中是注释符会把我们输入后面的所有内容包括原本SQL语句里可能存在的那个闭合单引号都注释掉确保语句完整。提交后页面正常显示说明这个查询结果至少有2列。我们再试1 ORDER BY 3 #页面报错了提示未知的列。这就确定了当前查询只返回2列数据。这个信息对我们后续使用UNION操作符至关重要。第三步寻找回显点。UNION操作符可以把我们自定义的查询结果拼接到原查询结果后面。但前提是两个查询的列数必须一致。刚才我们知道了是2列现在就可以构造1 UNION SELECT 1,2 #。提交后页面不仅显示了ID为1的用户信息还在下面多显示了“1”和“2”这两个数字。这说明页面会把查询结果的所有列都显示出来我们刚刚用数字1和2占位的这两列就是我们可以用来显示任意信息的“回显点”。这就像在墙上找到了两个可以贴小广告的空位。第四步信息收集与拖库。找到了回显点我们就可以把数字替换成我们想查询的数据库函数了。输入1 UNION SELECT database(), version() #。database()函数返回当前数据库名version()返回MySQL版本。提交后页面清晰地显示了数据库名通常是dvwa和版本号。拿到了数据库名我们就可以像翻阅目录一样查看里面有哪些“表”。输入1 UNION SELECT 1,group_concat(table_name) FROM information_schema.tables WHERE table_schemadatabase() #。information_schema是MySQL自带的元数据库存放了所有数据库、表、列的信息。这条语句的意思是从中查询当前数据库database()里所有表的名字并用group_concat()函数把它们合并成一个字符串显示在第二个回显点上。结果里我们一眼就看到了users表这显然是存放用户账号密码的表。接下来我们查看users表里有哪些列1 UNION SELECT 1,group_concat(column_name) FROM information_schema.columns WHERE table_nameusers #。注意这里的users用了单引号因为表名是字符串。查询结果会显示user_id,first_name,last_name,user,password等列名。最后就是收获的时刻了1 UNION SELECT user, password FROM users #。这条语句直接查询users表中的用户名和密码字段并利用两个回显点成对显示出来。一串看起来像乱码的password就是经过MD5哈希加密的密码。虽然不能直接看到明文但我们可以用这些哈希值去彩虹表网站破解很多弱密码瞬间就现原形了。整个Low级别的注入过程行云流水没有任何阻碍。我们通过手工一步步探测、推断、利用最终拿到了核心数据。这个过程虽然基础但它完整地展示了SQL注入的核心思想通过构造特殊输入欺骗后端数据库执行非预期的SQL命令。下面我们来看看背后的代码为什么如此脆弱。3. 源码审计漏洞究竟从何而来实战成功之后我们一定要养成看源码的习惯。知其然更要知其所以然这样才能真正理解漏洞的根源。在DVWA页面下方点击View Source就能看到Low级别的后端PHP代码。我把关键部分摘出来咱们一起分析?php if( isset( $_REQUEST[ Submit ] ) ) { // 获取用户输入没有任何过滤 $id $_REQUEST[ id ]; // 直接拼接字符串构造SQL语句灾难的起点 $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]获取用户输入$_REQUEST可以接收GET或POST请求的数据。然后它直接将$id变量用单引号包裹拼接进了SQL查询字符串$query里。这就是万恶之源当我们输入1 UNION SELECT user, password FROM users #时最终生成的SQL语句变成了SELECT first_name, last_name FROM users WHERE user_id 1 UNION SELECT user, password FROM users #;由于#注释掉了后面的所有内容这条语句实际上执行了两部分查询先查ID为1的用户再查所有用户的账号密码。数据库老老实实地执行了并把结果都返回给了前端。代码里还有一个致命问题or die(...)部分。当SQL语句执行错误时比如我们最开始输入单引号报错它会用mysqli_error函数将详细的数据库错误信息直接打印到页面上。这相当于给攻击者“递刀子”帮助他们快速判断注入类型和数据库类型。那么修复方向在哪里呢原始文章里提到了使用mysqli_real_escape_string函数进行转义以及使用预处理语句。在Low级别开发者完全没有做任何防护。而在Medium级别我们能看到第一种修复尝试。4. Medium难度升级绕过转义与POST型注入将DVWA安全级别调到Medium刷新SQL注入页面。你会发现界面变了输入框变成了一个下拉选择菜单只能选择预定义的数字ID。这其实是一种非常初级的防护思路前端限制输入格式。但作为攻击者我们从来不会只依赖于浏览器。打开浏览器开发者工具F12很容易就能把下拉菜单改成输入框或者更直接地使用代理工具拦截请求。这里我推荐使用Burp Suite或者浏览器插件HackBar。以HackBar为例它更轻量我们打开它将请求方式改为POST并按照格式填入参数。当我们选择ID1提交时用抓包工具可以看到实际的请求体是id1SubmitSubmit。那么我们直接在HackBar里修改id的值进行测试。第一步判断注入类型。输入1提交返回了语法错误。输入1也报错。等等这和Low级别不一样。因为前端是数字下拉框后端很可能直接用了数字类型SQL语句可能形如WHERE user_id $id没有单引号包裹。所以我们输入1 AND 11和1 AND 12来测试。1 AND 11逻辑永真页面应正常显示ID1的用户1 AND 12逻辑永假页面应返回空或错误。实测符合预期这确认了是数字型注入无需考虑引号闭合。第二步判断列数与回显点。和之前类似用ORDER BY判断出有2列然后用UNION SELECT确定回显点id1 UNION SELECT 1,2SubmitSubmit。成功在页面看到1和2。第三步拖库过程中的小障碍。当我们爆表名时很顺利id1 UNION SELECT 1,group_concat(table_name) FROM information_schema.tables WHERE table_schemadatabase()SubmitSubmit。但在爆users表的列名时直接使用table_nameusers却报错了错误信息提示在\users\附近语法错误。这说明后端代码对输入做了某种处理在我们的单引号前加了反斜杠\进行转义导致users变成了\users\破坏了SQL语法。这就是Medium级别引入的防护mysqli_real_escape_string()函数。它会将特殊字符如单引号、双引号、反斜杠转义使其失去在SQL中的特殊意义。但道高一尺魔高一丈我们可以绕过它。既然它过滤了引号我们就不直接用引号表示字符串。在SQL中可以用十六进制hex来表示一个字符串。users的十六进制是0x7573657273。所以我们把Payload改成id1 UNION SELECT 1,group_concat(column_name) FROM information_schema.columns WHERE table_name0x7573657273SubmitSubmit。成功爆出了列名后续爆数据就畅通无阻了。查看Medium级别的源码验证了我们的判断$id $_POST[ id ]; // 对输入进行了转义处理 $id mysqli_real_escape_string($GLOBALS[___mysqli_ston], $id); $query SELECT first_name, last_name FROM users WHERE user_id $id;;它虽然用了转义函数但SQL语句变成了WHERE user_id $id没有引号。对于数字型注入转义函数是无效的因为数字本身不需要引号攻击者输入的任何非数字字符都会导致查询错误或产生注入。这里的修复是不彻底的正确的做法应该是结合参数化查询或者至少确保输入是真正的整数例如使用intval($id)。5. High与Impossible难度防御措施的演进将安全级别调到High你会发现注入点换到了另一个页面并且输入是通过一个弹窗会话Session进行的这增加了攻击的复杂性但本质上还是基于字符型的注入只是利用了LIMIT 1子句。手工注入的思路和Low级别类似需要闭合单引号并用#注释掉后面的LIMIT 1。这个级别主要增加了攻击的步骤和迷惑性但核心漏洞依然存在它告诉我们仅仅隐藏入口或增加复杂度并不能从根本上解决问题。而Impossible级别才是真正给我们展示了如何从代码层面根治SQL注入。我们点开它的源码会发现它做了多重防护堪称教科书级别的安全代码Anti-CSRF Token每个表单提交都附带一个随机的token防止跨站请求伪造攻击确保请求来自合法的当前会话。严格的输入检查if( !is_numeric( $id ) ) { ... }这行代码直接判断输入是否为数字如果不是直接结束脚本不给任何执行机会。类型强制转换$id intval( $id );将输入强制转换为整数即使攻击者传入1 UNION ...也会被转换成纯数字1。参数化查询PDO预处理语句这是最核心、最有效的防御手段。我们重点看一下它的PDO代码// 准备SQL语句使用 :id 作为命名占位符 $stmt $db-prepare( SELECT first_name, last_name FROM users WHERE user_id :id LIMIT 1; ); // 将变量 $id 的值绑定到占位符 :id 上并指定其为整数类型PDO::PARAM_INT $stmt-bindParam( :id, $id, PDO::PARAM_INT ); // 执行查询 $stmt-execute(); // 获取结果 $result $stmt-fetch();这段代码就是“黄金标准”。prepare方法先将SQL语句的模板发送给数据库服务器进行编译告诉它“我要执行一个查询结构是这样的其中:id是一个待定的参数。” 数据库已经理解了这个查询的逻辑。然后bindParam方法将变量$id绑定到参数:id上。关键在这里绑定的值会被当作纯粹的数据来处理而不会被解释为SQL代码的一部分。即使$id变量被恶意赋值为1 OR 11在绑定时这个字符串整体会被当作查找的ID值数据库会去查找user_id字段等于这个字符串的记录而不是去改变查询逻辑。最后execute执行时数据库将编译好的计划与绑定的数据结合完成查询。参数化查询从原理上杜绝了SQL注入的可能因为它实现了SQL代码与数据的分离。攻击者无法再用输入的数据去改变SQL语句的骨架。这才是治本的方法。6. 安全加固实战从理论到代码的PDO改造理解了Impossible级别的代码我们现在就动手把前面那个漏洞百出的Low级别代码改造成一个坚固的版本。假设我们有一个新的PHP文件safe_query.php需要实现根据ID查询用户的功能。第一步建立安全的数据库连接。我们使用PDO并在连接时设置错误模式为异常方便调试。?php $host localhost; $dbname dvwa; $username root; $password pssw0rd; try { // 创建PDO实例字符集建议设置为utf8mb4 $pdo new PDO(mysql:host$host;dbname$dbname;charsetutf8mb4, $username, $password); // 设置错误模式为异常生产环境可改为 PDO::ERRMODE_SILENT 并记录日志 $pdo-setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 设置默认获取模式为关联数组方便使用 $pdo-setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); } catch (PDOException $e) { // 生产环境中不要直接输出错误信息应记录到日志 die(数据库连接失败请稍后再试。); } ?第二步接收并验证用户输入。这是防御的第一道关卡。// 假设通过GET请求接收id参数 if (!isset($_GET[id])) { die(请输入用户ID。); } $input_id $_GET[id]; // 验证1是否为数字 if (!is_numeric($input_id)) { die(用户ID必须为数字。); } // 验证2转换为整数过滤一切非数字字符 $user_id intval($input_id); // 验证3检查整数范围根据业务逻辑例如ID必须大于0 if ($user_id 0) { die(无效的用户ID。); }第三步使用PDO预处理语句执行查询。这是最核心的防御。try { // 1. 准备SQL语句模板使用命名占位符 :user_id $sql SELECT first_name, last_name FROM users WHERE user_id :user_id LIMIT 1; $stmt $pdo-prepare($sql); // 2. 将变量绑定到占位符并明确指定为整数类型 $stmt-bindParam(:user_id, $user_id, PDO::PARAM_INT); // 3. 执行查询 $stmt-execute(); // 4. 获取结果 $user $stmt-fetch(); // 5. 处理结果 if ($user) { echo 用户姓名: . htmlspecialchars($user[first_name]) . . htmlspecialchars($user[last_name]); } else { echo 未找到该用户。; } } catch (PDOException $e) { // 生产环境应记录异常信息 $e-getMessage() 到日志而非输出给用户 error_log(查询失败: . $e-getMessage()); die(查询过程中发生错误。); }注意代码中的几个关键点bindParam的第三个参数PDO::PARAM_INT明确指定了绑定类型是整数这提供了额外的类型安全。htmlspecialchars函数用于在输出时转义HTML特殊字符防止XSS跨站脚本攻击这是另一个常见的安全漏洞。异常信息被记录到错误日志而非显示给用户避免了信息泄露。第四步扩展思考查询多条数据与动态排序。有时候我们需要根据用户选择来动态排序比如ORDER BY username。这里千万不能直接将用户输入拼接进去。正确的做法是使用白名单机制$allowed_columns [first_name, last_name, user_id]; // 允许排序的字段白名单 $order_by isset($_GET[order]) ? $_GET[order] : user_id; // 检查用户输入的排序字段是否在白名单内 if (!in_array($order_by, $allowed_columns)) { $order_by user_id; // 默认值 } // 在预处理语句中排序字段不能使用占位符因为它不是数据而是SQL标识符。 // 但因为我们已用白名单严格过滤所以可以安全拼接。 $sql SELECT * FROM users ORDER BY $order_by ASC; $stmt $pdo-prepare($sql); // ... 执行查询通过这一套组合拳PDO预处理语句 严格的输入验证与过滤 输出编码 错误处理我们就能构建起应对SQL注入的铜墙铁壁。在实际开发中养成使用ORM框架如Laravel的Eloquent、ThinkPHP的Model的习惯它们底层通常都使用了参数化查询能进一步降低安全风险。安全不是一项功能而是一种贯穿始终的开发习惯。每次写数据库查询时都问自己一句“我这里用了预处理吗” 久而久之安全的代码风格就会成为你的肌肉记忆。