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="""
    

Login as {{ user }}...

""" 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="""
    
""" 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:


    JSON CSRF POC
    
    

JSON CSRF POC

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

1543979180735.png

防御

对于flask,使用

from flask_wtf.csrf import CSRFProtect

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

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


增加隐藏字段后,每次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="""
    
    Normal
    

Reset Password

""" 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合法的目的:


    
1543931144169.png

可以看到攻击成功

1543977726816.png

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


    JSON CSRF POC
    
    

JSON CSRF POC

可以看到,虽然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 = """
    
    Normal
    

Reset Password

""" return render_template_string(template)

你可能感兴趣的:(CSRF攻击模拟与防御——以Flask为例)