Python中从服务端模板注入到沙盒逃逸的源码探索 (一)

栏目: Python · 发布时间: 6年前

内容简介:不同的模板引擎,根据不同的解析方式相应的也是存在不同的利用方法。正常而言,出于安全考虑,模板引擎基本上都是拥有沙盒、命名空间的,代码的解析执行都是发生在有限的沙盒里面,因此,沙盒逃逸也成为 SSTI 不可或缺的存在。Tornado 中模板渲染函数在有两个

原理

顾名思义,服务端模板注入,就是通过在 服务端模板文件模板字符串 中注入特定的恶意代码导致产生代码执行的一种漏洞攻击方式。

不同的模板引擎,根据不同的解析方式相应的也是存在不同的利用方法。

正常而言,出于安全考虑,模板引擎基本上都是拥有沙盒、命名空间的,代码的解析执行都是发生在有限的沙盒里面,因此,沙盒逃逸也成为 SSTI 不可或缺的存在。

Python Web 模板引擎

  • [x] Jinja2
  • [x] Tornado.template
  • [ ] Django.template
  • ...

SSTI in Tornado

Tornado 中模板渲染函数在有两个

  • render
  • render_string

tornado/web.py:

class RequestHandler(object):
    ....
    def render(self, template_name, **kwargs):
        ...
        html = self.render_string(template_name, **kwargs)
        ...
        return self.finish(html)

    def render_string(self, template_name, **kwargs):

        template_path = self.get_template_path()
        ...
        with RequestHandler._template_loader_lock:
            if template_path not in RequestHandler._template_loaders:
                loader = self.create_template_loader(template_path)
                RequestHandler._template_loaders[template_path] = loader
            else:
                loader = RequestHandler._template_loaders[template_path]
        t = loader.load(template_name)
        namespace = self.get_template_namespace()
        namespace.update(kwargs)
        return t.generate(**namespace)

    def get_template_namespace(self):
        namespace = dict(
            handler=self,
            request=self.request,
            current_user=self.current_user,
            locale=self.locale,
            _=self.locale.translate,
            pgettext=self.locale.pgettext,
            static_url=self.static_url,
            xsrf_form_html=self.xsrf_form_html,
            reverse_url=self.reverse_url
        )
        namespace.update(self.ui)
        return namespace

render_string:通过模板文件名加载模板,然后更新模板引擎中的命名空间,添加一些全局函数或其他对象,然后生成并返回渲染好的 html内容

render:依次调用 render_string 及相关渲染函数生成的内容,最后调用 finish 直接输出给客户端。

我们跟进模板引擎相关类看看其中的实现。

tornado/template.py

class Template(object):
    ...
    def generate(self, **kwargs):
        namespace = {
            "escape": escape.xhtml_escape,
            "xhtml_escape": escape.xhtml_escape,
            "url_escape": escape.url_escape,
            "json_encode": escape.json_encode,
            "squeeze": escape.squeeze,
            "linkify": escape.linkify,
            "datetime": datetime,
            "_tt_utf8": escape.utf8,  # for internal use
            "_tt_string_types": (unicode_type, bytes),
            "__name__": self.name.replace('.', '_'),
            "__loader__": ObjectDict(get_source=lambda name: self.code),
        }
        namespace.update(self.namespace)
        namespace.update(kwargs)
        exec_in(self.compiled, namespace)
        execute = namespace["_tt_execute"]
        linecache.clearcache()
        return execute()

在上面的代码中,我们很容易看出命名空间namespace中有哪些变量、函数的存在。其中, handler 是一个神奇的存在。

tornado/web.py:

class RequestHandler(object):
    ....
    def __init__(self, application, request, **kwargs):
        super(RequestHandler, self).__init__()

        self.application = application
        self.request = request

RequestHandler 类的构造函数中,可以看到 application 的赋值。

tornado/web.py:

class Application(ReversibleRouter):
    ....
    def __init__(self, handlers=None, default_host=None, transforms=None,
                 **settings):
        ...
        self.wildcard_router = _ApplicationRouter(self, handlers)
        self.default_router = _ApplicationRouter(self, [
            Rule(AnyMatches(), self.wildcard_router)
        ])

class _ApplicationRouter(ReversibleRuleRouter):
    def __init__(self, application, rules=None):
        assert isinstance(application, Application)
        self.application = application
        super(_ApplicationRouter, self).__init__(rules)

因此,通过 handler.application 即可访问整个Tornado。

简单而言通过 {{handler.application.settings}} 或者 {{handler.settings}} 就可获得 settings 中的 cookie_secret

例题: 护网杯 2018 WEB (1) easy_tornado

另外,跟进 exec_in 中也有新发现。

tornado/util.py:

def exec_in(code, glob, loc=None):
    # type: (Any, Dict[str, Any], Optional[Mapping[str, Any]]) -> Any
    if isinstance(code, basestring_type):
        # exec(string) inherits the caller's future imports; compile
        # the string first to prevent that.
        code = compile(code, '<string>', 'exec', dont_inherit=True)
    exec(code, glob, loc)

这里用到了 compileexec

SSTI in Flask

Flask 中模板渲染函数也是有两个

  • render_template
  • render_template_string

Flask使用的是 Jinja2 模板引擎

flask/templating.py

def _default_template_ctx_processor():
    """Injects `request`,`session` and `g`."""
    reqctx = _request_ctx_stack.top
    appctx = _app_ctx_stack.top
    rv = {}
    if appctx is not None:
        rv['g'] = appctx.g
    if reqctx is not None:
        rv['request'] = reqctx.request
        rv['session'] = reqctx.session
    return rv

def _render(template, context, app):
    before_render_template.send(app, template=template, context=context)
    rv = template.render(context)
    template_rendered.send(app, template=template, context=context)
    return rv

def render_template(template_name_or_list, **context):
    ctx = _app_ctx_stack.top
    ctx.app.update_template_context(context)
    return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list),
                   context, ctx.app)

def render_template_string(source, **context):
    ctx = _app_ctx_stack.top
    ctx.app.update_template_context(context)
    return _render(ctx.app.jinja_env.from_string(source),
                   context, ctx.app)

render_template:通过模板文件加载内容并进行渲染

render_template_string:直接通过模板字符串进行渲染

这上下文、栈啥的看的有点懵,也不深入了。(有兴趣自行了解)

接着,我们看看 Flask 是怎么加载 Jinja2 的。 app.jinja_env

flask/app.py

class Flask(_PackageBoundObject):
    ...
    jinja_environment = Environment
    ...
    jinja_options = ImmutableDict(
        extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_']
    )
    ...
    @locked_cached_property
    def jinja_env(self):
        return self.create_jinja_environment()
    ...
    def create_jinja_environment(self):
        options = dict(self.jinja_options)
        rv = self.jinja_environment(self, **options)
        rv.globals.update(
            url_for=url_for,
            get_flashed_messages=get_flashed_messages,
            config=self.config,
            request=request,
            session=session,
            g=g
        )
        rv.filters['tojson'] = json.tojson_filter
        return rv

这里我们可以看见 jinja_environment 中有6个全局变量,也就是说在模板引擎的解析环境中可以访问这6个对象。

例题 TokyoWesterns CTF 4th 2018 shrine

接下来,我们跟进 Jinja2 的代码里看看还有什么有意思的东西。

jinja2/environment.py

class Environment(object):
    ...
    def _generate(self, source, name, filename, defer_init=False):
        return generate(source, self, name, filename, defer_init=defer_init,
                        optimized=self.optimized)

    def _compile(self, source, filename):
        return compile(source, filename, 'exec')

    @internalcode
    def compile(self, source, name=None, filename=None, raw=False,
                defer_init=False):
        source_hint = None
        try:
            if isinstance(source, string_types):
                source_hint = source
                source = self._parse(source, name, filename)
            source = self._generate(source, name, filename,
                                    defer_init=defer_init)
            if raw:
                return source
            if filename is None:
                filename = '<template>'
            else:
                filename = encode_filename(filename)
            return self._compile(source, filename)
        except TemplateSyntaxError:
            exc_info = sys.exc_info()
        self.handle_exception(exc_info, source_hint=source_hint)

class Template(object):
    ...
    def render(self, *args, **kwargs):
        vars = dict(*args, **kwargs)
        try:
            return concat(self.root_render_func(self.new_context(vars)))
        except Exception:
            exc_info = sys.exc_info()
        return self.environment.handle_exception(exc_info, True)
    ...

这里也是通过 compile 对模板进行编译的

jinja2/parser.py

_statement_keywords = frozenset(['for', 'if', 'block', 'extends', 'print',
                                 'macro', 'include', 'from', 'import',
                                 'set', 'with', 'autoescape'])
_compare_operators = frozenset(['eq', 'ne', 'lt', 'lteq', 'gt', 'gteq'])

_math_nodes = {
    'add': nodes.Add,
    'sub': nodes.Sub,
    'mul': nodes.Mul,
    'div': nodes.Div,
    'floordiv': nodes.FloorDiv,
    'mod': nodes.Mod,
}
...
class Parser(object):
    def parse_statement(self):
        ...
        try:
            if token.value in _statement_keywords:
                return getattr(self, 'parse_' + self.stream.current.value)()
        ...
    def parse_print(self):
        node = nodes.Output(lineno=next(self.stream).lineno)
        node.nodes = []
        while self.stream.current.type != 'block_end':
            if node.nodes:
                self.stream.expect('comma')
            node.nodes.append(self.parse_expression())
        return node

这里面, print 是个好东西,某些场景,限制了 {{}} 的使用,只能使用 {%%} 。这种清空,一般的想法是利用 if 来进行逻辑盲注,但是 {% print somedata %} 可以直接输出。

例题:网鼎杯 CTF 2018 第三场 Web mmmmy (暂无环境)

jinja2/defaults.py

DEFAULT_NAMESPACE = {
    'range':        range_type,
    'dict':         dict,
    'lipsum':       generate_lorem_ipsum,
    'cycler':       Cycler,
    'joiner':       Joiner,
    'namespace':    Namespace
}

默认的命名空间里面还有一些奇奇怪怪的对象存在的。

在探索测试的过程中发现,其实你随便输入一个字符串都是有用的。

比如 {{vvv}} ,一般情况你会发现页面空白啥的没有,但是加点东西就是新世界 {{vvv.__class__}} 。Jinja2对不存在的对象有一个特殊的定义 Undefined 类`

<class 'jinja2.runtime.Undefined'>`。

jinja2/runtime.py

@implements_to_string
class Undefined(object):
    ...

通过 {{ vvv.__class__.__init__.__globals__ }} 又能够搞事情了。

jinja2/filters.py

FILTERS = {
    'abs':                  abs,
    'attr':                 do_attr,
    'batch':                do_batch,
    'capitalize':           do_capitalize,
    'center':               do_center,
    'count':                len,
    'd':                    do_default,
    'default':              do_default,
    'dictsort':             do_dictsort,
    'e':                    escape,
    'escape':               escape,
    'filesizeformat':       do_filesizeformat,
    'first':                do_first,
    'float':                do_float,
    'forceescape':          do_forceescape,
    'format':               do_format,
    'groupby':              do_groupby,
    'indent':               do_indent,
    'int':                  do_int,
    'join':                 do_join,
    'last':                 do_last,
    'length':               len,
    'list':                 do_list,
    'lower':                do_lower,
    'map':                  do_map,
    'min':                  do_min,
    'max':                  do_max,
    'pprint':               do_pprint,
    'random':               do_random,
    'reject':               do_reject,
    'rejectattr':           do_rejectattr,
    'replace':              do_replace,
    'reverse':              do_reverse,
    'round':                do_round,
    'safe':                 do_mark_safe,
    'select':               do_select,
    'selectattr':           do_selectattr,
    'slice':                do_slice,
    'sort':                 do_sort,
    'string':               soft_unicode,
    'striptags':            do_striptags,
    'sum':                  do_sum,
    'title':                do_title,
    'trim':                 do_trim,
    'truncate':             do_truncate,
    'unique':               do_unique,
    'upper':                do_upper,
    'urlencode':            do_urlencode,
    'urlize':               do_urlize,
    'wordcount':            do_wordcount,
    'wordwrap':             do_wordwrap,
    'xmlattr':              do_xmlattr,
    'tojson':               do_tojson,
}

这些过滤器有些时候还是可以用到的,用法 {{ somedata | filter }} , {{url_for.__globals__.current_app.config|safe}}

结束语

本文到这里就告一段落了,主要收获就是模板引擎命名空间或全局变量中的各种对象和函数。另外,其实还有很多地方没深入研究,大家有兴趣不妨翻翻源码找找有意思的事物。

标题有个 (一) ,算是给自己挖个坑,至于后续能不能填上就另说啦。。。。emmmmmmm


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Smashing Book

The Smashing Book

Jacob Gube、Dmitry Fadeev、Chris Spooner、Darius A Monsef IV、Alessandro Cattaneo、Steven Snell、David Leggett、Andrew Maier、Kayla Knight、Yves Peters、René Schmidt、Smashing Magazine editorial team、Vitaly Friedman、Sven Lennartz / 2009 / $ 29.90 / € 23.90

The Smashing Book is a printed book about best practices in modern Web design. The book shares technical tips and best practices on coding, usability and optimization and explores how to create succes......一起来看看 《The Smashing Book》 这本书的介绍吧!

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具