Inyecciones SQL. Qué son y como combatirlas


Os voy a contar que son las inyecciones SQL (en adelante SQLi), cómo puede un atacante utilizarlas para obtener información o destruirla y además veremos un par de códigos vulnerables y como dejarían de serlo. Las inyecciones de código son la vulnerabilidad #1 en un ranking que hace OWASP del Top Ten de vulnerabilidades.


Empezaremos con unas nociones muy básicas de la sintáxis SQL.
El lenguaje SQL podríamos decir que tiene 3 grupos importantes de instrucciones (triggers aparte):

  • Data Manipulation (DML) es el subgrupo de instrucciones que permite añadir, reescribir y borrar datos. Dentro de este grupo estan: 

    • INSERT: permite añadir tuplas a la tabla. Por ejemplo:
      INSERT INTO users VALUES('sshMan','password', 'sshMan@vC.com');
      
      Si los campos de nuestra tabla users son username, userpass y usermail, al ejecutar esta instrucción creará una nueva tupla con sshMan, password y sshMan@vC.com.

    • UPDATE: modifica una tupla ya existente. Si tenemos la tupla del apartado anterior en una tabla:
      UPDATE users SET userpass='nuevapass' WHERE username='sshMan';
      
      Aquí se cambiaría el valor del campo userpass a nuevapass cuando el username sea sshMan.

    • DELETE: Elimina tuplas. Si tenemos la tupla del código anterior en nuestra tabla, para borrarla no hay más que hacer:
      DELETE FROM users WHERE username='sshMan' AND userpass='nuevapass';
      
      Y ya el usuario sshMan no estaría en nuestra BD.

    • Data Definition (DDL) es el subgrupo de instrucciones que se ocupa de las tablas y de la estructura de los indices. Permite crear, modificar y borrar tablas completas.

      • CREATE: crea una nueva tabla. Por ejemplo:
        CREATE TABLE users (
          username CHARACTER(12) PRIMARY KEY,
          userpass CHARACTER (20) NOT NULL,
          email CHARACTER (50) NOT NULL);
        
        Esta instrucción crearía la tabla users con los atributos username, userpass y email.

      • ALTER: modifica la estructura de una tabla existente:
        ALTER TABLE users ADD userwebpage CHARACTER(50);
        
        Crea el campo userwebpage en la tabla users, y si nos fijamos en el codigo anterior...los 3 campos (username, userpass y email) tienen que recibir información. Sin embargo el campo userwebpage puede estar vacio.

      • DROP: Borra toda la tabla, no se puede hacer rollback:
        DROP TABLE users;
        
        Y nuestra tabla users ha sido borrada (y su contenido también).

      • Sin embargo, la operación más común y la que más nos interesa para esta entrada son las consultas (queries) con SELECT. Una consulta sencilla sería:
      • SELECT * FROM users WHERE username='sshMan' AND userpass='mipassword';
        

      Introducción a las SQLi


      Ahora imaginemos un escenario más complejo (y es que raramente consultamos las bases de datos nosotros) normalmente, una aplicación web tiene una función de conexión a la base de datos. Un ejemplo en php sería:
      <?php
       $user = $GET['login'];
       $pass = $GET['password'];
       $query = "SELECT * FROM users WHERE username='".$user."'and userpass='".$pass."'";
       $conexion = pg_connect("dbname=webapp user=usuario");
       $result = pg_query($conexion, $consulta);
       if($result) {
         $rows = pg_num_rows($result);
         if($rows == 0) {?&>
           <h1><?php echo "Acceso denegado";?></h1>
         <?php}else{?>
           <h1><?php echo "Bienvenido".$user;?></h1>
         <?php}
       }?>
      }
      
      Esto lo que hace es obtener el usuario y contraseña que hayamos puesto y hacer una consulta a la BD. Pero centremonos en la consulta:
      SELECT * FROM users WHERE username='.$user.'and userpass='.$pass';
      

      Si en los campos user y pass de nuestra aplicación ponemos:
      username = Usuario1
      userpass = Passwd1
      la consulta quedaría:
      SELECT * FROM users WHERE username='Usuario1'and userpass='Passwd1';
      

      Sin embargo...si les damos estos valores a los campos de nuestra aplicación:
      username = 'OR''='
      userpass = 'OR''='
      la consulta resultante sería:
      SELECT * FROM users WHERE username='' OR ''=''and userpass='' OR ''='';
      
      Vale esto es raro, pero que es exactamente:
      le decimos que username es ' ' (vacío) y userpass es ' ' (vacío también). Sin embargo estamos diciendo OR ''=''. Queda claro que ''='' devuelve true y además con el OR estamos diciendo que username='' OR ''=''. Y username es distinto de '' pero '' es igual a '' con lo cual la sentencia SQL se validaría correctamente y obtendriamos acceso ilegitimo a la aplicación. Pero esto no queda aquí:

      username = loquesea
      userpass = ';DROP TABLE users;--
      Esta consulta es mucho más divertida, ya que borra la tabla users de la base de datos:
      SELECT * FROM users WHERE username='loquesea' and userpass='';DROP TABLE users;--';
      

      ¿Porqué ocurre esto?


      Ocurre por que no filtramos o escapamos los carácteres que forman la sintaxis de sql, no se filtran las comillas simples ('), el punto y coma(;), los iguales (=) ni los guiones (-).

      Solución


      La solución pasa por:
      • Aplicar validación (lo más estricta posible) a los datos.
      • Que no se pueda ejecutar más de una sentencia SQL en el mismo comando.
      • Restringir los tipos de dato aceptados.
      • Utilizar funciones ya preparadas para realizar el trabajo con la BD.
      • Escapar el caracter (').

       Una solución posible es con la función de php pg_escape_string()
      <?php
       $user = $GET['login'];
       $pass = $GET['password'];
       if(ereg("^[0-9A-Za-z]+$",$user)) && (ereg("^[0-9A-Za-z]+$",$pass)) {
         $query = "SELECT * FROM users WHERE username='".pg_escape_string($user)."'and userpass='".pg_escape_string($pass)."'";
         $conexion = pg_connect("dbname=webapp user=usuario");
         $result = pg_query($conexion, $consulta);
         if($result) {
           $rows = pg_num_rows($result);
           if($rows == 0) {?&>
             <h1><?php echo "Acceso denegado";?></h1>
           <?php}else{?>
             <h1><?php echo "Bienvenido".$user;?></h1>
           <?php}
         }
       }?>
      

      Otra posible solución sería con la función pg_prepare() que es una función que se encarga de interactuar con la BD:

      <?php
      <?php
       $user = $GET['login'];
       $pass = $GET['password'];
       if(ereg("^[0-9A-Za-z]+$",$user)) && (ereg("^[0-9A-Za-z]+$",$pass)) {
         $conexion = pg_connect("dbname=webapp user=usuario");
         $result = pg_prepare($conexion, "", 'SELECT * FROM users WHERE username =$1 and userpass=$2');
         $resultexec = pg_execute($conexion, "", array($user,$pass));
         if($resultexec) {
           $rows = pg_num_rows($resultexec);
           if($rows == 0) {?&>
             <h1><?php echo "Acceso denegado";?></h1>
           <?php}else{?>
             <h1><?php echo "Bienvenido".$user;?></h1>
           <?php}
         }}?>
      

      Estos ejemplos son de los más sencillos para ver como funciona la inyección SQL, pero hay muchas más maneras de realizar una SQLi (por ejemplo utilizando la función base64_decode() encodeando los campos en base64 y en la sentencia llamar a base64_decode('JyBPUiAnJz0n'). En esa sentencia no hay carácteres especiales que escapar, pero al hacer el decode se convierte en: ' OR ''='.