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中。
再访问http://127.0.0.1:8888/csrf提示没有权限
以Admin登陆,提示Admin用户权限,假设这个请求是某个操作,比如admin重置自身密码为1234567:
此时如果用户访问了恶意网页,恶意网页诱导用户访问http://127.0.0.1:8888/csrf,那么用户由于sessionID还存在于浏览器中,因此会在无意间使用自己的身份重置密码,如诱导用户点击“ClickHere”链接:
接着可以看到由于用户的session存在,因此用户点击诱导链接后就会无意地使用自己的权限:
换用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请求修改密码:
密码修改成功:
那么攻击者也可以模拟post请求,同样造成csrf:
JSON CSRF POC
JSON CSRF POC
把按钮去了就可以变成一个自动POST的恶意网站,只要用户访问即可重置密码:
防御
对于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里,攻击者仍然无法获取):
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进行密码重置时可行的:
但是攻击者使用相同的代码则会失败,原因是同源策略
但是,攻击者可以使用如下方法攻击,注意enctype是成功的关键,他能保证发过去的东西不被url编码,并且使用name和value配合来达到json合法的目的:
可以看到攻击成功
如果不考虑回显,我们还可以使用如下脚本,有人会怀疑,为什么这里同源没有效果呢?因为注意这里的Content-Type=text/plain,这时的策略时js的请求仍然能够发出去,但是不能获取结果,若Content-Type=text/json时,浏览器将会首先发送一个OPTIONS预检请求,如果服务器不允许跨域则不能发送请求。
JSON CSRF POC
JSON CSRF POC
可以看到,虽然ajax获取结果失败,但是依然可以发送请求:
通过抓包可以看到操作实际是成功的:
防御
同样还是先开启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)