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 漏洞总结
-
感觉实现rce的方法很多,应该可以尝试覆盖xml写配置实现解析jsp文件。
-
感觉linux下还有很多其他方法实现rce的
-
这个框架还有一个任意文件读取在/dolphinscheduler/resources/download下。但是最新版本好像对其进行了过滤。
-
windows下又可以怎么利用呢