前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java代码审计汇总系列(一)——SQL注入

Java代码审计汇总系列(一)——SQL注入

作者头像
Jayway
发布2019-11-07 11:12:34
3.7K0
发布2019-11-07 11:12:34
举报
文章被收录于专栏:卓文见识

一、代码审计

相比黑盒渗透的漏洞挖掘方式,代码审计具有更高的可靠性和针对性,更多的是依靠对代码、架构的理解;使用的审计工具一般选择Eclipse或IDEA;审计工作过程主要有三步:风险点发现——>风险定位追踪——>漏洞利用,所以审计不出漏洞无非就是find:“找不到该看哪些代码”和judge:“定位到代码但判断不出有没有问题”。而风险点发现的重点则在于三个地方:用户输入(入参)处+检测绕过处+漏洞触发处,一般审计代码都是借助代码扫描工具(Fortify/Checkmarx)或从这三点着手。

本系列选取WebGoat作为案例,讲解漏洞的特征发现、定位技巧、调试及触发利用的具体过程,尽量涵盖所有的挖掘场景,最后补充实战挖掘案例。

二、SQLi漏洞挖掘

1、介绍

SQLi是最著名也是影响最广的漏洞之一,注入漏洞都是程序把用户输入的数据当做代码执行,发现的关键有两个,第一是用户能够控制输入;第二是用户输入的数据被拼接到要执行的代码中从而被执行。

2、挖掘过程

这里以webgoat的数字型注入讲解SQLi漏洞的挖掘过程:

1) 定位特定功能模块的代码

了解不同框架特性,本系统的Springboot注解:

@RequestMapping(path= PATH)

@GetMapping(path= PATH)

@PostMapping(path= PATH)

通过抓取请求数据包获取path特征SqlInjection/assignment5b:

使用IDEA的全局搜索功能(SHIFT+CTRL+F)定位到代码:

2) 代码分析

SqlInjectionLesson5b.java类代码如下:

代码语言:javascript
复制
@PostMapping("/SqlInjection/assignment5b")
@ResponseBody
public AttackResult completed(@RequestParam String userid, @RequestParam String login_count, HttpServletRequest request) throws IOException {
  return injectableQuery(login_count, userid);
}
protected AttackResult injectableQuery(String login_count, String accountName) {
  String queryString = "SELECT * From user_data WHERE Login_Count = ? and userid= " + accountName;
  try {
    Connection connection = DatabaseUtilities.getConnection(getWebSession());
    PreparedStatement query = connection.prepareStatement(queryString, ResultSet.TYPE_SCROLL_INSENSITIVE,
            ResultSet.CONCUR_READ_ONLY);

这是一个典型的动态拼接用户输入和防范案例,系统接收两个参数login_count和userid,其中login_count通过“+”直接拼接,而userid首先通过类型转换为Integer赋值给count,并经过了预编译(参数化请求)处理,不存在SQLi漏洞。

3)漏洞验证

最后构造路径及参数POC验证漏洞存在:

3、漏洞分类挖掘技巧

根据挖掘经验,白盒挖掘层面大致可以将SQLi的类型分为六类:

1、入参直接动态拼接;

2、预编译有误;

3、框架注入(Mybatis+Hibernate);

4、order by 绕过预编译;

5、%和_绕过预编译;

6、SQLi检测绕过

1) 参数直接拼接

最明显的“+”拼接,思路一般有二:通过关键字定位到SQL语句,回溯参数是否是用户可控;或通过跟踪用户输入,是否执行SQL操作,搜索的关键词有:

代码语言:javascript
复制
Select|insert|update|delete|java.sql.Connection|Statement|.execute|.executeQuery|jdbcTemplate|queryForInt|queryForObject|queryForMap|getConnection|PreparedStatement|Statement|execute|jdbcTemplate|queryForInt|queryForObject|queryForMap|executeQuery|getConnection

2) 预编译有误

并不是使用了预编译PreparedStatement一定就可以防止SQL注入,动态拼接SQL同样存在SQLi注入,这也是实际审计中高发的问题,下面代码就是典型的预编译有误:

代码语言:javascript
复制
String query = "SELECT * FROM usersWHERE userid ='"+ userid + "'" + " AND password='" +password + "'";
PreparedStatement stmt =connection.prepareStatement(query);
ResultSet rs = stmt.executeQuery();

定位预编译可以通过搜索关键函数:

代码语言:javascript
复制
setObject()、setInt()、setString()、setSQLXML()

3) 框架注入

Hibernate典型的注入代码为:

代码语言:javascript
复制
session.createQuery("from Book wheretitle like '%" + userInput + "%' and published = true")

或形如:

代码语言:javascript
复制
{
StringBuffer queryString = newStringBuffer();
queryString.append(“from Test where id=’”);
queryString.append(id);
queryString.append(‘\’’);
}

定位此框架的SQL注入首先需要在xml配置文件或import包里确认是否使用此框架,然后使用关键字createQuery,session.save(,session.update(,session.delete进行定位。

Mybatis有两种变量方法,不安全的写法为:

代码语言:javascript
复制
select * from books where id= ${id}

安全的写法为JDBC预编译:

代码语言:javascript
复制
select * from books where id= #{id}

此外like、in和order by语句也需要使用#,挖掘技巧则是在注解中或者Mybatis相关的配置文件中搜索 $。

4) order by 绕过预编译

类似下面sql语句 order by 后面是不能用预编译处理的只能通过拼接处理,只能手动进行过滤,详见案例。

代码语言:javascript
复制
String sql = “Select * from news where title =?”+ “order by‘” + time + “’asc”

5) %和_绕过预编译

预编译是不能处理%,需要手动过滤,否则会造成慢查询和DOS。

6) SQLi检测绕过

若SQL在处理过程中经过黑/白名单(正则)或Filter检测,通常检测代码存在缺陷则可进行检测绕过。

4、漏洞防御

OWASP官方推荐的SQLi防御方案有四种:

1)预编译(参数化查询)

代码语言:javascript
复制
PreparedStatement stmt =connection.prepareStatement("SELECT * FROM users WHERE userid=? ANDpassword=?");
stmt.setString(1, userid);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();

2)存储过程

使用CallableStatement对存储过程接口的实现来执行数据库查询,SQL代码定义并存储在数据库本身中,然后从应用程序中调用,使用存储过程和预编译在防SQLi方面的效果是相同的。

代码语言:javascript
复制
String custname =request.getParameter("customerName");
try {
 CallableStatement cs = connection.prepareCall("{callsp_getAccountBalance(?)}");
 cs.setString(1, custname);
 ResultSet results = cs.executeQuery();     
} catch (SQLException se) {          
}

3)黑/白名单验证

属于输入验证的范畴,大多使用正则表达式限制,或对于诸如排序顺序之类的简单操作,最好将用户提供的输入转换为布尔值,然后将该布尔值用于选择要附加到查询的安全值。

代码语言:javascript
复制
public String someMethod(boolean sortOrder) {
String SQLquery = "someSQL ... order by Salary " + (sortOrder ? "ASC" :"DESC");`

4) 输出转义

将用户输入放入查询之前对其进行转义,OWASP企业安全性API(ESAPI)是一个免费的开源Web应用程序安全控制库。

代码语言:javascript
复制
CodecORACLE_CODEC = new OracleCodec();
Stringquery = "SELECT user_id FROM user_data WHERE user_name = '"
+ESAPI.encoder().encodeForSQL( ORACLE_CODEC,req.getParameter("userID"))
+ "'and user_password = '"
+ ESAPI.encoder().encodeForSQL( ORACLE_CODEC,req.getParameter("pwd")) +"'";

5)框架修复:

对于Mybatis框架:

代码语言:javascript
复制
select * from news where tile like concat(‘%’,#{title}, ‘%’),
代码语言:javascript
复制
select * from news where id in
<foreach collection="ids"item="item"open="("separator=","close=")">#{item}</foreach>

Mybatis的order by语句可以选择在java层做映射或过滤用户输入进行防御。

对于Hibernate(HQL)框架预编译:

方法一:

代码语言:javascript
复制
Query query=session.createQuery(“from Useruser where user.name=:customername and user:customerage=:age ”);
query.setString(“customername”,name);
query.setInteger(“customerage”,age);

方法二:

代码语言:javascript
复制
String hql ="FROM User user where user.name=? and user.age=?";
Query q =session.createQuery(hql);
q.setString(0, name);
q.setInteger(1,age);

5、实战案例

1) Mybatis框架

对文章删除功能进行审计,articelId参数前端可控:

代码语言:javascript
复制
@RequestMapping("/delete")
public ModelAndView delete(HttpServletRequestrequest) {
           ModelAndView model = newModelAndView();
           try {
                    model.setViewName(this.getRequestUri(request));
                    String[] aridArr = request.getParameterValues("articelId");
                    if (aridArr != null&& aridArr.length > 0) {
                             this.deleteArticle(aridArr);
                    }
           } catch (Exception e) {
                    model.setViewName(this.setExceptionRequest(request,e));
                    logger.error("AdminArticleController.delete()--error",e);
           }
           return model;
}

顺着变量的走向进行审计,articelId赋值给aridArr,而后进行了为空的判断,不为空则执行deleteArticle操作,跟踪定位此函数:

代码语言:javascript
复制
private void deleteArticle(String[]artidArr) {
                   //删除数据中记录

                   articleService.deleteArticleByIds(artidArr);
                   EHCacheUtil.remove(CacheConstans.ARTICLE_GOOD_RECOMMEND);

继续跟踪操作,articleService类的deleteArticleByIds函数,继而进入DAO层,在ArticleDaoImpl.java内:

代码语言:javascript
复制
public void deleteArticleByIds(StringarticleIds) {
                   this.delete("ArticleMapper.deleteArticleByIds",articleIds);
         }

进入ArticleMapper.xml,最终追踪到deleteArticleByIds的SQL语句,使用了$拼接,典型的Mybatis注入:

代码语言:javascript
复制
<deleteid="deleteArticleByIds"parameterType="java.lang.String">
         DELETEFROM EDU_ARTICLE WHERE EDU_ARTICLE.ARTICLE_ID IN (${value})
         </delete>

除了顺向思维,还可以通过逆向思维挖掘,pom.xml中看到系统使用的是Mybatis框架,可以直接去审查Maper.xml文件,查看是否使用$拼接:

代码语言:javascript
复制
<properties>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
       <spring.version>3.2.12.RELEASE</spring.version>
       <mybatis.version>3.2.7</mybatis.version>
       <aspectj.version>1.7.3</aspectj.version>
       <jdk.version>1.7</jdk.version>
</properties>

2) Order by绕过预编译

Webgoat一个order by的案例:

order by的参数orderExpression可以是一个selectExpression也可以是一个函数,比如使用一个case语句。

案例中可以根据IP或ID对数据进行排序:

对应代码为Server.java:

代码语言:javascript
复制
@GetMapping(produces =MediaType.APPLICATION_JSON_VALUE)
   @SneakyThrows
   @ResponseBody
    public List<Server> sort(@RequestParamString column) {
       Connection connection = DatabaseUtilities.getConnection(webSession);
       PreparedStatement preparedStatement =connection.prepareStatement("select id, hostname, ip, mac, status, descriptionfrom servers  where status <> 'outof order' order by " + column);
       ResultSet rs = preparedStatement.executeQuery();
       List<Server> servers = Lists.newArrayList();
       while (rs.next()) {
           Server server = new Server(rs.getString(1), rs.getString(2),rs.getString(3), rs.getString(4), rs.getString(5), rs.getString(6));
           servers.add(server);
       }

虽使用了预编译但仍拼接了order by参数column,使用case探测语句探测:(case when (true) then id else ip end),如果真则以id排序,结果为:

3) 预编译有误

查看FAQ页面数据功能getFaqPage函数,前端获取page等参数:

代码语言:javascript
复制
publicFaqPageInfo getFaqPage(String tenantId, Map<String, String> conditions,int page, int pageSize, String like) {
        try {
        tenantId= WebUtil.getLoginTenantId();
            FaqPageInfo fpi =FAQ_IO_SERVICE.getPageInfo(tenantId, conditions, page, pageSize, like);
            for (FaqModel fm : fpi.getData()) {
               fm.setWhitelistIds(WHITELIST_SERVICE.getWhiteListOnFaq(tenantId,fm.getId()));
            }
            return fpi;
        }

跟踪FAQ_IO_SERVICE.getPageInfo,调用FaqSqlAccess.queryByPage进行处理:

代码语言:javascript
复制
public FaqPageInfo getPageInfo(StringtenantId, Map<String, String> conditions, int page, int pageSize, Stringlike)
           throws SQLException {
       FaqPageInfo pageInfo = new FaqPageInfo();
       pageInfo.setData(FaqSqlAccess.queryByPage(tenantId, conditions, page,pageSize, like));
       pageInfo.setTotalSize(FaqSqlAccess.queryCount(tenantId, conditions,like));
       return pageInfo;
}

找到FaqSqlAccess.java里的queryByPage方法,追踪到SQL语句:

代码语言:javascript
复制
public static List<FaqModel>queryByPage(String tenantId, Map<String, String> conditions, int page,int pageSize, String like)
           throws SQLException {
       
       List<FaqModel> models = new ArrayList<>();
       String language = conditions.get("language");
       
       Connection connection = null;
       PreparedStatement stmt = null;
       try {
           connection = MysqlUtils.getConnection();
           String sql = "select id, name, description, language, update_time,is_on from TOC_FAQ where tenant_id=?";
           if (!CommonUtils.isEmptyStr(language))
                sql += " andlanguage='" + language + "'";
           if (!CommonUtils.isEmptyStr(like))
                sql += " and name like'%" + like + "%'";
           sql += " order by update_time desc limit ?,?";
           stmt = connection.prepareStatement(sql);
           stmt.setString(1, tenantId);
           stmt.setInt(2, (page - 1) * pageSize);
           stmt.setInt(3, pageSize);
           ResultSet resultSet = stmt.executeQuery();

发现此处使用了预编译,但language和like参数实际是直接拼接,存在SQL注入,对于getFaqPage功能构造参数"language":"'-if(substring(user(),1,1)=0x01,sleep(5),0)-'"}进行验证。

简单或复杂的SQL注入漏洞原理和审计方法相同,只是对于业务繁杂的系统,数据的走向和处理过程会比较复杂,调用链跟踪难度会稍大一些,需要更多耐心。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-10-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 卓文见识 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、代码审计
  • 二、SQLi漏洞挖掘
  • 1、介绍
    • 3、漏洞分类挖掘技巧
      • 5、实战案例
      相关产品与服务
      代码审计
      代码审计(Code Audit,CA)提供通过自动化分析工具和人工审查的组合审计方式,对程序源代码逐条进行检查、分析,发现其中的错误信息、安全隐患和规范性缺陷问题,以及由这些问题引发的安全漏洞,提供代码修订措施和建议。支持脚本类语言源码以及有内存控制类源码。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档