游客发表
2019年某电商平台被拖库,注种方安全团队复盘时发现攻击者根本没用什么高级0day——就是注种方一个拼接到SQL语句里的商品ID参数没过滤。注入点简单到离谱,注种方但造成的注种方损失是千万级用户数据。这件事说明一个问题:SQL注入的注种方花样远比大多数人想象的多,而每一种都可能要命。注种方
最经典的注种方是联合查询注入(Union-Based)。攻击者在参数后面接上 UNION SELECT username,注种方 password FROM users,只要页面有回显位,注种方数据就直接吐出来了。注种方这种方式效率极高,注种方因为一次请求就能拿到整张表的注种方内容。前提是注种方列数和数据类型要匹配,所以攻击者通常会先用 ORDER BY 试探列数。注种方我见过有人手工试了二十多次才对齐字段,注种方也见过用sqlmap三秒钟搞定。手工注入的过程其实挺像解谜,一个字符一个字符地猜,枯燥但有成就感。
回显被堵死的时候,报错注入(Error-Based)就派上用场了。MySQL里经典的 extractvalue() 或 updatexml() 函数,把查询结果拼进报错信息里带出来。比如 and extractvalue(1,concat(0x7e,(select database()))),页面报一个XML解析错误,错误信息里就夹着数据库名。这种方式对MySQL版本有要求,5.1以上才支持。SQL Server那边常用 convert(int, @@version) 制造类型转换错误。报错注入的精髓在于:你封了页面的正常回显,但错误信息本身就是一条回显通道。
如果连报错信息都被屏蔽了,那就玩布尔盲注(Boolean-Based Blind)。页面不会返回数据,也不会报错,但会根据SQL语句的真假呈现不同状态——可能是一个像素的差异,可能是加载时间的长短,甚至只是HTTP状态码的微妙变化。攻击者构造 and substring(database(),1,1)='a' 这样的语句,逐字符判断。一个数据库名八个字符,每个字符平均试十几次,几百个请求才能拿到一个库名。这种注入方式慢得让人抓狂,但一旦脚本跑起来,剩下的就是等。
比布尔盲注更隐蔽的是时间盲注(Time-Based Blind)。页面连状态差异都没有了,怎么办?用 sleep() 函数。MySQL里 and if(substring(database(),1,1)='a',sleep(5),0),如果第一个字符是a,页面就卡五秒。攻击者靠响应时间来判断。这种方式对网络稳定性要求高,延迟波动大的环境里误判率不低。有人用 benchmark() 替代sleep,效果类似但更耗CPU。时间盲注是最后的手段,也是最难防的——因为从HTTP层面看,请求都正常返回了,只是慢了一点。
堆叠查询注入(Stacked Queries)是另一种思路。利用分号在单个请求里执行多条SQL语句,比如 ; DROP TABLE users; --。这种注入的破坏力最大,但支持它的数据库和驱动不多。MySQL的PDO默认不支持堆叠,SQL Server倒是可以。很多年前PHP的mysql_query函数也允许堆叠,后来被废弃了。堆叠注入更像是一把重锤,能砸开就直接砸,砸不开就换工具。
还有一种容易被忽略的二次注入(Second-Order Injection)。恶意数据第一次被存入数据库时经过了转义,看起来无害。但当这个数据在另一个场景被取出并拼接到新SQL语句里时,转义消失了,注入生效。比如注册时用户名填 admin'--,存进去的时候被转义了,但修改密码功能从数据库读出这个用户名再拼接时,单引号就逃逸了。二次注入的检测难度极高,因为注入点和使用点不在同一个地方,传统的WAF很难覆盖这种跨请求的攻击链。
宽字节注入算是一个地域性很强的类型,主要出现在GBK编码的数据库里。攻击者输入 %df',转义函数加的反斜杠(%5c)和%df组合成一个合法GBK字符,单引号就逃出来了。这种注入在中文站点上曾经非常普遍,现在随着UTF-8的普及少了很多,但老系统里依然能碰到。
每种注入方式对应的是不同的信息反馈通道:有回显用联合,有报错用报错,什么都没有就用布尔或时间。攻击者的选择逻辑很务实——哪条路通就走哪条。防守方要做的不是堵某一种,而是从参数化查询这个根上解决问题。预编译语句把数据和指令彻底分开,上面这些花样全部失效。可惜的是,2024年了,我依然能在一些新上线的系统里看到字符串拼接的SQL。
友情链接