La última vulnerabilidad en WordPress: una serie de catastróficas desdichas

 

wordpress

 

Muchas veces mientras programamos nos damos cuenta de que hemos cometido alguna pequeña equivocación, un fallo mínimo por el que hay ciertas condiciones en las que una variable puede quedar sin inicializar, o una condición de carrera que estamos convencidos de que no ocurrirá nunca, y tenemos que luchar contra la tentación de dejarlo estar, con la esperanza de que nadie se dé cuenta. Sin embargo, a veces los astros se alinean y lo que parecía una serie de equivocaciones inofensivas se convierten en una catástrofe en toda regla.

 

En este post vamos a hablar sobre una vulnerabilidad en WordPress reportada hace unos días (https://blog.sucuri.net/2017/02/content-injection-vulnerability-wordpress-rest-api.html) y corregida en la versión 4.7.2. Esta vulnerabilidad afecta a la API REST incluida y activada por defecto, y que permite acceder a los posts y modificarlos sin necesidad de acceder al interfaz de usuario.

 

Como ejemplo, la siguiente petición nos permite modificar el título del post con id 4 y sustituir su contenido por el texto “Nuevo contenido”:

 

curl -XPOST http://localhost/index.php/wp-json/wp/v2/posts/4?content=Nuevo+contenido

 

Aunque lógicamente, esta petición requiere autenticación, ya que en caso contrario devuelve un error:

w1

 

A continuación, veremos tres errores simples (e individualmente inofensivos) en la gestión de estas peticiones que permitirán, de forma muy sencilla, modificar posts sin estar autenticado.

 

TRES ERRORES SIMPLES EN LA GESTIÓN DE LAS PETICIONES QUE PUEDEN CAUSAR EL CAOS

 

1.Varias vías de entrada de parámetros con diferentes validaciones

 

El endpoint anterior tiene la siguiente ruta como vía de entrada:

…/wp/v2/posts/(?P<id>[\\d]+)

 

Esto implica que lo que aparece después de “/posts/” debe ser un número entero y su valor será asignado a la variable “id”. Es decir, si intentamos hacer una petición para modificar un post llamado “aaa” se produce un error, ya que el valor “aaa” no cumple la expresión regular “[\\d]+”:

 

w2

 

Sin embargo, la forma en que la API REST trata los parámetros implica que si existe un parámetro llamado “id” enviado por GET o POST, tiene preferencia sobre el parámetro extraído de la ruta. Por ejemplo, es posible “engañar” a la API usando una ruta que haga referencia a un post pero luego modificando el post al que se hace referencia mediante un parámetro. Por ejemplo:

 

w4

 

Podemos ver que el mensaje de error ya no indica que no se puede modificar el post (como ocurría con el identificador 4) sino que el identificador es inválido (ya que se está usando el parámetro con valor 40). Además, debido a que la comprobación del formato del parámetro se realiza únicamente en la ruta, la API admite una petición con identificador alfanumérico como la siguiente:

 

De nuevo el mensaje indica que no se está usando el identificador 4, pero tampoco indica que el valor haya sido rechazado (como ocurría cuando intentábamos indicar el identificador aaa en la ruta).

 

Este comportamiento puede parecer extraño. Ahora bien… ¿cuál es el problema? Si usamos como valor del parámetro GET un valor numérico no es diferente a haberlo puesto en la propia ruta, mientras que si usamos un valor que no sea numérico, al no existir el post no habrá ningún efecto.

 

En resumen, este error no parece muy serio… Pero sigamos viendo.

 

2.  Gestión incorrecta de los permisos

 

El siguiente error se da en la comprobación de si la operación que se intenta realizar es válida o no. El problema es que se realiza en forma de “lista negra”. Es decir, cualquier operación de actualización de un post es válida salvo que se dé uno de los siguientes casos:

 

  • El post existe y el usuario no tiene permiso para editarlo
  • El usuario intenta que la modificación se asocie a un usuario distinto y no tiene permiso para ello
  • El usuario intenta marcar un post como “sticky” y no tiene permiso para ello
  • El usuario intenta modificar las etiquetas de un post y no tiene permiso para ello

 

Cualquier otra operación está permitida. Esto implica, si nos fijamos atentamente, en que una operación que intente modificar un post que no existe es permitida siempre (salvo que intentemos cambiar el usuario, marcarlo como “sticky” o modificar sus etiquetas).

 

De nuevo vemos un error que no es serio como tal… cualquier usuario puede modificar (que no crear) un post que no existe. Pero como no existe, por definición es imposible modificarlo y acabará fallando la operación así que… Sin problemas, ¿no?

 

Sólo queda un error más por presentar… y luego ya veremos.

 

3. Conversión de tipos incorrecta

 

El último error se produce tras comprobar que el usuario tiene permiso para efectuar la operación, cuando se recupera el post asociado al identificador. Recordemos que al hablar del primer error decíamos que si se usaba un identificador alfanumérico la API detecta que es un identificador inválido y devuelve un error. Esto no es del todo cierto.

 

En realidad lo que hace la API es una transformación del identificador (que hasta este momento se ha tratado como una cadena de texto) y se convierte directamente en un entero. Sin embargo, la conversión de cadena a entero usada en PHP tiene un comportamiento amigable para el programador pero propensa a errores… y es que en caso de que la cadena de texto que se le pasa como parámetro contenga caracteres no numéricos, la conversión se realiza hasta el primer carácter no numérico:

w5

 

 Por tanto, si estamos procesando una operación en la que intentamos modificar el post con identificador ‘4a’, esta conversión a entero nos devuelve el id 4 y por tanto la modificación se realizará sobre el post 4.

 

Sin embargo, este error por sí solo tampoco es serio porque ya hemos visto que el parámetro id siempre va a ser un entero, ¿no?

 

¿QUÉ OCURRE SI HACEMOS LA PETICIÓN SIN NINGUN TIPO DE AUTENTICACIÓN?

 

Una vez vistos estos errores poco importantes, llega la gran preguntA: ¿Qué pasarña si hacemos la siguiente petición sin autenticación?

 

curl -XPOST “http://localhost/index.php/wp-json/wp/v2/posts/4?id=4a&content=Nuevo+contenido”

 

Vamos a seguir el procesamiento tal y como hemos visto hasta ahora:

 

  • La API extrae el identificador de la ruta, y por tanto asocia esta operación al post 4
  • Existe un parámetro con nombre “id” y valor “4a”, que tiene preferencia sobre el parámetro de la ruta. Por tanto, el post sobre el que se opera pasa a ser el 4a
  • Llega la comprobación de permisos. El post 4a no existe, por tanto se sigue el comportamiento por defecto y se permite la operación, incluso aunque no hay usuario autenticado.
  • Se va a recuperar el post 4a, pero al realizarse la conversión a entero se obtiene 4. Por tanto, se accede al post 4.
  • Se modifica el post 4

 

Esto podemos verlo fácilmente si accedemos al post 4 (Post de prueba) antes de la operación. El contenido del post lo rodeamos con un recuadro verde para aclararlo:

w6

 

 

Ejecutamos la modificación. Puede verse que en este caso no devuelve el error de que no hay permisos (no se incluye en el ejemplo la respuesta del servidor completa por brevedad):

 

w7

 

 

 

Y volvemos a acceder al post 4:

w8

 

 

Podemos ver que el contenido ha sido modificado sin necesidad de usar autenticación. Analizando lo ocurrido, podemos ver que esta vulnerabilidad tan grave se evita si eliminamos cualquiera de los tres errores anteriores:

 

  • Si no se admite que los parámetros GET/POST tengan preferencia sobre los parámetros de la ruta (o les aplicáramos las mismas comprobaciones que a éstos) nunca conseguiríamos un identificador de post no numérico.
  • Si la respuesta de la comprobación de permisos fuese denegar cualquier operación sobre un post inexistente, cualquier intento de modificar un post con identificador no numérico fallaría.
  • Si al ir a recuperar el post se usara el identificador completo o se rechazaran identificadores con caracteres no numéricos, nunca se asociaría el identificador 4a con el post 4.

 

Conclusión

 

Nunca debemos dejar errores en nuestro código, por muy inofensivos que parezcan. Es cierto que detectarlos todos es prácticamente imposible, pero error detectado debe ser error corregido, o nos arriesgamos a que interacciones entre ellos acaben derivando en una tormenta perfecta que nos lleven a vulnerabilidades tan graves como esta.

 

En otro orden de cosas, este caso es otra demostración de por qué es importante mantener siempre las versiones de nuestros productos lo más actualizadas posibles. Así que si tenéis un servidor con WordPress, os recomendamos encarecidamente que lo actualicéis a la última versión (4.7.2 en este momento) si no queréis llevaros sorpresas muy desagradables.

 

ION LARRAÑAGA

Software Developer en S21sec

 

Recommended Posts

Leave a Comment