不出意外,groovy利用还是脱不开AST注解、GroovyShell、GroovyClassLoader这几个东西
fastjson groovy链
来自2022KCON 浅蓝师傅的议题
利用条件:
1.2.76 <= fastjson < 1.2.83
存在Groovy依赖
两次parse解析,且第一次会抛出异常,第一次异常不能中断程序
出网
看起来是个很鸡肋的洞,可以说fastjson在高版本的限制已经非常大了,不过其中提前缓存期望类的姿势依旧值得学习
根据fastjson反序列化匹配黑白名单过程(见下面链接),我们可以知道在fastjson >= 1.2.68版本下利用期望类绕过的可能仍然存在,虽然AutoCloseable被列入了期望类黑名单
但是Throwable.class依旧可用,但是我们今天要提到的,不是上传Exception子类这种鸡肋的利用手法,而是利用到了org.codehaus.groovy.control.CompilationFailedException
1 | <dependency> |
TypeUtils.cast缓存期望类
我们利用Throwable作为白名单期望类,可以轻易的利用ThrowableDeserializer反序列化CompilationFailedException类,我们这里关注的是,为什么要传入"unit":{}
?
1 | { |
第一次checkAutoType把Exception设置为了期望类,第二次通过ThrowableDeserializer调用到checkAutoType ,通过TypeUtils.loadClass加载了CompilationFailedException
我们看一下loadClass返回后的代码
注意到是个死循环解析json字符串, checkAutoType后应该解析"unit":{}
很显然会进入最后一个else,把待解析的内容先放到otherValues
key为unit,parser.parse()解析JSON字符串的unit属性的值,指定为空,所以这里值为空
注意以上都是在for循环中进行的,for循环结束后exClass为CompilationFailedException,otherValues为unit键和空值
然后otherValues不为空,取出exClass的反序列化器ThrowableDeserializer并强转为JavaBeanDeserializer
接着看下面的if,遍历所有额外字段,通过反射设置到异常对象的对应属性上;
若字段类型不匹配,则进行TypeUtils.cast类型转换后再赋值。
比如这里fieldInfo为”unit”,其fieldType为class org.codehaus.groovy.control.ProcessingUnit,而解析的value是为空的JSONObject,肯定不能直接反射赋值上去对吧,所以得先TypeUtils.cast类型转换,再setValue赋值
我们继续跟进TypeUtils.cast,中间略过,跟进到TypeUtils.castToJavaBean,这里会getDeserializer去取ProcessingUnit的反序列化器,然后调用createInstance去实例化
我们跟进这个getDeserializer,可以发现其对应的反序列化器是通过createJavaBeanDeserializer创建的,其实大部分类的反序列化器都是经过这个方法创建的。得到的反序列化器为JavaBeanDeserializer,然后一个非常关键的点,调用了putDeserializer
这里把ProcessingUnit放到了反序列化器缓存表deserializers里
我们想一下,这个deserializers是不是很眼熟?
对了!就是在checkAutoType中,会从deserializers缓存表中查找传入的typeName
这就导致经过TypeUtils.cast会缓存类,之后的程序可以直接传入该类,或者作为期望类(这样就能传入其子类)进行绕过
JavaStubCompilationUnit
ProcessingUnit有什么子类可以用吗?
JavaStubCompilationUnit继承了ProcessingUnit
JavaStubCompilationUnit构造函数接收三个参数,并调用了父类的构造函数
super向上传递会调用到ProcessingUnit的构造函数,进而调用到setClassLoader
setClassLoader中会实例化GroovyClassLoader
GroovyClassLoader会解析config中的Classpath,并调用addClasspath把url添加到ucp中
完成从config中提取path后,回到CompilationUnit,调用了ASTTransformationVisitor.addPhaseOperations
经过addPhaseOperations -> addGlobalTransforms -> doAddGlobalTransforms
在这个函数内获取了META-INF/services/org.codehaus.groovy.transform.ASTTransformation配置文件作为globalServices
根据配置文件去获取加载class,而且如果是ASTTransformation的子类,还会进行实例化
这里其实就是spi的思想,我们在snakeYaml等很多地方都遇到过spi机制的应用
哎有同学可能会有疑问,我们不是只用到了CompilationUnit吗,为什么要用JavaStubCompilationUnit封装?
因为CompilationUnit有无参构造方法,而fastjson检测到没有无参构造方法才会去调用有参构造
利用
见fastjsonVul
https://github.com/Lonely-night/fastjsonVul
先编译一个有spi服务的jar,然后需要有两次parse去解析两个payload,第一次解析是为了把org.codehaus.groovy.control.CompilationFailedException的unit字段,也就是ProcessingUnit设置为期望类,这样就能反序列化ProcessingUnit及其子类了。第二次解析是查找远程service,进而通过spi实例化恶意类
1 | //两次parse |
1 |
|
为什么fastjson >=1.2.76才能用这个链?
因为在此版本下ThrowableDeserializer中并没有TypeUtils.cast,也就无从设置期望类的缓存了
但是这种利用需要两次parse,因为第一次parse,ProcessingUnit是无法设置到Exception的字段中的,所以要求第一次parse后程序不能中断,利用条件很苛刻
另外还有配合ognl / commons-io / aspectj / commons-codec的写文件,属于是大师傅的炫技之作了
https://github.com/kezibei/fastjson_payload/blob/main/src/test/Fastjson26_ognl_io_write_4.java
总结说来,这个版本fastjson出现的漏洞源于可以将期望类进行缓存
参考: