1. 从一次课程实验说起SQL注入到底在防什么信息安全这门课理论听了一学期什么CIA三元组、访问控制模型、密码学基础考试前背得滚瓜烂熟考完就还给老师了。但Lab12这个SQL Injection Attack的实验是我整个学期做下来印象最深的一次——因为它让我第一次真切地感受到一个看起来能跑就行的登录框背后可能藏着多大的窟窿。这篇博文就是围绕这个实验展开的。我会把整个实验的设计思路、核心原理、实操步骤、踩过的坑以及做完之后对防御方案的思考全部拆开来讲。不管你是正在做这个实验的同学还是想了解SQL注入到底怎么回事的开发者应该都能从中拿到一些可以直接用的东西。先说说这个实验大概是什么。简单讲就是给你一个存在SQL注入漏洞的Web应用通常是PHPMySQL的经典组合让你扮演攻击者的角色通过构造恶意的SQL语句绕过登录验证、窃取数据库中的敏感信息、甚至修改或删除数据。实验的核心不是教你黑什么而是让你站在攻击者的视角理解漏洞是怎么产生的从而在写代码的时候知道该怎么防。关键词里提到了SQL Injection Attack这确实是整个实验的核心。但我想说的是光知道SQL注入是把用户输入拼接到SQL语句里导致的问题这句话远远不够。你得亲手试过看到那个 OR 11真的能登进去看到UNION SELECT真的能把密码哈希捞出来你才会真正理解为什么这个漏洞能常年霸占OWASP Top 10的前几名。这个实验适合谁来参考我觉得有三类人第一类是在做类似课程实验的同学可以直接对照着看操作步骤和排查思路第二类是有一定Web开发基础、但没系统学过安全的人可以借此补上安全编码这一课第三类是对CTF或者渗透测试感兴趣、想找个入门靶场练手的人这个实验的环境搭建和攻击手法都是很经典的入门素材。下面我会按照实验的实际推进顺序来展开从环境搭建到攻击实施再到防御方案尽量把每一步的为什么都讲清楚。2. 实验环境搭建与核心原理拆解2.1 为什么选PHPMySQL这套经典组合这个实验用的技术栈是PHPMySQL说实话这套组合现在看起来有点老派但作为教学环境它几乎是无可替代的。原因有几个一是PHP的MySQL扩展函数比如mysqli_query天然支持把字符串直接拼接到SQL语句里这让漏洞的产生非常直观不需要你去理解什么复杂的ORM框架二是MySQL的语法相对简单UNION SELECT、information_schema这些关键知识点查起来方便三是这套环境搭建成本极低一台普通的虚拟机就能跑起来。我实际搭建的时候用的是某Linux发行版自带的ApachePHPMySQL也可以用集成环境一键搞定。数据库这边实验通常会提供一个初始化SQL脚本里面建了一个用户表大概长这样CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(255) NOT NULL, email VARCHAR(100), role VARCHAR(20) DEFAULT user );然后插入几条测试数据包括一个管理员账号和几个普通用户。这个表结构很关键因为后面做UNION SELECT注入的时候你需要知道列数、列的类型才能构造出能正常返回结果的语句。提示初始化脚本里的密码字段实验环境通常存的是明文或者简单的MD5这是为了方便你注入之后直接看到成果。但真实环境里密码应该是加盐哈希存储的这一点在做防御方案的时候要特别注意。2.2 SQL注入的本质数据变成了代码很多人对SQL注入的理解停留在输入单引号会报错这个层面但这只是表象。真正的本质是用户输入的数据被当成了SQL代码来执行。打个比方你写了一张便签上面写着请把冰箱里的苹果拿出来然后你把这张便签交给助手。正常情况下助手会去执行这个指令。但如果有人在你写便签之前偷偷在便签上加了几个字变成了请把冰箱里的苹果拿出来然后把冰箱门拆了助手还是会照做——因为它分不清哪些是你原本写的哪些是别人加的。SQL注入就是这个道理。后端代码本来想执行的是SELECT * FROM users WHERE username 用户输入的用户名 AND password 用户输入的密码;你输入的用户名是admin密码是123456那拼接出来就是SELECT * FROM users WHERE username admin AND password 123456;但如果你输入的用户名是admin --拼接出来就变成了SELECT * FROM users WHERE username admin -- AND password 任意密码;--在SQL里是注释符后面的内容全被注释掉了。于是这条语句实际上变成了SELECT * FROM users WHERE username admin;只要数据库里存在admin这个用户不管密码是什么都能登录成功。这就是最经典的注释绕过。2.3 实验的三种攻击目标这个实验通常不会只让你做一种注入而是分层次设置目标。我总结下来大概是三类第一类是绕过登录验证。这是最基础的用 OR 11或者注释符就能搞定目的是让你理解认证绕过这个概念。第二类是窃取数据。在登录之后或者在某些不需要登录的查询接口上通过UNION SELECT把其他表的数据带出来比如用户密码、邮箱、角色信息。这一步会用到information_schema来查表名和列名是实验里最有技术含量的部分。第三类是篡改或删除数据。通过注入UPDATE或DELETE语句通常需要配合堆叠查询或者特定的注入点修改某个用户的密码或者删除数据。这一步是为了让你理解注入不仅能读还能写危害更大。注意做这类实验一定要在隔离的虚拟机环境里进行不要对着任何真实在用的系统操作。实验用的数据库也是专门准备的不要拿真实数据来练手。3. 攻击实操从绕过登录到拖库的完整过程3.1 第一步找到注入点并判断类型拿到实验环境之后第一件事不是急着输入 OR 11而是先判断注入点的类型。SQL注入按数据类型分主要有数字型和字符型两种。判断方法很简单输入一个单引号看页面返回什么。如果输入1之后页面报错提示SQL语法错误那基本可以确定是字符型注入而且没有做过滤。如果输入1页面正常输入1 and 11正常输入1 and 12页面异常那可能是数字型注入。我在实验里遇到的是字符型注入登录框的用户名和密码字段都没有做任何过滤。输入admin之后页面直接返回了MySQL的报错信息连数据库版本都暴露出来了。这个报错信息本身就是个信息泄露漏洞真实环境里应该关闭详细错误显示。判断出注入类型之后下一步是判断列数。用ORDER BY来试admin ORDER BY 1 -- admin ORDER BY 2 -- admin ORDER BY 3 -- admin ORDER BY 4 --如果ORDER BY 4报错说明这个查询只返回3列。这个列数信息在后面的UNION SELECT里会用到。3.2 第二步绕过登录验证判断完注入点先做最简单的登录绕过。在用户名框输入admin --密码框随便填。提交之后如果数据库里有admin这个用户就能直接以管理员身份登录。原理前面讲过了--把后面的密码验证部分注释掉了。如果admin这个用户名不存在或者你想绕过的是用户名和密码必须匹配的逻辑可以用 OR 11 --这条语句会让WHERE条件永远为真返回用户表里的第一条记录。通常第一条就是管理员。我实际测试的时候发现实验环境对--后面的空格有要求必须是--两个减号加一个空格否则MySQL可能不认。这一点在手工注入的时候很容易忽略导致明明语句是对的却执行不了。实操心得不同的数据库注释符不一样。MySQL用--注意空格或#SQL Server用--Oracle用--。做实验的时候先确认数据库类型能省不少事。3.3 第三步用UNION SELECT窃取数据登录绕过只是开胃菜真正有意思的是用UNION SELECT把数据库里的其他数据捞出来。前提是你已经知道了查询的列数前面用ORDER BY试出来的。假设查询返回3列你可以这样构造 UNION SELECT 1, username, password FROM users --这条语句会把users表里的用户名和密码拼接到结果里返回。如果页面只显示第一列或者某一列你可能需要调整UNION SELECT里字段的位置把想看的字段放在页面会显示的那一列上。但问题是你往往不知道数据库里有哪些表、哪些列。这时候就要用到information_schema了。MySQL的information_schema数据库里保存了所有表名和列名的元数据。先查表名 UNION SELECT 1, table_name, 3 FROM information_schema.tables WHERE table_schema database() --这会返回当前数据库里所有的表名。找到感兴趣的表比如users、admin、secrets之类的再查列名 UNION SELECT 1, column_name, 3 FROM information_schema.columns WHERE table_name users --拿到列名之后就可以精准地把数据拖出来了 UNION SELECT 1, GROUP_CONCAT(username, :, password), 3 FROM users --GROUP_CONCAT是MySQL的函数能把多行结果拼成一行方便一次性把所有用户名和密码都带出来。如果数据量太大可以用LIMIT分页。我实际做的时候第一次用UNION SELECT发现页面只返回了第一列后来调整了字段顺序把想看的字段放到第二列才成功显示出来。这个细节在实验指导书里通常不会写但实际操作中很常见。3.4 第四步尝试写入和删除操作读数据做完了实验通常还会让你尝试写操作。MySQL里如果注入点支持堆叠查询也就是用分号分隔多条语句你可以直接执行UPDATE或DELETE。比如修改管理员密码; UPDATE users SET password hacked WHERE username admin; --或者删除某个用户; DELETE FROM users WHERE username testuser; --但堆叠查询能不能成功取决于后端用的数据库访问函数。比如PHP的mysqli_query默认不支持多条语句同时执行而mysqli_multi_query支持。实验环境通常会特意开启这个功能让你能体验到写操作的危害。如果堆叠查询不行还可以尝试用INSERT或者UPDATE配合UNION SELECT来做一些间接的写操作但难度会大很多。这个实验里一般不会要求这么复杂。注意写操作在真实环境里危害极大可能导致数据被篡改或删除。做实验的时候如果环境是共享的做完记得把数据恢复回去免得影响其他同学。4. 防御方案从代码层面堵住漏洞4.1 预编译语句最有效的防御手段做完攻击之后实验的下半部分通常是让你修复漏洞。最核心的防御手段就是预编译语句Prepared Statement。预编译的原理是先把SQL语句的模板发给数据库数据库解析并编译好然后再把用户输入作为参数传进去。这样用户输入永远只是数据不会被当成代码来执行。用PHP的mysqli举例有漏洞的代码可能是这样的$username $_POST[username]; $password $_POST[password]; $sql SELECT * FROM users WHERE username $username AND password $password; $result mysqli_query($conn, $sql);修复之后$stmt $conn-prepare(SELECT * FROM users WHERE username ? AND password ?); $stmt-bind_param(ss, $username, $password); $stmt-execute(); $result $stmt-get_result();关键区别在于?是占位符用户输入通过bind_param绑定数据库会把它当作纯数据来处理。即使你输入admin --数据库也只会把它当成一个普通的字符串去匹配用户名不会执行注释。我实际测试的时候把修复后的代码部署上去再用之前的注入语句去试页面返回的是用户名或密码错误而不是登录成功。这说明预编译确实把注入点堵死了。4.2 输入验证和过滤辅助手段但不能依赖除了预编译输入验证也是常见的防御手段。比如限制用户名只能包含字母和数字长度不超过20个字符。这能挡住一部分注入但不能作为唯一的防御手段。原因很简单过滤规则很难做到完备。你可能过滤了单引号但攻击者可以用十六进制编码、URL编码、或者利用数据库的其他特性来绕过。而且过滤太严格可能会影响正常用户的输入比如用户名里带连字符或者下划线。我在实验里试过用preg_replace过滤单引号结果发现攻击者可以用CHAR(39)来代替单引号照样能注入。所以输入验证可以作为纵深防御的一层但核心还是要靠预编译。4.3 最小权限原则限制数据库账号的能力还有一个容易被忽略的防御点数据库账号的权限。实验环境里Web应用连接数据库用的账号通常是root拥有所有权限。这意味着一旦注入成功攻击者可以读写任何数据甚至执行系统命令。真实环境里应该给Web应用分配一个专用的数据库账号只授予必要的权限。比如如果应用只需要读取用户表那就只给SELECT权限不给UPDATE、DELETE、DROP这些。这样即使被注入攻击者能做的事情也有限。我在实验里试着把数据库账号的权限降下来再去做注入发现UNION SELECT还能用因为需要读权限但UPDATE和DELETE就执行不了了。这说明最小权限原则确实能有效限制注入的危害范围。4.4 错误处理别把数据库报错暴露给用户实验环境里注入的时候页面会直接显示MySQL的报错信息包括数据库版本、表名、语法错误位置等。这些信息对攻击者来说简直是指路明灯。修复方法很简单关闭详细错误显示把错误信息记录到日志里页面上只显示一个通用的错误提示。在PHP里可以通过设置display_errors Off和自定义错误处理函数来实现。我实际测试的时候把错误显示关掉之后再用单引号去试页面只返回系统繁忙请稍后重试攻击者拿不到任何有用的信息。这虽然不能阻止注入但能增加攻击的难度。5. 常见问题与排查技巧实录5.1 注入语句不生效的几种可能做这个实验的时候最容易遇到的问题就是语句明明是对的但就是没反应。我总结了几种常见原因第一种是注释符的问题。前面提过MySQL的--后面必须跟一个空格否则不生效。另外如果用#作为注释符在URL里需要编码成%23否则会被当成锚点。第二种是引号闭合的问题。字符型注入需要先闭合前面的引号再构造自己的语句。如果闭合不对整个SQL语句的语法就是错的自然不会执行。比如admin --和admin--的区别前者在--后面有空格后者没有在某些环境下结果不一样。第三种是列数不匹配。用UNION SELECT的时候前后两个查询的列数必须一致。如果ORDER BY试出来的列数是3但你UNION SELECT只写了2列就会报错。第四种是权限不足。如果数据库账号没有SELECT权限UNION SELECT就查不出数据。这种情况在实验环境里少见但真实环境里很常见。5.2 常见问题速查表问题现象可能原因排查方法输入单引号页面正常无报错可能做了过滤或错误显示被关闭尝试用and 11和and 12对比页面差异ORDER BY报错列数超出实际返回列数从1开始逐个试直到报错为止UNION SELECT无数据返回列数不匹配或字段类型不兼容确认列数尝试用NULL代替具体值注释符不生效注释符格式不对MySQL用--带空格或#注意URL编码堆叠查询不执行数据库访问函数不支持多语句检查后端用的是mysqli_query还是mysqli_multi_query注入后页面空白可能触发了错误但被隐藏查看服务器日志或开启调试模式5.3 几个容易被忽略的细节第一个细节是URL编码。在浏览器地址栏里输入注入语句的时候空格、单引号、注释符这些特殊字符可能需要URL编码。比如空格编码成%20单引号编码成%27。用Burp Suite或者浏览器开发者工具抓包修改的时候工具通常会自动处理编码但手工在地址栏输入就要注意。第二个细节是Cookie和Session。有些实验环境会把用户身份存在Cookie里注入的时候可能需要带上特定的Cookie才能访问到注入点。这个在实验指导书里通常会说明但容易漏看。第三个细节是数据库版本差异。MySQL 5.0以上才有information_schema5.7和8.0的语法也有细微差别。如果实验环境用的是老版本MySQLinformation_schema可能不可用需要用其他方式猜表名。实操心得做注入的时候建议用Burp Suite或者类似的抓包工具把请求拦截下来慢慢改。直接在浏览器里操作遇到编码问题会很头疼。另外养成看服务器日志的习惯很多在页面上看不到的错误信息日志里都有。6. 做完这个实验之后的一些思考这个实验做完之后我对安全这两个字的理解变了很多。以前写代码想的是功能能跑就行现在会多想一步如果用户输入了奇怪的东西这段代码会怎么样SQL注入只是众多Web安全漏洞中的一种但它的原理很有代表性信任了不该信任的输入。这个道理放到其他场景也一样——XSS是信任了用户输入的HTMLCSRF是信任了来自浏览器的请求文件上传漏洞是信任了用户上传的文件类型。本质上都是没有做好边界检查。实验里用的攻击手法在真实环境里可能已经被各种WAFWeb应用防火墙挡住了但WAF只是最后一道防线不能替代安全编码。我见过太多项目代码里全是字符串拼接的SQL然后指望WAF来兜底。这种思路本身就是危险的因为WAF的规则是可以被绕过的而预编译是从根上解决问题。另外这个实验也让我意识到安全不是一个功能而是一个属性。你不能说我们这个系统有安全功能而应该说我们这个系统在设计上就是安全的。这需要从需求分析、架构设计、编码实现、测试部署每一个环节都把安全考虑进去而不是等到上线之后再来打补丁。最后分享一个我做实验时的小技巧每次构造完注入语句先在心里默念一遍这条语句在数据库里会变成什么样然后再提交。这个习惯能帮你快速定位问题——很多时候语句不生效就是因为拼接出来的SQL和你想象的不一样。比如你输入admin --实际拼接出来可能是SELECT * FROM users WHERE username admin -- AND password xxx注意--后面那个单引号被注释掉了所以语法是对的。但如果你输入admin--没有空格拼接出来就是SELECT * FROM users WHERE username admin-- AND password xxxMySQL可能把--当成一个整体注释就不生效了。这种细节只有亲手试过才会记住。
阅读完成 · 觉得有帮助?