代码审计渗透测试(一) SQL注入

做为一枚纯正的甲方安全人员,代码审计是必备技能点。在实际工作中,代码审计无论是在安全测试中所占的投入比重,亦或是在发现产品/项目安全漏洞中的比例,都比黑盒渗透要大的多。因此,博主特意开辟了该代码审计系列博文,结合了博主在某厂实际工作中的工作经历和厂内大牛前辈们的经验,稍加提炼而成。
在正式开始专项之前,先简单介绍下“代码审计渗透测试”这个自创的新词:
一般来讲,Web安全测试主要分为黑盒测试白盒测试:
黑盒测试是指测试人员不清楚Web具体的架构和实现,通过模拟一个或多个攻击角色进行渗透测试,发现潜在的安全漏洞和风险,它不会泄露系统源码。
白盒测试则需要开发人员提供相关的设计文档、源码、使用手册、业务逻辑等信息,对源代码直接进行安全分析,来寻找安全漏洞和可能存在的风险,它主要用于公司内部进行深度安全测试。代码审计是白盒测试常用的一种方法。
代码审计渗透测试则是将黑盒渗透测试的思路用于代码审计的一种方法,博主将从漏洞介绍、漏洞原理、测试思路和测试方法四个方面来阐述TOP N的Web安全漏洞的代码审计渗透测试。

漏洞介绍

SQL注入漏洞是Web常见的漏洞,通过此漏洞可能用来拖库、获取Web管理员账号和密码、权限提升等攻击,是Web安全中最严重的漏洞之一。

注意:本章节仅讨论关系型数据库,不适用非关系型数据库。

漏洞原理

SQL注入(SQL Injection),就是通过把SQL命令插入到Web表单提交页面请求的字符串中,最终达到欺骗数据库服务器执行恶意的SQL命令,从而达到攻击的目的。
用户输入的内容,传递到服务端后没有做有效的过滤,后端拿到用户输入的不安全的内容后当做SQL命令直接执行,从而改变了原来的业务的SQL运行流程或者在执行了原有的SQL语句后,附加执行了用户的恶意SQL语句,导致数据库信息泄露或者权限提升。

举个栗子:
username:
password:
前台需要输入用户名和密码。
后台的处理代码如下:

1
2
3
username = getRequestName("UserName");
password = getRequestPassword("UserPasswd");
sql = "SELECT * FROM USER WHERE name = '" + username +"' AND passwd = '" + password + "'";

这里后台拿到前端传参后没有做任何处理,直接拼接了SQL语句,这就很容易被SQL注入了:
username: admin’ or ‘1’ = ‘1
password: anything
这时候,后台构造的语句即为:

1
sql = "SELECT * FROM USER WHERE name = 'admin' or '1' = '1' AND passwd = 'anything';

来分析这个SQL语句,你会发现,不管密码输入什么(anything),这条语句总会返回true,因为where xx or ‘1’ = ‘1’ 是恒成立的。因此用户不需要知道用户名和密码就能够成功登录到系统。

从上面例子可以看出,用户可以构造出改变原本程序的运行逻辑的恶意代码,从而导致SQL注入。

更多SQL相关的知识详见:SQL_Injection

测试思路

渗透测试前,先要与开发人员进行沟通确认,找到存在用户输入并且需要操作数据库的地方,再去看是否进行了防SQL注入的安全机制,该安全机制是否能被绕过等。常见的比如使用预编译:

PHP:可以使用mysqliPDO实现预编译
Java:可以使用preparestatement预编译

在SQL注入的测试中,仅仅通过黑盒渗透是远远不够的,原因如下:

  • AppScan/AWVS无法获取到所有的业务报文,Burp可以获取较多的报文,但是注入用例太少,不能进行全面的覆盖,因此导致很多工具都没法进行全面的SQL注入渗透测试
  • 渗透测试工具没法进行一些需要特定条件才能注入成功的操作,或者是正常场景不会出现的情况,容易产生遗漏

因此针对SQL注入,强烈推荐使用本文源码分析为主,黑盒测试为辅。源码分析遵从以下规则:

  • 未使用预编译的,必须换为预编译,如果存在不能预编的场景,需要单独拿出来进行安全分析;
  • 使用预编译的,必须使用占位符,不能针对sql拼接。

黑盒测试

在测试前,通过与开交流或者源码源码分析,了解Web中使用的所有数据库名称及版本号,包括关系型数据库(MySQL、Oracle、SQLite等),然后针对性测试,减少测试工作量。黑盒测试思路如下:

1)Web漏洞扫描
利用Web漏洞扫描工具如AppScan、AWVS、Burp自动扫描,可以发现部分注入SQL注入漏洞,针对发现的漏洞“点”,在源码分析漏洞产生的原因,然后全面排查类似问题。

2)手工SQL注入
在Web界面中找到可能存在数据库操作的点,例如用户用户、数据查询、添加用户等场景,然后尝试手工测试,来发现比较隐蔽,以及工具难以覆盖的SQL注入漏洞。

源码分析

通过分析源码,识别可能被绕过的SQL注入场景,例如绕过正则、关键字过滤、编码等,常见的SQL注入根因如下:

  • 直接拼接SQL语句,导致存在SQL注入
  • 不正确使用预编译导致存在SQL注入
  • 通过拼接,有过滤但过滤不全能被绕过导致SQL注入
  • 第三方框架导致SQL注入(如Hibernate、iBatis……)
  • 其它原因导致存在的SQL注入

测试方法

Web漏洞扫描在测试工具中有详细介绍,因此本章节测试方法重点介绍手工SQL注入和Web源码分析SQL注入。

非预编译导致的SQL注入

大部分SQL注入漏洞的根因都是因为没有使用预编译,使用SQL语句拼接,导致用户构造的恶意语句改变了原有的SQL流程。测试方法如下:
1)通过手动搜索关键字分析SQL拼接场景是否存在漏洞,关键字如下:
PHP关键字:querymysql_querymysql_fetch_array ……
Java关键字:Statement.execute.executeQueryjdbcTemplatequeryForIntqueryForObjectqueryForMapgetConnection
2)分析SQL执行的场景是否使用了预编译,以java为例,可以通过搜索关键字queryForInt搜索到的代码假设如下:

1
2
3
4
5
6
7
8
9
10
private static String STR_QUERY_FILE = "SELECT COUNT(1) FROM TBL_XXX WHERE XXX_FILENAME = '%s'";
public boolen isXXXFileUsed(String strFileName){
String strSql = String.format(STR_QUERY_FILE, strFileName);
if(Constant.NUMBER_ZERO < getSimpleJdbcTemplate().queryForInt(strSql)){
......
}
......
return flase;
}

STR_QUERY_FILE为SQL语句查询字符串,strFileName为客户端传参,也就是该参数由用户直接输入,即用户可控,这里并没有进行过滤,也没有进行预编译,直接使用format进行字符串拼接后执行SQL,因此此处是存在SQL注入漏洞的。
漏洞利用Exploit如下:

1
a.dat; delete from user;

错误的预编译导致的SQL注入

有时候开发人员虽然使用了预编译,但是错误的预编译仍然是存在SQL漏洞的,因此代码审计时需要分析PreparedStatement时是否使用占用符,测试方法如下:
1)通过搜索关键字,先找到使用预编译的场景,关键字如下:
PHP关键字:prepareexecute
Java关键字:preparedStatement
2)分析代码是否存在漏洞,预编译之前的参数绑定应该使用占位符,不应该直接使用拼接。例如:

1
2
3
4
5
6
7
8
strSql = "SELECT uu_name from table_appmanage ";
if(uu_key != null){
strSql.append("where key = '" + uu_k + "'");
}
sql = strSql.toString();
ps = conn.preparedStatement(sql);
re = ps.executeQuery();
.....

这里虽然使用了preparedStatement预编译,但是在预编译之前,append拼接参数时已经被污染,所以这里也是无法防御SQL注入的。正确的方法应该如下:

1
2
3
4
strSql = "SELECT uu_name from table_appmanage where key = ? ");
PreparedStatement ps = conn.preparedStatement(sreSql);
ps.setString(1, uu_key);
re = ps.executeQuery();

过滤不完整导致SQL注入

在有些场景没办法使用预编译。比如建表的SQL语句没法使用参数绑定+预编译来放回SQL注入,这个时候就需要进行SQL动态拼接,而防止SQL注入就需要进行特殊字符的过滤,一般大家都喜欢使用黑名单,而黑名单却是最容易绕过,无法达到防护的目的。这种情况的测试方法如下:
1)在源码中搜索如下关键字,找到相关的代码:
PHP关键字:querymysql_querymysql_fetch_array ······
Java关键字:statementexecutejdbcTemplatequeryForIntqueryForObjectqueryForMapexecuteQuerygetConnection ······
2)分析测试是否存在绕过的风险,例如(PHP):

1
2
3
4
5
6
7
8
9
$sid = strtolower($_REQUEST[id]);
$key1 = array("=", "order", "or", "xor", ">", "<", "null");
$id = str_replace($key1,"", $sid);
$conn = mysql_connect($servername, $dbusername, $dbpassword) or die("connect db failed");
mysql_select_db($dbname, $conn);
mysql_query('set names utf8');
$sql = "select * from article where articleid = $id";
$result = mysql_query($sql, $conn);
$row = mysql_fetch_array($result);

这里使用的是MYSQL数据库,没有使用参数绑定加预编译的形式,id参数来源于客户端请求,开发人员意识到存在SQL注入安全风险,进行SQL执行前使用了str_replace进行了字符过滤,将$key1中的字符替换为空,实际上这样过滤远远不够,使用union就可以轻松绕过,漏洞利用可以参考:

1
/index.php?id=-1 union select 1,2,3

Hibernate导致的SQL注入(仅存在于java)

Hibernate是一个开源的java框架,它对JDBC进行了非常轻量级的对象封装,它可以生成SQL语句,自动执行,使得java程序员可以自由的使用OOP思想来操纵数据库,因此深受欢迎。
Hibernate本身支持预编译,但是如果使用动态拼接,则也会存在SQL注入风险,测试方法如下:
1)判断web中是否使用了Hibernate,搜索如下关键字:
Hibernate关键字:org.hibernate
2)分析是否为SQL拼接,搜索一下关键字分析相关代码是否存在拼接:
关键字:.createQuery
例如:

1
2
3
String username = request.getParameter("username");
String str = "from user_table where name = " + username;
Query q = session.createQuery(str);

代码中通过Hibernate来查询数据库,username为外部输入参数,然后与sql动态拼接,因此存在SQL注入漏洞。

安全的使用方法应该是使用占位符进行参数绑定,再使用预编译

1
2
3
4
String username = request.getParameter("username");
String str = "from user_table where name = ?";
Query q = session.createQuery(str);
q.setString(0, username);

iBatis/myBatis导致的SQL注入(仅存在于java)

iBatis(后被谷歌托管,改名为myBatis)是用于使用方便的数据访问工具,也主要作为数据持久层,与ORM(Hibernate)类似。i/myBatis中#是占位符,$是字符串拼接,所以尽量使用#可以避免SQL注入,而$则会存在风险,测试方法如下:
1)判断web中是否使用了iBatis/myBatis,在源码中搜索以下关键字,如果存在说明项目中使用了iBatis/myBatis,关键字如下:
iBatis关键字:import com.ibatis
myBatis关键字:import org.mybatis
2)判断SQL调用时,是否使用了$拼接。例如:

1
2
3
<select id="testSQL" parameterClass="com.it.users" resultClass="users">
SELECT * FROM users WHERE username = '$username$'
</select>

这里使用的就是$username$,进行了SQL拼接,username被用户恶意输入则造成了SQL漏洞。
正确的使用应该是:

1
2
3
<select id="testSQL" parameterClass="com.it.users" resultClass="users">
SELECT * FROM users WHERE username = #username#
</select>

其他原因导致的SQL注入

PHP的SQL注入更加灵活复杂,也存在上面类似的ORM框架,同理也就可能存在SQL注入风险,但是往往结果框架的包装后,就很难轻易的识别出这些安全风险,给白盒渗透测试带来困难。除了上述因素外,还有其他场景和因素导致SQL注入:
1)字符编码导致SQL宽字节注入
2)不正确的使用安全函数导致的SQL注入,尤其是PHP弱语言最容易发生
3)代码逻辑导致的SQL注入
尤其是代码逻辑,这个需要深入熟悉业务,也需要有较强的逻辑思维能力。

PS:
1)产品安全测试过程中测试量大,参数多,因此最高速有效的测试方法就是基于正常报文添加单双引号,然后查看数据库日志。当然如果你非常熟悉手工注入也可以直接构造出SQL来证明是存在漏洞的。
2)在实际排查的过程中,使用关键字搜索,大多数会得到很多结果,如果一个一个的去分析调用,参数是否可控,工作量相当大,并且很容易产生漏洞。这时,就需要开发人员先将能使用预编译的地方使用参数绑定+预编译,然后再去详细分析不能使用预编译的地方进行定点分析,提高开发和测试效率。

文章作者:Cookia

原始链接:http://cookia.cc/2017/12/26/sql_injection/

许可协议: CC BY-NC-ND 4.0 转载请保留原文链接及作者。