Apereo CAS 4.X反序列化漏洞分析

 

0x01 漏洞描述

其实这个洞在2016年时候就出来了,Apereo Cas一般是用来做身份认证的,所以有一定的攻击面,漏洞的成因是因为key的默认硬编码,导致可以通过反序列化配合Gadget使用。

 

0x02 环境搭建

下载地址,直接选择对应的版本,选择war进行下载,然后导入到tomcat中,运行即可。

 

0x03 漏洞分析

这里做个区分,因为内部逻辑稍微有点不一样。

Apereo CAS 4.1.X ~ 4.1.6

因为我不确定这个的流程是什么样的,但是我知道一点这个的 demo 目前是基于 spring mvc 的,因此我在org/springframework/web/servlet/FrameworkServlet处的 doPost 方法下断点,为什么是 doPost 原因就是我现在登录的数据包是 Post

doPost 方法中调用 FrameworkServlet#processRequest 进行处理 request 对象和 response 对象。

protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.processRequest(request, response);
    }

跟进 FrameworkServlet#processRequest ,调用 DispatcherServlet#doService 处理 request 对象和 response 对象。

protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
        try {
            this.doService(request, response);

继续跟进 DispatcherServlet#doService ,调用 DispatcherServlet#doDispatch 处理 request 对象和 response 对象。

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ...
        try {
            this.doDispatch(request, response);
        } finally {
            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
                this.restoreAttributesAfterInclude(request, attributesSnapshot);
            }

        }

继续跟进 DispatcherServlet#doDispatch ,而 handle 实际上是一个 Implement ,而这里进行经过处理之后来到的是 FlowHandlerAdapter.handle 中。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
        ...
                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

跟进 FlowHandlerAdapter.handle ,这里首先通过 getFlowExecutionKey 方法从 request 对象中获取我们传入的 execution 中的值,也就是 flowExecutionKey ,然后把这个 flowExecutionKey 交给 resumeExecution 方法进行处理。

跟进 resumeExecution 方法,来到org/springframework/webflow/executor/FlowExecutorImpl这个类中,调用 getFlowExecution 方法处理,从前面 execution 中的获取到的 key 值。

public FlowExecutionResult resumeExecution(String flowExecutionKey, ExternalContext context) throws FlowException {
        ...
        FlowExecutionKey key = this.executionRepository.parseFlowExecutionKey(flowExecutionKey);
        ...
            try {
                FlowExecution flowExecution = this.executionRepository.getFlowExecution(key);
                flowExecution.resume(context);

跟进 getFlowExecution 方法,先把之前的 key 调用 getData 方法,转变化成byte数组,之后调用 transcoder.decode 处理前面的byte数组。

public FlowExecution getFlowExecution(FlowExecutionKey key) throws FlowExecutionRepositoryException {
        if (!(key instanceof ClientFlowExecutionKey)) {
            throw new IllegalArgumentException("Expected instance of ClientFlowExecutionKey but got " + key.getClass().getName());
        } else {
            byte[] encoded = ((ClientFlowExecutionKey)key).getData();

            try {
                ClientFlowExecutionRepository.SerializedFlowExecutionState state = (ClientFlowExecutionRepository.SerializedFlowExecutionState)this.transcoder.decode(encoded);

跟进 transcoder.decode ,首先需要先cipherBean.decrypt进行一步解密操作,解密传入的byte数组,然后就会来到反序列化入口 readObject 了。

再看一下加密算法,算法是 AES/CBC/PKCS7KeyStore 是硬编码在 spring-webflow-client-repo-1.0.0.jar/etc/keystore.jcek

而实际上当你全局搜索login关键字的时候,你会在 WEB-INF/cas-servlet.xml 中看到 loginHandlerAdapter 对应的类是 SelectiveFlowHandlerAdapter ,而 loginFlowExecutor 对应的类是 FlowExecutorImpl ,实际上和上面从输入下断点来看是一样的。

  <bean id="loginHandlerAdapter" class="org.jasig.cas.web.flow.SelectiveFlowHandlerAdapter"
        p:supportedFlowId="login" p:flowExecutor-ref="loginFlowExecutor" p:flowUrlHandler-ref="loginFlowUrlHandler" />

  <bean id="loginFlowUrlHandler" class="org.jasig.cas.web.flow.CasDefaultFlowUrlHandler" />

  <bean name="loginFlowExecutor" class="org.springframework.webflow.executor.FlowExecutorImpl" 
        c:definitionLocator-ref="loginFlowRegistry"
        c:executionFactory-ref="loginFlowExecutionFactory"
        c:executionRepository-ref="loginFlowExecutionRepository" />

Apereo CAS 4.1.7 ~ 4.2.X

这个版本的key默认是随机生成的,所以需要先硬编码一下WEB-INF/cas.properties这个文件,方便调试。

实际上到ClientFlowExecutionRepository#getFlowExecution这里之前数据流还是没有变。

改变的地方是 decode 时候解密 decrypt 操作来到的是 CasWebflowCipherBean 当中。

CasWebflowCipherBean 当中调用的是 webflowCipherExecutor.decode 进行操作。

    public byte[] decrypt(final byte[] bytes) {
        return webflowCipherExecutor.decode(bytes);
    }

跟进 webflowCipherExecutor.decode ,这里的 key 就是从我们刚刚默认的配置文件WEB-INF/cas.properties中拿出来的。

 

0x04漏洞利用

Apereo CAS 4.1.X ~ 4.1.6

由于在当前版本中有 Commons-collections4 这个库。

使用可以利用 yso 中的 CC2 这个payload进行poc构造,构造时候直接使用

org.jasig.spring.webflow.plugin.EncryptedTranscoder.encode 进行加密即可,然后替换 execution 这个参数位置的值。

漏洞复现。

根据,这个文章里面,提到的

对整个项目进行搜索发现了一个静态方法满足我们的需求

org.springframework.webflow.context.ExternalContextHolder.getExternalContext()

通过这个方法可以获取到当前进行关联的上下文信息,然后通过“getNativeRequest()”方法获取request对象通过getNativeResponse()方法获取response对象。

可以通过 “org.springframework.cglib.core.ReflectUtils.defineClass().newInstance();”这个public方法来加载我们的payload。

根据这个方式,自己写一个 ApereoExec 类,修改 ysoserial/payloads/util/Gadgets 里面的部分东西,使用javassist进行patch就好了。

Apereo CAS 4.1.7 ~ 4.2.X

这里需要自己构造了,看一下前面的加密过程,加密的时候把 Object 传入,然后调用 GZIPOutputStream 进行压缩,序列化,然后调用 cipherBean.encrypt 进行加密,最后 base64 再加密一下。

    public byte[] encode(Object o) throws IOException {
        if (o == null) {
            return new byte[0];
        } else {
            ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
            ObjectOutputStream out = null;

            try {
                if (this.compression) {
                    out = new ObjectOutputStream(new GZIPOutputStream(outBuffer));
                } else {
                    out = new ObjectOutputStream(outBuffer);
                }

                out.writeObject(o);
            } finally {
                if (out != null) {
                    out.close();
                }
            }
            try {
                return this.cipherBean.encrypt(outBuffer.toByteArray());
            } catch (Exception var7) {
                throw new IOException("Encryption error", var7);
            }
        }
    }

而这个版本demo中存在C3P0 gadget,因此可以利用这个来操作。

当然也可以把这部分内容抓出来,手动写一个反序列化解密操作。

 

Reference

https://apereo.github.io/2016/04/08/commonsvulndisc/

http://www.00theway.org/2020/01/04/apereo-cas-rce/

(完)