Python中從服務端模板注入到沙盒逃逸的原始碼探索 (一)
原理
不同的模板引擎,根據不同的解析方式相應的也是存在不同的利用方法。
正常而言,出於安全考慮,模板引擎基本上都是擁有沙盒、名稱空間的,程式碼的解析執行都是發生在有限的沙盒裡面,因此,沙盒逃逸也成為 SSTI 不可或缺的存在。
Python Web 模板引擎
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
。
例題:ofollow,noindex">護網杯 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)
這裡用到了compile 和exec
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