Bobby Tables, params y el día en que entendí para qué sirve un pentesting

Hay días en los que empiezas preparando material docente sobre R y terminas escribiendo sobre ciberseguridad.

En realidad, pensándolo bien, en mi caso tampoco es tan extraño.

Estoy preparando material de R para la escuela de programación de una universidad y uno de los temas en los que estoy trabajando estos días tiene que ver con la conexión desde R a bases de datos.

El objetivo del curso no es enseñar ciberseguridad.

Ni SQL avanzado.

Ni mucho menos convertir una clase sobre DBI en un curso de hacking.

La idea es bastante más sencilla: explicar cómo conectarse a una base de datos desde R, realizar consultas, recuperar información y utilizar esos datos posteriormente en nuestros análisis.

Pero llegué a un punto en el que tenía que explicar algo aparentemente pequeño:

cómo pasar parámetros a una consulta SQL.

Y ahí apareció mi otra personalidad profesional.

La que no puede escribir tranquilamente:

consulta <- paste0(
  "SELECT * FROM alumnos WHERE nombre = '",
  nombre,
  "'"
)

y continuar con el temario como si nada hubiera pasado.

Porque funcionar, funciona.

Hasta que deja de hacerlo.

O, peor todavía:

hasta que funciona exactamente como un atacante quiere que funcione.

paste0() no tiene la culpa

Antes de que alguien salga en defensa de R con antorchas y horcas: no, paste0() no es una función insegura.

El problema no es paste0().

El problema aparece cuando utilizamos concatenación de cadenas para introducir directamente en una sentencia SQL valores que pueden proceder de un usuario, de un formulario, de una API, de un fichero o de cualquier otra fuente que no controlamos completamente.

Imaginemos algo muy sencillo:

nombre <- "Javier"

consulta <- paste0(
  "SELECT * FROM alumnos WHERE nombre = '",
  nombre,
  "'"
)

dbGetQuery(con, consulta)

El resultado final será algo parecido a:

SELECT * FROM alumnos WHERE nombre = 'Javier'

Perfecto.

¿Qué podría salir mal?

Es una pregunta maravillosa en ciberseguridad.

Normalmente es justo cuando empiezan los problemas.

La alternativa que quería enseñar en el material es utilizar consultas parametrizadas, separando la consulta SQL del valor que queremos introducir en ella.

Por ejemplo, utilizando un backend que admita ? como marcador:

dbGetQuery(
  con,
  "SELECT * FROM alumnos WHERE nombre = ?",
  params = list(nombre)
)

Aquí hay una diferencia conceptual enorme.

Ya no estamos construyendo una instrucción SQL juntando trozos de texto.

Tenemos, por una parte:

SELECT * FROM alumnos WHERE nombre = ?

y, por otra:

params = list(nombre)

La propia especificación de DBI contempla las consultas parametrizadas y explica precisamente una de sus ventajas: separar la sintaxis de la consulta de los parámetros protege frente a SQL Injection. El formato exacto del marcador depende del backend; por ejemplo, ? es habitual en RMariaDB y RSQLite, mientras que otros pueden utilizar $1 u otros formatos.

Para el curso probablemente con eso habría sido suficiente.

Pero pensé que podía dedicar unas líneas a explicar por qué era mejor hacerlo así.

Y fui a buscar un meme.

Yo buscaba un coche

Tenía en la cabeza uno de esos memes que llevamos años viendo los informáticos.

Un coche llega a un aparcamiento automatizado con una matrícula que contiene algo parecido a una sentencia SQL. La cámara lee la matrícula, el sistema mete aquello alegremente en la base de datos y, en la versión humorística de la historia, se monta la fiesta.

Era ese el ejemplo que estaba buscando.

Pero Google me llevó a otro.

Y descubrí a Bobby Tables.

Ver la tira original “Exploits of a Mom” (#327) en xkcd →

No sé cómo había conseguido vivir todos estos años sin conocerlo.

La tira es la número 327 de xkcd, se titula Exploits of a Mom y fue publicada en 2007.

Sí.

2007.

Y casi veinte años después seguimos pudiendo utilizarla para explicar uno de los errores clásicos de seguridad en aplicaciones.

Eso quizá ya debería hacernos reflexionar un poco.

El pequeño Bobby Tables

En el cómic, un colegio llama por teléfono a una madre.

Tienen algunos problemas informáticos y quieren comprobar una cosa bastante peculiar.

Le preguntan si realmente ha llamado a su hijo:

Robert'); DROP TABLE Students;--

Y la madre responde tranquilamente:

“Little Bobby Tables, we call him.”

Algo así como:

«Nosotros le llamamos el pequeño Bobby Tables».

Y aquí está la primera parte de la gracia.

Bobby es un diminutivo habitual de Robert.

Y Tables viene del DROP TABLE que forma parte de su supuesto nombre.

Es decir, para nosotros podría ser algo parecido a haber inscrito al niño como:

Francisco'); DROP TABLE Alumnos;--

y decir después:

—Sí, hombre. Paco Tablas. En casa le llamamos Paco Tablas.

Solo por esto ya merece la pena el meme.

Pero falta lo mejor.

El colegio informa a la madre de que han perdido los registros de los estudiantes.

Y ella, lejos de disculparse, básicamente les viene a decir:

espero que hayáis aprendido a tratar correctamente las entradas de vuestra base de datos.

Brutal.

La madre acaba convirtiendo la destrucción de la base de datos del colegio en una peculiar clase de desarrollo seguro.

¿Qué ha hecho realmente Bobby?

Simplifiquemos muchísimo el ejemplo.

Supongamos que la aplicación del colegio construye una sentencia SQL concatenando directamente el nombre introducido:

INSERT INTO Students (name)
VALUES ('Robert');

Pero en lugar de Robert, recibe:

Robert'); DROP TABLE Students;--

Una aplicación vulnerable podría terminar construyendo algo conceptualmente parecido a esto:

INSERT INTO Students (name)
VALUES ('Robert'); DROP TABLE Students;--');

De repente ya no tenemos únicamente un nombre.

Tenemos código SQL.

La comilla cierra el valor que esperaba la aplicación.

El punto y coma finaliza una instrucción.

Después aparece:

DROP TABLE Students;

que intenta eliminar la tabla Students.

Y finalmente:

--

puede hacer que determinadas bases de datos interpreten el resto de la línea como un comentario.

Naturalmente, para que el ejemplo del cómic funcionase exactamente así tendrían que darse más circunstancias: el motor y el controlador deberían aceptar esa construcción, la ejecución tendría que permitir las instrucciones correspondientes y, sobre todo, la cuenta utilizada por la aplicación tendría que disponer de permisos suficientes para eliminar la tabla.

Pero el concepto importante no cambia.

La aplicación esperaba un dato.

El atacante consiguió introducir algo que terminó siendo interpretado como código.

Eso es lo realmente peligroso.

Bobby no borró la base de datos

Esta es probablemente la frase con la que me quedaría de toda esta historia:

Bobby Tables no borró la base de datos del colegio.

La borró una aplicación que decidió confiar en Bobby Tables.

Y esa distinción es importantísima.

Durante muchos años hemos explicado seguridad utilizando expresiones como:

«El usuario ha introducido un valor malicioso».

Pero hay otra forma de mirarlo.

El usuario ha introducido un valor.

Ha sido nuestra aplicación la que ha decidido hacer algo peligroso con él.

Porque una cadena como:

Robert'); DROP TABLE Students;--

puede ser absurda como nombre de una persona.

Pero sigue siendo una cadena de caracteres.

Si mi aplicación necesita almacenar ese texto en un campo, debería poder hacerlo sin que el sistema interprete parte de ese contenido como una orden.

Ahí está la diferencia entre datos y código.

Y esa es precisamente la razón por la que OWASP continúa recomendando como defensa principal frente a SQL Injection el uso de prepared statements y consultas parametrizadas, además de otros controles como la validación y el principio de mínimo privilegio. OWASP desaconseja confiar simplemente en escapar manualmente todo lo que introduce el usuario como estrategia principal.

Por eso, volviendo a R, prefiero enseñar esto:

dbGetQuery(
  con,
  "SELECT * FROM alumnos WHERE nombre = ?",
  params = list(nombre)
)

antes que esto:

consulta <- paste0(
  "SELECT * FROM alumnos WHERE nombre = '",
  nombre,
  "'"
)

Aunque el segundo ejemplo sea quizá más fácil de entender durante los primeros cinco minutos de una clase.

Porque enseñar programación también consiste, cuando podemos, en no enseñar primero una mala costumbre para tener que desaprenderla después.

Y hay otra pregunta incómoda

Volvamos al colegio de Bobby.

Supongamos que efectivamente existe una SQL Injection.

Todavía podríamos hacer otra pregunta:

¿Por qué la cuenta que utiliza la aplicación puede ejecutar un DROP TABLE?

Porque esa es otra capa de esta historia.

Imaginemos una aplicación que únicamente necesita:

  • consultar determinados datos;
  • insertar nuevos alumnos;
  • modificar algunos campos.

¿Por qué debería conectarse a la base de datos utilizando una cuenta con permisos para destruir su estructura?

Aquí entra otro viejo conocido de la seguridad:

el principio de mínimo privilegio.

Si algo falla, el objetivo debería ser que el daño posible también esté limitado.

La parametrización podría evitar la vulnerabilidad.

La validación podría añadir otra barrera.

Los permisos de la cuenta podrían limitar sus consecuencias.

La monitorización podría ayudarnos a detectar comportamientos anómalos.

No deberíamos depender de que una única barrera sea perfecta.

Porque rara vez lo es.

«Pero ¿para qué queremos hacer un pentesting?»

Y aquí es donde Bobby Tables me llevó a una reflexión que ya no tenía mucho que ver con el curso de R.

Durante años he escuchado esta pregunta en organizaciones:

«¿Pero para qué queremos hacer un pentesting?»

También me la han planteado amigos cuando hemos hablado de seguridad.

—La aplicación funciona.

—La ha hecho un proveedor.

—Ya tenemos firewall.

—Tenemos antivirus.

—Está actualizada.

—Solo la utilizan unas pocas personas.

—No tenemos nada especialmente importante ahí.

—Nunca nos ha pasado nada.

Y voy a reconocer una cosa.

Yo también fui durante un tiempo de los que pensaban algo parecido.

No necesariamente que los pentesting no sirvieran para nada, pero sí podía preguntarme qué sentido tenía dedicar recursos a atacar deliberadamente algo que aparentemente funcionaba correctamente.

Hasta que empezamos a hacerlos.

Y mi opinión cambió.

Mucho.

No voy a contar casos concretos.

Ni aplicaciones.

Ni vulnerabilidades.

Ni resultados.

Creo que se entiende perfectamente por qué.

Pero sí puedo contar la conclusión a la que llegué después de ver unos cuantos:

merecen la pena.

Y algunas veces, mucho.

Funcionar correctamente y ser seguro no son lo mismo

Este es quizá uno de los problemas fundamentales.

Cuando desarrollamos una aplicación comprobamos normalmente que haga aquello para lo que ha sido diseñada.

Que el usuario pueda iniciar sesión.

Que pueda crear un registro.

Que pueda modificarlo.

Que pueda descargar un documento.

Que una API responda.

Que los permisos aparentemente funcionen.

Que el formulario no dé errores.

Todo correcto.

El problema es que un pentester no utiliza necesariamente la aplicación como nosotros esperamos.

Ahí está buena parte de su valor.

Nosotros preguntamos:

«¿Hace lo que tiene que hacer?»

El pentester pregunta:

«¿Qué puedo conseguir que haga?»

La diferencia entre ambas preguntas es enorme.

¿Qué ocurre si modifico este identificador?

¿Qué pasa si llamo directamente a esta API?

¿Y si intento acceder al registro de otro usuario?

¿Y si cambio el orden previsto de las operaciones?

¿Y si manipulo este parámetro?

¿Y si envío algo que nadie esperaba recibir?

¿Y si utilizo una funcionalidad legítima de una forma para la que nunca fue diseñada?

¿Y si combino dos pequeños problemas que, individualmente, parecían poco importantes?

Eso no significa que cualquier pentesting vaya a descubrir una vulnerabilidad crítica.

Ni mucho menos.

Pero introduce algo que internamente resulta difícil de reproducir:

una mirada adversarial.

El problema de conocer demasiado nuestra propia aplicación

Cuando trabajamos todos los días con un sistema acumulamos contexto.

Sabemos cómo funciona.

Sabemos qué botón hay que pulsar.

Sabemos qué campos son obligatorios.

Sabemos qué pasos debemos seguir.

Sabemos incluso que «eso no se hace así».

Y precisamente ahí puede aparecer un sesgo.

Un atacante no sabe que eso no se hace así.

Y, sobre todo:

le da exactamente igual.

No lee nuestro procedimiento de usuario.

No respeta el flujo que dibujamos en el PowerPoint.

No sabe que ese parámetro «nunca debería tener ese valor».

Lo prueba.

Un pentesting introduce precisamente esa mentalidad.

OWASP mantiene desde hace años su Web Security Testing Guide, un marco específico para probar la seguridad de aplicaciones web. Y resulta interesante revisar todo lo que contempla: autenticación, autorización, gestión de sesiones, validación de entradas, criptografía, lógica de negocio, APIs o configuración, entre otros muchos aspectos.

No se trata únicamente de lanzar un escáner y esperar que aparezca algo rojo.

Ahí está precisamente otra confusión habitual.

Un pentesting no es pasar una herramienta automática

Las herramientas automáticas son necesarias.

Los análisis de vulnerabilidades son necesarios.

SAST, DAST, revisión de dependencias, análisis de código, gestión de vulnerabilidades y muchas otras medidas tienen su lugar.

Pero un buen pentesting aporta otra cosa.

Aporta contexto.

Aporta interpretación.

Aporta creatividad.

Y, sobre todo, permite intentar encadenar situaciones que una herramienta automática puede analizar de manera independiente.

Una vulnerabilidad de severidad aparentemente baja puede no preocuparnos demasiado.

Otra también.

Pero quizá juntas permiten hacer algo que ninguna de las dos parecía permitir por separado.

Eso es mucho más difícil de detectar mirando únicamente una lista de CVE o esperando el resultado de una herramienta automática.

OWASP plantea precisamente la seguridad de aplicaciones desde una perspectiva mucho más amplia que un pentesting aislado y recomienda integrar las pruebas de seguridad a lo largo del ciclo de vida del desarrollo.

Y eso también es importante.

Porque tampoco quiero transmitir la idea contraria.

El pentesting tampoco es magia

Hacer un pentesting una vez al año y guardar el PDF en una carpeta no convierte automáticamente una aplicación en segura.

Sería fantástico.

Pero no.

Un pentesting es una fotografía realizada en unas determinadas condiciones:

una versión concreta de la aplicación, un alcance determinado, unas credenciales, un tiempo disponible y una metodología.

Al día siguiente podemos desplegar una nueva funcionalidad y crear una vulnerabilidad.

Podemos actualizar una librería.

Cambiar una API.

Modificar permisos.

Publicar un nuevo endpoint.

O simplemente introducir un error.

Por eso la seguridad de aplicaciones debería parecerse mucho más a un proceso que a un examen final.

Desarrollo seguro.

Revisión de código.

Gestión de dependencias.

Configuración segura.

Pruebas automatizadas.

Control de cambios.

Análisis de vulnerabilidades.

Pentesting.

Monitorización.

Y vuelta a empezar.

No porque cada aplicación necesite absolutamente todas las herramientas existentes en el mercado, sino porque la seguridad se construye mediante capas.

«Pero nunca hemos tenido un problema»

Hay otra frase que me gusta especialmente:

«Nunca nos ha pasado nada».

Puede ser verdad.

Aunque demostrarlo es bastante más complicado de lo que parece.

Pero incluso aceptando que sea completamente cierto, que algo no haya ocurrido todavía no demuestra que no pueda ocurrir.

El colegio de Bobby Tables probablemente podía haber matriculado cientos o miles de alumnos sin ningún problema.

Ana.

Pedro.

Lucía.

Carlos.

Marta.

Todos funcionaban perfectamente.

El sistema estaba probado.

Hasta que llegó:

Robert'); DROP TABLE Students;--

Y Bobby hizo una pregunta que nadie había hecho antes.

Bueno.

Su madre.

La seguridad empieza muchas veces con una pregunta absurda

Creo que esa es una de las cosas que más me gusta de la ciberseguridad.

Muchas mejoras empiezan con alguien planteando una situación aparentemente absurda.

—¿Y si el usuario pone una comilla aquí?

—¿Para qué va a ponerla?

—Pero ¿puede?

—Sí.

—Pues probemos.

O:

—¿Y si cambio este número de la URL?

—Pero nadie hace eso.

Error.

Alguien terminará haciéndolo.

Puede ser un pentester.

Puede ser un trabajador.

Puede ser un usuario curioso.

O puede ser alguien que no tenga precisamente buenas intenciones.

La pregunta correcta no es siempre:

«¿Por qué iba alguien a hacer esto?»

A veces es simplemente:

«¿Qué ocurre si alguien lo hace?»

Y Bobby Tables es probablemente una de las mejores representaciones que he visto de esa idea.

Casi veinte años después

Hay además algo especialmente llamativo en esta historia.

La tira de xkcd es de 2007.

No estamos hablando de una técnica descubierta anteayer.

Y aun así seguimos explicando SQL Injection.

Seguimos encontrando concatenaciones de cadenas.

Seguimos teniendo problemas de validación.

Seguimos encontrando aplicaciones con cuentas que disponen de más privilegios de los necesarios.

Y seguimos encontrándonos con la eterna pregunta:

«¿Para qué vamos a hacer un pentesting?»

Quizá porque desarrollar software seguro es bastante más difícil que escribir software que simplemente funcione.

Y porque nosotros conocemos nuestro sistema demasiado bien.

Sabemos cómo queremos que se utilice.

Un atacante intenta descubrir cómo puede utilizarlo.

Al final solo quería explicar params

Lo curioso es que todo esto empezó porque quería añadir unas pocas líneas a un material de R.

Explicar que, si tenemos un valor que queremos utilizar en una consulta, es preferible hacer algo conceptualmente parecido a:

dbGetQuery(
  con,
  "SELECT * FROM alumnos WHERE nombre = ?",
  params = list(nombre)
)

en lugar de construir alegremente la consulta mediante:

paste0(...)

con valores que no controlamos.

Nada más.

Un pequeño detalle dentro de un tema que trata principalmente sobre R y bases de datos.

Pero terminé buscando un meme.

Buscaba un coche entrando en un parking.

Y encontré a una madre que había llamado a su hijo:

Robert'); DROP TABLE Students;--

El colegio perdió la tabla.

Y la madre, en lugar de pedir disculpas, les dio una lección de desarrollo seguro.

Tengo que reconocerlo.

La señora me ha ganado.

Y además me ha recordado algo que tardé algunos años en aprender por experiencia propia:

cuando alguien externo intenta romper deliberadamente aquello que tú estás convencido de que funciona, el resultado puede ser bastante incómodo.

Pero esa incomodidad es precisamente parte del valor de un buen pentesting.

Mucho mejor que encuentre el problema Bobby Tables.

Y que después podamos corregirlo.

Porque la alternativa es que lo encuentre alguien que no llame después para explicárnoslo.

Comentarios

No hay comentarios aún. ¿Por qué no comienzas el debate?

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *