¿Por qué algunas API de Java omiten las comprobaciones estándar de SecurityManager?

15

En Java, las comprobaciones de permisos normalmente son manejadas por el Administrador de Seguridad. Para evitar que el código no confiable invoque código privilegiado y explote algún error en el código privilegiado, SecurityManager comprueba toda la pila de llamadas; Si alguna de las personas que llaman en el seguimiento de la pila no tiene privilegios, de forma predeterminada, se deniega la solicitud. Al menos, así es cómo funcionan las comprobaciones estándar de SecurityManager .

Sin embargo, algunas API de Java especiales siguen reglas diferentes. Pasan por alto las comprobaciones estándar de SecurityManager y sustituyen a las comprobaciones más débiles. En particular, verifican solo al llamante inmediato, no a toda la pila de llamadas. (Consulte Pauta 9-8 de las Pautas de codificación segura de Java para obtener más información. Las API especiales incluya, por ejemplo, Class.forName() , Class.getMethod() , y más.)

¿Por qué? ¿Por qué estas API especiales omiten las comprobaciones estándar y sustituyen a las débiles? Y, ¿por qué es esto seguro? En otras palabras, ¿por qué es suficiente para ellos verificar solo a la persona que llama de inmediato? ¿Esto no reintroduce todos los riesgos contra los cuales se diseñaron las comprobaciones estándar de SecurityManager?

Me enteré de esto al leer un análisis de la reciente exploit de día cero en Java (CVE-2012-4681). Ese análisis deconstruye cómo funciona el exploit. Entre otras cosas, el ataque implica aprovechar la comprobación más débil realizada por estas API especiales. En particular, el código Java malintencionado se las arregla para obtener una referencia a una clase de sistema confiable (a través de un error separado), luego engaña a esa clase de sistema confiable invocando una de estas API especiales. La verificación de permisos resultante solo se ve en su interlocutor inmediato, ve que el interlocutor inmediato es de confianza y permite la operación, aunque la operación se inició originalmente con un código que no es de confianza. Por lo tanto, las comprobaciones más débiles no detienen el ataque, pero, por lo que puedo ver, este ataque se habría prevenido mediante las comprobaciones estándar de SecurityManager (ya que la persona que llama no es confiable). En otras palabras, este ataque reciente parece un ejemplo de por qué el cheque más débil es riesgoso.

Sin embargo, sé que los diseñadores de Java son gente inteligente. Sospecho que los diseñadores de Java deben haber considerado estos problemas y tener una buena razón para eludir las comprobaciones estándar y sustituir las débiles de estas API especiales, o, al menos, pensaron que tenían una buena razón por la que esto era seguro. Entonces, tal vez me esté perdiendo algo.

¿Alguien puede arrojar alguna luz sobre esto? ¿Se equivocaron los diseñadores de Java con estas API especiales o hubo razones válidas para sustituir los cheques más débiles?

Editar 9/1: no pregunto cómo funciona el exploit; Creo que entiendo cómo funciona el exploit. Tampoco estoy preguntando por qué, en este ejemplo en particular, el código de confianza que invocó estas API especiales tenía errores. Más bien, me pregunto por qué las API especiales, como Class.forName() , Class.getMethod() , etc., se especifican e implementan para usar la verificación de permisos más débiles no estándar (solo busque en el llamante inmediato) en lugar de Verificación de permisos estándar de SecurityManager (mire la pila de llamadas completa). Esta decisión de diseño (de utilizar comprobaciones de permisos más débiles para esas API especiales) permitió la reciente vulnerabilidad, por lo que sería fácil criticar la decisión de diseño. Sin embargo, me imagino que podría haber algunas buenas razones para hacer las cosas de esta manera, y me pregunto cuáles podrían ser.

    
pregunta D.W. 01.09.2012 - 07:14
fuente

2 respuestas

8

(Esto es solo un comentario general sobre el "por qué", no sobre el ataque específico al que te estás refiriendo).

Desafortunadamente, los diseñadores de Java encontraron que era muy posible pintarse en un rincón, estructuralmente hablando. Por ejemplo, hay clases en java.io y en java.net , que están involucradas en hacer I / O. Supongamos que una JVM dada tiene un código nativo especial de interacción con el sistema operativo en java.io.FileDescriptor , lo que le permite hacer llamadas al sistema send() y receive() (no es el caso de la JVM Sun / Oracle, pero podría suceder en otra JVM, y de hecho lo hace en al menos la que una vez escribí). Para imponer la semántica de la zona de pruebas, estos métodos son no public , por supuesto.

La implementación de java.net.Socket desearía, naturalmente, utilizar estos métodos. Pero, según las convenciones de nomenclatura , Socket es una clase que se encuentra en el paquete java.net , no en java.io . ¿Cómo puede acceder a los métodos no públicos de java.io.FileDescriptor , considerando que debe hacerlo incluso si se invoca desde un código que no es de confianza (el código que no es de confianza puede abrir sockets, aunque no para todos los destinos)? Hay principalmente dos formas:

  • Agregue algunos métodos "bridge" native en java.net.Socket , que reenvían las llamadas a los métodos en java.io.FileDescriptor . Código nativo se burla de los paquetes y la visibilidad; El código nativo puede hacer todo.

  • Permita que el código de java.net haga alguna reflexión para acceder a métodos no públicos en otros paquetes. El código que puede hacer eso puede hacer todo, porque podría modificar las estructuras de datos utilizadas por SecurityManager (oye, recuerdo que tú mismo me lo dijiste, tal vez hace diez años, en una discusión de Usenet en la que ambos estábamos usando nuestros nombres reales). Por lo tanto, la reflexión ilimitada no se debe otorgar a todos, pero, en la situación que describo aquí, se debe otorgar para codificar en un paquete de sistema específico ( java.net ) aunque el código que se llama desde un applet no confiable.

El segundo método requiere el debilitamiento del modelo de seguridad. En realidad, es bastante genérico: si el applet no confiable debe hacer algo útil en algún momento, debe poder acceder al sistema (solo para mostrar el resultado de los cálculos o enviarlo de vuelta al servidor ), que necesita una especie de puerta. El código nativo es tal puerta. El modelo de debilitamiento de la seguridad es otro tipo de puerta, que tiene el beneficio adicional de permanecer en el mundo "Java puro" (puedo entender que en el equipo de mantenimiento de JVM, el código nativo probablemente esté mal visto porque hace las cosas más caras). El lado negativo es que al debilitar el modelo de seguridad, la superficie de ataque se amplía enormemente: ahora, todo el código Java en los paquetes con privilegios se ha vuelto crítico. Para reutilizar la analogía de @ Hendrik, se trata de dar una raíz setuid a una porción sustancial de código (de los paquetes java.* ).

En particular, la forma en que Java debilitó el modelo de seguridad es designar algunas API especiales, como Class.findClass() , Class.newInstance() , Class.getMethod() y otras API relacionadas con la reflexión, como el uso de controles de permisos más débiles. En el ejemplo anterior, esto permite que el código de sistema confiable en java.net.Socket use estas API especiales para obtener una referencia a un método no público en java.io.FileDescriptor e invocarlo de manera reflexiva.

  • Por ejemplo, el código java.net.Socket puede usar Class.getMethod() para obtener una referencia al método no público en java.io.FileDescriptor (esto está permitido ya que el llamador inmediato de Class.getMethod() es java.net.Socket() , que es confiable código) y luego invocarlo.

    Tenga en cuenta que esta estrategia de implementación se basa en Class.getMethod() para usar las comprobaciones de permisos más débiles. Simplemente no funcionaría si Class.getMethod() usara las verificaciones de permisos estándar. Si el código que no es de confianza invoca a java.net.Socket , y luego el java.net.Socket código llama a Class.getMethod() , las verificaciones de permisos estándar rechazarán esta llamada porque hay un código no confiable en algún lugar de la pila de llamadas, y las cosas de java.net.Socket no funcionarán bien (En escenarios de no ataque). En contraste, las comprobaciones de permisos más débiles utilizadas en el modelo de seguridad debilitado lo permiten.

Por lo tanto, las comprobaciones de permisos más débiles ayudan a los diseñadores de Java a salir del rincón en el que se pintaron.

Supuestamente, con una estructura "perfecta" del código del sistema y las convenciones de nomenclatura, bastaría con un puñado de métodos nativos, pero la presencia de llamadas específicas en el paquete JVM es de llamadas inmediatas desde una persona de confianza. El código de la biblioteca del sistema muestra que la estructura de dicho código no es perfecta.

    
respondido por el Tom Leek 01.09.2012 - 14:59
fuente
3

Resumen

Algunos métodos confiables necesitan más permisos internos para cumplir sus tareas. Pero si esos métodos tienen errores, pueden permitir que un atacante que no es de confianza ejecute acciones no deseadas en un nivel de privilegio aumentado.

Fondo

Tenemos una situación muy similar a nivel de sistema operativo. En Unix hay setuid / setgid flags y el comando sudo. Permiten a un usuario sin privilegios ejecutar tareas, que requieren un nivel más alto de privilegios internos.

Por ejemplo, un usuario normal no puede modificar /etc/shadow . Pero queremos que los usuarios puedan cambiar su contraseña. Por lo tanto, el comando passwd se marca como confiable (setuid root) y se le permite modificar ese archivo. Por supuesto, tiene que realizar sus propios controles de seguridad.

La misma situación se aplica a la zona de pruebas de Java. Por ejemplo, el código no privilegiado no puede invocar dinámicamente métodos. Pero partes del sistema deben hacerlo internamente.

Al igual que el comando passwd permite a los usuarios normales cambiar su propia contraseña solamente, se supone que MethodFinder.findMethod() solo permite que el código de confianza llame a métodos arbitrarios.

Hasta ahora, todo está bien.

Explotación de carga de clase

ClassFinder.findClass() es un método tan confiable. Carga clases adicionales con los privilegios del código de llamada. Al igual que passwd le permite cambiar su propia contraseña.

Pero en caso de error, trató de recuperarse cargando la clase con permiso completo. Si pensamos en passwd , un error similar causaría que passwd cambie la contraseña de root en lugar del usuario actual.

Los sistemas operativos permitirán que passwd cambie la contraseña de root, al igual que Class.forName() permite que ClassFinder.findClass() cargue las clases en el contexto de seguridad incorrecto.

Explotación de invocación de método

Este es un poco más complejo. El método de confianza es Method.invoke() aquí.

Pero hubo un segundo método de confianza involucrado llamado MethodFinder.findMethod() . Para seguir con la analogía del sistema operativo, considérelo como un script de shell que se ejecuta con privilegios de root a través de sudo.

Este método / programa no valida sus parámetros, solo los pasó a passwd . Ahora passwd se invoca en un contexto de confianza. Por lo tanto, cambiará la contraseña de cualquier persona.

    
respondido por el Hendrik Brummermann 01.09.2012 - 10:25
fuente

Lea otras preguntas en las etiquetas