BUUCTF/护网杯 easy_tornado 模板注入

首先简单认识一下模板注入

模板注入涉及的是服务端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); twig= new Twig_Environment(new Twig_Loader_String());output= twig->render("Hello {_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}}

测试结果如下:

image

而如果我们使用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()
image.png

参考文章:
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
https://www.jianshu.com/p/aef2ae0498df
https://www.cnblogs.com/hackxf/p/10480071.html
https://www.freebuf.com/vuls/83999.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容