对chameleon模板的探究

浅挖chameleon的渲染机制

之前偶然玩Python项目的时候看到Pyramid有一个叫做chameleon的模板

感觉挺新的就做了几个demo玩玩 ,又仔细看了下官方文档,发现它支持直接用<?python标签来执行Python代码

感觉挺有趣的于是调试调试,看看源码之类的

简单的入口-render函数

和大部分模板一样,渲染时需要调用render函数来对输入的模板进行处理

一般是像这样

tpl = 	PageTemplate(template_str)
return tpl.render()

入口位于这里

前面都是对渲染配置参数的一些初始化,默认的目标语言就是python

image-20260427153317176

接着步入

image-20260427153440005

感觉想想都知道关键的渲染部分肯定是在_render函数进行处理(我的输入是<?python print(1) ?>)

image-20260427153728249image-20260427153737238

这里看不到细节,调用的是PageTemplate的_render,并且它没有重写_render方法

那么就看看其他的逻辑

内部函数的生成-cook函数

往上面找找可以看到这里

def _cook(
    self,
    body: str,
    name: str,
    builtins: Collection[str]
) -> dict[str, Any]:

    filename = self._get_module_name(name)
    if DEBUG_MODE or (cooked := self.loader.get(filename)) is None:
        try:
            source = self._compile(body, builtins)
            if self.debug:
                source = "# template: {}\n#\n{}".format(
                    self.filename, source)
            if self.keep_source:
                self.source = source
            cooked = self.loader.build(source, filename)
        except TemplateError as exc:
            # normalize to str
            exc.token.filename = str(self.filename)
            raise
    elif self.keep_source:
        module_name = cooked.get('__name__')
        module = sys.modules.get(module_name) if module_name else None
        if module is not None:
            self.source = inspect.getsource(module)
        else:
            self.source = None
    return cooked

但是看了一圈发现根本没有调用_cook?怎么会是呢?

我们看看BaseTemplate的__init__魔术方法,发现我们的输入其实在对象创建完成后就已经被渲染完毕了

image-20260427160956741image-20260427161026422

这里调用了write方法,跟进看看

image-20260427161105605

发现这里就直接调用cook来进行编译了

image-20260427161550697

于是乎就会对_cook进行调用,再将编译结果包装成新的方法

image-20260427165035862

image-20260427165425781

接着继续进行build

image-20260427165950388

会发现在这里直接调用了exec来执行代码
但是只执行了函数定义而不是内部内容

那么如果这个source可控。这里即使无法成功渲染,在传入的时候就可以实现命令执行

而source是由_compile函数控制,我们来主要看一眼这里的逻辑有没有危险的地方在

def _compile(self, body: str, builtins: Collection[str]) -> str:
    program = self.parse(body)
    module = Module(PROGRAM_NAME, program)
    compiler = Compiler(
        self.engine,
        module,
        str(self.filename),
        body,
        builtins=builtins,
        strict=self.strict
    )
    return compiler.code  # type: ignore[no-any-return]

生成的source

__filename = '<string>'

__tokens = {8: (' print(1) ', 1, 8)}
from sys import exc_info as _exc_info
import re
import functools
from itertools import chain as __chain
from sys import intern
__default = intern('__default__')
__marker = object()
g_re_amp = re.compile('&(?!([A-Za-z]+|#[0-9]+);)')
g_re_needs_escape = re.compile('[&<>\\"\\\']').search
__re_whitespace = functools.partial(re.compile('\\s+').sub, ' ')

def initialize(macros, nothing, template):

    def render(__stream, econtext, rcontext, __i18n_domain=None, __i18n_context=None, target_language=None):
        __append = __stream.append
        __re_amp = g_re_amp
        __token = None
        __re_needs_escape = g_re_needs_escape

        def __convert(target):
            if target is None:
                return
            __tt = type(target)
            if __tt is bytes:
                target = decode(target)
            elif __tt is not str:
                if __tt is int or __tt is float:
                    target = str(target)
                else:
                    __markup = getattr(target, '__html__', None)
                    if __markup is None:
                        __converted = translate(target, domain=__i18n_domain, context=__i18n_context, target_language=target_language)
                        target = str(target) if target is __converted else __converted
                    else:
                        target = __markup()
            return target

        def __quote(target, quote, quote_entity, default, default_marker):
            if target is None:
                return
            if target is default_marker:
                return default
            __tt = type(target)
            if __tt is bytes:
                target = decode(target)
            elif __tt is not str:
                if __tt is int or __tt is float:
                    return str(target)
                __markup = getattr(target, '__html__', None)
                if __markup is None:
                    __converted = translate(target, domain=__i18n_domain, context=__i18n_context, target_language=target_language)
                    target = str(target) if target is __converted else __converted
                else:
                    return __markup()
            if target is not None:
                try:
                    escape = __re_needs_escape(target) is not None
                except TypeError:
                    pass
                else:
                    if escape:
                        if '&' in target:
                            target = target.replace('&', '&amp;')
                        if '<' in target:
                            target = target.replace('<', '&lt;')
                        if '>' in target:
                            target = target.replace('>', '&gt;')
                        if quote is not None and quote in target:
                            target = target.replace(quote, quote_entity)
            return target
        translate = econtext['__translate']
        decode = econtext['__decode']
        on_error_handler = econtext['__on_error_handler']
        try:
            getname = econtext.get_name
            get = econtext.get
            __token = 8
            print(1)
        except:
            if __token is not None:
                rcontext.setdefault('__error__', []).append(__tokens[__token] + (__filename, _exc_info()[1]))
            raise
    return {'render': render}

只能说基本就没啥可控点了

因为如果真要执行必须initialize函数之前的参数全部可控

然鹅很明显的,这里的参数除了print(1)都是被直接注入进去的,所以基本无法实现注入之类的