“ 作者:blckder02”
前言
该漏洞由浅蓝研究发现,可在特定条件下绕过 AutoType 关闭限制加载远程对象进行反序列化。
影响版本: 特定依赖存在下影响 ≤1.2.80。
漏洞公告:
https://github.com/alibaba/fastjson/wiki/security_update_20220523
Groovy 命令执行
Fastjson
1
环
境
搭建
1. Fastjson 1.2.80
2. Groovy 3.0.8
3. JDK 8u12
在 pom.xml中导入依赖:
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.80version>
dependency>
<dependency>
<groupId>org.codehaus.groovygroupId>
<artifactId>groovy-allartifactId>
<version>3.0.8version>
dependency>
漏
洞
复现
构造恶意类,要有@GroovyASTTransformation注解,并且实现ASTTransformation接口;
起一个服务端,把恶意类的 class 文件放到根目录下,再创建一个META-INF/services/org.codehaus.groovy.transform.ASTTransformation文件,在文件中写入恶意类的名称;
python -m http.server 8081
POC,classpathList为远程服务端地址;
命令执行成功;
POC 调试
在parseObject()中获取到第一个@type字段中的类名,带入checkAutoType();
从mapping中获取到了 clazz,然后返回java.lang.Exception;
因为java.lang.Exception是继承 Throwable 类的,所以获取到的是ThrowableDeserializer类型的deserializer;
这里目的是要利用ThrowableDeserializer.deserializer()中调用的checkAutoType()方法中第二个参数不为 null ,和 1.2.68 版本的绕过原理一样;
跟进deserialze(),获取到第二个@type字段中的类名,带入checkAutoType();
跟进,expectClass参数值为Throwable.class,所以expectClassFlag赋为 true;
然后进行黑名单校验,CompilationFailedException不在黑名单中,能顺利通过校验;
因为expectClassFlag为 true, 所以在1185 行加载了CompilationFailedException,可以绕过后面对 AutoType 的校验,并且 1190 行把它添加进了 mapping;
跟进到ThrowableDeserializer.deserialze(),调用了cast(),成员变量类型是ProcessingUnit;
跟进TypeUtils.cast(),调用castToJavaBean();
获取到JavaBeanDeserializer类型的deserializer;
获取到deserializer之后,把org.codehaus.groovy.control.ProcessingUnit put 进了this.deserializers,这是后面绕过 AutoType 校验的关键!
1195 行进行实例化,实例化的结果是 null ;
返回 null 值,调用setValue()进行赋值,由于值为 null ,抛出异常;
然后进入 POC 的 catch 分支,反序列化 poc2。
获取到第一个@type字段,带入checkAutoType();
因为前面 把ProcessingUnit put 进this.deserializers了,所以这里可以直接从this.deserializers中找到ProcessingUnit,返回 clazz;
获取到JavaBeanDeserializer类型的deserializer,调用deserialze();
ref是第二个@type值JavaStubCompilationUnit,expectClass是ProcessingUnit类,带入checkAutoType();
跟进,expectClassFlag为true,在 1185 行加载JavaStubCompilationUnit,并且将它添加进 mapping;
调用JavaBeanDeserializer.deserialze();
跟进调用了parseField();
跟进调用了deserialze(),返回的value是 CompilerConfiguration 类型,并且把传入的远程地址赋给了classpath;
117 行又把整个 CompilerConfiguration 对象赋给JavaStubCompilationUnit的config参数;
在 982 行的 for 循环中,从fieldValues中获取参数值存入params[]中,因为我们只给config传了值,所以params[]只有一个参数值;
1044 行 new 了一个JavaStubCompilationUnit对象;
JavaStubCompilationUnit 的构造方法有三个参数,第一个参数是我们自定义的,第二个参数是调用setClassLoader()获取的,第三个参数是 null;
setClassLoader()中 new 了 GroovyClassLoader 对象;
跟到GroovyClassLoader的构造方法,从config中获取classpath添加进GroovyClassLoader对象;
返回到CompilationUnit构造方法,184 行调用了addPhaseOperations(),跟进再调用了ASTTransformationVisitor.addPhaseOperations() ;
跟进addGlobalTransforms() -> doAddGlobalTransforms(),transformLoader是从JavaStubCompilationUnit对象中获取到的GroovyClassLoader;
使用getResource()从加载器中获取META-INF/services/org.codehaus.groovy.transform.ASTTransformation文件下的资源
跟进getResource(),可以看到,加载资源遵循双亲委派型,会首先委托父类加载器,委托到启动类加载器时,会从 Bootstrap classpath 对应的 jar 包或目录中加载资源;
getBootstrapClassPath()最终获取到的是bcp字段的值,也就是说从bcp中的 jar 包中加载资源;
然后调用findResources(),跟进,再调用ucp的findResources(),ucp是 URLClassPath 类型,一共会调用三次ucp.findResources(),每次的ucp都不一样;
因为每次的类加载器不一样,所以 ucp 不一样,第三次使用的是GroovyClassLoader,而GroovyClassLoader中含有指定的classpath http://127.0.0.1:8081/,所以会从远程地址加载资源;
资源加载进来后,就开始逐行读取每个资源的内容,把不以#开头的行放到transformNames中;
循环到第三次globalServices时,transformNames中已经有四个值了;
接着探测远程地址的资源,跟进hasMoreElements() -> next() -> 第二次的hasMoreElements() -> next() -> e.hasMoreElements() -> next();
跟进findResource(),创建了与远程地址资源的 http 连接,设置请求头为HEAD,对远程资源进行探测;
然后读取远程资源,使用了URLStreams.openUncachedStream();
跟进可以看到调用getInputStream(),向远程资源发起了 http 请求;
命令行中可以看到两次请求的记录;
然后也是将远程资源中的内容读到transformNames中,这样就获取到恶意类的名称;
接着下面调用addPhaseOperationsForGlobalTransforms();
跟进,在该方法中依次加载并实例化了transformNames中的所有类;
校验了加载的类上是否含有@GroovyASTTransformation注解,所以在构造恶意类时要加上注解;
并且得实现或继承ASTTransformation接口,所以恶意类要实现ASTTransformation,然后实例化触发命令执行。
jython+spring-context+postgresql 命令
Fastjson
2
环
境
搭建
1. Fastjson 1.2.80
2. jython 1.1
3. spring-context 5.0.2.RELEASE
4. postgresql 42.3.0 (42.3.0 < 版本 < 42.3.2)
在 pom.xml中添加如下依赖:
漏
洞
复现
利用org.springframework.context.support.ClassPathXmlApplicationContext来加载远程指定的 XML 配置文件,在配置文件中定义一个 bean ,bean 中通过使用 Spring EL 表达式来调用java.lang.ProcessBuilder的start()方法来执行命令。
构造 bean 文件spel.xml:
起一个服务端,把 bean 文件放在根目录下;
构造POC:
ParseException继承Exception的子类,有一个PyObject类型参数type;
PyConnection继承PyObject,有一个Connection类型的参数connection;
PgConnection实现了Connection的子接口,构造函数中有的五个参数,连接了数据库。
host、port、user、database参数值可以随便传,不影响命令执行;
socketFactory得是ClassPathXmlApplicationContext,socketFactoryArg是远程 bean 文件地址。
POC 调试
首先对a进行反序列化,原理作用和 Groovy 链一样;
获取到第一个@type的值java.lang.Exception,带入checkAutoType();
从 mapping 中获取并返回 clazz;
Exception继承了Throwable,所以下面调用ThrowableDeserializer.deserialze,接着获取到第二个@type的值,带入checkAutoType(),第二个参数值不为 null;
expectClassFlag为 true ,加载ParseException类并返回;
获取了type参数的类型,调用cast();
跟进到castToJavaBean(),把PyObject类型 put 进了this.deserializer,为后面绕过 AutoType 检测做准备;
返回的value不为 null,所以下面调用setValue()不会报错,所以不用像 Groovy 链那样使用try-catch。
然后接着反序列化b,获取到第一个@type值带入checkAutoType(),能直接从this.deserializer获取到clazz;
PyObject对应的deserializer类型是FastjsonASMDeserializer_1_PyObject,但还是调用的JavaBeanDeserializer.deserialze();
获取到第二个@type的值带入checkAutoType(),第二个参数不为 null;
加载并返回PyConnection;
跟进JavaBeanDeserializer.deserialze() -> DefaultFieldDeserializer.parseField() -> getFieldValueDeserilizer(),返回JavaBeanDeserializer类型;
调用deserialze(),经过一堆循环,获取了下一个@type的值,带入checkAutoType(),第二个参数不为null;
加载并返回PgConnection;
调用deserialze(),遍历到hostSpecs参数时,逐步调用到getFieldValueDeserilizer(),返回一个ObjectArrayCode类型;
然后调用ObjectArrayCode.deserialze();
跟进parserArray(),调用deserialze();
获取host和port参数值,实例化了一个HostSpec对象并返回;
把HostSpec对象转换为数组类型,赋给hostSpecs参数;
接着调用paseField() ,跟进到 MapDeserializer.deserialze(),调用了parseObject();
在parseObject()中把socketFactory和socketFactoryArg的存入了 map 中;
然后将info放入了fieldValues,另外几个参数赋值都差不多;
参数全部赋值完毕后,实例化一个PgConnection对象;
跟进到PgConnection的构造函数,调用了ConnectionFactory.openConnection();
逐步跟进到getSocketFactory(),调用了ObjectFactory.instantiate();
反射获取了ClassPathXmlApplicationContext类和构造函数,最后实例化ClassPathXmlApplicationContext对象;
加载配置文件,实例化文件中的恶意对象。
aspectjtools 读文件
Fastjson
3
环
境
搭建
1. Fastjson 1.2.80
2. aspectj 1.8.6
在pom.xml中导入依赖:
漏
洞
复现
直接来看看网上普遍使用的POC,要分三次打:
由于SourceTypeCollisionException的newAnnotationProcessorUnits参数是接口类型;
会在TypeUtils.castToJavaBean()中返回一个代理对象,并不会像前两条链一样将ICompilationUnit put 进this.deserializer;
所以 jsonStr1 中不添加newAnnotationProcessorUnits参数,它的作用是把SourceTypeCollisionException添加进mapping,以便后面使用。
MiscCodec.deserialze()中调用了toJavaObject(),可以把 JSON对象转换成 Java 对象,然后把该参数类型 put 进this.deserializer;
MapDeserializer.deserialze()可以把传入的各参数的值赋给对应的对象;
jsonStr2 中 各类对应的deserializer如下:
Class |
deserializer |
java.lang.Class |
MiscCodec |
java.lang.String |
StringCode |
java.util.Locale |
MiscCodec |
com.alibaba.fastjson.JSONObject |
MapDeserializer |
jsonStr2 的作用就是将org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit类型 put 进this.deserializer;
同时构造 json 语法错误,使之抛出异常,进入下一个反序列化。
jsonStr3 的作用就是利用BasicCompilationUnit类中的getContents()方法来读取文件内容;
“x”:{}的作用是将BasicCompilationUnit对象序列化为 JSON 数据,否则输出结果如下:
经过分析发现,其实 jsonStr2 简化为如下代码,同样可以实现读文件。
POC 调试
获取到第一个@type的值,带入checkAutoType(),从mapping中获取并返回Exception;
调用ThrowableDeserializer.deserialze(),获取到第二个@type的值,带入checkAutoType(),expectClassFlag为 true,加载并返回SourceTypeCollisionException,还把SourceTypeCollisionException添加进了mapping,这是后面绕过 AutoType 关闭限制的关键;
然后创建并返回一个 Exception ,结束第一个反序列化。
开始对第二个字符串进行反序列化,获取到第一个@type的值,带入checkAutoType(),从this.deserializers中获取并返回Class;
然后调用MiscCodec.deserialze();
获取到第二个@type的值,带入checkAutoType(),从mapping中获取并返回String;
接着调用StringCode.deserialze(),获取到下一个@type的值,带入checkAutoType(),从mapping中获取并返回Locale;
调用MiscCodec.deserialze(),获取到下一个@type的值,直接在 310 行获取到com.alibaba.fastjson.JSONObject,不用再调用checkAutoType();
接着往下就是调用MapDeserializer.deserialze();
获取到java.lang.String,然后调用StringCode.deserialze(),返回@type;
接着分别获取后面的两个值,存入map;
一直返回到MiscCodec.deserialze(),调用了toJavaObject();
跟进toJavaObject() -> TypeUtils.cast() -> cast() -> castToJavaBean(),获取SourceTypeCollisionException带入checkAutoType(),从mapping中获取并返回 clazz;
然后又调用castToJavaBean();
跟进createInstance(),获取到ICompilationUnit[]类型的参数newAnnotationProcessorUnits,调用parseField() -> getFieldValueDeserilizer() -> getDeserializer();
因为参数是数组类型,所以得到ObjectArrayCode类型的deserializer,然后把它 put 进this.deserializer,这里的type是带[L…;字符的;
调用ObjectArrayCode.deserialize(),获取到interface org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit,带入parseArray();
跟进到getDeserializer()将ICompilationUnit接口类型的deserializer put 进this.deserializer,这里就和前面两条链的目的一样,为后续绕过 AutoType 做准备;
然后逐步返回,到结束反序列化时,抛出了一个异常,由于构造的 json 字符串闭合不正确;
接着就进入到 catch 分支,对第三个 json 数据进行反序列化。
获取到第一个@type,带入checkAutoType(),从this.deserializer中找到并返回ICompilationUnit类;
接着调用deserialize(),获取到第二个@type的值,带入checkAutoType(),第二个参数不为 null;
加载并返回BasicCompilationUnit,然后调用deserialize();
跟进,在deserialize()中把BasicCompilationUnit类型中不为 null 的参数放到params中,然后实例化该类;
最后返回JSONObject对象,到这里还没读到文件中的内容;
继续跟进println(),到MapSerializer.write(),再继续跟进,看不到调试过程了;
但是一直点,会跳到BasicCompilationUnit.getContents(),这里将D:/1.txt转换为 File 对象,调用了Util.getFileCharContent();
在getFileCharContent()中创建输入流,以字符数组的形式读取文件中的内容;
然后就是返回、输出文件内容。
补丁分析
Fastjson
4
更新到 1.2.83 版本。
checkAutoType()中,在从mapping获取到 java.lang.Execption后,添加了一个校验;
如果满足expectClass为 null 、clazz不为 null 、clazz实现或继承Throwable.class、未开启 AutoTpye,那把 clazz赋为 null;
最后只能返回一个 null。
参考链接
https://github.com/su18/hack-fastjson-1.2.80
https://y4er.com/posts/fastjson-1.2.80
https://github.com/knownsec/KCon/blob/master/2022/Hacking%20JSON%E3%80%90KCon2022%E3%80%91.pdf
原文始发于微信公众号(中孚安全技术研究):Fastjson 1.2.80 反序列化利用链分析