对chameleon模板的探究
浅挖chameleon的渲染机制
之前偶然玩Python项目的时候看到Pyramid有一个叫做chameleon的模板
感觉挺新的就做了几个demo玩玩 ,又仔细看了下官方文档,发现它支持直接用<?python标签来执行Python代码
感觉挺有趣的于是调试调试,看看源码之类的
简单的入口-render函数
和大部分模板一样,渲染时需要调用render函数来对输入的模板进行处理
一般是像这样
tpl = PageTemplate(template_str)
return tpl.render()
入口位于这里
前面都是对渲染配置参数的一些初始化,默认的目标语言就是python

接着步入

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


这里看不到细节,调用的是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__魔术方法,发现我们的输入其实在对象创建完成后就已经被渲染完毕了


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

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

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


接着继续进行build

会发现在这里直接调用了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('&', '&')
if '<' in target:
target = target.replace('<', '<')
if '>' in target:
target = target.replace('>', '>')
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)都是被直接注入进去的,所以基本无法实现注入之类的