SQL注入漏洞发现之旅

百家 作者:百度安全应急响应中心 2020-04-24 17:20:14

概述

感谢朋友竹子的邀请。
SQLi漏洞是一个老生常谈的话题,这里仅分享一下笔者自己对SQLi漏洞挖掘的一些感悟及总结,送给有需要的人。
本文只讨论如何去发现SQLi漏洞,而不讨论发现漏洞后面的利用。
黑盒测试的角度,笔者觉得:
当你不确定漏洞是否真实存在时,就像大海上的一叶孤舟;
而当确定漏洞真实存在后,则就像大海上的一叶孤舟,多了一个灯塔。
本文主要从两个方面进行讨论,一方面将会介绍易出现SQLi漏洞的场景,另一方面将主要介绍MySQL中不同位置的注入点如何去快速证明漏洞的存在。
本文假设你已经对MySQL数据库和SQLi漏洞有所了解。

易出现问题的场景

排序功能
预编译对ORDER BY子句、表名、列名不生效。
这一点很多研发不清楚,可能会一股脑的使用预编译认为万事大吉。
因此在遇到这类的参数时,一定要格外留意,经常会出现SQLi漏洞。
持久性框架
企业系统的研发中,大部分都是调用现成的持久层框架来对数据库进行操作,但是每个框架都可能因为研发的误使用导致SQLi漏洞的产生。
像现在Java Web中最流行的SSM框架中的MyBatis,
#{param} 表示对param这个参数使用预编译,而${param}则表示对param这个参数不使用预编译,直接拼接。
研发本着省事省力的原则写着代码,写出了MyBatis框架最常见的漏洞点。

模糊查询

错误的写法
正确的写法,可见,确实比错误的写法多写很多个字符。

批量处理

错误的写法,直接传入一个字符串类型的ids,例如ids = 1,2,3,4
正确的写法,传入一个集合类型的ids,可见,这个比错误的写法多写更多的字符。
RPC
在企业中总会出现这种情况:系统A专门写了可被其他系统调用的接口a,这个接口a可能被系统B、C、D调用,但是就是不被系统A直接使用。
因此当这个接口a未作任何防御,存在SQLi漏洞的风险时,较难被公司安全部发现。
当安全部代码审计系统A,发现了接口a未作任何防御,多半是存在SQLi漏洞;
但是安全部没证据啊,没法构造数据包进行证明,鬼知道这个接口a到底被哪个系统调用了。
因此对于系统A提供的这个接口a可能要在安全部黑盒测试系统B的时候,才能证明接口a存在SQLi漏洞;然后通知系统A的研发进行修改。
所以在测试时发现某些功能突然调用了其他系统的接口时,需要格外留意。
仿废弃接口
接口已经废弃了,但是研发人员为了图省事,只在前端限制了接口的使用,通过构造数据包仍旧可以正常使用,因此我称它为伪废弃接口
以前在挖洞的时候遇到很多次,后来在企业中与研发的同学沟通多了,才真正领悟到一些内涵。
场面一:
系统下线了,直接在登录页面把相关JS注释掉
场面二:
模块下线了,直接把这个模块在HTML删了
场面三:
系统下线了,把首页所有代码全删了
研发本着少做少错,多做多错的原则小心翼翼的下线了
而且研发还怀着侥幸心理,说不定啥时候这个系统再用或者优化呢,到时候岂不是可以省大把时间。
像下面这个场景,在正常登录时各种参数缺失没法通过点击登录按钮来进行登录

但是却可以通过JS里面的内容来手动构造登录请求
编程语言
从编程语言角度上,不同类型的语言出现的问题侧重点会有些不同。
我理解为主要是因为强类型语言与弱类型语言之间的区别
强类型语言以JAVA为例,声明变量时便确定了数据类型,如果给的类型不一致那么便会导致错误。
因此对于像idnumberpageNopageSize等这类数字类型非常明显的参数,存在SQLi漏洞及XSS漏洞(暂不考虑DOM型XSS)的可能性非常小。
例如下面这个,userId参数是Long类型的,act参数是String类型的。
因此这个userId参数通常不会存在SQLi漏洞跟XSS漏洞,而act参数确有测试的必要。
而向PHPPython这种弱编译型语言,则仍旧需要对数字类型非常明显的参数做测试。
但是也不能一概而论,这类语言的一些开发框架会强制参数的类型。
例如Python里面的Django 框架
在接收参数时限定了参数question_id必须是int类型的,当给出的字符串时,便会异常,无法匹配这个接口。

隐藏参数

ORM框架会为数据库中的字段与实体对象一一映射,即数据库中有哪些字段,那么建立的实体对象中便有那些属性。
例如下面这个SQL模板,接收的是一个User对象,返回的也是一个User对象
必须的参数是startIndex 、limitSize 参数,而usernameemaildep 参数都是可有可无的。
当然这三个参数也很可能在你测试的功能点上找不到。
也就是说常见的黑盒测试、扫描器扫描等不容易发现,因此在像这种ORM框架中隐藏参数出现SQLi漏洞的概率会大一些。
例如之前有一次,推漏洞修复,代码审计发现的查询功能的一个隐藏参数;研发的同学说这是他们研发自用的参数,在前台是找不到的,因此不用修。
结合一下上面RPC那个场景所说的问题
在黑盒测试时,较难测试到系统中的隐藏参数;
在白盒测试时,较难证明系统提供的RPC接口的安全性。

前端固定参数

前端中固定的一些参数,例如下拉列表、readonly的参数、disabled的参数等,研发认为只能是这几个值,肯定不会有什么问题,因此认为参数可控,可以不对参数做任何处理。
这种情况在企业中特别常见,因此这类参数出现问题的概率会更大。
例如下面这个真实片段
楼兰:你这个漏洞没修复啊,还是存在啊研发:不会吧,这次我在前端设置了一个下拉框,这个参数只能是固定的几个值楼兰:这不行啊,你这个漏洞在前端做的任何限制都没用啊,攻击者直接在数据包中进行修改,不通过前端的研发:...

HTTP Header头

Header头中最常写入数据库的字段: Client-ipX-Forwarded-For ,用来记录客户端IP。
研发认为客户端IP,不都是固定的那个格式么,安全,没问题。
但是研发的同学却不知道获取客户端IP的方式有很多种,很多个字段都可以在客户端进行任意伪造,因此在测试时可以判断一下目标功能点是否可能记录客户端IP,如果可能那么便可以对记录客户端IP的字段进行SQLi漏洞的测试。
例如下面这段PHP代码,研发的先通过Client-ip来获取IP,获取不到时,再尝试通过X-Forwarded-For来获取,最后才通过不可伪造的Remote-Addr来获取。

时间参数

在笔者与研发线沟通漏洞修复的问题时,发现有些研发竟然会对时间型的参数保持怀疑态度,可能是因为时间型的参数在插入时要保持格式的一致的原因,导致有些研发不敢去处理这个时间型的参数。
总觉得使用预编译的话可能会引起异常,因此便直接拼接了。

MySQL中各位置的测试

在具体测试每个参数的时候需要先判断该参数位于SQL语句中的什么位置,然后有针对性的进行测试。
在开始之前,先看一下MySQL中SELECT语句的语法,因为查询功能是最为常见的功能。

表名字段名

这里仅说一下注意点,闭合字符的使用。
像下面这个情况,你在闭合的时候则需要考虑使用反单引号,而不是单引号、或者双引号
SELECT * FROM `bsrc` WHERE `{输入点}` = '';
一般来说表名字段名,要么没有闭合字符,要么便是反单引号。

where子句

普通条件

假设输入点在下面这个位置
SELECT * FROM `bsrc` WHERE `bug_name` = '{输入点}';
因此可以用最简单的代码去发现漏洞
index.php?bug_name=x 与index.php?bug_name=x'-'x
看一下数据库的执行结果
再来一探究竟
而对于数字型的参数则更好证明了
index.php?id=4 与index.php?id=5-1来查看回显的数据是否一致

模糊查询

假设输入点在下面这个位置
SELECT * FROM `bsrc` WHERE `bug_name` like '%输入点%';
再去用刚才这个方法去测试,发现结局却大相径庭
诺,这个时候既不是结果为空,也不是显示全部数据。
此时可以这样来证明
index.php?bug_name=' and '%'=' 和 index.php?bug_name=' and 'x'='
数据库查询效果图如下

order by子句

上面已经说过了,order by子句出现SQLi漏洞的概率非常大。
假设输入点在下面这个位置
SELECT * FROM `bsrc` WHERE `bug_name` like '%' ORDER BY {输入点1} {输入点2}
先来看一个正常的使用
先对bug_name字段进行降序,再对bug_rank字段进行升序(默认),再对第3列进行升序(默认)。
因此最快速的测试方法便出来了
页面正常
index.php?order=1,1
页面异常
index.php?order=1,0
MySQL效果图如下
当列数为0或者超出所有列数时便会报错。

limit子句

通过上面所说的MySQL中的SELECT语句的语法规定可以得知Limit子句后面只能跟PROCEDURE子句、INTO子句;
因此当注入点是limit位置的时候,你可以使用INTO子句来写入文件进而证明;
但是在真实情况中,拥有可以通过INTO子句来写入文件的并且读取该文件的条件很少;
因此通过情况下是使用PROCEDURE子句。
关于PROCEDURE子句的详情可以看官方文档中给出的介绍。
https://dev.mysql.com/doc/refman/5.7/en/procedure-analyse.html
PROCEDURE syntax is deprecated as of MySQL 5.7.18, and is removed in MySQL 8.0.
Payload
SELECT * FROM `bug_name` limit 10 procedure analyse(updatexml(1,concat(0x7e, user()),1), 1);
效果图

limit子句前面没有使用order by子句时,可以在limit后面直接使用联合查询。
如果limit前面使用了order by子句时,那么便只能考虑前面的Payload了。

其他情况

仅语法错误时
在真实情况中,在测试时发现很多时候只有语法错误时,页面才会跟语法正确时有所区别。
先看一下最简单的证明方法
语法正常,页面正常
index.php?bug_name=1' and if(1=2,(select 1 union select 2),1) and '
语法错误,页面出现差别
index.php?bug_name=1' and if(1=1,(select 1 union select 2),1) and '
语法错误,页面出现差别
index.php?bug_name=1' and if(1=1,(select 1,2),1) and '
再来看一下数据库中的执行效果
在MySQL中,在使用子查询时,子查询的结果必须且只能是一行一列

时间型盲注

SELECT * FROM `bsrc` WHERE `id` >= 5 AND sleep(1)
此时如果符合条件id>=5 的有2行,那么便会执行2次sleep(1)
SELECT * FROM `bsrc` WHERE `id` >= 5 OR sleep(1)
此时如果不符合条件id>=5的有4行,便会执行4次sleep(1)
假设符合条件或不符合条件的数据行数过多时,那么便会休眠太久会影响数据库的运行;
因此在进行时间型盲注时,需要注意的时尽量不要直接使用sleep函数。
可以使用子查询的方式来执行睡眠。
SELECT * FROM `bsrc` WHERE `id` >= 5 AND (SELECT 1 FROM (SELECT sleep(1))a);
括号内的语句优先级高,会先执行高优先级的语句然后把这个语句替换会执行后的结果。
因此不管前面的条件有多少符合要求的,该语句只会睡眠1秒。

短路原则

SELECT * FROM `bsrc` WHERE 条件1 and 条件2
当条件1查询出info数据表中的数据是0行时,即没有任何符合条件1的数据时,条件2将不会执行。
SELECT * FROM `bsrc` WHERE 条件1 or 条件2
当条件1能查询出info数据表中的所有数据时,即所有数据均符合条件1时,条件2将不会执行。
假设输入点在下面这个位置
SELECT * FROM `bsrc` WHERE `id` = {插入点};
假设id=1的数据是不存在的,那么当你在页面输入
index.php?id=1 and sleep(1)
或者输入
index.php?id=1 and sleep(0)
这两种输入的现象将会一模一样,因为and后面的语句MySQL将不会再执行。
因此在构造SQLi的Payload时要考虑短路原则,根据情景选择逻辑运算符或者不使用运算符。

结语

遇到一个问题解决一个,好似永远的无底洞,不如去理解它的所有构造;去思考这些东西怎么被创造的,然后再学会创造,破坏、发现Bug就非常容易了。
TEag1e@米斯特的精彩分享,知识因分享而更具价值,欢迎大家惠赐作品~
*SQL注入漏洞专项活动正在进行中,点击阅读原文查看活动详情~

百度安全应急响应中心


百度安全应急响应中心,简称BSRC,是百度致力于维护互联网健康生态环境,保障百度产品和业务线的信息安全,促进安全专家的合作与交流,而建立的漏洞收集以及应急响应平台。地址:https://bsrc.baidu.com

长按关注

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接