JAVA代码审计之SQL注入代码审计

前言

SQL注入漏洞是对数据库进行的一种攻击方式。其主要形成方式是在数据交互中,前端数据通过后台在对数据库进行操作时,由于没有做好安全防护,导致攻击者将恶意代码拼接到请求参数中,被当做SQL语句的一部分进行执行,最终导致数据库被攻击。可以说所有可以涉及到数据库增删改查的系统功能点都有可能存在SQL注入漏洞。虽然现在针对SQL注入的防护层出不穷,但大多情况下由于开发人员的疏忽或特定的使用场景,还是会存在SQL注入漏洞的代码。

环境搭建

首先创建相关项目,源码这儿较多,后台私信即可获取演示案例源码

首先是根据提示,创建一个名为security的数据库,并想数据库中写入相关数据。这里我用的是phpStudy的mysql数据库

接着使用Navicate创建相关数据

DROP DATABASE IF EXISTS security;
CREATE DATABASE security;
USE security;

CREATE TABLE users (
    id INT(3) NOT NULL AUTO_INCREMENT,
    username VARCHAR(20) NOT NULL,
    password VARCHAR(20) NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE emails (
    id INT(3) NOT NULL AUTO_INCREMENT,
    email_id VARCHAR(30) NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE uagents (
    id INT(3) NOT NULL AUTO_INCREMENT,
    uagent VARCHAR(256) NOT NULL,
    ip_address VARCHAR(35) NOT NULL,
    username VARCHAR(20) NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE referers (
    id INT(3) NOT NULL AUTO_INCREMENT,
    referer VARCHAR(256) NOT NULL,
    ip_address VARCHAR(35) NOT NULL,
    PRIMARY KEY (id)
);

INSERT INTO users (id, username, password) VALUES 
(1, 'Dumb', 'Dumb'), 
(2, 'Angelina', 'I-kill-you'), 
(3, 'Dummy', 'p@ssword'), 
(4, 'secure', 'crappy'), 
(5, 'stupid', 'stupidity'), 
(6, 'superman', 'genious'), 
(7, 'batman', 'mob!le'), 
(8, 'admin', 'admin');

INSERT INTO emails (id, email_id) VALUES 
(1, 'Dumb@dhakkan.com'), 
(2, 'Angel@iloveu.com'), 
(3, 'Dummy@dhakkan.local'), 
(4, 'secure@dhakkan.local'), 
(5, 'stupid@dhakkan.local'), 
(6, 'superman@dhakkan.local'), 
(7, 'batman@dhakkan.local'), 
(8, 'admin@dhakkan.com');

接着连接数据库,这里数据库是本地的,可以直接连接

然后启动即可。访问目标地址,搭建成功

http://127.0.0.1:7089/sqli/jdbc/dynamic?id=2

漏洞分析

jdbc中的SQL注入

动态拼接

SQL语句动态拼接导致的SQL注入漏洞是先前最为常见的场景。其主要原因是后端代码将前端获取的参数动态直接拼接到SQL语句中使用java.sql.Statement执行SQL语句从而导致SQL注入漏洞的出现。

两个关键点如下:动态拼接参数、使用java.sql.Statement执行SQL语句

Statement:对象用于执行一条静态的 SQL 语句并获取它的结果。
createStatement():创建一个 Statement 对象,之后可使用executeQuery()方法执行SQL语句。
executeQuery(String sql)方法:执行指定的 SQL 语句,返回单个 ResultSet 对象。	

演示案例如下:

我们来到JdbcDynamicContriller下,相关代码已经做了详细注释,这里不一一讲解。

我们主要观察以下代码:

// 创建Statement对象用于执行SQL查询
Statement statement = conn.createStatement();
//动态拼接字符串
String sql = "select * from users where id = '" + id + "'";
// 执行SQL查询并返回结果集
ResultSet rs = statement.executeQuery(sql);

id是通过请求参数传入来获取的,这里直接进行了拼接,并且是通过创建Statement对象用于执行SQL查询,executeQuery执行查询语句,这里就存在sql注入。实际演示看看。

访问以下地址可以看到正常显示

http://127.0.0.1:7089/sqli/jdbc/dynamic?id=2

后面加一个单引号,可以看到报错了

那再加一个,输入 2'' 试试呢

可以看到正常显示了,为什么会这样子呢?回到源码来进行分析一下,这里修改源码返回的内容,将返回的result修改成sql语句。

接着访问源地址,可以看到完整的sql语句了

可以看到,造成输入2''还可以正常显示的原因是,字符串造成了拼接,我们输入的2'拼接在sql语句中,闭合了单引号,因而造成的sql注入,这里简单演示一下注入流程。我就简单查询一下数据库版本吧。

这里通过order by语句已知有三列,我们数据一下语句,查询数据库版本。

' union select 1,2,version()--+

但是存在一种情况,即使是拼接,也不会造成sql注入(目前还未发现绕过方法),那就是限制输入的类型为int,我们输入其他的就会报错。

正常输入,可以看到显示正常

但是一旦输入其他的,非数字的情况,就会报错。这种情况就基本不存在sql注入了。

错误的预编译

在动态拼接中是使用Statement执行SQL语句。如果使用PreparedStatement预编译参数化查询是能够有效防止SQL注入的。但如果没有正确的使用PreparedStatement预编译还是会存在SQL注入风险的。

PreparedStatement是继承Statement的子接口。

PreparedStatement会对SQL语句进行预编译,不论输入什么,经过预编译后全都以字符串来执行SQL语句。

PreparedStatement会先使用?作为占位符将SQL语句进行预编译,确定语句结构,再传入参数进行执行查询。如下述代码:

PreparedStatement是继承Statement的子接口。
PreparedStatement`会对SQL语句进行预编译,不论输入什么,经过预编译后全都以字符串来执行SQL语句。
PreparedStatement会先使用`?`作为占位符将SQL语句进行预编译,确定语句结构,再传入参数进行执行查询。如下述代码:

首先讲解正确的预编译,示例代码如下:

这里就不存在sql注入了(还未有绕过方法)

访问目标地址,发现数据无法进行注入

但是存在一种情况,就是虽然使用的是预编译,但是还是进行的拼接进行查询。由于开发人员疏忽或经验不足等原因,虽然使用了预编译PreparedStatement,但没有根据标准流程对参数进行标记,依旧使用了动态拼接SQL语句的方式,进而造成SQL注入漏洞。

示例代码如下

重点关注下述代码:

String sql = "select * from users where username = '" + username + "'";
PreparedStatement preparestatement = conn.prepareStatement(sql);
ResultSet rs = preparestatement.executeQuery();

可以看到这里用的拼接,所以存在漏洞。

' union select 1,2,version()'

Order by注入

在SQL语句中,order by语句用于对结果集进行排序。order by语句后面需要是字段名或者字段位置。在使用PreparedStatement预编译时,会将传递任意参数使用单引号包裹进而变为了字符串。如果使用预编译方式执行order by语句,设置的字段名会被数据库认为是字符串,而不在是字段名。因此,在使用order by时,就不能使用PreparedStatement预编译了。

示例代码如下:

这里我们使用时延注入,发现有明显的时间延迟。

Mybatis中的SQL注入

在Mybatis中拼接SQL语句有两种方式:一种是占位符#{},另一种是拼接符${}。

占位符#{}:对传入的参数进行预编译转义处理。类似 JDBC 中的PreparedStatement。

比如:select * from user where id = #{number},如果传入数值为1,最终会被解析成select * from user where id = "1"。

拼接符${}:对传入的参数不做处理,直接拼接,进而会造成SQL注入漏洞。

比如:比如:select * from user where id = ${number},如果传入数值为1,最终会被解析成select * from user where id = 1。

#{}可以有效防止SQL注入漏洞。${}则无法防止SQL注入漏洞。

因此在我们对JavaWeb整合Mybatis系统进行代码审计时,应着重审计SQL语句拼接的地方。除非开发人员的粗心对拼接语句使用了${}方式造成的SQL注入漏洞。在Mybatis中有几种场景是不能使用预编译方式的,比如:order by、in,like。

漏洞案例

示例代码如下:

该网站SQL是Mabatis类型的,sql语句一般写在Mapper中

通过关键词进行全局搜索,找到Dao层,以及Servers层

跟进代码

找到相关接口以及参数

直接拿到sqlmap中跑

python sqlmap.py -u http://127.0.0.1:7089/sqli/mybatis/orderby?sort=id --batch

漏洞出发点找到

修复方案

in注入

正确的做法是需要使用foreach配合占位符#{}实现IN查询。比如:

<!-- where in 查询场景 -->
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
    SELECT *
    FROM user
    WHERE name IN
    <foreach collection="names" item="name" open="(" close=")" separator=",">
      #{name}
    </foreach>
</select>
like注入

下面代码是正确的做法,可以防止SQL注入漏洞,如下。

SELECT * FROM users WHERE name like CONCAT("%", #{name}, "%")
表,字段名称

(Select, Order by, Group by 等)

// 插入数据用户可控时,应使用白名单处理
// example for order by

String orderBy = "{user input}";
String orderByField;
switch (orderBy) {
    case "name":
        orderByField = "name";break;
    case "age":
        orderByField = "age"; break;
    default:
        orderByField = "id";
}
JDBC
String name = "foo";

// 一般查询场景
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement pre = conn.prepareStatement(sql);
pre.setString(1, name);
ResultSet rs = pre.executeQuery();

// like 模糊查询场景
String sql = "SELECT * FROM users WHERE name like ?";
PreparedStatement pre = conn.prepareStatement(sql);
pre.setString(1, "%"+name+"%");
ResultSet rs = pre.executeQuery();

// where in 查询场景
String sql = "select * from user where id in (";
Integer[] ids = new Integer[]{1,2,3};

StringBuilder placeholderSql = new StringBuilder(sql);
for(int i=0,size=ids.length;i<size;i++) {
    placeholderSql.append("?");
    if (i != size-1) {
        placeholderSql.append(",");
    }
}
placeholderSql.append(")");

PreparedStatement pre = conn.prepareStatement(placeholderSql.toString());
for(int i=0,size=ids.length;i<size;i++) {
    pre.setInt(i+1, ids[i]);
}
ResultSet rs = pre.executeQuery();
Spring-JDBC
JdbcTemplate jdbcTemplate = new JdbcTemplate(app.dataSource());

// 一般查询场景
String sql = "select * from user where id = ?";
Integer id = 1;
UserDO user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(UserDO.class), id);

// like 模糊查询场景
String sql = "select * from user where name like ?";
String like_name = "%" + "foo" + "%";
UserDO user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(UserDO.class), like_name);

// where in 查询场景
NamedParameterJdbcTemplate namedJdbcTemplate = new NamedParameterJdbcTemplate(app.dataSource());

MapSqlParameterSource parameters = new MapSqlParameterSource();
parameters.addValue("names", Arrays.asList("foo", "bar"));

String sql = "select * from user where name in (:names)";
List<UserDO> users = namedJdbcTemplate.query(sql, parameters, BeanPropertyRowMapper.newInstance(UserDO.class));
Mybatis XML Mapper
<!-- 一般查询场景 -->
<select id="select" parameterType="java.lang.String" resultMap="BaseResultMap">
    SELECT *
    FROM user
    WHERE name = #{name}
</select>

<!-- like 查询场景 -->
<select id="select" parameterType="java.lang.String" resultMap="BaseResultMap">
    SELECT *
    FROM user
    WHERE name like CONCAT("%", #{name}, "%")
</select>

<!-- where in 查询场景 -->
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
    SELECT *
    FROM user
    WHERE name IN
    <foreach collection="names" item="name" open="(" close=")" separator=",">
      #{name}
    </foreach>
</select>
Mybatis Criteria
public class UserDO {
    private Integer id;
    private String name;
    private Integer age;
}

public class UserDOExample {
    // auto generate by Mybatis
}

UserDOMapper userMapper = session.getMapper(UserDOMapper.class);
UserDOExample userExample = new UserDOExample();
UserDOExample.Criteria criteria = userExample.createCriteria();

// 一般查询场景
criteria.andNameEqualTo("foo");

// like 模糊查询场景
criteria.andNameLike("%foo%");

// where in 查询场景
criteria.andIdIn(Arrays.asList(1,2));

List<UserDO> users = userMapper.selectByExample(userExample);

  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

B10SS0MS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值