Weak Master Encryption Key Generation.

This is a public disclosure, WHAT? It's okay, I feel like public disclosure in this scenario is okay for a couple reasons. One, even though the flaw is serious it still leaves the customers with enough protection against most people. Two, all of ManageEngine products seems to have a few vulnerabilities due to lack of effort. Why should I go through the extra effort getting them to fix code that shouldn't be broken in the first place. I feel like the public should know. Really the only people that can still crack the encryption key is state sponsored actors.

Below I show my process of how I tracked down the function that generates the Master Encryption Key.

After installing ManageEngine Password Pro evaluation copy. Started poking around, one of the first items I went looking for was the encryption key that encrypts the data in the database. The Encryption key can be found here:

~/ManageEngine/PMP/conf/pmp_key.key

Contents of pmp_key.key

#This file contains the master AES encryption key for this installation, automatically generated by Password Manager Pro. #The default location of this file is <PMP_HOME>conf and it is not secure to leave this file here, unless #the server is sufficiently hardened to protect any illegal access of this file. #It is highly recommended to move this file out of its default location and for instructions to securely store this file refer. #Thu Nov 30 13:48:09 CST 2017 ENCRYPTIONKEY=x7J2821f*831*7ub*oXVsJsHbeTbmaZY

Awesome, Now how is the key made.

Found a helper tool in /bin/ called 'RotateKey.sh'. Which is used as a helper script to rotate the master key.

Looking at 'RotateKey.sh'

../jre/bin/java -Dswing.aatext=true -Djsse.enableCBCProtection=false -Djava.library.path=../lib/native -Dserver.dir="$SERVER_HOME" -Dserver.home="$SERVER_HOME" -cp "$CLASS_PATH" com.adventnet.pmp.PMPStarter "com.adventnet.passtrix.utils.RotateKey" "rotatekey"

You can see which java file handles this function "com.adventnet.passtrix.utils.RotateKey"

Using cfr.jar you can decompile all the Java files. I typically just decompile them all into one directory so it keeps the file structure.

find . -name '*.jar' -exec java -jar /opt/cfr.jar --outputdir output/ {} \;

After that magic runs, its time to find the RotateKey function. Its located here:

~/ManageEngine/PMP/output/com/adventnet/passtrix/utils/RotateKey

Looking at this code you see the libraries imported and the function call.

import com.adventnet.mfw.ConsoleOut; import com.adventnet.passtrix.role.RoleConstants; import com.adventnet.passtrix.utils.KeyRotationUtils; import com.adventnet.passtrix.utils.StandAloneUtils; ... else { KeyRotationUtils.rotatePasswords();

Next in the chain, look at "com.adventnet.passtrix.utils.KeyRotationUtils;". Find the rotatePasswords() function and looked for what might be getting or setting the key.

PMPAPI.initializePMPED(); String oldKey = PMPAPI.get32BitKey(); ClientUtil.setPMPMasterKey(); if (!KeyRotationUtils.backupDatabase()) { ConsoleOut.println((String)PMPApplicationResourcesUtil.getMsg(rb, "java.KeyRotationUtils.Error_backup", null)); throw new Exception("Error occurred while taking backup of the database."); } String new256BitKey = KeyRotationUtils.createPMPEncryptKey(); pmpEncryptDecrypt = KeyRotationUtils.getNewPMPEDInstance();

Here you see the 'createPMPEncryptKey();'. Searching for this in the same file you find the following:

public static String createPMPEncryptKey() throws Exception { String generatedKey = null; ... generatedKey = PasswordGenerator.generatePassword(32, 32, true, true, 3, true, true); String keyFile = PMPAPI.getConfKeyPath(); ... try { File f = new File(keyFile); Properties props = new Properties(); props.load(new FileInputStream(f)); String oldKey = props.getProperty("ENCRYPTIONKEY"); String comments = "#OLDENCRYPTIONKEY=" + oldKey; Properties properties = new Properties(); properties.setProperty("ENCRYPTIONKEY", generatedKey); key = new BufferedWriter(new FileWriter(keyFile)); ... log.log(Level.INFO, "Encryption key file has been updated with the new key successfully."); String string = generatedKey; return string; } ...

Alright, Getting closer. 'PasswordGenerator.generatePassword(32, 32, true, true, 3, true, true);' Matches the size of the key in the pmp_key.key file above. If you run 'RotateKey.sh' you get the line that says #OLDENCRYPTIONKEY. Now we need to find PasswordGenerator.generatePassword()

It's located in the same directory. After opening 'PasswordGenerator.java'. Look for generatePassword(). There are a couple functions named generatePassword(). Just need to match up with the arguments to the expected arguments to find the correct one. Below is the function it calls. I added my notes below marked with ##comment

## Min and Max Password length is 32. ## Mixed Case is required ## Special Chars are required ## 3 Special chars ## Must start with Letter ## Must have numbers public static String generatePassword(int minPassLen, int maxPassLen, boolean mixedCaseRqd, boolean splCharRqd, int noOfSplChar, boolean startWithLetter, boolean isNumber) { ## Sets up 4 Arrays for lower case, upper case, digits, and special chars. ## As you can see they aren't using all the special chars. char[] alpha = new char[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; char[] upperAlpha = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; char[] numeric = new char[]{'0', '1', '2', '3', '3', '4', '5', '6', '7', '8', '9'}; char[] splchar = new char[]{'@', '$', '-', '%', '&', '*', '(', ')', '=', '^'}; int passMinLength = 0; ## Starts with letter increase passMinLength if (startWithLetter) { ++passMinLength; } ## Has mixed case and startWithLetter is true so adding 2, total is 3 now. if (mixedCaseRqd) { passMinLength = !startWithLetter ? (passMinLength += 2) : ++passMinLength; } ## is True so add 1, total is 4 if (isNumber) { ++passMinLength; } ## Is True so add number of special char what is 3, total 7. if (splCharRqd) { passMinLength += noOfSplChar; } ## Verify that minPassLen is large enough to meet requirements, If not, increase minPassLen. if (minPassLen < passMinLength) { minPassLen = passMinLength; } ## Verify that maxPassLen is large enough, if not Increase. if (maxPassLen < passMinLength) { minPassLen = maxPassLen; } ## more checks and gets the password length to generate. In our case its always 32. int passLen = PasswordGenerator.getPasswordLength(minPassLen, maxPassLen);

Now the fun begins:

## Setup string buffer to hold chars being generated. StringBuffer password = new StringBuffer(passLen); ## Upper case count. int iCount = 0; ## This little guy is called a Ternary. I had to ask a friend. Not a Java Programer I guess it was frist used in C and someother languages. ## If splChar equals True and number of special is greater than 0 types = 3 else if number of special chars 0 or less, types = 2 ## type 1 = pick a lower or upper case alpha ## type 2 = pick a number ## type 3 = pick a special char int types = splCharRqd && noOfSplChar > 0 ? 3 : 2; int splCharsAdded = 0; boolean toggleCase = false; int mixedCount = 0; ## This is the start of the password generation. block5 : for (int i = 0; i < passLen; ++i) { ## arrayId determines which array to pick from. int arrayId = 0; ## First round if startWithLetter is True use case 0 if (i == 0 && startWithLetter) { arrayId = 0; ## If isNumber use case 1 ## Because isNumber = True. The second round will always be a number. } else if (isNumber) { isNumber = false; arrayId = 1; ## For each round after the second round, if mixed case is required and mixedCount equals 0 use case 0 and increase mixedCount. ## Third Round will always be alpha. Below it shows it will always be Uppercase. } else if (mixedCaseRqd && mixedCount == 0) { arrayId = 0; ++mixedCount; ## If special char required and number of special chars is greater than 0. Then check if special charsAdded is less than number of special chars. If so, do some crazy stuff. ## skip rounds 1 and 2 because thats caught above. ## round 3 ## 32 - 3 == 3 - 0 (False so arrayId = random(0,2) aka 0,1, or 2. ## round 4 ## 32 - 4 == 3 - 0 (False so arrayId = random(0,2) ## blah blah, Its there way of making sure that the required amount of specials chars is added before running out of password length. If the statement is true arrayId = 2 else you are picking a random number between 0 and 2. } else if (splCharRqd && noOfSplChar > 0) { if (splCharsAdded < noOfSplChar) { arrayId = passLen - i == noOfSplChar - splCharsAdded ? 2 : sRnd.nextInt(types); ## After the password has the required amount special chars, change types to equal 2. Removing the option to pick special chars. } else { types = 2; } ## If none of the above is triggered pick a type. } else { arrayId = sRnd.nextInt(types); } switch (arrayId) { ## Case 0 is for lower and upper case. case 0: { ## Pick a random char from lower and upper case array. int alphaIdx = sRnd.nextInt(alpha.length); int upperAlphaIdx = sRnd.nextInt(upperAlpha.length); ## If not mixed case it just uses lower case. if (mixedCaseRqd) { ## First round always use lower case. if (i == 0) { password.append(alpha[alphaIdx]); continue block5; } ## All other rounds pick a random int 0 or 1 ## 0 = Uppercase ## 1 = Lowercase int charCase = sRnd.nextInt(2); ## If there are no upper case chars yet, Force Upper case. Because a number is always in the second position. The third char position is always Upper case. if (iCount == 0) { charCase = 0; ## When the next alpha case 0 is picked it has to be a lower case because toggleCase = False. Meaning 4th char position can never be Uppercase. Uppercase can't be picked until case 0 is picked and sets the toggleCase to True. So position 5 - 10 are less likely to be uppercase. } else if (!toggleCase) { charCase = 1; toggleCase = true; } ## Once a lower case is choosen. It will either be a zero and this will add a upper case char and increase the uppercase counter or if one is picked it will move on to the next section. if (charCase == 0) { password.append(upperAlpha[upperAlphaIdx]); ++iCount; continue block5; } ## if One is picked it will add lowercase. password.append(alpha[alphaIdx]); continue block5; } ## Never used as mixedCaseRqd = True password.append(alpha[alphaIdx]); continue block5; } case 1: { ## If Case one is picked it will add a number to the password. Numbers tend not to exist towards the end of the password. int numIdx = sRnd.nextInt(numeric.length); password.append(numeric[numIdx]); continue block5; } case 2: { ## If case 2 is picked add special char to the password. Increase special chars added. Case 2 can only be choosen when specialCharsAdded is less than Number of Specials which is 3. Special chars favors the start of the password. int splCharIdx = sRnd.nextInt(splchar.length); password.append(splchar[splCharIdx]); ++splCharsAdded; } } } return password.toString(); }

So from reading the code we know a few things. First position will always be a lower case char. Second position will always be a digit. Third position will always be upper case char. Forth position can't be a upper case. Special chars will favor the first half the encryption key.

I know that might have been confusing. The developers made it way to complex, They were trying to ensure randomness but by doing so cut massive amount of the key space.

Remember boys and girls, NEVER limit what chars can be part of a password as this lowers your entropy.

I wanted to test my theory, I don't code in Java so I didn't want to pull the code out. So I wrote BASH script to execute the RotateKey.sh, read the pmp_key.key file and extract the encryption key and save it in a file. Looping thousands of times.

#!/bin/bash count=5000 while [ $count -gt 0 ] do ./RotateKey.sh grep 'ENCRYPTIONKEY=' ../conf/pmp_key.key | cut -d'=' -f2- >> keys.log done

Next, I wrote a python program to analyze the data.

import re import sys import os def run(): with open('keys.log', 'r') as f: data = f.readlines() mask = {1: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 2: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 3: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 4: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 5: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 6: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 7: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 8: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 9: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 10: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 11: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 12: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 13: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 14: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 15: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 16: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 17: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 18: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 19: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 20: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 21: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 22: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 23: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 24: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 25: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 26: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 27: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 28: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 29: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 30: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 31: {'l': 0, 'u': 0, 'd': 0, 's': 0}, 32: {'l': 0, 'u': 0, 'd': 0, 's': 0} } alpha_freq = {'a': 0, 'b': 0, 'c': 0, 'd': 0, 'e': 0, 'f': 0, 'g': 0, 'h': 0, 'i': 0, 'j': 0, 'k': 0, 'l': 0, 'm': 0, 'n': 0, 'o': 0, 'p': 0, 'q': 0, 'r': 0, 's': 0, 't': 0, 'u': 0, 'v': 0, 'w': 0, 'x': 0, 'y': 0, 'z': 0 } number_freq = {'0': 0, '1': 0, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0, '7': 0, '8': 0, '9': 0 } special_freq = {'@': 0, '$': 0, '-': 0, '%': 0, '&': 0, '*': 0, '(': 0, ')': 0, '=': 0, '^': 0 } for line in data: idx = 1 for x in line.strip('

'): if x.islower(): char_type = 'l' alpha_freq[x] += 1 elif x.isupper(): char_type = 'u' alpha_freq[x.lower()] += 1 elif x.isdigit(): char_type = 'd' number_freq[x] += 1 else: char_type = 's' special_freq[x] += 1 mask[idx][char_type] += 1 idx += 1 for k,v in mask.iteritems(): print(k,v) for k,v in alpha_freq.iteritems(): print(k,v) for k,v in number_freq.iteritems(): print(k,v) for k,v in special_freq.iteritems(): print(k,v) if __name__ == '__main__': run()

Now for the results.

Count of type per position:

My friend @bxrobertz thought my raw numbers could use a facelift, so he created some nice charts for me.

(1, {'s': 0, 'u': 0, 'l': 4712, 'd': 0}) (2, {'s': 0, 'u': 0, 'l': 0, 'd': 4712}) (3, {'s': 0, 'u': 4712, 'l': 0, 'd': 0}) (4, {'s': 1601, 'u': 0, 'l': 1579, 'd': 1532}) (5, {'s': 1619, 'u': 261, 'l': 1314, 'd': 1518}) (6, {'s': 1546, 'u': 446, 'l': 1152, 'd': 1568}) (7, {'s': 1511, 'u': 533, 'l': 1134, 'd': 1534}) (8, {'s': 1332, 'u': 815, 'l': 1165, 'd': 1400}) (9, {'s': 1230, 'u': 1027, 'l': 1224, 'd': 1231}) (10, {'s': 1031, 'u': 1240, 'l': 1345, 'd': 1096}) (11, {'s': 875, 'u': 1398, 'l': 1484, 'd': 955}) (12, {'s': 759, 'u': 1573, 'l': 1594, 'd': 786}) (13, {'s': 590, 'u': 1752, 'l': 1758, 'd': 612}) (14, {'s': 444, 'u': 1851, 'l': 1895, 'd': 522}) (15, {'s': 368, 'u': 1957, 'l': 1981, 'd': 406}) (16, {'s': 295, 'u': 2031, 'l': 2083, 'd': 303}) (17, {'s': 250, 'u': 2115, 'l': 2097, 'd': 250}) (18, {'s': 158, 'u': 2158, 'l': 2209, 'd': 187}) (19, {'s': 154, 'u': 2202, 'l': 2229, 'd': 127}) (20, {'s': 120, 'u': 2329, 'l': 2180, 'd': 83}) (21, {'s': 55, 'u': 2277, 'l': 2314, 'd': 66}) (22, {'s': 62, 'u': 2251, 'l': 2358, 'd': 41}) (23, {'s': 38, 'u': 2362, 'l': 2273, 'd': 39}) (24, {'s': 28, 'u': 2310, 'l': 2346, 'd': 28}) (25, {'s': 25, 'u': 2306, 'l': 2364, 'd': 17}) (26, {'s': 10, 'u': 2377, 'l': 2308, 'd': 17}) (27, {'s': 5, 'u': 2341, 'l': 2352, 'd': 14}) (28, {'s': 10, 'u': 2355, 'l': 2341, 'd': 6}) (29, {'s': 5, 'u': 2349, 'l': 2357, 'd': 1}) (30, {'s': 5, 'u': 2358, 'l': 2346, 'd': 3}) (31, {'s': 3, 'u': 2398, 'l': 2307, 'd': 4}) (32, {'s': 7, 'u': 2354, 'l': 2351, 'd': 0})

Char frequency:

('a', 4446) ('c', 4589) ('b', 4416) ('e', 4459) ('d', 4572) ('g', 4515) ('f', 4544) ('i', 4502) ('h', 4539) ('k', 4430) ('j', 4467) ('m', 4579) ('l', 4494) ('o', 4553) ('n', 4454) ('q', 4539) ('p', 4669) ('s', 4499) ('r', 4463) ('u', 4521) ('t', 4678) ('w', 4531) ('v', 4434) ('y', 4595) ('x', 4488) ('z', 4614) ('1', 1772) ('0', 1733) ('3', 3436) ('2', 1780) ('5', 1656) ('4', 1755) ('7', 1738) ('6', 1751) ('9', 1673) ('8', 1764) ('@', 1380) ('%', 1443) ('$', 1414) ('=', 1435) ('&', 1377) (')', 1369) ('(', 1434) ('*', 1415) ('-', 1464) ('^', 1405)

Is this the end of the world? No. However, I wouldn't trust a company that makes this kind of mistakes. This is spouse to be a password manager that protects all your passwords for your company. If someone was able to steal the password manager database with enough time I believe the bad actor could crack the password and gain access to all the sensitive information.

This isn't the first time I picked on ManageEngine and wont be the last. I got 3 CVE's on ManageEngine EventLog Analyzer you can check out that Blog entry here