ysoserial 是一个开源的 Java 工具,专门用于生成各种反序列化漏洞的 payload。这些 payload 可以被用来在易受攻击的 Java 应用中触发远程代码执行(RCE)漏洞。通过利用 Java 序列化机制的特性,ysoserial 构造恶意对象图,在目标应用反序列化这些对象时执行任意代码。该工具支持多种 payload 类型,适用于安全测试、漏洞验证。
源码解析
参考:
ysoserial 结构分析与使用-安全客 - 安全资讯平台 (anquanke.com)
exploit包
这个包内的内容主要用于主要是开启交互式服务,对不同的目标进行实际的攻击。例如如下使用方法
1 | java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1199 CommonsCollections5 "Calc" |
目前包含了多种利用方式JBoss、Jenkins、JMX、JRMP、JSF、RMI等。这里以JRMPListener为样例,分析该模块的编写方法。
payloads包
该包下是ysoserial工具的核心,里面包含各种反序列化链,同时还有两个软件包:annotation
和util
annnotation包
这个包内主要包含了一些注解相关的信息。
Authors注解
这个文件定义了一个注解,其中包含了一些作者信息。
Dependencies注解
这个代码定义了一个自定义的Java注解
@Dependencies
,以及一个嵌套的静态工具类Utils
,用于处理这个注解的信息。让我们分步解析这个代码的意义和用途。1. 注解定义
1
2
3
4
5
6
7
8
9
10
11
12 >package ysoserial.payloads.annotation;
>import java.lang.annotation.ElementType;
>import java.lang.annotation.Retention;
>import java.lang.annotation.RetentionPolicy;
>import java.lang.annotation.Target;
>
>
>public Dependencies {
>String[] value() default {};
>}注解属性解析:
@Target(ElementType.TYPE)
: 这个注解只能应用于类、接口(包括注解类型)或枚举声明。@Retention(RetentionPolicy.RUNTIME)
: 这个注解在运行时保留,因此可以通过反射机制读取。String[] value() default {}
: 注解的属性定义,这里定义了一个名为value
的属性,它是一个字符串数组类型,默认为空数组。2. 工具类定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 >public static class Utils {
public static String[] getDependencies(AnnotatedElement annotated) {
Dependencies deps = annotated.getAnnotation(Dependencies.class);
if (deps != null && deps.value() != null) {
return deps.value();
} else {
return new String[0];
}
}
public static String[] getDependenciesSimple(AnnotatedElement annotated) {
String[] deps = getDependencies(annotated);
String[] simple = new String[deps.length];
for (int i = 0; i < simple.length; i++) {
simple[i] = deps[i].split(":", 2)[1];
}
return simple;
}
>}工具类方法解析:
getDependencies
方法:
annotated
: 接受一个AnnotatedElement
类型的参数,这是一个可以被注解的元素,例如类、方法、字段等。
deps = annotated.getAnnotation(Dependencies.class)
: 通过反射获取Dependencies
注解实例。如果注解存在且其值不为空,则返回注解的值;否则返回一个空数组。
getDependenciesSimple
方法:通过调用
getDependencies
方法获取Dependencies
注解的值。对每个依赖项进行字符串分割,并返回第二部分(假设依赖项格式为
group:artifact
)。示例
假设我们有一个类如下应用了
@Dependencies
注解:
1
2
3 >
>public class MyClass {
>}那么我们可以通过以下方式来获取和处理注解的值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 >import ysoserial.payloads.annotation.Dependencies;
>public class Main {
public static void main(String[] args) {
// 获取MyClass的注解信息
String[] dependencies = Dependencies.Utils.getDependencies(MyClass.class);
for (String dep : dependencies) {
System.out.println(dep); // 输出 "group:artifact1" 和 "group:artifact2"
}
// 获取简化后的依赖信息
String[] simpleDependencies = Dependencies.Utils.getDependenciesSimple(MyClass.class);
for (String dep : simpleDependencies) {
System.out.println(dep); // 输出 "artifact1" 和 "artifact2"
}
}
>}
PayloadTest注解
用来标记gadgate是否需要被测试,是否测试的时候会引发什么异常情况之类的东西,是用来测试gadgate的。
这段代码定义了一个名为
PayloadTest
的自定义注解,用于在 Java 程序中标记测试相关的信息。以下是对该注解的详细解析:注解定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 >package ysoserial.payloads.annotation;
>import java.lang.annotation.Retention;
>import java.lang.annotation.RetentionPolicy;
>/**
>* @author mbechler
>*
>*/
>
>public PayloadTest {
String skip() default "";
String precondition() default "";
String harness() default "";
String flaky() default "";
>}注解属性解析
@Retention(RetentionPolicy.RUNTIME)
:
- 这个注解的保留策略是
RUNTIME
,意味着该注解在运行时可以通过反射机制读取。
String skip() default ""
:
- 定义了一个名为
skip
的属性,类型为String
,默认值为空字符串。这个属性可能用于标记某个测试是否应该被跳过。
String precondition() default ""
:
- 定义了一个名为
precondition
的属性,类型为String
,默认值为空字符串。这个属性可能用于指定测试执行前的先决条件。
String harness() default ""
:
- 定义了一个名为
harness
的属性,类型为String
,默认值为空字符串。这个属性可能用于指定测试执行的环境或框架。
String flaky() default ""
:
- 定义了一个名为
flaky
的属性,类型为String
,默认值为空字符串。这个属性可能用于标记某个测试是否是不稳定的(即可能会随机失败)。使用示例
假设我们有一个测试类
MyTest
,我们可以使用@PayloadTest
注解来标记测试的相关信息:
1
2
3
4
5
6
7
8
9
10
11 >import ysoserial.payloads.annotation.PayloadTest;
>
>public class MyTest {
// Test methods go here
>}
Utils包
Utils中主要利用反射生成对应类
ClassFiles类
这段代码定义了一个名为 ClassFiles
的工具类,其中包含了一些用于处理 Java 类文件的静态方法。下面是对这些方法的详细解析:
1. classAsFile
方法
这个方法将一个 Class
对象转换为对应的类文件路径字符串。
1 | public static String classAsFile(final Class<?> clazz) { |
方法解析
classAsFile(Class<?> clazz)
: 这是一个重载方法,调用了classAsFile(Class<?> clazz, boolean suffix)
并将suffix
参数设为true
。classAsFile(Class<?> clazz, boolean suffix)
:clazz.getEnclosingClass() == null
: 判断当前类是否是一个内部类。如果不是内部类,则将类名中的.
替换为/
,形成类文件的路径格式。- 如果是内部类,则递归调用
classAsFile
方法获取外部类的路径,并在其后加上$
和当前内部类的简单名称。 suffix
参数决定是否在字符串的末尾加上.class
后缀。
示例
假设有一个类 com.example.MyClass
和一个内部类 com.example.MyClass$InnerClass
:
1 | System.out.println(ClassFiles.classAsFile(MyClass.class)); |
2. classAsBytes
方法
这个方法将一个 Class
对象转换为对应的类文件的字节数组。
1 | public static byte[] classAsBytes(final Class<?> clazz) { |
方法解析
classAsBytes(Class<?> clazz)
:- 调用
classAsFile(clazz)
获取类文件的路径。 - 使用
ClassLoader
的getResourceAsStream(file)
方法获取类文件的输入流。 - 如果找不到类文件,抛出
IOException
。 - 使用一个
ByteArrayOutputStream
将输入流的数据读入一个字节数组,并返回这个字节数组。
- 调用
示例
假设有一个类 com.example.MyClass
,我们可以获取它的字节码:
1 | byte[] classData = ClassFiles.classAsBytes(MyClass.class); |
总结
ClassFiles
工具类提供了两种主要功能:
- 将
Class
对象转换为类文件的路径字符串。 - 将
Class
对象转换为类文件的字节数组。
这些方法在需要处理类文件的场景下非常有用,比如动态类加载、类文件分析等。
Gadgets类
这段代码是 ysoserial 工具中的一个实用类 Gadgets
,它包含了一系列用于生成反序列化 payload 的辅助方法。以下是对各个部分的详细解析:
静态初始化块
1 | static { |
在静态初始化块中,设置了两个系统属性:
DESERIALIZE_TRANSLET
设置为"true"
,用于在启用SecurityManager
的情况下使用TemplatesImpl
gadgets。java.rmi.server.useCodebaseOnly
设置为"false"
,用于 RMI 远程加载。
常量和内部类
1 | public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler"; |
ANN_INV_HANDLER_CLASS
是一个常量,表示AnnotationInvocationHandler
类的全限定名。StubTransletPayload
是一个实现了AbstractTranslet
和Serializable
接口的内部类,用于生成TemplatesImpl
的 payload。Foo
是一个简单的可序列化类,用于辅助生成 payload。
辅助方法
createMemoitizedProxy
1 | public static <T> T createMemoitizedProxy(final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces) throws Exception { |
这个方法用于创建一个代理对象,该代理对象使用 AnnotationInvocationHandler
作为调用处理器。
createMemoizedInvocationHandler
1 | public static InvocationHandler createMemoizedInvocationHandler(final Map<String, Object> map) throws Exception { |
这个方法用于创建一个 AnnotationInvocationHandler
实例,并将其与一个 Map
对象关联。
createProxy
1 | public static <T> T createProxy(final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces) { |
这段代码通过指定的 InvocationHandler
和接口,使用 Java 反射生成一个动态代理对象。它首先创建一个包含所有接口类型的数组,然后调用 Proxy.newProxyInstance
创建代理实例,并将其强制转换为第一个接口类型。该方法允许代理对象同时实现多个接口。
createMap
1 | public static Map<String, Object> createMap(final String key, final Object val) { |
使用给定的key
和value
创建一个HashMap
,因为ysoserial在使用过程中需要频繁地创建HashMap
所以将这个操作封装。
createTemplatesImpl
1 | public static Object createTemplatesImpl(final String command) throws Exception { |
这个方法通过检查系统属性 properXalan
来决定使用哪个 TemplatesImpl
类。如果 properXalan
设置为 true
,则动态加载 org.apache.xalan
包下的类;否则,使用默认的 TemplatesImpl
、AbstractTranslet
和 TransformerFactoryImpl
类。
然后调用重载的createTemolatesImpl来真正创建恶意 TemplatesImpl
对象。
createTemplatesImpl
(重载)
1 | public static <T> T createTemplatesImpl(final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory) |
这段代码通过 javassist
库动态生成一个包含恶意代码的类,并将其字节码注入到一个新的 TemplatesImpl
实例中。恶意代码在类加载时执行指定的系统命令。该方法利用反射设置必要的字段,使 TemplatesImpl
在反序列化时触发命令执行。
makeMap
1 | public static HashMap makeMap(Object v1, Object v2) throws Exception, ClassNotFoundException, NoSuchMethodException, InstantiationException, |
这段代码通过反射机制创建并初始化一个 HashMap
,其中包含两个键值对。它首先设置 HashMap
的大小,然后根据JDK版本的不同动态获取 HashMap$Node
或 HashMap$Entry
类的构造器,并利用该构造器创建两个节点对象。接着,将这两个节点对象放入一个新创建的数组中,并将该数组设置为 HashMap
的内部表结构。最终返回这个定制的 HashMap
实例。
JavaVersion类
检测Java版本
PayloadRunner类
测试payload,执行一次序列化和反序列化的过程,看能否达到预期的目的。
Reflections类
这个类是将yso中经常使用的反射操作做一个封装来方便使用。
setAccessible(AccessibleObject member)
:- 作用:根据当前 JDK 版本设置反射对象的可访问性。对 JDK 版本 12 之前,使用
Permit
工具类来静默设置;对 JDK 12 及之后,直接调用setAccessible(true)
,但可能会有警告。 - 参数:
AccessibleObject
是 Java 反射 API 中的父类,包含Field
、Method
和Constructor
。
- 作用:根据当前 JDK 版本设置反射对象的可访问性。对 JDK 版本 12 之前,使用
getField(Class<?> clazz, String fieldName)
:- 作用:递归获取类及其父类中的指定字段,并设置其可访问性。
- 参数:
clazz
要操作的类,fieldName
要获取的字段名称。 - 返回值:返回找到的字段。
setFieldValue(Object obj, String fieldName, Object value)
:- 作用:设置给定对象
obj
中指定字段fieldName
的值为value
。 - 参数:
obj
要操作的对象,fieldName
字段名称,value
要设置的值。
- 作用:设置给定对象
getFieldValue(Object obj, String fieldName)
:- 作用:获取给定对象
obj
中指定字段fieldName
的值。 - 参数:
obj
要操作的对象,fieldName
字段名称。 - 返回值:返回字段的当前值。
- 作用:获取给定对象
getFirstCtor(String name)
:- 作用:获取指定类名的第一个构造函数,并设置其可访问性。
- 参数:
name
类名(包括包名)。 - 返回值:返回第一个构造函数。
newInstance(String className, Object ... args)
:- 作用:通过指定类名和构造函数参数创建类的新实例。
- 参数:
className
类名(包括包名),args
构造函数参数。 - 返回值:返回创建的新实例。
createWithoutConstructor(Class<T> classToInstantiate)
:- 作用:不调用构造函数实例化指定类(通过序列化机制)。
- 参数:
classToInstantiate
要实例化的类。 - 返回值:返回新实例。
createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs)
:- 作用:通过构造函数参数创建类的新实例(通过序列化机制)。
- 参数:
classToInstantiate
要实例化的类,constructorClass
构造函数所在的类,consArgTypes
构造函数参数类型数组,consArgs
构造函数参数值数组。 - 返回值:返回新实例。
ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons)
用途是利用 Java 的反射机制通过序列化的方式为特定类创建一个构造函数,以便实例化对象,而无需调用该类的常规构造函数。具体解释如下:
ReflectionFactory
ReflectionFactory
是 Java 内部类,通常用于提供更底层的反射操作。
newConstructorForSerialization
newConstructorForSerialization
方法用于创建一个特殊的构造函数,该构造函数可以用来实例化一个对象而不调用其常规构造函数。这个方法通常用于反序列化过程中,但在这里被用于绕过正常的构造函数调用。代码解释
1 >Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
- **
ReflectionFactory.getReflectionFactory()
**:获取ReflectionFactory
的单例实例。- **
newConstructorForSerialization(classToInstantiate, objCons)
**:classToInstantiate
:要实例化的类,即目标类。objCons
:一个构造函数对象,用于指定访问权限。例如,可以使用某个父类的无参构造函数来作为创建目标类实例的基础。- 这个方法会返回一个
Constructor
对象,该对象是为序列化而创建的特殊构造函数。工作原理
newConstructorForSerialization
创建的构造函数具有以下特点:- 它是一个特殊的构造函数,不会调用目标类的常规构造函数。
- 它可以实现实例化类对象的目的,即使常规构造函数是私有的或受保护的。
- 通常用于反序列化过程,但在这个上下文中被用于绕过访问限制。
例子
假设有一个类
MyClass
,其构造函数是私有的或受保护的,通常情况下无法直接实例化:
1
2
3
4
5 >public class MyClass {
private MyClass() {
// 私有构造函数
}
>}你可以使用
ReflectionFactory
来实例化这个类:
1
2
3
4
5
6 >Class<MyClass> clazz = MyClass.class;
>Constructor<Object> constructor = Object.class.getDeclaredConstructor();
>constructor.setAccessible(true);
>Constructor<?> serializationConstructor = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(clazz, constructor);
>serializationConstructor.setAccessible(true);
>MyClass instance = (MyClass) serializationConstructor.newInstance();在这个例子中,我们通过
ReflectionFactory
创建了一个特殊的构造函数serializationConstructor
,它允许我们实例化MyClass
的对象,而不调用其私有的常规构造函数。
ObjectPayload接口
这个接口是所有payload的父类,也就是链子具体实现的父类,接口本身没什么,不过它里面还有一个Utils
的内部类,可以详细说说这个内部类中的各种方法。
Utils
静态工具类
getPayloadClasses
方法:使用Reflections
库扫描当前包及其子包,获取所有实现ObjectPayload
接口的类,并过滤掉接口和抽象类。getPayloadClass
方法:根据类名获取ObjectPayload
的实现类。首先尝试直接加载类名,如果失败则尝试在payloads
包下加载。makePayloadObject
方法:根据payloadType
和payloadArg
创建并返回一个 payload 对象。releasePayload
方法:释放 payload 对象。如果ObjectPayload
实现了ReleaseableObjectPayload
接口,则调用其release
方法。
ReleaseableObjectPayload接口
这个接口是用来和releasePayload
方法配合使用,用来清理释放指定Payload
的。
secmgr包
这个包里面包含了两个SecurityManager
的子类,用来更改安全检查的一些逻辑。
DelegateSecurityManager类
继承了 Java 的 SecurityManager
类,并提供了一个委托机制,允许将安全管理任务委托给另一个 SecurityManager
实例。这个类的主要目的是在运行时动态地替换或增强现有的安全管理策略。它通过重写 SecurityManager
的许多方法,将这些方法的调用转发给内部持有的 SecurityManager
实例来实现这一点。此外,为了兼容 JDK 10 及以上版本,对一些已废弃的方法进行了处理,确保在调用这些方法时不会引发异常或错误。
ExecCheckingSecurityManager类
这个类同样继承自SecurityManage
类,用来检查是否执行命令,并决定是否抛出异常。
Deserializer类
它负责将序列化的字节数组或输入流反序列化为 Java 对象。Deserializer
类实现了 Callable<Object>
接口,这意味着它可以在多线程环境中使用。可以提供一个字节数组给 Deserializer
实例,它会把这个字节数组转换成一个 Java 对象。主方法还允许从指定的文件或标准输入中读取序列化数据,并将其反序列化为对象。这段代码的核心功能是通过 ObjectInputStream
读取序列化的字节流并恢复成原始的 Java 对象。
Serializer类
这个类封装了yso中经常使用的序列化操作,提供便捷,和上面的Deserializer
类很相似。
GeneratePayload类
主要用于生成和序列化恶意的Java对象。该类通过命令行接收两个参数:一个payload类型和一个命令。它首先检查参数数量,如果不正确则打印使用说明并退出。然后,它根据提供的payload类型查找对应的 ObjectPayload
类,并尝试实例化该类以生成恶意对象。生成的对象随后被序列化并通过标准输出打印。如果过程中发生任何错误,程序会捕获异常并打印错误信息,然后退出并返回特定的错误代码。此外,printUsage
方法用于打印帮助信息,列出所有可用的payload类型及其作者和依赖项。
Strings类
这个类就是将一些常用的字符串方法进行一个封装,如连接,重复,格式化、比较操作。
工作流程
springkill师傅的一张图
由控制台进行输入,获取gadget
和需要执行的命令传入到入口GeneratePayload
中,然后由GeneratePayload
调用具体的ObjectPayload
接口的实现来获取实例,在这个过程中ObjectPayload
又去调用了Gadgets
、Reflections
等进行初始化然后将对象返回给GeneratePayload
,最后GeneratePayload
调用Serializer
的序列化方法将其序列化后返回并打印到控制台。