ARTICLE DETAIL

资讯详情

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

SQL注入原理详解:从联合查询到万能密码与靶场实战

SQL注入原理详解:从联合查询到万能密码与靶场实战 学SQL注入之前我一直觉得这是个很玄的东西。直到在小迪安全课程里把这节《简要SQL注入》听完又在靶场上亲手试了一遍我才发现核心就一句话程序把用户输入直接拼进了SQL语句于是输入从“数据”变成了“代码”。这篇笔记我会把原理、判断方法、靶场实操、万能密码这几个部分串起来写顺带解决几个新手最容易卡住的问题。适合刚接触Web安全、或者刷SQL注入题总是不出效果的读者如果你已经能独立盲注脱库这节内容可以快速略过前半部分直接看后面的靶场记录和排错清单。1. 先把SQL注入的原理拆开1.1 一条查询语句是怎么被拼出来的我最早看SQL注入的讲解总是一堆payload堆在眼前看得人头皮发麻。后来换了个思路先看后端代码一下就通了。拿一个最经典的登录框举例假设后端PHP写成这样$sql SELECT * FROM users WHERE username . $_POST[username] . AND password . $_POST[password] . ;这段代码的逻辑很简单用户输入用户名和密码程序把它们原封不动地拼进SQL语句再去数据库里查询。正常输入admin和123456时拼出来的语句是SELECT * FROM users WHERE usernameadmin AND password123456如果用户名和密码匹配这条语句能查出记录登录成功。问题在于当攻击者输入的不是“正常数据”而是SQL片段时这段片段会直接成为语句的一部分。比如用户在用户名框里输入 OR 11 --密码随便填拼出来就变成了SELECT * FROM users WHERE username OR 11 -- AND password123456留意三个关键动作。单引号把前面的username闭合掉让username的条件彻底结束OR 11构造了一个恒为真的条件--是SQL里的注释符把后面原本用来校验密码的AND password...全部注释掉。整条语句的条件变成了“用户名为空或者1等于1”数据库一执行发现条件必真就把第一条用户记录返回了。如果这条记录恰好是管理员那攻击者就以管理员的身份进了后台。这是字符型注入里最典型的一段SQL注入其他花里胡哨的姿势本质都是在做“闭合、拼接、注释”这三件事。1.2 字符型和数字型攻击入口的差异既然说到了拼接就顺便把另一个基础概念理清楚字符型和数字型注入。刚才登录框那个例子username的值被单引号包住这种叫字符型注入攻击者必须考虑如何闭合引号。但有些接口的参数是数字比如一个商品详情页$id $_GET[id]; $sql SELECT * FROM product WHERE id . $id;这里id没有被引号包裹SQL语句里直接就是数字。攻击者输入1 AND 11拼出来是SELECT * FROM product WHERE id1 AND 11页面正常换成1 AND 12条件变假页面可能就查不到数据了。因为没有引号需要闭合数字型注入的操作空间更大payload写起来也更随意。我见过不少新手在数字型参数上还老老实实地加引号结果页面报告语法错误半天反应不过来——先分清参数是字符型还是数字型比急着背payload重要得多。判断方法其实不难在参数后面加一个单引号看报错是字符型大概率会报语法错误如果没反应再加AND 11看页面变化能变说明大概率是数字型。更省事的办法是用Burp Suite抓包看请求直接看后端传参的位置是否带引号。1.3 了解防护为什么预编译能拦得住学注入必须同时学防护不然你只能当“会用payload的脚本小子”。现代开发里最推荐的防护手段是参数化查询也叫预编译。以PHP的PDO为例$stmt $pdo-prepare(SELECT * FROM users WHERE username? AND password?); $stmt-execute([$_POST[username], $_POST[password]]);这里SQL语句的结构在传入参数之前就已经固定了用户输入再离谱也只能作为“值”传给数据库数据库不会把它解析成代码。用点菜的类比说预编译相当于菜单已经印好你只能在对应的空格里填菜名不能把“再加一个菜”写进菜名栏里让厨师当成指令执行。理解这一点再看为什么老代码漏洞多就清楚了——老代码喜欢用字符串拼接SQL本质是把菜单内容和点餐指令混在一起自然容易被改写法。除了预编译还有输入校验、最小权限、WAF等兜底措施。但记住过滤和WAF都只能降低风险参数化查询才是根治方案。这条结论在我后面的靶场实操里体会特别深同样的注入payload打在预编译接口上完全无效说明漏洞根子就是拼接方式。2. 如何判断一个参数是否存在SQL注入2.1 先从请求里找到入口参数很多初学者上来就拿工具扫扫了半天不知道哪些请求有参数、参数在哪里这是方向性问题。在实际测试里注入点不一定在页面上能看到的输入框。URL里的?id1、POST请求体里的JSON字段、Cookie里的用户标识、甚至User-Agent和Referer头都可能是后端拼SQL的数据来源。我见过不止一次页面表单做了很严的过滤但URL参数漏成了筛子。标准操作是先开Burp随便点几个页面把流量看一遍。重点找三种情况一是URL后面有参数参数值看起来像数字或简短字符串这最可疑二是POST请求体里有字段名和值比如用户名、搜索关键词、用户ID三是Cookie里有类似uid123或tokenabc这种也要留意。找到之后再想一个问题这个参数值传给后端最可能拼进什么SQL语句是查用户信息还是查商品还是更新操作这样后续构造payload才有方向。2.2 手工验证四板斧我判断一个参数是否存在注入很少一次性上工具而是先手工走四个步骤每一步都能看到明确信号。第一板斧是单引号探测在参数值后面加一个如果页面报SQL语法错误或者页面结构明显变化、响应长度突变说明引号进入了SQL语句很可能存在字符型注入。第二板斧是布尔对比加AND 11和AND 12看两次返回是否不同——这一步在数字型和部分过滤场景下尤其好用。第三板斧是延时对比加上AND SLEEP(3)如果请求明显慢了3秒左右说明逻辑条件被数据库执行了就算页面完全无回显也能判断。第四板斧才轮到工具复核用sqlmap之类跑一遍确认结果。第二板斧有个细节对比的是整个响应不只看页面有没有数据。有些页面查询失败时会跳到错误页有的会返回空列表有的只是长度变化你要用Burp或浏览器开发者工具看响应包的状态码、长度和关键内容。我实测下来长度变化是最可靠的指标肉眼扫页面容易错过。这四步必须在授权目标上做。我习惯在DVWA、Pikachu这类本地靶场里练熟了再去参加有授权的众测或比赛这样心里有数知道每个信号对应什么。2.3 真实渗透中怎么从“有洞”走到“拿到数据”靶场里判断出注入往往就完事了但真实渗透里这只是第一步。后面的链路是确认数据库类型确认注入类型联合、报错、盲注拿到当前数据库名再依次拖出表名、字段名和最终数据。举个我练手时常走通的流程假设一个搜索接口存在字符型注入手工确认后下一步是这样推进的先用ORDER BY判断列数比如1 ORDER BY 3--报错说明只有两列于是可以上联合查询-1 UNION SELECT database(),user()--拿到库名和数据库账号后再查所有表-1 UNION SELECT 1,group_concat(table_name) FROM information_schema.tables WHERE table_schemadatabase()--接着查关键表里的字段最后把数据拖出来。整个链路里有一个新手常踩的坑union前面的查询结果必须为空否则页面显示的是前面查出来的正常数据union的结果反而看不到。解决办法是把参数值改成负的、不存在的比如-1或99999让前面的条件查不到东西。这里要特别强调真实渗透中能不能拿到数据和网站架构、数据库权限、WAF强度都有关系有时候注入点确认存在但页面没回显就得转报错注入或盲注复杂度直接上升。这也是为什么我一直建议新手先在靶场把联合查询和盲注两种都练熟不然真到实战里遇到第一步没回显就卡住了。3. 靶场实操DVWA、Pikachu与CTF题3.1 DVWA Low级注入完整记录DVWA是入门绕不开的靶场它的SQL Injection模块在Low级别基本是“裸奔”状态拿来练手最合适。我记录一下自己完整跑通的过程照着敲一遍就能理解联合注入的全貌。先在SQL Injection输入框里输入1页面正常显示第一个用户的信息。再输入1页面直接报语法错误错误信息里能看到SQL语句的片段据此确认是字符型注入。接下来判断列数依次输入1 ORDER BY 1-- 1 ORDER BY 2-- 1 ORDER BY 3--前两条正常第三条报错说明查询返回2列。有了列数就能做联合查询让前面的查询结果为空然后用union注入自己想要的查询-1 UNION SELECT database(),user()--页面上第一项显示数据库名第二项显示数据库账号。继续拖表名-1 UNION SELECT 1,group_concat(table_name) FROM information_schema.tables WHERE table_schemadatabase()--group_concat会把所有表名用逗号连成一行这样不用翻页就能一眼看全。拿到users表后查字段-1 UNION SELECT 1,group_concat(column_name) FROM information_schema.columns WHERE table_schemadatabase() AND table_nameusers--看到user和password字段后直接拖数据-1 UNION SELECT user,password FROM users--密码字段是MD5哈希拿到之后再拿去解或碰撞。整个流程走下来你会发现联合注入的核心就三点列数对齐、前面的查询置空、字段类型尽量用能显示的方式输出。DVWA的Level Medium和High增加了代码层面的防护比如转义单引号、用mysqli预处理正好用来对照理解防御手段是怎么生效的。3.2 Pikachu的两种注入形态字符型与搜索型Pikachu靶场比DVWA更贴近国内Web开发习惯界面是中文的练起来很舒服。它的SQL注入模块分了字符型、搜索型、POST型、报错型等多个关卡其中字符型和搜索型对比着练特别开窍。字符型那关和DVWA的Low很像输入框里填名字后端照样拼接SQL。搜索型则多了一个陷阱查询条件用的是LIKE模糊匹配语句大概是SELECT * FROM users WHERE username LIKE %$name%;直接在搜索框里输入 OR 11 --效果会和字符型不一样因为前面的%也要闭合。实际测试时我输入% OR 11 --拼出来就是SELECT * FROM users WHERE username LIKE %% OR 11 -- %;LIKE %%本身就能匹配所有字符串后面还有个恒真的OR 11整条语句条件必真所以能查出全部用户。搜索型注入在真实站点里很常见特别是站内搜索、商品筛选这些功能后端为了做模糊匹配用了LIKE又没做参数化结果整个搜索框就成了突破口。尝到甜头后我在Pikachu的POST型关卡里又练了一遍参数从URL挪到了请求体里但注入逻辑完全一样只是手工改包时要把payload写在POST数据里。3.3 CTFshow和Bugku的题目该怎么刷打完靶场可以进CTF题了。CTFshow和Bugku里SQL注入题目数量多、难度阶梯清晰我用它们检验自己是不是真的理解了注入原理。CTFshow的WEB入门模块里很多SQL注入题特点是会把WAF、过滤、编码等真实因素加进来比如有的题过滤了空格有的题注释符只能用#有的题把SELECT关键字转义了。刚开始刷大概率卡住这不代表你没学会注入只是没适应出题人的过滤规则。我的刷题方法是先不看writeup把题目给的源码或过滤规则读一遍用前面学的闭合思路手工试试不出来再去看writeup看完一定自己复现一遍把payload重新敲进Burp里走通。Bugku的SQL注入题目偏基础适合检验“手工联合查询”的熟练度。实际做题时注释符的选择、大小写绕过、/**/代替空格都是高频考点这些只有亲手踩过坑才能内化成直觉。CTFshow里还有一种“SQL注入生成文件”的题比如通过INTO OUTFILE把查询结果写到服务器文件或者利用LOAD_FILE读文件这属于进阶方向考察的是MySQL文件读写权限和Web根目录路径探测。这类题靠在比赛环境里理解考点即可实际渗透中能不能用完全取决于数据库账号有没有FILE权限——大部分真实站点不会给Web应用这么高权限。4. 万能密码、登录绕过与Python验证脚本4.1 万能密码背后的逻辑“万能密码绕过”这个话题在热搜上很常见初学时会觉得神奇但放在前面讲的原理框架里其实就是一句话攻击者在登录参数里注入SQL片段把条件查询改成恒真查询。下面这几个变体我都实测过看表就能明白为什么说是“万能”输入内容SQL拼接后的效果利用要点 OR 11 --WHERE username OR 11 -- AND ...经典版返回第一条记录admin --WHERE usernameadmin -- AND ...直接注释掉密码校验 OR 11 --WHERE username OR 11 -- AND ...变体适合部分过滤场景拼接过程我上面已经拆过不再重复这里想提醒的是万能密码能生效的前提是后端存在字符串拼接SQL的漏洞。如果后端用了预编译这些payload打过去就是普通字符串跟输入一个“abc”没有区别。所以看到网上有人宣传什么“万能密码破解一切站点”基本可以判断是在夸大漏洞场景早就被参数化堵住了。实操中还有个经验很多登录框不止一个参数有时后端把用户名和密码拼在同一条SQL里有时是分开查两次。分开查的时候光在用户名字段注入不一定能绕过密码校验需要同时看两个字段的拼接方式。我碰到过一种情况后端先查用户名是否存在用户名字段做了参数化但密码字段安全级别低这时就要换思路了。4.2 用Python写一个简单的注入探测脚本学了原理、会了手工不妨用Python把探测过程自动化这也是从“手工”走向“工具”的必经一步。下面的脚本用法很简单对着本地靶场跑一遍分别发几个测试payload通过对比响应长度和耗时判断是否存在注入import requests import time # 以Pikachu字符型注入为例本地环境要按实际URL和参数名修改 TARGET http://127.0.0.1/pikachu/vul/sqli/sqli_str.php COOKIES {PHPSESSID: your_session_id_here} def send(payload): data {name: payload, submit: 查询} start time.time() resp requests.post(TARGET, datadata, cookiesCOOKIES) return resp.text, time.time() - start normal, t0 send(admin) true_page, t1 send(admin AND 11) false_page, t2 send(admin AND 12) delay_page, t3 send(admin AND SLEEP(3)) print(f普通请求长度: {len(normal)}) print(f恒真请求长度: {len(true_page)}) print(f恒假请求长度: {len(false_page)}) print(f延时请求耗时: {t3:.2f}s) if len(true_page) ! len(false_page): print([] 布尔条件生效疑似存在字符型注入) if t3 - t2 2.5: print([] 延时生效疑似存在时间盲注)脚本逻辑很简单恒真和恒假两个请求如果返回长度不一样说明输入内容影响了SQL查询结果延时请求如果明显多花了3秒说明数据库执行了SLEEP函数。这个脚本只适合本地靶场练习而且用的时候一定要看清楚响应差异有时候长度只差几十个字节判断阈值也别写太死。真正做批量验证时我会在脚本里加上用户-Agent、Cookie、请求间延时避免把靶场请求打得太猛。学Python写探测脚本这件事本身比脚本结果更重要。刚开始我只会用sqlmap遇到复杂的登录流程、带动态令牌的请求就抓瞎。后来自己写脚本被迫去理解HTTP请求的构建、会话Cookie的维持、响应差异的判定这些能力在真实测试里比任何现成工具都耐用。4.3 登录场景的两种攻击思路围绕“sql注入 登录密码”这个热搜词登录场景的攻击其实可以拆成两条线。第一条是黑盒绕过就是我们说的万能密码直接以某个用户身份进系统不需要读取数据库内容危害是权限被绕过。第二条是拖库拿密码注入点权限够高时直接查询用户表的账号和密码哈希拿回来离线破解或直接撞库。这两条线最大的区别是黑盒绕过只改变了登录逻辑拖库则是把整个用户体系的数据都握在手里后者危害大得多。在真实渗透里遇到登录框我一般两头都试先试着绕绕不过再看注入点能不能读到users表。但要注意就算读到密码哈希能不能解出来是另一回事。很多站点虽然存的是MD5但用户密码设置简单用字典一跑就出来了。这也提醒开发者光靠哈希存储还不够加盐、bcrypt、强制强密码策略才是正路。有个段子说得好SQL注入都打到脸上了开发者还在纠结要不要给小字段加索引——数据泄露的锅往往不是某一个漏洞而是从漏洞到弱口令整个链条都太脆。5. 常见问题与排查技巧实录5.1 新手最容易踩的六个坑学SQL注入的过程里我踩过的坑基本可以列成一张表下面这六条是周围朋友和我的共鸣最高的。直接对号入座省得走弯路。现象原因解决思路加单引号页面完全没变化错误显示被关闭或参数是数字型先试AND 11和AND 12对比再试延时执行ORDER BY 3都报错空格或关键字被过滤用/**/代替空格或用大小写混写union select之后页面还在显示正常数据前面查询结果非空union排在后面把参数值改成-1或999让前面查不到注释符--不生效URL里#被当成锚点或--后面没空格URL编码写成--或用%23代替#查information_schema失败数据库版本或权限受限先确认数据库类型和版本再选对应元数据表工具跑不出结果请求需要登录Cookie或带CSRF令牌先手工确认注入再把Cookie、POST数据配置进工具第一条最普遍。很多开发者会把错误提示关掉页面只显示空白或友好提示你以为没注入其实SQL可能已经报错后走入了提前写好的兜底逻辑。所以我的习惯是单引号没反应就立刻转布尔和时间盲注用响应长度和耗时找差异别死磕报错。第二条我特别有感触。CTFshow里有一道题过滤了空格我第一次做的时候用标准payload直接报错后来想起来的解法是把SELECT里的空格换成/**/比如-1/**/UNION/**/SELECT/**/1,database()--数据库解析SQL时会把注释内容忽略所以/**/完全可以当分隔符用。这个技巧在真实环境的应用价值不大因为WAF不比这个简单但在题目里练过之后你对“过滤不是万能的”这句话理解会深很多。5.2 我自己的排查流程把这几条经验串起来我在遇到一个可疑参数时实际执行的排查流程是这样的。先加一个单引号看响应没有异常就上AND 11 / AND 12对比对比没差异就上AND SLEEP(3)同时观察耗时延时也没反应再用 AND SLEEP(3)这类带引号的版本因为字符型注入前面需要闭合。整个流程里我用Burp的Repeater反复修改payload并且始终开着响应时间显示这个习惯帮我省了很多无用功。还有一次踩坑的教训一个POST登录接口我测了很久都判断不出来最后发现请求体里有个csrf_token字段每次请求都要先从前一个页面抓取payload没变但令牌过期导致所有响应都是跳转登录页长度全部一样自然测不出东西。解决办法是先写一个带会话的脚本自动获取令牌再发包。这个经历也验证了那句话工具和手工判断的前提是先把业务逻辑理解透否则你发出去哪些请求、响应代表什么心里都是糊的。5.3 推荐的练手路径与授权底线最后说练手路径。我的建议是按“本地靶场 → 开源靶场 → CTF题”的顺序走别跳级。DVWA先刷Low把联合查询、报错注入、布尔盲注三种手法练到闭眼能写Pikachu接着刷感受一下POST注入和搜索型注入的差异然后可以上SQLi-Labs那个靶场把各种过滤和闭合方式拆得非常细每一关对应一种技巧最后去CTFshow、Bugku刷题把手工思路和CTF套路结合。这条路径走到一半你会发现自己不再需要死记payload了因为看到参数过滤规则能条件反射地写出对应绕过方式。到那时候SQL注入对你来说不再是“玄学”而是“猜后端怎么写代码”的游戏。不管练到哪一步我都要反复提醒所有测试都在自己的靶场、授权的比赛或众测目标上做。未经授权的站点再简单的漏洞也不要去试这不是技术问题是做安全的基本底线。技术可以慢慢练路一旦走歪就回不了头了。
返回列表