java反序列化以及一些前置知识

最近学习了java反序列化以及反射 的相关知识,就在这稍微做一些总结。

一、Java反射

java反射其实也就是比较基础简单的,主要就是集中在一些函数上。

首先就是反射的作用是什么

1、让java具有动态性

2、修改已有对象的属性的值

3、动态生成对象

4、动态调用方法

5、操作内部类和私有方法私有变量

其次就是最主要的一点

Java反射在Java反序列化中的应用

1、定制需要的对象

2、通过invoke去调用同名函数以外的函数

3、通过Class类创建对象,引入不能序列化的类然后通过反射去执行

首先就是介绍总结一下java反射中用到的一些相关方法,当然也不是直接对着方法名记忆,那样太枯燥了,主要就是在使用的过程中去学习。

1
2
3
4
5
6
7
8
Class clazz = Class.forName("java.lang.Runtime");
//这个的意思就是获取到java.lang.Runtime这个类,是一种获取类最常见的方法,然后获取到的类就被“等同”于clazz(!!!这里要想起来类和对象之间的区别,这两个不是同一种东西!!!)
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
//这里我们就逐步分析,首先就是getMethod就是获取一个类的方法
//格式: 类.getMethod(“方法名”, 这个方法需要传递的参数类型)
//然后就是invoke,这是一个调用上面获得的方法。
//格式: 方法名.(类的一个对象,需要传递给方法的参数)
//newInstance就是调用该类的构造方法去实例化一个对象

当然这样直接去运行时会产生报错的!!!这又是为什么呢?

这里主要就是因为我们无法通过newInstance去获得类的对象,而这里是因为Runtime的构造方法是私有的,是一种”单例”的设计模式

单例的设计模式:主要是考虑到某些类一般只需要类的初始化时使用一次构造方法,而不是每次都需要去再重新使用构造方法,但这样的话我们又应该怎么样去调用构造方法呢?所以设计者就设计了一个静态方法,像这里的就是getRuntime这个方法,使用这个方法就会返回一个对象

所以修改之后的代码就是

1
2
3
4
5
class clazz = class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec",String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethed.invoke(clazz);
execMethod.invoke(runtime,"calc");

这样就可以去调用到我们需要的方法了。

当然这样也是还有点缺点的,就比如如果没有这个getRuntime的方法呢?这又应该怎么办?

这个时候就可以使用getConstructor这个方法去获取到我们想要的类。

格式: 类.getConstructor(构造方法的参数类型)

eg.

1
2
class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

然后就是如果构造方法是私有方法,我们应该去使用getDeclaredMethod这个方法。

getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法

getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私 有的方法,但从父类里继承来的就不包含了

还有就是getConstructor和getDeclaredConstructor基本上相似的,这里就不过多解释了

1
2
3
4
class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");

setAccessible必须要设置为true,不然无法去使用。

二、JDK动态代理

首先就是个人感觉java的动态代理技术就有点与Python中的装饰器

image-20240311143731878

通过调用jdk自带的相关方法,从而去省略跳过自身创建静态代理

ProxyTest.java

1
2
3
UserInvocationHandler userinvocationhandler = new UserInvocationHandler();
IUser userProxy = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(),user.getClass().getInterfaces(),userinvocationhandler)
userProxy.method; //这样就可以去代理user的方法

UserInvocationHandler.java

1
2
3
4
5
6
7
IUser user;
public UserInvocationHandler(IUser user){this.user = user;}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
method.invoke(user,args); //进行反射去调用
return null;
}

这样也就成功实现了简单的动态代理了

三、类的动态加载

1
ClassLoader cl = ClassLoader.getSystemClassLoader();

获得系统当前的类加载器

构造代码块,静态代码块———————无论调用什么构造方法都会先调用构造代码块。同理,静态代码块也是如此

1
2
3
4
5
6
7
8
//构造代码块
{
xxxxxx
}
静态代码块
static{
xxxxx
}

然后就去实现一个具体的代码

1
2
3
4
5
6
7
8
9
10
byte[] code = Files.readAllBytes(Paths.get("E:\\Test.class"));
// Class c = (Class) defineClassMethod.invoke(cl, "Test", code,0,code.length);
// c.newInstance();

Class c = Unsafe.class;
Field theUnsafeField = c.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
Class<?> test = unsafe.defineClass("Test", code, 0, code.length, cl, null);
test.newInstance();

通过类加载器可以去实现加载远程或者本地的其他目录下的类

四、RMI

RMI全称是Remote Method Invocation,远程⽅法调⽤。听这名字应该也就知道,就是去从调用一个远程主机上的java方法,在这里就挑一些重点的代码片段进行讲解。

1
2
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/Hello", new RemoteHelloWorld());(这里是服务器上启动的)

首先第一行这里就是创建并且执行Registry服务,这个服务就是相当于一个中继器,我将类和一个名字绑定丢到这里面去,别的人就可以通过名字去拿到这个对应的类,这就是Registry所起到的一个作用。然后就是第二行,就是将本机的一个类给绑定到了一个Registry服务上,这样等之后就可以去直接拿到这个类了。

1
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)Naming.lookup("rmi://vps_ip:1099/Hello");(本机运行)

这里就通过Naming的lookup方法去寻找这个rmi类,我们就可以在Registry中拿到我们想要的类。

codebase的利用方法

在以前的有段时间,java是可以运行在浏览器上的,就有一个codebase属性,这是一个地址,去告诉哪个地方寻找类,这个时候我们就可以使用rmi的相关操作,去使其加载我们自己部署的服务器上的一些恶意类。

就比如在log4j这个CVE中,也是可以去使用rmi去实现的,去完成反弹shell从而获得权限。

🌟补充:

🌟关于rmi服务的具体流程调试

参考文章和视频(我认为讲的特别好,强烈推荐!)

https://halfblue.github.io/2021/10/26/RMI%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E4%B9%8B%E4%B8%89%E9%A1%BE%E8%8C%85%E5%BA%90-%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/

https://www.bilibili.com/video/BV1L3411a7ax/?p=3&spm_id_from=pageDriver&vd_source=2e4979b0a36df1ed7510f03f3d33843f

1、远程对象的创建过程

主要的作用点就在远程方法所继承的UnicastRemoteObject类中,是用于将本地方法发布的

image-20240309103527834

然后就是由于端口是未知的,端口为零,然后调用exportObject方法区将方法给发布,exportObject就是发布的重要函数,继续跟进,就会发现是在一层层去调用不同类中的exportObject函数

image-20240309103730436

image-20240309103742756

image-20240309103823798

最后在这里,会发现一个createProxy,看名字就能看出来,是一个创建代理的方法,我们跟进看看

发现是一个判断是否有_stub后缀的方法,如果没有,就创建一个,后边就是一些不太好理解的东西了

image-20240309104201242

然后就跳到了这里,我们跟进函数,发现也是一个判断_Skel后缀的方法,在继续调试,发现创建了一个Target,一个很关键的东西

image-20240309104525491

里面把所有相关的stud,ID等都记录进去了,然后就又是一系列的exportObject去将Target给发布出去了。

image-20240309104817139

最后发现服务端还会建一张表,去将Target给存入其中,从而保留让服务端知道有关这个远程方法的所有信息。

这差不多 就是远程方法的创建的过程了

2、注册中心的创建

相比来说注册中心的创建就简单很多了,我们直接调试跟进

image-20240309105935201

在这里就发现了重点—–LiveRef

在上面之前忘记将了,LiveRef又是什么,这是一个在rmi远程方法调用中非常重要的一个,他其中封装了服务端中所有相关的信息,比如远程方法的ID,地址和端口,以及各种信息

然后就发现是调用TCPEndpoint,去获取本地的Endpoint服务,从而以此去完成相关TCP服务的操作。

image-20240309110603314

发现就又是exportObject方法,发现后边就是和之前的远程方法调用基本就是一致的了,就能够知道注册中心的创建,其实也是一个远程方法的发布过程实际上。

3、远程方法绑定

这里就比较简单了,因为这里并不是远程绑定,就直接调用了RegistryImpl类,把名字和远程对象放到一个叫bindings的HashTable里面。
到这里服务端的流程就走完了。

三、serialize🌟

这是学习的重难点,我现在也还知识初步了解一些,还没完全弄完😭

一般web手初识反序列化都是从php开始,首先就java的反序列化和php的还是有很大区别的,php是直接利用serialize和unserialize这两个函数进行序列化和反序列化的过程,我们无法去控制这个过程中的任何东西,而java就不一样了。

  • writeObject:序列化
  • readObject:反序列化

这两个主要是java中序列化反序列化所需要使用到的,一般需要搭配一些其他的东西去使用。

eg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void serialize() throws IOException
{
FileOutputStream out = new FileOutputStream("result.txt"); //用于保存序列化数据
ObjectOutputStream obj_out=new ObjectOutputStream(out); //实例化一个对象输出流
User u = new User();
u.setName("6pc1");
obj_out.writeObject(u); //利用writeObject方法将序列化对象存储在本地
obj_out.close();
System.out.println("User对象序列化成功!");
}
public void unserialize() throws IOException, ClassNotFoundException
{
FileInputStream in = new FileInputStream("result.txt"); //读取之前保存的序列化数据
ObjectInputStream ins = new ObjectInputStream(in); //实例化一个对象输入流
User u = (User)ins.readObject(); //利用readObject方法将序列化对象转为对象
system.out.println("User对象反序列化成功!");
System.out.println(u.getName());
ins.close();
}

这就是java反序列话的主要过程,是可以自己去操作序列化反序列化的一些过程的,就比如在序列化后的文件中添加一些数据一类的

这也不是唯一一种,我们也可以用byte流的方法代替文件流

1
2
3
4
5
6
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream obj_out=new ObjectOutputStream(baos);


ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ins = new ObjectInputStream(bais);

四、CC1链