Aunque no hayas oído hablar del proyecto Ghostscript, es muy posible que lo hayas utilizado sin saberlo.
También es posible que lo hayas incorporado a un servicio en la nube que ofreces, o que lo tengas preinstalado y listo para usar si utilizas un software basado en paquetes, como una distribución BSD o Linux, Homebrew en Mac o Chocolatey en Windows.
Ghostscript es una implementación gratuita y de código abierto del ampliamente utilizado sistema de composición de documentos PostScript de Adobe y de su aún más utilizado formato de archivo PDF, abreviatura de Portable Document Format (Formato de Documento Portátil). Internamente, los archivos PDF se basan en el código PostScript para definir cómo componer un documento.
Por ejemplo, el popular programa de gráficos de código abierto Inkscape utiliza Ghostscript entre bastidores para importar archivos de gráficos vectoriales EPS (Embedded PostScript), como los que puedes descargar de una biblioteca de imágenes o recibir de una empresa de diseño.
En pocas palabras, Ghostscript lee el código del programa PostScript (o EPS, o PDF), que describe cómo construir las páginas de un documento, y lo convierte, o lo renderiza (por usar la palabra técnica), en un formato más adecuado para su visualización o impresión, como datos de píxeles sin procesar o un archivo gráfico PNG.
Por desgracia, hasta la última versión de Ghostscript, ahora la 10.01.2, el producto tenía un error, denominado CVE-2023-36664, que podía permitir que documentos maliciosos no solo crearan páginas de texto y gráficos, sino que también enviaran comandos de sistema al motor de renderizado de Ghostscript y engañaran al software para que los ejecutara.
Pipes y pipelines
El problema surgió porque el manejo de Ghostscript de los nombres de archivo para la salida permitía enviar la salida a lo que en la jerga se conoce como un “pipe” en lugar de a un archivo normal.
Los “pipes”, como sabrás si alguna vez has programado o escrito scripts, son objetos del sistema que simulan ser archivos, en el sentido de que puedes escribir en ellos como en un disco, o leer datos de ellos, utilizando funciones normales del sistema como read() y write() en sistemas tipo Unix, o ReadFile() y WriteFile() en Windows, pero los datos no terminan realmente en el disco.
En lugar de eso, el extremo de “escritura” de un pipe simplemente transporta los datos de salida a un bloque temporal de memoria, y el extremo de “lectura”, lee cualquier dato que ya esté en la “pipeline” de memoria, como si procediera de un archivo permanente en el disco.
Esto es muy útil para enviar datos de un programa a otro.
Cuando quieras tomar la salida del programa UNO.EXE y utilizarla como entrada para DOS.EXE, no necesitas guardar primero la salida en un archivo temporal y luego volver a leerla utilizando los caracteres “>” y “<” para redirigir el archivo, de esta forma:
C:\Users\duck> UNO.EXE > TEMP.DAT C:\Users\duck> DOS.EXE < TEMP.DAT
Este método conlleva varios inconvenientes, entre los que se incluyen los siguientes:
- Tienes que esperar a que el primer comando termine y cierre el archivo TEMP.DAT antes de que el segundo comando pueda empezar a leerlo.
- Podrías acabar con un archivo intermedio enorme que consuma más espacio en disco del que deseas.
- Podrías liarte si otra persona toquetea el archivo temporal entre la finalización del primer programa y el lanzamiento del segundo.
- Tienes que asegurarte de que el nombre del archivo temporal no choque con un archivo existente que quieras conservar.
- Te quedas con un archivo temporal que limpiar más tarde y que podría filtrar datos si se olvida.
Con un “pseudoarchivo” intermedio basado en memoria en forma de pipe, puedes condensar este tipo de cadena de procesos en:
C:\Users\duck> UNO.EXE | DOS.EXE
Puedes ver en esta notación de dónde proceden los nombres de pipe y pipeline, y también por qué el símbolo de barra vertical (|) elegido para representar el conducto (tanto en Unix como en Windows) se conoce más comúnmente en el mundo de la informática como el carácter pipe.
Dado que los archivos-que-son-en-realidad-pipes-a-nivel-del-sistema-operativo se utilizan casi siempre para la comunicación entre dos procesos, ese carácter mágico del pipe suele ir seguido no de un nombre de archivo en el que escribir para su uso posterior, sino del nombre de un comando que consumirá la salida de inmediato.
En otras palabras, si permites que el contenido suministrado remotamente especifique un nombre de archivo que se utilizará para la salida, debes tener cuidado si permites que ese nombre de archivo tenga una forma especial que diga: “No escribas en un archivo; en su lugar, inicia un pipe, utilizando el nombre de archivo para especificar un comando que ejecutar”.
Cuando las funciones se convierten en errores
Al parecer, Ghostscript tenía una “función” de este tipo, por la que podías decir que querías enviar la salida a un nombre de archivo con un formato especial que empezara por %pipe% o simplemente |, dándote así la posibilidad de lanzar sigilosamente un comando de tu elección en el ordenador de la víctima.
No lo hemos probado, pero suponemos que también puedes añadir opciones de línea de comandos, así como un nombre de comando para ejecutar, lo que te da un control aún más preciso sobre qué tipo de comportamiento malicioso provocar en el otro extremo.
Curiosamente, si esa es la palabra correcta, el problema de “a veces los parches necesitan parches” volvió a surgir en el proceso de corrección de este error.
En el artículo de esta semana sobre un fallo en un plugin de WordPress, describimos cómo los creadores del plugin con el fallo (Ultimate Member) han pasado reciente y rápidamente por cuatro parches intentando solucionar un fallo de escalada de privilegios.
También hemos escrito recientemente sobre el software de intercambio de archivos MOVEit, que publicó tres parches en rápida sucesión para hacer frente a una vulnerabilidad de inyección de comandos que apareció por primera vez como un día cero en manos de delincuentes de ransomware.
En este caso, el equipo de Ghostscript añadió primero una comprobación como ésta, para detectar la presencia del peligroso texto %pipe… al principio de un nombre de archivo:
/* "%pipe%" no sigue las reglas normales de definición de rutas, por lo qu no los "reducimos" para evitar resultados inesperados */ if (len > 5 && memcmp(ruta, "%pipe", 5) != 0) { . . .
Entonces los programadores se dieron cuenta de que su propio código aceptaría tanto un carácter | plano como el prefijo %pipe%, por lo que el código se actualizó para tratar ambos casos.
Aquí, en lugar de comprobar que la ruta variable no empieza por %pipe… para detectar que el nombre de archivo es “seguro”, el código declara que el nombre de archivo no es seguro si empieza por un carácter de tubería (|) o por el temido texto %pipe…:
/* "%pipe%" no sigue las reglas normales de las definiciones de ruta, por lo que no las "reducimos" para evitar resultados inesperados */ if (ruta[0] == '|' || (len > 5 && memcmp(ruta, "%tubería", 5) == 0)) { . . .
Arriba, verás que si memcmp() devuelve cero, significa que la comparación fue VERDADERA, porque los dos bloques de memoria que estás comparando coinciden exactamente, aunque cero en los programas en C se utilice convencionalmente para representar FALSO. Esta molesta incoherencia surge del hecho de que memcmp() en realidad te indica el orden de los dos bloques de memoria. Si el primer bloque se ordenara alfanuméricamente antes que el segundo, te devuelve un número negativo, para que puedas saber que aardvark precede a zymurgy1. Si son al revés, obtienes un número positivo, que deja el cero para denotar que son idénticos. Así
#include <string.h> #include <stdio.h> int main(void) { printf("%d\n",memcmp("aardvark", "zymurgy1",8)); printf("%d\n",memcmp("aardvark", "00NOTES1",8)); printf("%d\n",memcmp("aardvark", "aardvark",8)); return 0; } ---output--- -1 1 0
¿Qué puedes hacer?
- Si tienes un paquete Ghostcript independiente gestionado por tu distribución Unix o Linux (o por un gestor de paquetes similar, como el mencionado Homebrew en macOS), asegúrate de que tienes la última versión.
- Si tienes un programa que incluye una versión de Ghostscript, consulta con el proveedor para obtener información sobre cómo actualizar el componente Ghostscript.
- Si eres programador, no aceptes cualquier corrección de errores inmediatamente evidente como principio y fin de tu trabajo de eliminación de vulnerabilidades. Pregúntate, como hizo el equipo de Ghostscript: “¿Dónde más podría haber ocurrido un tipo similar de error de codificación, y qué otros trucos podrían utilizarse para desencadenar el fallo que ya conocemos?”.