ARTICLE DETAIL

资讯详情

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

SQL注入从原理到防御:手工注入全流程与实战笔记

SQL注入从原理到防御:手工注入全流程与实战笔记 这几天在整理自己的安全学习笔记把SQL注入这块又重新过了一遍。从最开始在学校里只会用工具跑站到后来在DVWA、Pikachu、ctfshow这些靶场上一个个手工复现再到真正理解为什么会有这个漏洞、怎么从源头堵住它这个过程踩了不少坑。这篇笔记就把我目前对SQL注入的理解、手工注入的完整流程、靶场刷题经验以及防御修复方案一次性梳理清楚给同样在学安全的朋友做个参考。老实说SQL注入算得上是Web安全里最经典、也是新手最容易上手理解的一个漏洞类型但它绝对不是“会跑工具就完事”那么肤浅。理解它的底层原理自己动手手工注一遍再看那些自动化工具的行为才会恍然大悟。这篇文章内容偏学习笔记向适合刚接触Web安全、准备打CTF、或者正在刷靶场的朋友。1. SQL注入的本质与成因——先搞懂为什么1.1 一个让你瞬间明白的拼接例子很多人第一眼看到SQL注入都会觉得神秘其实真揭开来看核心就一句话程序把用户输入的内容当成了SQL代码来执行。拿一个最经典的登录场景举例。正常开发时代码可能是这样的SELECT * FROM users WHERE username$username AND password$password这是典型的字符串拼接SQL。如果用户输入的用户名是admin or 11密码随便填一个比如123拼接出来的SQL语句就变成SELECT * FROM users WHERE usernameadmin or 11 AND password123这个SQL的判断逻辑就完全变形了。因为or 11恒为真前面的用户名判断被短路掉整条语句不管密码对不对都会返回查询结果登录直接被绕过。这就是网上经常说的“万能密码”本质上是用户输入的单引号闭合了程序原有的引号又用or改变了原有判断逻辑。理解了这个例子SQL注入就没什么神秘的了——程序把用户可控的输入当作SQL语句的一部分去执行了。代码里的单引号本来是语法标记当用户也能注入自己的引号和关键字时双方就“合谋”把一句原本设计好的SQL改写成了攻击者想要的样子。1.2 为什么“输入”能变成“代码”很多人会问我输入的就是一串字符而已怎么就成了代码这其实是“数据”和“代码”边界被打破的问题。正常的SQL语句里WHERE usernameadmin中的admin是字符串字面量属于“数据”。但如果参数是拼接的用户输入直接嵌入到SQL语句的语法结构中那么用户输入里只要包含引号、注释符、SQL关键字这些特殊字符就相当于获得了修改“代码”的能力。我打个比方就像你去银行填取款单单子上只该写金额和账号结果有人发现填账号那一栏如果顺便写上“顺便把柜员机里的钱也给我”柜员居然照着做了。这就是因为银行没有把“用户填写的内容”和“柜员的执行指令”分开处理。所以SQL注入的根源在于开发者在构建SQL时没有区分“指令”和“数据”。这也引出了后面防御方案里最重要的一条用参数化查询让数据库天然区分数据和指令。1.3 SQL注入的常见分类学习SQL注入先要学会看分类不同分类代表着不同的利用思路和防御侧重点。按注入位置分GET参数注入比如URL里的?id1直观简单适合入门。POST表单注入比如登录框、查询框需要抓包看请求体。Cookie、User-Agent、Referer头注入这类往往容易被忽略但现实中很常见因为这些位置也常被后端用来拼SQL比如统计访问来源。Search搜索框注入本质上是模糊查询语句的拼接比如Pikachu靶场里专门有个搜索型注入。按数据类型分数字型注入SQL语句里不带引号常见形式是WHERE id1判断方法是and 11和and 12。字符型注入SQL语句里带引号常见形式是WHERE usernameadmin判断方法先要闭合引号。按回显方式分联合查询注入页面有数据显示位用union select直接并上查询结果效率最高。报错注入页面不回显查询数据但会回显数据库报错信息利用updatexml、extractvalue这些函数制造报错来带出数据。布尔盲注页面只返回“正常”和“异常”两种状态靠问“是或否”的问题逐位猜解数据。时间盲注页面连状态都不变靠sleep()函数制造时间差判断条件真假。按数据库类型分MySQL、Oracle、SQL Server、Access的语法和利用方式差异非常大判断错了数据库类型后面步骤基本白做。这一点我在下文“实战踩坑”部分会详细说。2. 手工注入的完整流程——从判断注入点到提取数据2.1 第一步判断注入点不管目标是什么系统第一步永远是确认参数是否存在注入。我不会一上来就上工具手工先探一遍能让你对漏洞的判断更准确。最常见的做法是加单引号?id1页面如果报SQL语法错误、显示异常、或者直接空白说明这个参数很可能拼接进了SQL语句。接下来进一步确认是数字型还是字符型?id1 and 11正常?id1 and 12异常基本可以判断是数字型注入。如果and不生效可能是字符串型尝试?id1 and 11闭合引号思路对了页面就正常。这里有个小细节在MySQL里#和--都能注释掉后面内容所以字符型注入判断时常会看到 and 11 --这样的payload这是先把前面引号闭上再用注释符把后面多余的语句吞掉。2.2 第二步order by确定字段数确认注入点后先别急着爆数据得知道这个查询查了几个字段。用order by是一个经典办法?id1 order by 1 ?id1 order by 2 ?id1 order by 3依次数字往上试当数字超过实际字段数时页面会报错。比如order by 4报错说明当前查询只有3个字段。为什么一定要先数字段数因为后面联合查询要求前后查询的列数一致不然union直接报错。这一步是联合查询注入的基础省不掉。2.3 第三步union select定位回显位知道字段数后用?id-1 union select 1,2,3注意这里用了id-1目的是让前半句查不到数据后一半的union select结果才能显示到页面上。如果直接用id1页面往往只会显示原来的数据联合查询的结果会被挡掉。页面显示2,3或者其中某个数字出现在页面上说明这些位置是回显点。接下来就把回显点替换成我们要查的内容。要特别提醒一下不是每个位置都能回显所以先看哪个数字能显示在页面上这是习惯问题。有些人上来就写union select database(),2,3结果database()的位置恰好不显示就直接蒙了。2.4 第四步爆库名、表名、字段、数据以MySQL为例确定回显点后获取库名?id-1 union select 1,database(),3拿到库名后查这个库下有哪些表?id-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema目标数据库名group_concat能把多条结果拼成一行显示这也是它成了出镜率超高的函数的原因。拿到表名后再查字段名?id-1 union select 1,group_concat(column_name),3 from information_schema.columns where table_name目标表名 and table_schema目标数据库名最后查数据?id-1 union select 1,group_concat(username,0x3a,password),3 from 目标表名information_schema是MySQL自带的“数据库信息字典”里面存储了所有数据库、表、字段的元数据信息。只要权限够、存在注入点整个数据库结构都能被这样一层层扒出来。如果遇到的是盲注场景联合查询不出东西就得换报错注入或者盲注脚本了。比如用updatexml报错注入?id1 and updatexml(1,concat(0x7e,database(),0x7e),1)原理是让updatexml的第二个参数格式错误把数据库名带进报错信息里。盲注就像玩“猜数字”游戏用substr()和ascii()把结果逐位拆出来比对手工很慢一般配合Python脚本跑。这里我想强调一句手工注入流程一定要多练直到形成肌肉记忆。不用工具不为了装而是让你理解每一步在做什么后面看工具日志、写自动化脚本、做绕过都是建立在这个理解上的。我自己刷靶场时前几个题目是纯手工跑的后面才配合脚本提速效果比一开始就拖工具要好得多。3. 靶场刷题与通关思路——DVWA、Pikachu、ctfshow、bugku3.1 DVWA SQL注入模块分级体验DVWADamn Vulnerable Web Application是最适合入门的靶场它的SQL Injection模块按安全级别分Low、Medium、High、Impossible四档同一道题四个难度能明显看出防护升级后攻击难度的变化非常建议一档一档刷。Low级别就是纯纯的拼接上述手工流程直接就能走通。Medium级别会对输入做一点简单的防御比如用mysqli_real_escape_string过滤单引号再比如用POST传参最开始可能会没反应过来还在用URL的?id去试怎么试都试不出因为代码拿到的是POST参数。这一步其实很关键——它提醒你注入测试要覆盖不同的输入位置。High级别在这个靶场里换了个查询方式用LIMIT 1限制输出同时把参数从URL参数改成了Cookie传递如果只盯着URL注入就会扑空。这个设计挺有意思很多初学的人卡在这一关问题不在SQL注入本身而是输错位置。Impossible级别用的是参数化查询直接让注入无计可施算是防御正确示范。刷DVWA我的体会是别老想着跳过级别直接刷最高难度每一档都花点时间看源码搞明白代码改了什么导致攻击失效。看源码是靶场学习里最值回票价的部分。3.2 Pikachu靶场的注入类型全覆盖Pikachu是另一个我很喜欢的靶场它的SQL注入分类做得比较细有数字型注入、字符型注入、搜索型注入、POST型注入、报错注入、盲注布尔盲注和时间盲注等基本把一个新手该见的注入类型都覆盖了。比如搜索型注入后台SQL大概率是SELECT * FROM stu WHERE name like %$搜索词%测试的时候输入%或者 or 11 --这类payload很容易看到整表数据。它的特点是搜索框本来就是模糊匹配用户输入被包裹在%通配符里闭合思路和普通字符型稍有不同但核心还是那句让输入的引号闭合原SQL再塞额外条件。盲注关卡则特别适合练脚本能力。我当时写了一个小的Python脚本先确认布尔条件是否生效比如and (select ascii(substr(database(),1,1)))100之类的语句再写循环二分法逐位跑库名和表名。等真正理解了这个过程再看那些自动盲注工具的输出心里就非常有数了。Pikachu的题目难度设置更适合“学习”因为它分类明确每一道题都在训练一个特定能力不急着综合考察。刷完一遍Pikachu手工SQL注入的基本功就练得差不多了。3.3 ctfshow和bugku的实战向题目刷完前面两个靶场可以试试ctfshow和bugku上面的SQL注入题目。这类平台题目更偏向CTF经常会有过滤、拼接、编码等考点综合性更强。比如ctfshow里有的sql注入题会涉及“生成文件”这个考点本质上是利用into outfile把查询结果写入服务器文件这属于MySQL注入的进阶利用要求当前用户有FILE权限并且需要知道可写路径。正常做题思路是先确认注入点、爆出数据库名再通过某些信息泄露点比如phpinfo路径确认网站根目录最后利用写文件功能生成包含目标数据的文件。这类题目在CTF中很常见能加深对“注入能力边界”的理解。bugku的SQL注入题风格也比较典型有些题目一开始就过滤了关键字比如select、union、空格。这时候就考验绕过思路了关键字被过滤可以尝试大小写混写SeLeCt、双写ununionion如果过滤机制只是简单替换一次。空格被过滤可以用注释符/**/代替空格或者用括号把表达式括起来。等号被过滤可以用like、in、等替代。网上的绕过姿势多到能刷一屏但我的建议是别死记硬背而是先理解过滤的实现逻辑。比如后端如果是preg_replace把union替换为空那ununionion替换后正好剩下union如果是正则匹配直接拒绝那就要想其他表达方式。只有理解了过滤器的行为才能灵活构造payload。3.4 万能密码与常用绕过思路仅限靶场练习这里单独讲讲万能密码因为网上问到它的频率极高。所谓“万能密码”本质是利用SQL拼接漏洞改变原查询逻辑的payload配方比如admin or 11 -- or 11 # admin--它们在登录场景的作用是绕过认证或者绕过用户规则实现无密码登录或越权查询。但我要强调一点这些姿势全部要在你自己的靶场环境里测试比如DVWA登录框或Pikachu的登录表单不要拿去打别人的线上系统这是安全底线。做安全测试必须有授权否则就是违法。学习漏洞是为了防御和提升自己目标永远是你看过源码、知道防护原理的那个靶场环境。再说细一点万能密码能生效往往是因为后端拼接了SELECT * FROM user WHERE username$name AND password$pass输入admin --后注释符把后面的AND passwordxxx吞掉实际上就只校验用户名了。有些后端傻乎乎地只检查有没有错没有去限制返回记录条数就实现越权登录了。如果后端代码有LIMIT 1这种直接闭合的方式就未必好用得改别的思路——这也是为什么我反复强调要看源码。绕过思路练得再多最终都要回到“为什么能绕过去”这个问题上。我自己在笔记本上列过一张对照表左边是过滤手段右边是绕过思路每次做到新题目就往上加一行。这张表越写越长但底层的原理始终没变。4. 常见报错与排坑实录——我踩过的那些坑4.1 单引号被吃掉了怎么办这是新手最容易碰到的问题。明明判断好了注入点输入页面也报错了但后面构造带引号的payload就是不生效。原因大概率是后端开启了magic_quotes_gpc或者使用了转义函数把输入里的单引号前面加上了反斜杠变成了\。这会导致我们尝试闭合SQL引号时直接被系统转义掉无法真正带到SQL语句里。早期的绕过方法叫宽字节注入思路是利用MySQL对GBK等宽字节编码会把%df%27当做一个合法汉字来解析的特性让前面加的反斜杠失效。CTF题目里经常出现这类考点本质上是编码与转义处理不当造成的。实际开发中这种风险早该用统一参数化查询来避免但在学习和CTF中它仍然值得手写一遍能帮你把编码问题理解得透透的。简单说当单引号被转义时不要只在输入层面硬碰硬要去看后端的编码处理逻辑利用它解析规则的漏洞来做闭合。不过这种高级操作对新手略复杂建议先把基础流程练熟再来研究宽字节。4.2 空格被过滤的N种姿势有些题目会把空格过滤掉导致union select完全没有办法正常书写。我第一次遇到时愣了很久后来才知道可以用/**/代替空格因为MySQL会把这个注释符当空白处理?id1/**/union/**/select/**/1,2,3如果union结合空格一起过滤还可以同时双写注释结合起来使用。做CTF题时经常要组合使用所以建议在本地搭个简单的PHP环境把各种过滤规则都试一遍比看十篇WriteUp都有用。还有一类思路是用括号和别名来绕过空格限制在复杂的查询上下文里确实管用但更考验对SQL语法的熟悉程度。我倾向于先把/**/、Tab键、换行这些手段用熟练再看场景选用更高级的方式。4.3 数据库类型判断错误导致前功尽弃在一次练习中我用MySQL的information_schema去爆表名结果怎么都报错后来发现目标是Oracle。不同数据库的系统表名、注释符号、字符串连接方式都不一样MySQL的库信息表是information_schema.tables注释是#和--。Oracle用的是all_tables、all_tab_columns注释只有--字符串拼接用||。SQL Server用的是sysobjects、syscolumns常用;做堆叠查询。Access数据库系统表名是msysobjects语法差异也大。判断数据库类型可以从报错信息、服务器指纹、以及参数行为来综合判断。比如MySQL的报错里常出现near 1 at line 1而SQL Server报错信息往往带XML字样。如果题目给了技术栈指纹比如后台是ASP优先考虑Access或SQL Server后台是PHP优先考虑MySQL。信息收集做到位能省很多无用功。4.4 过滤函数与大小写绕过的边界很多人以为绕过过滤一定要写一堆复杂payload但有时候一个大小写就绕过去了。原因在于不少过滤代码是直接对关键字做黑名单匹配而没考虑大小写变体$id preg_replace(/select/i, , $id);这个/i修饰符是不区分大小写的正则所以SeLeCt照样会被替换掉。但如果开发写的是if(strpos($input, select) ! false) { die(); }这种简单的字符串查找也区分大小写那么SeLeCt就能绕过。这说明什么说明测试过滤规则时不能只看效果要看代码实现。手工多试几种写法用union、UnIoN、union双写、uunionnion轮番测试再决定下一步用哪种绕过思路。如果过滤是基于关键字黑名单终极解法往往是参数化查询严格白名单因为白名单校验只认允许的字符从根上堵死了这些花活。攻击者绕的永远是黑名单防不住白名单。这也是为什么我在写防御部分时一直强调白名单优先。5. 防御与修复——学了攻击更要懂防守5.1 参数化查询是最有效的防线聊到防御我见过不少开发者觉得“过滤一下特殊字符就够了”但除非过滤做得极其严格否则总有绕过空间。最靠谱、也最省事的方案其实是参数化查询。以PHP PDO为例$stmt $pdo-prepare(SELECT * FROM users WHERE username ? AND password ?); $stmt-execute([$username, $password]);关键点在于参数化之后用户输入永远只是“数据”数据库不会再把它当作“SQL代码”去解析。哪怕你输入admin or 11它也只是被当作一个普通的字符串参数去匹配查询逻辑完全不受影响。各种语言都有对应的预处理写法Java里用PreparedStatementPython里用cursor.execute(sql, (params))只要把SQL语句和参数分开传就从根本上消灭了SQL注入。这一点在开发新代码时必须成为习惯没有商量余地。5.2 输入验证白名单优先参数化查询解决的是“数据不被当代码执行”的问题输入验证负责的是“业务上不该有的输入直接拒掉”。比如某个参数本来应该是数字ID那就用$id filter_var($_GET[id], FILTER_VALIDATE_INT); if($id false) { exit(非法参数); }或者用Java、Python等语言的正则白名单校验。只允许数字的接口就只接受数字只允许固定枚举值的接口就只接受枚举值。白名单的哲学是默认所有输入都不可信除非它符合预期格式。这比纯黑名单过滤不知道高到哪里去了黑名单永远有漏洞白名单天然封闭。我在内部培训时经常举一个例子如果小区只有一张“禁止进入人员名单”那每隔一阵就会有人被漏掉但如果你要求所有进小区的人必须出示门禁卡没卡一律不让进这才叫白名单。安全设计永远要建立“默认拒绝”的思维。5.3 配置与权限层面的兜底代码层面做对了还得看数据库权限配置。很多SQL注入最终造成重大损失是因为数据库账号权限太大注入点一旦被利用直接拿最高权限跑into outfile写文件甚至提权。开发中的数据库账号应该遵循最小权限原则只给这个业务需要的库的增删改查权限不轻易给FILE、PROCESS、SUPER等高危权限生产环境的数据库用户不能直接用root。权限越小就算注入点不幸被发现攻击者的活动半径也有限。再补一条数据库必须定期备份并做恢复演练别等到被删库才想起来备份这回事。WAF、数据库审计、日志监控这些手段也建议配合使用但它们都只是检测和拦截层面的补充不能替代代码层的根本修复。一个负责任的开发团队应该做到“即使WAF透明系统也是安全的”。5.4 安全编码规范与上线前自测清单最后把预防工作从“个人技能”变成“团队规范”。我们团队在项目里的做法是把SQL注入检查写进Code Review和CI流水线。看代码时重点扫三块是否有直接字符串拼接SQL的写法比如Java里的SELECT * FROM xxx WHERE id id出现就要求改成PreparedStatement。是否有对输入只做过滤、没做白名单校验的接口有就去补。是否在代码里硬编码了数据库管理员账号或敏感SQL权限有就申请降权。上线前自测也可以用一份简单的清单[ ] 所有涉及用户输入的SQL是否都用了参数化查询[ ] 数值型参数是否都做了类型校验[ ] 字符串型参数是否有限制长度和字符集[ ] 数据库账号是否最小权限[ ] 错误回显是否被处理成通用提示页[ ] WAF和生产日志、监控是否就位不要小看错误回显这一条很多注入点能快速爆数据就是因为数据库的详细报错直接展示在页面上给攻击者省了大量时间。生产环境把错误信息统一屏蔽掉对防御很有帮助。想专门说说我的学习顺序。我并不是一上来就去看各种绕过姿势那容易变成背口诀忘得也快。我是先在DVWA和Pikachu上把基础手工流程彻底走熟然后尝试不看任何资料复现一遍完整过程卡住了再回去看笔记。重复两三遍之后再开始接触CTF平台那些带过滤的进阶题这时候每个绕过技巧都能在脑子里对应到背后的代码逻辑。如果你也是刚入门我的建议是给自己定个计划拿一周时间只刷DVWA的SQL注入模块每关至少写500字的笔记记录当时的思路和报错。这个办法看起来很笨但效果比看十遍视频好得多。安全这条路没有捷径每一步手工操作、每一个报错排查最后都会变成你真正的判断力。等你哪一天看到系统时脑子里自动能浮现出它背后可能存在的SQL拼接、参数校验和权限设计那SQL注入这门课就算真正过关了。
返回列表