JavaSec 入门-02
前言
离散被捞了,好事
反序列化入门和动态代理九月看过一次就没再学了,回顾一下。
sun.misc.Unsafe
sun.misc.Unsafe
是 Java 底层 API(仅限Java内部使用,反射可调用
)提供的一个神奇的Java类,Unsafe
提供了非常底层的内存、CAS、线程调度、类、对象等操作、Unsafe
正如它的名字一样它提供的几乎所有的方法都是不安全的
获取Unsafe对象
Unsafe
是Java内部API,外部是禁止调用的,在编译Java类时如果检测到引用了Unsafe
类也会有禁止使用的警告:Unsafe是内部专用 API, 可能会在未来发行版中删除。
sun.misc.Unsafe
代码片段:
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
public final class Unsafe {
private static final Unsafe theUnsafe;
static {
theUnsafe = new Unsafe();
省去其他代码......
}
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (var0.getClassLoader() != null) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
省去其他代码......
}
由上代码片段可以看到,Unsafe
类是一个不能被继承的类且不能直接通过new
的方式创建Unsafe
类实例,如果通过getUnsafe
方法获取Unsafe
实例还会检查类加载器,默认只允许Bootstrap Classloader
调用。
既然无法直接通过Unsafe.getUnsafe()
的方式调用,那么可以使用反射的方式去获取Unsafe
类实例。
反射获取Unsafe
类实例代码片段:
// 反射获取Unsafe的theUnsafe成员变量
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
// 反射设置theUnsafe访问权限
theUnsafeField.setAccessible(true);
// 反射获取theUnsafe成员变量值
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
当然我们也可以用反射创建Unsafe
类实例的方式去获取Unsafe
对象:
// 获取Unsafe无参构造方法
Constructor constructor = Unsafe.class.getDeclaredConstructor();
// 修改构造方法访问权限
constructor.setAccessible(true);
// 反射创建Unsafe类实例,等价于 Unsafe unsafe1 = new Unsafe();
Unsafe unsafe1 = (Unsafe) constructor.newInstance();
获取到了Unsafe
对象我们就可以调用内部的方法了。
allocateInstance无视构造方法创建类实例
假设我们有一个叫com.anbai.sec.unsafe.UnSafeTest
的类,因为某种原因我们不能直接通过反射的方式去创建UnSafeTest
类实例,那么这个时候使用Unsafe
的allocateInstance
方法就可以绕过这个限制了。
UnSafeTest代码片段:
public class UnSafeTest {
private UnSafeTest() {
// 假设RASP在这个构造方法中插入了Hook代码,我们可以利用Unsafe来创建类实例
System.out.println("init...");
}
}
使用Unsafe创建UnSafeTest对象:
// 使用Unsafe创建UnSafeTest类实例
UnSafeTest test = (UnSafeTest) unsafe1.allocateInstance(UnSafeTest.class);
Google的GSON
库在JSON反序列化的时候就使用这个方式来创建类实例,在渗透测试中也会经常遇到这样的限制,比如RASP限制了java.io.FileInputStream
类的构造方法导致我们无法读文件或者限制了UNIXProcess/ProcessImpl
类的构造方法导致我们无法执行本地命令等。
defineClass直接调用JVM创建类对象
ClassLoader
章节我们讲了通过ClassLoader
类的defineClass0/1/2
方法我们可以直接向JVM中注册一个类,如果ClassLoader
被限制的情况下我们还可以使用Unsafe
的defineClass
方法来实现同样的功能。
Unsafe
提供了一个通过传入类名、类字节码的方式就可以定义类的defineClass
方法:
public native Class defineClass(String var1, byte[] var2, int var3, int var4);
public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);
写一个想要被加载的类,注意这里由于类初始化的原因,我们要把想在类加载时触发的方法写到 static 里面
package test02;
import java.io.IOException;
public class Hello {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
}
}
package test02;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
public class UnsafeTest {
public static void main(String[] args) throws Exception {
byte[] words = Files.readAllBytes(Paths.get("D:\\Java\\test\\target\\classes\\test02\\Hello.class"));
ClassLoader c = ClassLoader.getSystemClassLoader();
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
Class<?> hello = unsafe.defineClass("test02.Hello", words, 0, words.length, c, null);
hello.newInstance();
}
}
为什么这么写呢,在这里我们获取的是 unsafe
的属性,而不是defineClass
方法,同样的 unsafe
类也有 definclass
方法,但是他是原生的类(底层C加载),因此反射过来无法调用,而Classloader
里的definclass
就不是了
Unsafe
还可以通过defineAnonymousClass
方法创建内部类,这里不再多做测试。
注意:
这个实例仅适用于Java 8
以前的版本如果在Java 8
中应该使用应该调用需要传类加载器和保护域的那个方法。Java 11
开始Unsafe
类已经把defineClass
方法移除了(defineAnonymousClass
方法还在),虽然可以使用java.lang.invoke.MethodHandles.Lookup.defineClass
来代替,但是MethodHandles
只是间接的调用了ClassLoader
的defineClass
,所以一切也就回到了ClassLoader
。
动态代理
区别于静态代理,Java
反射提供了一种类动态代理机制,可以通过代理接口实现类来完成程序无侵入式扩展。
Java动态代理主要使用场景:
- 统计方法执行所耗时间。
- 在方法执行前后添加日志。
- 检测方法的参数或返回值。
- 方法访问权限控制。
- 方法
Mock
测试。
为什么类可以动态的生成?
这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。
Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据访问入口
由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:
- 从ZIP包获取,这是JAR、EAR、WAR等格式的基础
- 从网络中获取,典型的应用是 Applet
- 运行时计算生成,这种场景使用最多的是动态代理技术,在
java.lang.reflect.Proxy
类中,就是用了ProxyGenerator.generateProxyClass
来为特定接口生成形式为*$Proxy
的代理类的二进制字节流 - 由其它文件生成,典型应用是 JSP,即由 JSP 文件生成对应的 Class 类
- 从数据库中获取等等
所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到 JVM 中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。
常见的字节码操作类库
这里有一些介绍:https://java-source.net/open-source/bytecode-libraries
- Apache BCEL (Byte Code Engineering Library):是 Java classworking 广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。
- ObjectWeb ASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。
- CGLIB(Code Generation Library):是一个功能强大,高性能和高质量的代码生成库,用于扩展 JAVA 类并在运行时实现接口。
- Javassist:是 Java 的加载时反射系统,它是一个用于在 Java 中编辑字节码的类库; 它使 Java 程序能够在运行时定义新类,并在 JVM 加载之前修改类文件。
JDK动态代理
先来个接口
package test;
public interface Rentinterace {
void rent();
void pay();
}
再来个实现类
package test;
public class RentDirect implements Rentinterace{
@Override
public void rent() {
System.out.println("租房");
}
@Override
public void pay() {
System.out.println("付钱");
}
}
再写我们的动态代理
package test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class Userinvocationhandler implements InvocationHandler {
Rentinterace user;
public Userinvocationhandler(Rentinterace user) {
this.user=user;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("这里是动态代理,我调用了方法:"+method.getName());
method.invoke(user,args);
return null;
}
}
在客户端,我们调用了Proxy.newProxyInstance
方法:
package test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Test {
public static void main(String[] args) {
Rentinterace user = new RentDirect();
//动态代理
//要代理的接口、要做的事情、classLoader
InvocationHandler userinvocationhandler = new Userinvocationhandler(user);
Rentinterace actionuser = (Rentinterace) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(),userinvocationhandler);
actionuser.rent();
actionuser.pay();
}
}
可以看到上图中需要的参数就是,要代理的接口,类加载器,要处理的事情,其中要处理的事情就是一个InvocationHandler
接口,我们要新建一个类去重写 invoke 方法
CGLIB动态代理
不太懂
maven 引入 CGLIB 包
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
写一个类
public class UserDao {
public void select() {
System.out.println("UserDao 查询 selectById");
}
public void update() {
System.out.println("UserDao 更新 update");
}
}
编写一个 LogInterceptor ,继承了 MethodInterceptor,用于方法的拦截回调
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.Date;
public class LogInterceptor implements MethodInterceptor {
/**
* @param object 表示要进行增强的对象
* @param method 表示拦截的方法
* @param objects 数组表示参数列表,基本数据类型需要传入其包装类型,如int-->Integer、long-Long、double-->Double
* @param methodProxy 表示对方法的代理,invokeSuper方法表示对被代理对象方法的调用
* @return 执行结果
* @throws Throwable
*/
@Override
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(object, objects); // 注意这里是调用 invokeSuper 而不是 invoke,否则死循环,methodProxy.invokesuper执行的是原始类的方法,method.invoke执行的是子类的方法
after();
return result;
}
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
写个代理
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class DaoProxy implements MethodInterceptor {
@Override
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("Logging: Method " + method.getName() + " is called.");
return methodProxy.invokeSuper(object, objects); // 调用原方法
}
}
客户端
import net.sf.cglib.proxy.Enhancer;
public class CglibTest {
public static void main(String[] args) {
DaoProxy daoProxy = new DaoProxy();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserDao.class); // 设置超类,cglib是通过继承来实现的
enhancer.setCallback(daoProxy);
UserDao dao = (UserDao)enhancer.create(); // 创建代理类
dao.update();
dao.select();
}
}
CGLIB 创建动态代理类的模式是:
- 查找目标类上的所有非final 的public类型的方法定义;
- 将这些方法的定义转换成字节码;
- 将组成的字节码转换成相应的代理的class对象;
- 实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求
序列化与反序列化
Java 中的序列化与反序列化是将对象与其状态转换为字节流(序列化)并从字节流还原为对象(反序列化)的过程,主要用于对象的持久化、网络传输等。
这中间需要一个规则,规则中描述了序列化和反序列化时究竟该如何把一个对象处理成字符串,又如何把字符串变回对象,因为这一过程必须是可逆的。
序列化与反序列化
序列化过程
序列化将 Java 对象及其状态转换为字节流。这个过程通常会创建 FileOutputStream 或 ByteArrayOutputStream,指定文件或内存中的一个缓冲区作为目标来创建输出流,用 ObjectOutputStream 包装输出流,调用 writeObject() 方法将对象写入字节流,并且要求待序列化的类必须实现 Serializable 接口。
// 创建对象输出流
FileOutputStream fileOut = new FileOutputStream("object.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
// 序列化对象
out.writeObject(someObject);
out.close();
fileOut.close();
//简化
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
这个过程会将对象的属性、状态以字节流的形式写入文件、内存或者网络流中。
反序列化过程
反序列化则是将字节流重新转换为 Java 对象的过程,通常使用 FileInputStream 或 ByteArrayInputStream 来指定从文件或内存读取字节流,使用 ObjectInputStream 包装输入流,调用 readObject() 方法读取字节流,并将其转换回对象,在此过程中类必须存在:反序列化时,JVM 必须能够找到与序列化时相同的类定义,否则会抛出 ClassNotFoundException ,并且该类也必须实现 Serializable 接口。
// 创建对象输出流
FileOutputStream fileOut = new FileOutputStream("object.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
// 序列化对象
out.writeObject(someObject);
out.close();
fileOut.close();
//简化
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
Object obj = ois.readObject();
如果有些字段不想进行序列化怎么办?
对于不想进行序列化的变量,可以使用 transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
关于 transient
还有几点注意:
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化。
这会引发什么问题呢?
反序列化漏洞
初见
几种常见形式:
- 入口类的readObejct直接调用危险方法
- 入口类参数包含可控类,可控类里有危险方法
- 入口类参数包含可控类,该类又调用其他含危险方法的类
- 构造函数/静态代码块等类加载时隐式执行
第一个的关键点是readObject,如果被反序列化的类重写了writeObject,readObject方法,就会在反序列化时调用,如果这个方法中存在一些恶意的调用就会产生危害
需满足的条件
- 都继承了Serializeable接口
- 入口类source(重写readObject、参数类型宽泛、jdk自带就更好、常见函数)
- 调用链(gaget chain)
- 执行类 sink (ssrf,rce….)
简单示例:
Person类
import java.io.*;
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
Runtime.getRuntime().exec("calc");
}
}
执行序列化操作类
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("zhangsan", 24);
serialize(person);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
Object obj = ois.readObject();
}
}
原理( readObject 的具体实现)
下好断点开始调试
实际调用
readObject0
方法
readObject0
方法以字节的方式去读,如果读到 0x73
,则代表这是一个对象的序列化数据,将会调用 readOrdinaryObject
方法进行处理
readOrdinaryObject
方法会调用 readClassDesc
方法读取类描述符,并根据其中的内容判断类是否实现了 Externalizable 接口,如果是,则调用 readExternalData
方法去执行反序列化类中的 readExternal
,如果不是,则调用 readSerialData
方法去执行类中的 readObject
方法。
在 readSerialData
方法中,首先通过类描述符获得了序列化对象的数据布局。通过布局的 hasReadObjectMethod
方法判断对象是否有重写 readObject
方法,如果有,则使用 invokeReadObject
方法调用对象中的 readObject
。
参考文章
P神《Java安全漫谈》
https://www.javasec.org/
https://boogipop.com/2023/03/02/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%A0%94%E7%A9%B6/#1X1-5-JDK%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86
https://www.cnblogs.com/whirly/p/10154887.html