前言

最近在疯狂打游戏哈,有点懈怠了,所以赶紧去来补一下java的代码审计,上一篇的xss是找了个视频,随便写了点,这个就认真一点,依旧找个博客跟着大佬学习。
项目地址:JoyChou93/java-sec-code: Java web common vulnerabilities and security code which is base on springboot and spring security

jdbc的拼接不当导致的sql注入

在jdbc中存在两个sql语句来执行sql,一个是Statement,另一个是PreparedStatement。
那么他们的写法分别是

Statement

1
2
3
4
Connection con = DriverManager.getConnection(url, user, password);
Statement statement = con.createStatement();
String sql = "select * from users where username = '" + username + "'";
ResultSet rs = statement.executeQuery(sql);

这里直接复制项目里的了。漏洞点就是username这里进行了拼接。
image.png
这里的绕过就很简单了
万能密码就可以了1' or '1' = '1
image.png

然后为了为了修复这个漏洞,官方这边又给出了PreparedStatement,进行预编译。

PreparedStatement

1
2
3
4
5
Connection con = DriverManager.getConnection(url, user, password);
String sql = "select * from users where username = ?";
PreparedStatement st = con.prepareStatement(sql);
st.setString(1, username);
ResultSet rs = st.executeQuery();

image.png
至于为什么能够进行防御,这里口述一下:主要是因为在Statement中,数据库接受到的是一个完整的sql语句,但是这个语句已经是我们修改过的了,所以会实现恶意攻击。而在PreparedStatement中参数和sql语句是分开的,这里会用一个?(占位符),先将sql语句进行发送给数据库,数据库看到明白要执行什么操作,然后在去接收参数,这样就可以避免sql注入的产生。总的来说就是预编译的过程就是参数和sql语句进行分开。

漏洞点总结

未使用占位符

PreparedStatement只有在使用?占位符的时候才能够预防SQL注入,否则还是会产生sql注入的(现在php好像也是用占位符进行预防的)。

使用in语句

在删除语句中,如果使用的是in语句,并且并且无法确定in内的对象个数的话,就有可能照成sql注入。
String sql= "delete from users where id in("id1,id2...")"
解决方法:对对象进行一个遍历,每一个都加上?作为占位符就好了。

使用like语句

在使用like语句时,如果直接做拼接的话,就会导致sql注入漏洞
例如:

1
2
3
4
5

String userInput = "admin";

String sql = "SELECT id, username, email FROM users WHERE username LIKE '%" + userInput + "%'";
//...执行sql语句

%和_

%和_作为通配符,一个是匹配多个字符,一个匹配一个字符,在预编译中,它们依旧是作为通配符进行使用的。
这就会导致慢查询,形成dos
解决办法就是手动过滤掉就好

order by和from等关键词无法进行预编译

师傅讲解的很明白,就是主要原因就是,order by后是需要加字段名的,而字段名是不能加引号的,如果使用了引号就是一个字符串,而不是一个字段名了,然而在使用预编译的时候会自动添加‘,所以导致我们在使用order by的时候是不能使用预编译技术的,只能使用statement,这就会导致sql注入,当然解决问题的方法也很简单,直接进行过滤就好。

Mybatis下的SQL注入

Mybatis算是jbdc的一个代替,在mybatis中也是有两个可以执行sql语句的方法,一个是${},另一个是#{}。

${}

在项目中,sql语句的执行代码是写在了Mapper中的,所以我们一般看sql注入的话是可以直接去Mapper里看的,这样节省时间。
还是那这个项目举例

1
2
3
4
5
6
7
8
@GetMapping("/mybatis/vuln01")  
public List<User> mybatisVuln01(@RequestParam("username") String username) {
return userMapper.findByUserNameVuln01(username);
}


@Select("select * from users where username = '${username}'")
List<User> findByUserNameVuln01(@Param("username") String username);

这里的${}是直接进行拼接的。

1
joychou' or '1'='1

image.png
因为是直接拼接,所以payload就是上述那样

#{}

这种就是做了一个防护,也就是预编译

1
2
3
4
5
6
7
8
@Select("select * from users where username = #{username}")  
User findByUserName(@Param("username") String username);
List<User> findByUserNameVuln02(String username);

@GetMapping("/mybatis/vuln02")
public List<User> mybatisVuln02(@RequestParam("username") String username) {
return userMapper.findByUserNameVuln02(username);
}

具体的话就和上述是一样的

MyBatis产生的三种sql注入

like关键字

因为在使用like进行模糊查询的时候,使用#{}会报错,所以一些程序员,会将#{}换成${},最终导致拼接的注入,在项目中体现为:

1
2
3
<select id="findByUserNameVuln02" parameterType="String" resultMap="User">  
select * from users where username like '%${_parameter}%'
</select>

image.png

修复方法

1
2
3
<select id="findByUserNameVuln02" parameterType="String" resultMap="User">  
select * from users where username like concat('%',#{_parameter},'%')
</select>

正确写法:

1
2
3
4
5
6
mysql:
select * from users where username like concat('%',#{username},'%')
oracle:
select * from users where username like '%'||#{username}||'%'
sqlserver:
select * from users where username like '%'+#{username}+'%'

使用in语句

这个漏洞会出现的原因呢就是当你在使用in的时候如果传入id=1,2,3这种,预编译的判断就会将参数1,2,3直接赋值为一个占位符?,这样的话查询就变成了去查1,2,3,(这个显然会报错)而不是分别查1,2,3,返回结果。所以程序员为了避免这个报错,就将#{}换成${},这样的话就会导致拼接的sql注入。
具体是
controller中添加

1
2
3
4
@GetMapping("/mybatis/vuln04")  
public List<User> mybatisVuln04(@RequestParam("id") String id) {
return userMapper.findByUserNameVuln04(id);
}

接口类添加

1
List<User> findByUserNameVuln04(@Param("id") String id);

UserMapper.xml中添加

1
2
3
<select id="findByUserNameVuln04" parameterType="String" resultMap="User">  
select * from users where id in (${id})
</select>

image.png
解决方法:
就是使用foreach标签对其遍历,动态的生成占位符。
也就是说使用foreach之后对于in(1,2,3)预编译的结果是in(?,?,?),而不是in(?)
具体的使用方法是:

1
2
3
4
5
<select id="findByUserNameVuln04" parameterType="String" resultMap="User">  
select * from users where id in <foreach collection="id" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>

也就是会将我们的输入按照,分隔开,然后进行预编译。

使用order by语句

这里和jdbc的一样

Mybatis-Plus的sql注入

我是让ai写了个小项目来辅助学习一下(这里ai写的项目就不放了哈)
没有涉及到的是手动补充的
image.png
这里是可以正常查询的

apply拼接产生的sql注入

apply直接进行拼接产生的漏洞

1
2
3
4
5
@GetMapping("/vuln1")  
public List<User> getVuln1(String name) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.apply("name = '" + name + "'");
return userMapper.selectList(wrapper);

这里是对name直接进行了拼接,然后调用selectlist直接执行sql语句,然后可以返回多条结果。
image.png
这里可以使用万能密码返回所有数据,但是使用selectlist的场景是很少见的。
在看一个场景:

1
2
3
4
5
6
@GetMapping("/vuln3")  
public User getVuln3(String name, Long id) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", name).apply("id=" + id);
return userMapper.selectOne(wrapper);
}

这算是一个常见的场景,也是使用apply进行的直接拼接,但是selectone只能返回单个结果(这里就不能使用万能密码了,如果使用万能密码的话,会返回多个结果最终就会报错)。
那么我们sql注入就可以使用报错注入:

1
1%20and%20extractvalue(1,concat(0x7e,(select%20database()),0x7e))

前提还得是使用的mysql数据库版本还得在5,因为得能用extractvalue
image.png
这边也是能成功的注入出来
updatexml也可以

1
2 and updatexml(1,concat(0x7e,(select database()),0x7e),1)

image.png
floor我就不试了。
对于怎么进行防护的,直接进行预编译就好了

1
2
3
4
5
6
7
@GetMapping("/sec3")  
public User getSec3(String id) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.apply("id={0}", id);
return userMapper.selectOne(wrapper);

}

image.png

last产生的sql注入

先说明一下last的作用:直接向sql语句后面拼接语句(有拼接这里就会出现sql注入)
image.png
在由上图可知last是可以直接传入sql语句的。
所以可见:

1
2
3
4
5
6
@GetMapping("/vuln2")  
public List<User> getVuln2(String id) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.last("where id = " + id);
return userMapper.selectList(wrapper);
}

image.png
使用万能密码
image.png

exists和notexists拼接导致的sql注入

关于这两个其实是在sql语句中拼接一个子查询,只返回true和flase

1
2
3
4
5
6
@GetMapping("vuln4")  
public List<User> getVuln4(String id) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.exists("select * from users where id = " + id);
return userMapper.selectList(wrapper);
}
1
2
3
4
5
6
@GetMapping("vuln4")  
public List<User> getVuln4(String id) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.notExists("select * from users where id = " + id);
return userMapper.selectList(wrapper);
}

image.png

having语句

having主要是对分组后的数据进行过滤
接口:

1
2
3
4
5
6
@GetMapping("vuln5")  
public List<User> getVuln5(String id) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select().groupBy(id).having("id > "+id);
return userMapper.selectList(wrapper);
}

image.png

order by和group by语句

这个的话还是和上述,以及jdbc的情况一致,因为无法进行预编译所以容易造成sql注入漏洞。

insql和notinsql

主要作用是向sql语句中拼接 字段 in (sql语句),notinsql同理
接口:

1
2
3
4
5
6
@GetMapping("vuln6")  
public List<User> getVuln6(String id) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select().inSql("id","select * from users where id = " + id);
return userMapper.selectList(wrapper);
}

写法是这样一种写法,notinsql的接口是:

1
2
3
4
5
6
@GetMapping("vuln6")  
public List<User> getVuln6(String id) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select().notinSql("id","select * from users where id = " + id);
return userMapper.selectList(wrapper);
}

有报错应该是我的数据库字段和逻辑矛盾产生的,这到无所谓

wrapper自定义sql

和上述一样

有关mp中分页插件的的sql注入

分页插件的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo.config; // 替换成你实际的包名

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件 (如果不配置这个,所有 selectPage 方法都不会生效)
// 这里的 DbType.MYSQL 需要根据你实际使用的数据库进行修改 (比如如果是 H2 就改成 DbType.H2)
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/vulnpage")  
public IPage<User> getVulnPage(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam String sortColumn) { // 前端传入要排序的列名,比如 "age"
Page<User> page = new Page<>(current, size);

page.addOrder(OrderItem.desc(sortColumn));

// 执行分页查询
return userMapper.selectPage(page, null);
}

这里是addorder导致的sql注入漏洞
正常的请求如果是
current=1&size=10&sortColumn=age
它执行的sql命令类似于这种

1
SELECT id, name, age, email FROM users ORDER BY age DESC LIMIT ?

image.png
age位置是我们能够控制的,这时候就可以使用报错注入,或者是时间盲注
例如
image.png
image.png

image.png
也可以触发延迟导致盲注
最后就是pagehelper的几种形式,1.setorderby 2.startpage 其实也都是和order by有关的注入。
修复的话就加filter就好。

Hibernate下的sql注入

Hibernate是一种ORM(对象,关系数据库,映射)框架,在java对象与数据库之间建立某种映射,以实现直接存取java对象。
在这个框架下,用的较多的还是hql进行查询,但是,hql语句是和php很像的,所以也就是拼接产生sql注入漏洞
例如:

1
String hql = "from users where id ='" +id+ "'"

至于防护方法,这里直接copy了

命名参数

1
2
3
4
Query<User> query = session.createQuery("from users name = ?1", User.class);
String parameter = "g1ts";
Query<User> query = session.createQuery("from users name = :name", User.class);
query.setParameter("name", parameter);

位置参数

1
2
3
String parameter = "g1ts";
Query<User> query = session.createQuery("from users name = ?1", User.class);
query.setParameter(1, parameter);

命名参数列表

1
2
3
List<String> names = Arrays.asList("g1ts", "g2ts");
Query<User> query = session.createQuery("from users where name in (:names)", User.class);
query.setParameter("names", names);

类实例

1
2
3
user1.setName("g1ts");
Query<User> query = session.createQuery("from users where name =:name", User.class);
query.setProperties(user1);

HQL拼接方法

1
2
3
4
5
import org.apache.commons.lang.StringEscapeUtils;
public static void main(String[] args) {
String str = StringEscapeUtils.escapeSql("'");
System.out.println(str);
}

参考

Java OWASP 中的 SQL 注入代码审计 | Drunkbaby’s Blog
xz.aliyun.com/news/11118