萌新第一次分析Java漏洞,大佬勿喷哦。
时间仓促(我要蹭热度+今天公司活多),就不写得很详细了,大概知道这个漏洞是怎么回事危害到底大不大(请跳到第三节)就行。
1 分析一下EXP
先看了下EXP:
config.py,确实是纯配置文件,没啥好说的。
service.py,建了一个flask,实际上是host了一个文件,也没什么问题:
jar包内容是啥我不关心,反正我没运行。
exploit.py,这里是重头戏:
可以看到是拼接了一个sql语句,然后构造一个随机字符串,反复循环访问removal_url,如果返回值中有message和data就执行derby_url传递sql语句(说明此POC需要爆破一个8位字符串),使用语句下载安装jar包再移除。命令执行毫无疑问是发生在安装jar包的过程中。
这里还暴露了一点,那就是RCE的条件与数据库类型有关,因为install_jar这个函数只在少数数据库如apache derby中有效。
2 简单瞅瞅源码
心里有数之后,转回去看对应版本的nacos源码(我真心不想搭环境)。
参照POC作者的描述选择2.3.2版本,找到derby语句的controller:
https://github.com/alibaba/nacos/blob/2.3.2/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigOpsController.java
对这个函数简单写了下注释:
@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 { #如果使用的数据源不是derby就返回
if (!DatasourceConfiguration.isEmbeddedStorage()) {
return RestResultUtils.failed("The current storage mode is not Derby");
}
LocalDataSourceServiceImpl dataSourceService = (LocalDataSourceServiceImpl) DynamicDataSource
.getInstance().getDataSource();
if (StringUtils.startsWithIgnoreCase(sql, selectSign)) { #如果以select开头
if (!StringUtils.containsIgnoreCase(sql, limitSign)) { #如果不包含ROWS FETCH NEXT,就添加
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"); #如果不以select开头就返回
} catch (Exception e) {
return RestResultUtils.failed(e.getMessage());
}
}
回过来看POC,derby传入的语句倒确实符合这个规则(ROWS FETCH NEXT有点好笑不知道为啥):
select * from (select count(*) as b, S_EXAMPLE_{id}('{cmd}') as a from config_info) tmp /*ROWS FETCH NEXT*/
再看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));
databaseOperate.dataImport(file).whenComplete((result, ex) -> { #使用body执行dataImport,需要跟入
NotifyCenter.publishEvent(new DerbyImportEvent(true));
if (Objects.nonNull(ex)) { #如果ex(看来是exception,能不能写全)不为空,返回失败提醒
response.setResult(RestResultUtils.failed(ex.getMessage()));
return;
}
response.setResult(result);
});
}, response);
return response;
}
跟进DatabaseOperate,目前来看符合条件(这里用了抽象类)的类只有两个,由于没有详细看代码,这里又用了抽象类,我理解为两个类都是可能代入的(也许derby数据库只会用到其中一个类):
随便抽了一个类的dataImport看一下:
@Override #覆写自interface DatabaseOperate
public CompletableFuture<RestResult<String>> dataImport(File file) {
return CompletableFuture.supplyAsync(() -> {
try (DiskUtils.LineIterator iterator = DiskUtils.lineIterator(file)) { #读取每一行
int batchSize = 1000;
List<String> batchUpdate = new ArrayList<>(batchSize);
List<CompletableFuture<Void>> futures = new ArrayList<>();
List<Boolean> results = new CopyOnWriteArrayList<>();
while (iterator.hasNext()) {
String sql = iterator.next(); #取出每一行到sql
if (StringUtils.isNotBlank(sql)) {
batchUpdate.add(sql); #如果非空,加入batchUpdate
}
if (batchUpdate.size() == batchSize || !iterator.hasNext()) { #如果读到最大值 或者读完了
List<ModifyRequest> sqls = batchUpdate.stream().map(s -> { #应该是一种lambda函数?
ModifyRequest request = new ModifyRequest(); #处理每一个成员,setSQL
request.setSql(s);
return request;
}).collect(Collectors.toList());
futures.add(CompletableFuture.runAsync(() -> results.add(doDataImport(jdbcTemplate, sqls)))); #这里单看函数名是执行语句去了
batchUpdate.clear();
}
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); #下面懒得看,
int code = 500;
if (!CollectionUtils.isEmpty(results)) {
code = (!results.stream().anyMatch(Boolean.FALSE::equals)) ? 200 : 500;
}
return RestResult.<String>builder().withCode(code).withData("").build();
} catch (Throwable ex) {
LOGGER.error("An exception occurred when external data was imported into Derby : ", ex);
return RestResultUtils.failed(ex.getMessage());
}
});
}
然后看一下doDataImport:
default Boolean doDataImport(JdbcTemplate template, List<ModifyRequest> requests) { #两个类都继承自BaseDatabaseOperate
final String[] sql = requests.stream().map(ModifyRequest::getSql).map(DerbyUtils::insertStatementCorrection) #有点像linq 我天哪太优雅辣^ ^
.toArray(String[]::new); #对MAP中的成员依次执行ModifyRequest::getSql DerbyUtils::insertStatementCorrection 然后转回成字符串
int[] affect = template.batchUpdate(sql); #这里最后会执行语句
return IntStream.of(affect).count() == requests.size();
}
跟到batchUpdate,这里终于执行了:
3 危害大不大
得从漏洞是否未授权和漏洞产生条件两个方面看。
首先derby数据库是nacos默认数据库,理论上看有漏洞的数据库应该比较多。
从授权条件来看这里是需要鉴权的(但是默认某些配置会让他未授权访问):
笔者个人认为这个漏洞最有可能用于提权,加上官方的态度(意思是不修)应该会是一个比较常驻的提权手段。
彩蛋
这个洞也是早有渊源。
https://github.com/alibaba/nacos/issues/10613
虽然他说的挺有道理的,但熟悉我的读者已经知道我要说什么了,feature!
原文始发于微信公众号(重生之成为赛博女保安):原来是老生常谈?网传nacos RCE 0day分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论