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

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

assert eval("2 + 3 * len('hello')") == 17

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

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

>>> import os
>>> eval("os.system('ls')", {})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'os' is not defined
>>> 

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

>>> eval("__import__('os').system('ls')", {})
Desktop                burp.der
Documents            ctf
Downloads            flag5.txt
Library                gitee
Movies                github
Music                kali
Parallels            key.txt
Pictures            log
...

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

>>> eval("__import__('os').system('ls')", {'__builtins__':{}})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name '__import__' is not defined

那现在真的安全了吗?一些人可能会认为这下安全了。

其实仍然不安全。

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

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

用代码来解释下:

比如我们要执行:

import os
os.system('ls')

Python 解释器会先编译成 code 对象,然后执行的:

>>> code_str = '''
... import os
... os.system('ls')
... '''
>>> code_obj = compile(code_str,'<string>','exec')
>>> code_obj
<code object <module> at 0x7fc5741175b0, file "<string>", line 2>
>>> eval(code_obj)
Desktop                burp.der
Documents            ctf
Downloads            flag5.txt
Library                gitee
Movies                github
Music                kali
Parallels            key.txt
Pictures            log

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

>>> 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 对象,可以这样做:

>>> [].__class__.__bases__[0]
<class 'object'>

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

>>> [].__class__.__bases__[0].__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_reverseitemiterator'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, 
......

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

>>> all_classes = [].__class__.__bases__[0].__subclasses__()
>>> len(all_classes)
181
>>> [c for c in all_classes if c.__name__ == 'code'][0]
<class 'code'>
>>>

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

>>> code_str = "__import__('os').system('ls')"
>>> code_obj = compile(code_str,'<string>','single')
>>> code_obj
<code object <module> at 0x7fd06074abe0, file "<string>", 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'),(),'<string>','<module>',1,bytes.fromhex(''),(),())

code、bytes 都可以从上述 all_classes 获取,这样我们分部执行,就可以执行我们的代码:

>>> 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'),(),'<string>','<module>',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) 已经成功执行, 转换成一个字符串就是:

>>> 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'),(),'<string>','<module>',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 "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name '__import__' is not defined

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

(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 "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
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 的字符串是排除任何带有双下划线的字符串,那么也许是安全的。因为某些操作依然可以构造出双下划线,如下所示:

>>> eval('eval("()._" + "_class_" + "_._" + "_bases_" + "_[0]")')
<class 'object'>
>>>

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

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

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

  • Exploring Python Code Objects

  • Python沙箱?不存在的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值