Thursday, July 11, 2013

osTicket: Fail Open, Fail Often

osTicket is (according to them) the "world's most popular open source customer support ticket system." They use one of the worst-designed encryption functions I've ever seen.

First of all, they're trying to use symmetric encryption to secure passwords. They should be using a slow hash (pbkdf2, bcrypt, scrypt) with salt. Edit: Actually, they have to be able to decrypt passwords, because they are used in POP3/IMAP connections. They have to be able to provide the plaintext passwords to a third party, not just verify them. Users' passwords are hashed.

Second, the encryption function fails open. If the mcrypt library is missing, encryption does nothing, and passwords are stored in the database in plain text. It does log a warning message, but who's going to see that?

I can almost feel the programmer's frustration, trying all possible combinations of trim(), IVs, and cipher modes to get the damn thing to decrypt properly. They tried to generate a random IV at encryption time, but did not realize they need to encode it with the ciphertext so it can be given to mcrypt_decrypt(). Instead, they use ECB mode, which ignores the IV. They even generate a random IV in the decryption function!

 function encrypt($text, $salt) {  
   
   //if mcrypt extension is not installed--simply return unencryted text and log a warning.  
   if(!function_exists('mcrypt_encrypt') || !function_exists('mcrypt_decrypt')) {  
     $msg='Cryptography extension mcrypt is not enabled or installed. IMAP/POP passwords are being stored as plain text in database.';  
     Sys::log(LOG_WARN,'mcrypt missing',$msg);  
     return $text;  
   }  
   
   return trim(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256,$salt, $text, MCRYPT_MODE_ECB,  
             mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND))));  
 }  
   
 function decrypt($text, $salt) {  
   if(!function_exists('mcrypt_encrypt') || !function_exists('mcrypt_decrypt'))  
     return $text;  
   
   return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $salt, base64_decode($text), MCRYPT_MODE_ECB,  
           mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND)));  
 }  

The "salt" (which is really the encryption key) is "randomly" generated at install time and stored in a source code file. If that happens to be missing, they use the md5 hash of the admin's email address.

 # This is to support old installations. with no secret salt.  
 if(!defined('SECRET_SALT')) define('SECRET_SALT',md5(TABLE_PREFIX.ADMIN_EMAIL));  

Here's how they generate the "salt." I shouldn't need to explain why this is wrong.

 function randCode($len=8) {  
   return substr(strtoupper(base_convert(microtime(),10,16)),0,$len);  
 }  
 $configFile= str_replace('%CONFIG-SIRI',Misc::randcode(32),$configFile);

Lessons Learned:
  • Fail safe. If your encryption library isn't available, fail hard, don't just return the plaintext.
  • Use a cryptographically secure random number generator to generate secrets.
  • Protect passwords by hashing them.

3 comments:

  1. I'm not going to defend the terrible code, but you're wrong to say they should be using a hash function. After doing a quick grep of the source, it appears this code is only used for encrypting SMTP/IMAP passwords, which are used to send and retrieve e-mails. Since osTicket needs the plain-text password to authenticate with the mail server, hashing is not an option.

    I'm not involved in osTicket at all, but I recently had to do the same sort of thing: implement e-mail sending in PHP using user-provided SMTP connection details. Here's the code I ended up (redacted to remove some sensitive data):

    function encrypt($password) {
    $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-128-cbc'));
    $encryptedValue = openssl_encrypt($plainTextValue, 'aes-128-cbc', $password, 0, $iv);
    if ($encryptedValue === false) throw new Exception("Couldn't encrypt password");
    $settingValue = base64_encode($iv) . '_' . $encryptedValue;
    return $settingValue;
    }

    function decrypt($settingValue) {
    @list($iv, $encryptedValue) = explode('_', $setting->value);
    if (!$encryptedValue) throw new Exception("Malformed setting");
    $iv = base64_decode($iv);
    if ($iv === false) throw new Exception("Couldn't decode initialization vector for setting");
    $plainTextValue = openssl_decrypt($encryptedValue, 'aes-128-cbc', $password, 0, $iv);
    if ($plainTextValue === false) throw new Exception("Couldn't decrypt setting");
    return $plainTextValue;
    }

    ReplyDelete
  2. As commenter #1 pointed out the encrypted SMTP/IMAP/POP passwords need to be decrypted on the fly when connecting to the mail server. User passwords are hashed not encrypted as implied (it's important to make the distinction).

    That said, perhaps we should do a hard fail and force users to install one of the cryptographic extensions such as mcrypt or alternatively openssl as pointed out by commenter #1.

    We will address the issue in the upcoming release. I've created a gitHub issue #627 https://github.com/osTicket/osTicket-1.7/issues/627

    ReplyDelete
    Replies
    1. Thanks for the reply. I have replied to the github issue. :-)

      Delete