Apache DolphinScheduler任意文件写到RCE
Harder 发表于 天津 漏洞分析 434浏览 · 2024-09-01 14:03

0x01 产品介绍

Apache DolphinScheduler 是一个开源的分布式工作流任务调度系统,旨在通过图形化界面帮助用户更高效地管理任务调度和数据流控制。它特别适用于大数据领域的任务调度和工作流编排。

0x02漏洞危害

Apache DolphinScheduler由于资源管理功能未对 currentDir 参数进行有效校验,攻击者可以利用此漏洞进行任意文件读写,有可能导致敏感信息泄露和写入恶意后门等危害。

影响版本:

Apache DolphinScheduler <= 3.2.1

0x03环境搭建

docker run --name dolphinscheduler-standalone-server -p 12345:12345 -p 25333:25333 -d apache/dolphinscheduler-standalone-server:3.2.0

0x04漏洞分析

网站首页:

登录到后台发现有个接口可以更新任意已经存在的文件在接口org.apache.dolphinscheduler.api.service.impl#updateResource函数里面

public Result<Object> updateResource(User loginUser,
                                         String resourceFullName,
                                         String resTenantCode,
                                         String name,
                                         ResourceType type,
                                         MultipartFile file) {
        Result<Object> result = new Result<>();

        User user = userMapper.selectById(loginUser.getId());
        if (user == null) {
            log.error("user {} not exists", loginUser.getId());
            putMsg(result, Status.USER_NOT_EXIST, loginUser.getId());
            return result;
        }

        String tenantCode = getTenantCode(user);

        if (!isUserTenantValid(isAdmin(loginUser), tenantCode, resTenantCode)) {
            log.error("current user does not have permission");
            putMsg(result, Status.NO_CURRENT_OPERATING_PERMISSION);
            return result;
        }

        String defaultPath = storageOperate.getResDir(tenantCode);

        StorageEntity resource;
        try {
            resource = storageOperate.getFileStatus(resourceFullName, defaultPath, resTenantCode, type);
        } catch (Exception e) {
            log.error("Get file status fail, resource path: {}", resourceFullName, e);
            putMsg(result, Status.RESOURCE_NOT_EXIST);
            throw new ServiceException((String.format("Get file status fail, resource path: %s", resourceFullName)));
        }

        // TODO: deal with OSS
        if (resource.isDirectory() && storageOperate.returnStorageType().equals(ResUploadType.S3)
                && !resource.getFileName().equals(name)) {
            log.warn("Directory in S3 storage can not be renamed.");
            putMsg(result, Status.S3_CANNOT_RENAME);
            return result;
        }

        // check if updated name of the resource already exists
        String originFullName = resource.getFullName();
        String originResourceName = resource.getAlias();

        // the format of hdfs folders in the implementation has a "/" at the very end, we need to remove it.
        originFullName = originFullName.endsWith("/") ? StringUtils.chop(originFullName) : originFullName;
        name = name.endsWith("/") ? StringUtils.chop(name) : name;
        // updated fullName
        String fullName = String.format(FORMAT_SS,
                originFullName.substring(0, originFullName.lastIndexOf(FOLDER_SEPARATOR) + 1), name);
        if (!originResourceName.equals(name)) {
            try {
                if (checkResourceExists(fullName)) {
                    log.error("resource {} already exists, can't recreate", fullName);
                    putMsg(result, Status.RESOURCE_EXIST);
                    return result;
                }
            } catch (Exception e) {
                throw new ServiceException(String.format("error occurs while querying resource: %s", fullName));
            }

        }

        result = verifyFile(name, type, file);
        if (!result.getCode().equals(Status.SUCCESS.getCode())) {
            return result;
        }

        Date now = new Date();

        resource.setAlias(name);
        resource.setFileName(name);
        resource.setFullName(fullName);
        resource.setUpdateTime(now);
        if (file != null) {
            resource.setSize(file.getSize());
        }

        // if name unchanged, return directly without moving on HDFS
        if (originResourceName.equals(name) && file == null) {
            return result;
        }

        if (file != null) {
            // fail upload
            if (!upload(loginUser, fullName, file, type)) {
                log.error("Storage operation error, resourceName:{}, originFileName:{}.",
                        name, RegexUtils.escapeNRT(file.getOriginalFilename()));
                putMsg(result, Status.HDFS_OPERATION_ERROR);
                throw new ServiceException(
                        String.format("upload resource: %s file: %s failed.", name, file.getOriginalFilename()));
            }
            if (!fullName.equals(originFullName)) {
                try {
                    storageOperate.delete(originFullName, false);
                } catch (IOException e) {
                    log.error("Resource delete error, resourceFullName:{}.", originFullName, e);
                    throw new ServiceException(String.format("delete resource: %s failed.", originFullName));
                }
            }

            ApiServerMetrics.recordApiResourceUploadSize(file.getSize());
            return result;
        }

        // get the path of dest file in hdfs
        String destHdfsFileName = fullName;
        try {
            log.info("start  copy {} -> {}", originFullName, destHdfsFileName);
            storageOperate.copy(originFullName, destHdfsFileName, true, true);
            putMsg(result, Status.SUCCESS);
        } catch (Exception e) {
            log.error(MessageFormat.format(" copy {0} -> {1} fail", originFullName, destHdfsFileName), e);
            putMsg(result, Status.HDFS_COPY_FAIL);
            throw new ServiceException(MessageFormat.format(
                    Status.HDFS_COPY_FAIL.getMsg(), originFullName, destHdfsFileName));
        }

        return result;
    }

然后我就立即想到了一点修改web根目录下已经存在的文件,看是否有jsp文件。

然后我们在路径/opt/dolphinscheduler/ui下翻找了很久包括它的子目录也翻找了并没有发现任何jsp文件

在这里我们又找到一个接口 可以改任意存在文件的名字,那不就RCE了嘛!!!说干就干尝试利用

然后我们又发现任意目录下必又favicon.ico这个文件,然后我们尝试更改这个文件名字为jsp文件

最后一步可把人熬坏了,好不容易以为RCE了,结果没注意到不解析jsp文件。又联想到了覆盖xml热部署加载实现jsp解析。

但是我突然想到做权限维持的时候,我们直接组修改/etc/shadow文件,修改账户密码即可。(限制很大)

所以我们利用这两个漏洞进行尝试修改/etc/shadow文件,来添加后门。

最后成功添加后门:

如果所修改的用户有ssh权限,我们就可以直接ssh登录了。如果没有还可以尝试用同样的方法修改/etc/ssh/sshd_config文件。

0x05 漏洞总结

  1. 感觉实现rce的方法很多,应该可以尝试覆盖xml写配置实现解析jsp文件。

  2. 感觉linux下还有很多其他方法实现rce的

  3. 这个框架还有一个任意文件读取在/dolphinscheduler/resources/download下。但是最新版本好像对其进行了过滤。

  4. windows下又可以怎么利用呢

0 条评论
某人
表情
可输入 255