前言
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);