JAVA代码审计之WebGoat靶场SQL注入

Posted Tr0e

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA代码审计之WebGoat靶场SQL注入相关的知识,希望对你有一定的参考价值。

前言

为了从自己相对熟悉的 JAVA 语言开始着手学习代码审计(渗透工程师的必经之路啊……),本文利用 WebGoat 源码在本地 IDEA 搭建 WebGoat 站点并进行漏洞的审计分析。WebGoat8 是基于 Spring boot 框架开发的、故意不安全的 Web 应用程序,旨在教授 Web 应用程序安全性课程,该程序演示了常见的服务器端应用程序缺陷。

WebGoat

实际上选择 WebGoat 进行源代码审计学习的主要原因是很多开源靶场都是 php 语言的(比如 DVWA),而 WebGoat 是为数不多的 JAVA 语言开发且基于SpringBoot 主流框架的靶场。

IDEA部署靶场

搭建 WebGoat 环境需要的条件:

  1. Java 11;
  2. Maven > 3.2.1;
  3. IDEA 2021;
  4. Github 下载源码:https://github.com/WebGoat/WebGoat(本文选取 8.0.0.M25 版本)。

完成以上准备工作后,本地打开 IDEA 并导入 maven 项目,自动 build 完成之后,run 运行 StartWebGoat.java 的主函数开始启动项目:
然后浏览器打开http://localhost:8080/WebGoat,注册账户并登录即可:

No.1 回显注入

先来看看此题要求:
该题的主要目的是让我们使用 union 查询注入、堆叠查询注入两种方式来获得 Dave 这个账号的密码,同时已知密码存在于 user_system_data 表中、列名为 password。

1、输入测试语句,将回显后台 SQL 查询语句:

2、故使用 Payload:' or 1=1--+可成功查询出第一个表格 user_data 的所有数据:

从上面返回的结果的已知结果已经给出的表结构,我们可知道第一个表格 user_data 的列数是7。但是发现 Dave 的名字并不在该表格的名单里(密码更不在这里面),所以需要查询另一张表格。

解法1:堆叠注入

利用堆叠注入,注入 Payload: '; select * from user_system_data --+,可成功得到的第二张表格 user_system_data 的数据,获得 Dave 的密码信息(passW0rD):

解法2:联合查询

从上面已经知道,第一个表格有 7 列,而表 user_system_data 只有4列,所以需要将从 user_system_data 表中查到的数据补齐到 7 列才能进行联合查询,故注入 Payload:' or 1=1 union select userid,user_name,password,cookie,null,null,null from user_system_data --+,如下图所示:
以上可以看到得到是上面 2 个表的数据之和(union),同样可获得 Dave 的密码为 passW0rD 。

源码审计

下面对上面的漏洞环境进行源码审计分析。

1、抓包确认路由/SqlInjection/attack6a

2、到 IDEA 进行路由搜索/SqlInjection/attack6a,定位到SqlInjectionLesson6a.java文件:
3、审计源码可以看到,Post 请求数据包中的userid_6a参数对应的 SQL 查询语句中对应的 accountName 字段直接拼接进 SQL 语句进行查询,显然存在 SQL 注入漏洞:

No.2 布尔盲注

先来看看题目:
存在两个功能:登录和注册。题目提示尝试使用 Tom 账户进行登录,故猜测是利用 SQL 注入获取 Tom 的账户密码,从而完成该题。

布尔盲注

1、先注册一个账户 admin:
数据包如下:
2、重放观察注册请求包,发现提示 admin 账户已存在:
3、进一步测试发现注册功能的用户名username_reg存在布尔盲注,输入admin' and '1'='1(条件为真)提示用户已存在,而输入admin' and '1'='2(条件为假)则提示用户创建成功:
利用 SQLMap 也可判定存在 SQL 注入:
4、根据布尔盲注可获得 Tom 账户的密码为 thisisasecretfortomonly:
因本文主要是学习代码审计,故此处不深究手工注入过程,有兴趣可参阅:WebGoat SQL盲注 解题思路

源码审计

1、同样在 IDEA 中根据请求包的路由定位到登录功能的源码文件为SqlInjectionChallengeLogin.java,发现已采用了预编译 PrepareStatement 实现了数据代码分离,不存在 SQL 注入:
2、而审计注册功能的源码SqlInjectionChallenge.java ,可以直观发现 username_reg 参数直接拼接进入 SQL 语句并执行,明显存在 SQL 注入漏洞:

No.3 Order by

先来看看题目:
题目的意思就是借助 order by 注入猜解一个完整的主机 IP(已知主机名为 webgoat-prd 且部分 IP 为 XXX.130.219.202),其实上一页已经做了相关提示:可利用 case when 语法来达到 SQL 注入,需要利用的是when (true)中的真假判断,具体语法:

select * from users order by (case when (true) then lastname else firstname end)

Order by注入

1、点击上面第一张图里面箭头指出的主机排序按钮,获得数据包如下:
2、将 column=hostname 修改为 column=id,可根据 id 对查询结果进行重排列:
3、构造以下两个 Payload 进行 order by 注入的测试:

?column=(case when (true) then id else hostname end)
?column=(case when (flase) then id else hostname end)

如果存在 SQL 注入,那么当 when 中的条件为 true,将按 id 排序,否则按hostname 排序。如下图的测试结果可判定存在 SQL 注入:
4、进一步借助以下 Payload 可猜解主机名为 webgoat-prd 的 IP 首位是 1:

?column=(case when (substring((select ip from servers where hostname=’webgoat-prd’),1,1)=1) then id else hostname end)

如下图所示:
同理最终可获得目标主机的完整 IP 为 104.130.219.202。

源码审计

1、利用路由/SqlInjection/servers在 IDEA 中搜索该排序功能的源码文件,成功定位到Servers.java文件:

2、分析源码可以看到,服务端虽然使用了预编译但仍拼接了 order by 参数 column,导致存在 SQL 注入漏洞:

代审技巧

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

SQL注入挖掘

在上古时期,JAVA 程序员往往使用 JDBC 从数据库这么获取数据:

public User getUserById(String id) throws SQLException {
    Connection connection = JDBCTOOLS.getConnection();
	String sql = "select id,username from user where id=" + id;
	Statement statement = connection.createStatement();
    ResultSet resultSet = statement.executeQuery(sql);
    resultSet.next();
    int userId = resultSet.getInt(1);
    String username = resultSet.getString(2);
    User user = new User(userId, username);
    return user;
}

以上示例通过拼接字符串来构建 SQL 语句,而拼接的字符是用户可控的,这便不可避免地形成了 SQL 注入漏洞。后来,出现了预编译机制,可以有效避免多数场景下的 SQL 注入漏洞:

public User getUserById(String id) throws SQLException {
    Connection connection = JDBCTOOLS.getConnection(); //获取连接,细节省略
    String sql = "select id,username from user where id=?";
    PreparedStatement preparedStatement = connection.prepareStatement(sql); //预编译
    preparedStatement.setString(1, id); //绑定参数
    ResultSet resultSet = preparedStatement.executeQuery();
    resultSet.next();
    int userId = resultSet.getInt(1);
    String username = resultSet.getString(2);
    User user = new User(userId, username);
    return user;
}

但是预编译只能处理查询参数,很多场景下仅仅使用预编译是不够的,下面逐一进行分析。

SQL注入漏洞挖掘技巧

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

  1. 入参直接动态拼接;
  2. 预编译有误;
  3. 框架注入(Mybatis+Hibernate);
  4. order by 排序查询绕过预编译;
  5. like 模糊查询绕过预编译;
  6. SQL 注入过滤的绕过。

下面对上述 SQL 注入挖掘场景的分类进行逐一介绍:

1、参数直接拼接

最明显的可能存在注入的地方当然是 “+” 拼接,寻找思路一般有两种:通过关键字定位到 SQL 语句,回溯参数是否是用户可控;或通过跟踪用户输入,是否执行 SQL 操作。其中搜索的关键词有:

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 同样存在 SQL 注入,这也是实际审计中高发的问题,下面代码就是典型的预编译有误:

String query = "SELECT * FROM usersWHERE userid ='"+ userid + "'" + " AND password='" +password + "'";
PreparedStatement stmt =connection.prepareStatement(query);
ResultSet rs = stmt.executeQuery();

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

setObject()、setInt()、setString()、setSQLXML()

3、框架注入

Hibernate 典型的注入代码为:

session.createQuery("from Book wheretitle like '%" + userInput + "%' and published = true")

或形如:

StringBuffer queryString = newStringBuffer();
queryString.append(“from Test where id=’”);
queryString.append(id);
queryString.append(‘\\’’);

定位此框架的 SQL 注入首先需要在 xm l配置文件或 import 包里确认是否使用此框架,然后使用关键字 createQuery,session.save(,session.update(,session.delete进行定位。此处附上上面两种 Hibernate SQL 场景的正确写法:

场景一:
Query query=session.createQuery(from Useruser where user.name=:customername and user:customerage=:age ”);
query.setString(“customername”,name);
query.setInteger(“customerage”,age);
场景二:
String sql ="FROM User user where user.name=? and user.age=?";
Query q =session.createQuery(sql);
q.setString(0, name);
q.setInteger(1,age);

而 Mybatis 框架有两种变量引用方法:

不安全的方法安全的写法(JDBC预编译)
select * from books where id= ${id}select * from books where id= #{id}

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

4、order by绕过预编译

类似下面 SQL 语句 order by 后面是不能用预编译处理的,只能通过拼接处理:

String sort = req.getParameter("sort");
String sql = "select * from user order by ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql); //预编译
preparedStatement.setString(1, sort); //绑定参数
ResultSet resultSet = preparedStatement.executeQuery();

如果像上面这样强行使用预编译,数据库会将字段名解析为字符串,其实际执行的 SQL 语句将为:

select * from user order by 'username';

这将导致程序无法达到实际需求,所以对 order by 后面的参数只能进行拼接,并单独对其进行过滤(实际漏洞演示案例详见下面 WebGoat 案例3)。

5、like 绕过预编译

在使用 like 关键词进行模糊查询(SQL语句中拼接的变量会使用%_)的场景中,以下写法也是无法进行预编译的,程序会报错:

String search = req.getParameter("search");
String sql = "select * from user where username like '%?%'";
PreparedStatement preparedStatement = connection.prepareStatement(sql); //预编译
preparedStatement.setString(1, search); //绑定参数
ResultSet resultSet = preparedStatement.executeQuery();

同上,此类场景的参数只能进行拼接,并单独对其进行过滤。

6、SQL 过滤绕过

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

SQL注入防御

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

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

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 方面的效果是相同的。

String custname =request.getParameter("customerName");
try {
    CallableStatement cs = connection.prepareCall("{callsp_getAccountBalance(?)}");
    cs.setString(1, custname);
    ResultSet results = cs.executeQuery();     
} catch (SQLException se) {          
}

3、黑/白名单验证

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

public String someMethod(boolean sortOrder) {
    String SQLquery = "someSQL ... order by Salary " + (sortOrder ? "ASC" :"DESC");`
}

针对 order by 的场景也可进行如下白名单过滤:

private String checkSort(String sortBy) {
    List<String> columns = new ArrayList<>(Arrays.asList("id", "username"));
    return (columns.contains(sortBy)) ? sortBy : "''";
}

而对于模糊查询可以使用以下方式处理:

//like
String sql = "select * from user where username like concat('%', ?, '%')";
//预编译

4、框架修复

(1)Hibernate 框架:

@Autowired CategoryDAO categoryDAO; //依赖注入
@RequestMapping("/hibernate")
public String hibernate(@RequestParam(name = "id") int id) {
  Category category = categoryDAO.getOne(id);
  return category.getName();
}

Hibernate 即我们经常使用的 ORM 的一种实现,如果使用已封装好的方法,那么默认是使用预编译的。但是需要注意的有这么几种情况:对于一些复杂的SQL 语句,需要开发手写 SQL,此时要严格过滤用户输入;上面提到的预编译不生效的几种场景。

(2)对于 mybatis 框架有两种写法,一种是基于注解:

@Mapper
public interface CategoryMapper {
    @Select("select * from category_ where name= #{name} ")
    public CategoryM getByName(String name);
}

另一种是基于xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.seaii.springboot.mapper.CategoryMapper">
    <select id="get" resultType="cn.seaii.springboot.pojo.CategoryM">
        select * from category_ where id= ${id}
    </select>
</mapper>

Fortify体验

Fortify 全名叫:Fortify SCA ,是一个静态的、白盒的软件源代码安全测试工具,支持 JAVA、PHP、ASP、html等21种编程语言。它通过内置的五大主要分析引擎:数据流、语义、结构、控制流、配置流等对应用软件的源代码进行静态的分析,分析的过程中与它特有的软件安全漏洞规则集进行全面地匹配、查找,从而将源代码中存在的安全漏洞扫描出来,并给予整理报告。

为了感受下这款商业级别的代码审计神器的强大,尝试使用 Fortify 对 WebGoat 进行代码审计扫描,扫描的代码文件夹为 WebGoat 工程中包含各类漏洞源码的 webgoats-lessons 文件夹:

1、但是一开始看扫描结果似乎不太理想,查出的 5 个 “制命漏洞” 全是密钥硬编码、密码泄露问题,而对于 SQL 注入、XXE、SSRF等漏洞,此处一个都没扫描出来……

2、结果发现就是自己的查看方式不对!需要在 “Filter Set” 处勾选如下选项:

此时就能在 Low 级别的问题列表中看到 SQL 注入等漏洞信息(猜测是工具无法像“密钥硬编码”漏洞一样直接 100% 确定是存在相关漏洞,所以将一律其列入 Low 等级的风险列表里):

3、来重点关注下上面 WebGoat 演示的三处 SQL 注入是否被扫描出来了:


可以看到,Fortify 准确地识别了上面演示的三处 WebGoat 存在的 SQL 注入的源码的位置和存在的风险!

总结

本文通过 WebGoat 靶场对 SQL 注入进行黑盒测试、白盒审计,并进一步分析了代码审计中 SQL 注入的挖掘技巧。通过与 Fortify 的自动化代码审计扫描结果对比,也体现了人工代码审计的作用和意义!本文是学习代码审计的第一篇,后面还会持续更新学习!Keep Studying!

本文参考:

  1. 工具| WebGoat源码审计之SQL注入篇
  2. Webgoat白盒审计+漏洞测试
  3. Java代码审计-sqli
  4. Java代码审计汇总系列(一)——SQL注入

以上是关于JAVA代码审计之WebGoat靶场SQL注入的主要内容,如果未能解决你的问题,请参考以下文章

Java代码审计连载之—SQL注入

Java代码审计之SQL注入

1012.Web安全攻防靶场之WebGoat – 3

Java代码审计连载之—添油加醋

PHP 代码审计之死磕 SQL 注入

代码审计之SQL注入