Descubriendo el método en que joomla encripta los password

4 comments
Descubriendo y aprendiendo el dia de hoy venimos con esta entrada de manos de @Yoya el cual nos brinda una excelente entrada, para analizar y poder ver como trabaja Joomla.. sin  mas que decir  a leer.

Buscando un ejemplo de la estructura de los hash que genera joomla para agregarlo a la lista de hash que detecta la herramienta IHash. Opte por descargarme joomla y instalarlo (la versión 2.5.8 que es la que recomiendan) para verlo con mis propios ojos. Luego de instalarlo, cree un usuario directamente desde el panel de administración, llamado example con el password "example" para luego no olvidarme.

Ahora toca ver todas las tablas que contiene la base de datos donde instale joomla.

mysql> use joomla;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
 
Database changed
mysql> show tables;
+-----------------------------+
| Tables_in_joomla            |
+-----------------------------+
| jom_assets                  |
| jom_associations            |
| jom_banner_clients          |
| jom_banner_tracks           |
| jom_banners                 |
| jom_categories              |
| jom_contact_details         |
| jom_content                 |
| jom_content_frontpage       |
| jom_content_rating          |
| jom_core_log_searches       |
| jom_extensions              |
| jom_finder_filters          |
| jom_finder_links            |
| jom_finder_links_terms0     |
| jom_finder_links_terms1     |
| jom_finder_links_terms2     |
| jom_finder_links_terms3     |
| jom_finder_links_terms4     |
| jom_finder_links_terms5     |
| jom_finder_links_terms6     |
| jom_finder_links_terms7     |
| jom_finder_links_terms8     |
| jom_finder_links_terms9     |
| jom_finder_links_termsa     |
| jom_finder_links_termsb     |
| jom_finder_links_termsc     |
| jom_finder_links_termsd     |
| jom_finder_links_termse     |
| jom_finder_links_termsf     |
| jom_finder_taxonomy         |
| jom_finder_taxonomy_map     |
| jom_finder_terms            |
| jom_finder_terms_common     |
| jom_finder_tokens           |
| jom_finder_tokens_aggregate |
| jom_finder_types            |
| jom_languages               |
| jom_menu                    |
| jom_menu_types              |
| jom_messages                |
| jom_messages_cfg            |
| jom_modules                 |
| jom_modules_menu            |
| jom_newsfeeds               |
| jom_overrider               |
| jom_redirect_links          |
| jom_schemas                 |
| jom_session                 |
| jom_template_styles         |
| jom_update_categories       |
| jom_update_sites            |
| jom_update_sites_extensions |
| jom_updates                 |
| jom_user_notes              |
| jom_user_profiles           |
| jom_user_usergroup_map      |
| jom_usergroups              |
| jom_users                   |
| jom_viewlevels              |
| jom_weblinks                |
+-----------------------------+
61 rows in set (0.00 sec)

Directamente me fije en la tabla jom_users. Supuse que hay es donde se guardan los datos que tienen que ver con el nombre de usuario y password. Ahora toca ver la estructura de la tabla jom_users para verificar los campos que tiene para realizar la próxima consulta SQL.

| id            | int(11)      | NO   | PRI | NULL                | auto_increment |
| name          | varchar(255) | NO   | MUL |                     |                |
| username      | varchar(150) | NO   | MUL |                     |                |
| email         | varchar(100) | NO   | MUL |                     |                |
| password      | varchar(100) | NO   |     |                     |                |
| usertype      | varchar(25)  | NO   | MUL |                     |                |
| block         | tinyint(4)   | NO   | MUL | 0                   |                |
| sendEmail     | tinyint(4)   | YES  |     | 0                   |                |
| registerDate  | datetime     | NO   |     | 0000-00-00 00:00:00 |                |
| lastvisitDate | datetime     | NO   |     | 0000-00-00 00:00:00 |                |
| activation    | varchar(100) | NO   |     |                     |                |
| params        | text         | NO   |     | NULL                |                |
| lastResetTime | datetime     | NO   |     | 0000-00-00 00:00:00 |                |
| resetCount    | int(11)      | NO   |     | 0                   |                |
+---------------+--------------+------+-----+---------------------+----------------+
14 rows in set (0.00 sec)

Directamente vi que los campos que me interesaban, era id, username (contiene el nombre de usuario para loguearse) y password (el campo mas importante, contiene el hash que genera joomla).


mysql> select id, username, password from jom_users;
+----+----------+-------------------------------------------------------------------+
| id | username | password                                                          |
+----+----------+-------------------------------------------------------------------+
| 73 | admin    | 07ac4817e34b93ee42e88ea0f0705e9a:3uQaeUYsbQf2F7aeehj75gYiuHARWpdJ |
| 74 | example  | edbea885d26f0557e748605c5a5707cd:NXhARsuArVRSZcs4U0lSDrpCDs0Le1Ch |
+----+----------+-------------------------------------------------------------------+
2 rows in set (0.00 sec)

Hay muestra los 2 usuarios, el primero que se crea cuando se instala joomla y el segundo usuario que cree directamente desde el panel de administración.

Hasta hay todo bien, había hecho todo lo que necesitaba pero me dio un poco de curiosidad ver como joomla genera los hash y también me servia para asegurarme de todo. Lo primero que hice fue buscar en google, ya que no quería leer el código de joomla para poder entender como generaba los hash joomla, mejor ver algún post de alguien que ya lo había leído .

Me encontré con este hilo: Using php to create a joomla user password

Pero vi que fue hace 2 años y no estaba seguro si joomla utilizaba el mismo método, así que me puse a revisar el source.

Indagando un poco, el método que generaba los hash era bind() que pertenecía a la clase JUser del archivo user.php que se encontraba en el directorio libraries/joomla/user.

user.php - metodo bind
public function bind(&$array)
        {
                // Let's check to see if the user is new or not
                if (empty($this->id))
                {
                        // Check the password and create the crypted password
                        if (empty($array['password']))
                        {
                                $array['password'] = JUserHelper::genRandomPassword();
                                $array['password2'] = $array['password'];
                        }
 
                        // TODO: Backend controller checks the password, frontend doesn't but should.
                        // Hence this code is required:
                        if (isset($array['password2']) && $array['password'] != $array['password2'])
                        {
                                $this->setError(JText::_('JLIB_USER_ERROR_PASSWORD_NOT_MATCH'));
                                return false;
                        }
 
                        $this->password_clear = JArrayHelper::getValue($array, 'password', '', 'string');
 
                        $salt = JUserHelper::genRandomPassword(32);
                        $crypt = JUserHelper::getCryptedPassword($array['password'], $salt);
                        $array['password'] = $crypt . ':' . $salt;
 
                        // Set the registration timestamp
 
                        $this->set('registerDate', JFactory::getDate()->toSql());
 
                        // Check that username is not greater than 150 characters
                        $username = $this->get('username');
                        if (strlen($username) > 150)
                        {
                                $username = substr($username, 0, 150);
                                $this->set('username', $username);
                        }
 
                        // Check that password is not greater than 100 characters
                        $password = $this->get('password');
                        if (strlen($password) > 100)
                        {
                                $password = substr($password, 0, 100);
                                $this->set('password', $password);
                        }
                }
                else
                {
                        // Updating an existing user
                        if (!empty($array['password']))
                        {
                                if ($array['password'] != $array['password2'])
                                {
                                        $this->setError(JText::_('JLIB_USER_ERROR_PASSWORD_NOT_MATCH'));
                                        return false;
                                }
 
                                $this->password_clear = JArrayHelper::getValue($array, 'password', '', 'string');
 
                                $salt = JUserHelper::genRandomPassword(32);
                                $crypt = JUserHelper::getCryptedPassword($array['password'], $salt);
                                $array['password'] = $crypt . ':' . $salt;
                        }
                        else
                        {
                                $array['password'] = $this->password;
                        }
                }
 
                // TODO: this will be deprecated as of the ACL implementation
                //              $db = JFactory::getDbo();
 
                if (array_key_exists('params', $array))
                {
                        $params = '';
 
                        $this->_params->loadArray($array['params']);
 
                        if (is_array($array['params']))
                        {
                                $params = (string) $this->_params;
                        }
                        else
                        {
                                $params = $array['params'];
                        }
 
                        $this->params = $params;
                }
 
                // Bind the array
                if (!$this->setProperties($array))
                {
                        $this->setError(JText::_('JLIB_USER_ERROR_BIND_ARRAY'));
                        return false;
                }
 
                // Make sure its an integer
                $this->id = (int) $this->id;
 
                return true;
        }

El método recibe como parámetro un array, pero si se fijan ponen al inicio el símbolo &. Eso indica que el array $array se pasa por referencia y por lo tanto los cambios que se hagan a la variable $array, dentro del metodo bind, afectara directamente al array que se le pase como parámetro al metodo bind(). El método bind retorna un booleano, si el usuario existe entonce retornaba un false, si era nuevo entonce un true (despues de haber generado el hash).

Nota: Argumentos de funciones, ejemplo #2

Bueno aquí las 3 lineas que mas me interesa del metodo bind(), van desde linea 660 hasta la 662:

$salt = JUserHelper::genRandomPassword(32);
                                $crypt = JUserHelper::getCryptedPassword($array['password'], $salt);
                                $array['password'] = $crypt . ':' . $salt;

Al final el array guardaba el hash de esta forma crypt:salt

En la primera linea, llama el método estático genRandomPassword() de la clase abstracta JUserHelper que pertenece al archivo helper.php en el directorio libraries/joomla/user. Al método se le pasa como parámetro el numero 32.

Aquí las primeras 3 lineas del método genRandomPassword(), que basta con las 3 primera mas el nombre de la función para darse cuenta cual es el propósito.

public static function genRandomPassword($length = 8)
        {
                $salt = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

El metodo retorna por defecto 8 caracteres al azar (a-z-A-Z0-9). Pero como se le pasa un 32 como parámetro, retornara 32 caracteres al alzar.

Eso quiere decir que la variable $salt contendrá 32 caracteres al azar. Ahora vamos a la segunda linea:


$salt = JUserHelper::genRandomPassword(32);
                                $crypt = JUserHelper::getCryptedPassword($array['password'], $salt);
                                $array['password'] = $crypt . ':' . $salt;

La variable $crypt almacena el valoro del resultado del método getCryptedPassword de la clase abstracta JUserHelper; se les pasa 2 parámetros al método getCryptedPassword que es el password (en texto plano) y los 32 caracteres generados de forma aleatoria. Aquí el método getCryptedPassword:

public static function getCryptedPassword($plaintext, $salt = '', $encryption = 'md5-hex', $show_encrypt = false)
        {
                // Get the salt to use.
                $salt = JUserHelper::getSalt($encryption, $salt, $plaintext);
 
                // Encrypt the password.
                switch ($encryption)
                {
                        case 'plain':
                                return $plaintext;
 
                        case 'sha':
                                $encrypted = base64_encode(mhash(MHASH_SHA1, $plaintext));
                                return ($show_encrypt) ? '{SHA}' . $encrypted : $encrypted;
 
                        case 'crypt':
                        case 'crypt-des':
                        case 'crypt-md5':
                        case 'crypt-blowfish':
                                return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt);
 
                        case 'md5-base64':
                                $encrypted = base64_encode(mhash(MHASH_MD5, $plaintext));
                                return ($show_encrypt) ? '{MD5}' . $encrypted : $encrypted;
 
                        case 'ssha':
                                $encrypted = base64_encode(mhash(MHASH_SHA1, $plaintext . $salt) . $salt);
                                return ($show_encrypt) ? '{SSHA}' . $encrypted : $encrypted;
 
                        case 'smd5':
                                $encrypted = base64_encode(mhash(MHASH_MD5, $plaintext . $salt) . $salt);
                                return ($show_encrypt) ? '{SMD5}' . $encrypted : $encrypted;
 
                        case 'aprmd5':
                                $length = strlen($plaintext);
                                $context = $plaintext . '$apr1$' . $salt;
                                $binary = JUserHelper::_bin(md5($plaintext . $salt . $plaintext));
 
                                for ($i = $length; $i > 0; $i -= 16)
                                {
                                        $context .= substr($binary, 0, ($i > 16 ? 16 : $i));
                                }
                                for ($i = $length; $i > 0; $i >>= 1)
                                {
                                        $context .= ($i & 1) ? chr(0) : $plaintext[0];
                                }
 
                                $binary = JUserHelper::_bin(md5($context));
 
                                for ($i = 0; $i < 1000; $i++)
                                {
                                        $new = ($i & 1) ? $plaintext : substr($binary, 0, 16);
                                        if ($i % 3)
                                        {
                                                $new .= $salt;
                                        }
                                        if ($i % 7)
                                        {
                                                $new .= $plaintext;
                                        }
                                        $new .= ($i & 1) ? substr($binary, 0, 16) : $plaintext;
                                        $binary = JUserHelper::_bin(md5($new));
                                }
 
                                $p = array();
                                for ($i = 0; $i < 5; $i++)
                                {
                                        $k = $i + 6;
                                        $j = $i + 12;
                                        if ($j == 16)
                                        {
                                                $j = 5;
                                        }
                                        $p[] = JUserHelper::_toAPRMD5((ord($binary[$i]) << 16) | (ord($binary[$k]) << 8) | (ord($binary[$j])), 5);
                                }
 
                                return '$apr1$' . $salt . '$' . implode('', $p) . JUserHelper::_toAPRMD5(ord($binary[11]), 3);
 
                        case 'md5-hex':
                        default:
                                $encrypted = ($salt) ? md5($plaintext . $salt) : md5($plaintext);
                                return ($show_encrypt) ? '{MD5}' . $encrypted : $encrypted;
                }
        }

Observando los parametros:

getCryptedPassword($plaintext, $salt = '', $encryption = 'md5-hex', $show_encrypt = false)

el primer parámetro, corresponde al password (en texto plano), el salt que por defecto esta vació pero recordar que se envían 32 caracteres aleatorio, por lo tanto en este caso la variable $salt contendrá los 32 caracteres aleatorios. El tercer parámetro por defecto contiene el valor de md5-hex y el cuarto un booleano establecido como false. Una linea importante es esta (la cuarta linea):

$salt = JUserHelper::getSalt($encryption, $salt, $plaintext);

Ya que le esta dando un nuevo valor a la variable $salt. Aquí el método getSalt():

public static function getSalt($encryption = 'md5-hex', $seed = '', $plaintext = '')
        {
                // Encrypt the password.
                switch ($encryption)
                {
                        case 'crypt':
                        case 'crypt-des':
                                if ($seed)
                                {
                                        return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 2);
                                }
                                else
                                {
                                        return substr(md5(mt_rand()), 0, 2);
                                }
                                break;
 
                        case 'crypt-md5':
                                if ($seed)
                                {
                                        return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 12);
                                }
                                else
                                {
                                        return '$1$' . substr(md5(mt_rand()), 0, 8) . '$';
                                }
                                break;
 
                        case 'crypt-blowfish':
                                if ($seed)
                                {
                                        return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 16);
                                }
                                else
                                {
                                        return '$2$' . substr(md5(mt_rand()), 0, 12) . '$';
                                }
                                break;
 
                        case 'ssha':
                                if ($seed)
                                {
                                        return substr(preg_replace('|^{SSHA}|', '', $seed), -20);
                                }
                                else
                                {
                                        return mhash_keygen_s2k(MHASH_SHA1, $plaintext, substr(pack('h*', md5(mt_rand())), 0, 8), 4);
                                }
                                break;
 
                        case 'smd5':
                                if ($seed)
                                {
                                        return substr(preg_replace('|^{SMD5}|', '', $seed), -16);
                                }
                                else
                                {
                                        return mhash_keygen_s2k(MHASH_MD5, $plaintext, substr(pack('h*', md5(mt_rand())), 0, 8), 4);
                                }
                                break;
 
                        case 'aprmd5': /* 64 characters that are valid for APRMD5 passwords. */
                                $APRMD5 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
 
                                if ($seed)
                                {
                                        return substr(preg_replace('/^\$apr1\$(.{8}).*/', '\\1', $seed), 0, 8);
                                }
                                else
                                {
                                        $salt = '';
                                        for ($i = 0; $i < 8; $i++)
                                        {
                                                $salt .= $APRMD5{rand(0, 63)};
                                        }
                                        return $salt;
                                }
                                break;
 
                        default:
                                $salt = '';
                                if ($seed)
                                {
                                        $salt = $seed;
                                }
                                return $salt;
                                break;
                }
        }

Al parámetro getSalt() se le pasa como parámetro la variable $encryption que por defecto contiene el valor de md5-hex. De ese primer parámetro depende el comportamiento del método getSalt(). Ya que en el método getSalt() utiliza un switch y dependiendo del valor que contenga $encryption se comportara de forma diferente. Por defecto como $encryption contiene md5-hex, se ejecutara el default: del swtich. Que simplemente retorna el salt sin ningún tipo de cambio. Por lo tanto en la linea:

$salt = JUserHelper::getSalt($encryption, $salt, $plaintext);

La variable $salt contendrá el mismo valor que antes. Bueno volviendo al metodo getCryptedPassword(), vemos que luego sigue un switch que evalúa la variable $encryption que dependiendo de esta se puede comportar de manera diferente. Recordando que la variable $encryption por defecto contiene el valor de md5-hex entonce vamos directamente a ese case

case 'md5-hex':
                        default:
                                $encrypted = ($salt) ? md5($plaintext . $salt) : md5($plaintext);
                                return ($show_encrypt) ? '{MD5}' . $encrypted : $encrypted;

Vemos que se utiliza el operador ternario para darle un nuevo valor a la variable $encrypted. Bueno si la variable salt contiene algún valor, entonce la variable $encrypted contendrá el valor del password+salt (32 caracteres alertorios) cifrado a MD5, sino la variable encrypted tendrá el valor del password cifrado en MD5. Luego, en la linea siguiente hay se retorna la variable $encrypted pero se utiliza el operador ternario para indicar 2 posibles casos. Si la variable $show_encrypt es verdadera, entonce retorna $encrypted pero le agrega al inicio la cadena {MD5} sino retorna simplemente el valor de $encrypted. Recordar que en el método getCryptedPassword() la variable $show_encrypt por defecto es false, por lo tanto en este caso retornara simplemente el valor de $encrypted. Ahora vamos a la ultima linea:

$salt = JUserHelper::genRandomPassword(32);
                                $crypt = JUserHelper::getCryptedPassword($array['password'], $salt);
                                $array['password'] = $crypt . ':' . $salt;

Que simplemente contiene el valor de la variable $crypt el signo de : y luego el salt. Que es la estructura en que joomla guarda los hash que genera.

mysql> select id, username, password from jom_users;
+----+----------+-------------------------------------------------------------------+
| id | username | password                                                          |
+----+----------+-------------------------------------------------------------------+
| 73 | admin    | 07ac4817e34b93ee42e88ea0f0705e9a:3uQaeUYsbQf2F7aeehj75gYiuHARWpdJ |
| 74 | example  | edbea885d26f0557e748605c5a5707cd:NXhARsuArVRSZcs4U0lSDrpCDs0Le1Ch |
+----+----------+-------------------------------------------------------------------+
2 rows in set (0.00 sec)

Ahora por lo tanto, es simple hacer un pequeño PoC (prueba de concepto).

 ".$hash."\n\n";
echo "Salt                    -> ".$salt."\n";
echo "password encriptado     -> ".$estructura[0]."\n";
echo "[*]md5(password+salt)   -> ".md5($password.$salt)."\n";
 
?>
yoya@OpenSUSE:~/www/public$ php a.php
[+]Hash                    -> edbea885d26f0557e748605c5a5707cd:NXhARsuArVRSZcs4U0lSDrpCDs0Le1Ch
 
Salt                    -> NXhARsuArVRSZcs4U0lSDrpCDs0Le1Ch
password encryptado     -> edbea885d26f0557e748605c5a5707cd
[*]md5(password+salt)   -> edbea885d26f0557e748605c5a5707cd
yoya@OpenSUSE:~/www/public$ 


Autor del Documento Yoya
Blog

4 comentarios

  1. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  2. Password(string encriptado) que sirve para abrir archivo encriptado que tiene datos(string) encriptados de 512 bits de solo lectura
    y todavía es vulnerable xD

    ResponderEliminar
  3. Este comentario ha sido eliminado por un administrador del blog.

    ResponderEliminar
  4. Este comentario ha sido eliminado por el autor.

    ResponderEliminar