CouchDB exploit: vulnerabilidad y ejecución remota

Si usaste npm para el manejo de las dependencias de tu código, usaste CouchDB más veces de lo que imaginas.

CouchDB es una base de datos no relacional “NoSQL” y cuyo propósito principal es el de hacer la replicación de los datos en múltiples dispositivos de la forma más sencilla posible. Es algo así como una fuente de almacenamiento KVS para guardar blobs de JSON (documents), con agregados como validaciones de datos, querying y autenticación integrada de usuarios. De esta forma, tenemos una API muy completa para el manejo de nuestros datos y con autenticación incorporada, todo corriendo en una misma instancia, y con capacidad para replicarse y sincronizarse con otras instancias o con otros entornos compatibles de una forma rápida, sencilla y automatizada.

Pero no estamos acá para dar mucho más detalle de esto. Si quieren aprender más sobre CouchDB y otras formas de almacenar datos, acá dejamos una charla nuestra sobre almacenamiento del lado cliente que resume un poco mejor este tema y lo compara con otras formas tradicionales de persistir datos.

Lo que hoy venimos a contarles es sobre un exploit para CouchDB que permite a un usuario malintencionado (u ocioso) ejecutar código en una instancia productiva.

¿Cómo logran esto? Bueno, CouchDB está originalmente escrito en Erlang, pero permite especificar validaciones de documentos en JavaScript. Estos scripts se evalúan automaticamente cada vez que se crea o se actualiza un documento en la base de datos. Se ejecutan en un nuevo proceso y están disponibles inmediatamente después de que se serializan a formato JSON desde el lado de Erlang. El asunto entonces es que CouchDB maneja las cuentas de los usuarios a través de una tabla especial llamada _users. Cada vez que creas o modificas un usuario, el servidor chequea tus cambios con una función en JavaScript validate_doc_update que asegura que no estes intentando cosas no permitidas, como por ejemplo darte de alta como un usuario administrador.

¿Quién dijo… “bugs”?

Bien, pero no me dijiste cómo lo logran… ¿Cuál es la Vulnerabilidad entonces?

El problema está en que existe una discrepancia entre el JSON parser de Javascript (que se usa en las validaciones de los scripts) con el parser que usa CouchDB internamente llamado jiffy, que es la instancia que lo serializa antes de dárselo a Javascript. Cuando intentas parsear un objeto con keys duplicadas, por ejemplo {"foo":"bar", "foo":"baz"} pasa lo siguiente:

Erlang:

> jiffy:decode("{\"foo\":\"bar\", \"foo\":\"baz\"}"). 
{[{<<"foo">>,<<"bar">>},{<<"foo">>,<<"baz">>}]}

Javascript:

> JSON.parse("{\"foo\":\"bar\", \"foo\": \"baz\"}")
{foo: "baz"}

Para una key dada, el parser de Earlang guardará los valores, pero para el parser de Javascript solamente guardará el último valor repetido. Desafortunadamente, la función interna de CouchDB que muestra estos datos devolverá solamente el primer valor:

% Within couch_util:get_value 
lists:keysearch(Key, 1, List).

Entonces, podemos saltear todos los chequeos relevantes y crear así un usuario administrador, de la siguiente manera:

curl -X PUT 'http://localhost:5984/_users/org.couchdb.user:ouch'
--data-binary '{
  "type": "user",
  "name": "ouch",
  "roles": ["_admin"],
  "roles": [],
  "password": "password"
}'

Internamente, desde Erlang tendremos permisos de administrador, mientras que desde el lado de Javascript nuestro roles[] aparecerá vacío, sin ningún permiso especial. Afortunadamente para el atacante, toda la lógica importante que concierne a la autenticación ocurre del lado de Erlang.

Ahora que tenemos un usuario administrador, tenemos total control de toda la base de datos. Lo más curioso de este exploit es que no es fácil darse cuenta de esta situación desde la interfaz gráfica de administración (usualmente conocida como Fauxton), dado que cuando investigas el usuario que acabas de crear, el campo roles se muestra vacío, como si no tuviese ningún permiso especial, y esto es porque de este lado el parseo se realiza desde Javascript!

¿Qué podemos hacer para evitar esto?

  1. Bueno, actualizar a la versión 2.2 de CouchDB o superior es una forma de lidiar con este bug. De esta forma podemos evitar esta inconsistencia entre los parsers y evitar este inconveniente.
  2. Otra cosa que podemos hacer es sobreescribir el comportamiento nativo que tiene la base _users. Dicho comportamiento es que un usuario anónimo puede crear usuarios en esta base de datos. Para limitar esto, se puede modificar la función validate_doc_update interna del documento _design/_auth, que ya posee una laxa limitación. El chequeo !is_server_or_database_admin(userCtx, secObj) ya existe ahí, sólo hay que usarlo. Para permitir crear usuarios nuevos sólo a usuarios administradores, se puede agregar la siguiente excepción:
    throw({forbidden : 'Users can only be created by server or db admins in this specific CouchDB installation'})

Se agrega al principio del chequeo, para prohibir cualquier acción posterior cuando el usuario no es un usuario administrador.

Entonces, validate_doc_update quedaría así:

//[… existing code …]
if(!is_server_or_database_admin(userCtx, secObj)) {
    throw({forbidden : 'Users can only be created by server or db admins in this specific CouchDB installation'})
    //[… existing code …] 
}

3. Si estamos usando Docker para crear nuestras instancias productivas, hay que tener mucho cuidado de dónde provienen. Existen repositorios como este que permiten crear imágenes de Docker con vulnerabilidades ocultas preexistentes. De esta forma, estamos armando una instancia que parece funcionar normalmente, pero que en realidad tiene un agujero oculto que le permite a un atacante tomar posesión de nuestros datos. Para el caso de CouchDB, en el repositorio se ve claramente que se expone la vulnerabilidad que mencionamos, y acá se ve como un script crea un usuario con nombre ‘wooyun’ de forma automática. Puede suceder que encontremos estos usuarios extraños que aparecen imprevistamente creados en la DB. Si eso sucede, quiere decir que tu imagen está infectada con estas vulnerabilidades preexistentes.

Espero que este post les haya sido de utilidad para mantener sus bases de CouchDB más seguras, y para que consideren muy bien las decisiones de diseño que toman a la hora de validar datos y operaciones con 2 lenguajes de programación distintos, dado que no ser consciente de sus diferencias puede acarrear estos inconvenientes.

Hasta la próxima!

Músico. Compositor. Creador de proyectos. Desarrollador de alma. Frontender por vicio. Aprendiz de fullstack :P

Leave a Comment

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