vlambda博客
学习文章列表

SQL注入漏洞发现之旅

概述

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

易出现问题的场景

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

模糊查询

错误的写法
正确的写法,可见,确实比错误的写法多写很多个字符。
SQL注入漏洞发现之旅

批量处理

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

SQL注入漏洞发现之旅

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

隐藏参数

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

前端固定参数

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

HTTP Header头

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

时间参数

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

MySQL中各位置的测试

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

表名字段名

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

where子句

普通条件

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

SQL注入漏洞发现之旅

模糊查询

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

order by子句

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

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);
效果图
SQL注入漏洞发现之旅
limit子句前面没有使用 order by 子句时,可以在limit后面直接使用联合查询。
如果limit前面使用了 order by 子句时,那么便只能考虑前面的Payload了。
SQL注入漏洞发现之旅

其他情况

仅语法错误时
在真实情况中,在测试时发现很多时候只有语法错误时,页面才会跟语法正确时有所区别。
先看一下最简单的证明方法
语法正常,页面正常
  
    
    
  
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 '
再来看一下数据库中的执行效果
SQL注入漏洞发现之旅
在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);
SQL注入漏洞发现之旅
括号内的语句优先级高,会先执行高优先级的语句然后把这个语句替换会执行后的结果。
因此不管前面的条件有多少符合要求的,该语句只会睡眠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将不会再执行。
SQL注入漏洞发现之旅
因此在构造SQLi的Payload时要考虑短路原则,根据情景选择逻辑运算符或者不使用运算符。

结语

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

百度安全应急响应中心


长按关注