Twosmi1e's Blog.

url跳转漏洞

Word count: 1,632 / Reading time: 7 min
2019/12/25 Share

概念及危害

现在web应用越来越多的需要和其他第三方应用交互,以及在自身应用内部根据不同的逻辑引向不同的页面。例如一个典型的场景就是:web站点可以使用第三方账号(如qq,微博等)进行登录,在登录时就会发生跳转,如果在这个过程中没有做好安全策略就会产生安全漏洞。例如可以利用恶意的URL跳转进行钓鱼等。

攻击方式

  1. 恶意用户借助URL跳转构造钓鱼页面欺骗其他用户,以及获取敏感信息等,在有在线业务的站点危害较大。
  2. 借助URL跳转突破一些基于白名单的安全机制。如:传统的IM对URL的传播进行安全校验,但对于大站点的域名及URL直接允许通过并显示可信的URL,如果该URL中包含恶意跳转可能会导致安全限制被绕过。
  3. 基于白名单引用的资源,这种方式与上面的类似。比如:引入youku.com的视频,白名单中检测的时youku.com,如果包含恶意链接还是可能突破限制。
  4. 在带referer传输的站点中,就不只会产生任意URL跳转这个问题,同时可能会造成所有基于referer的安全策略失效(比较少见)

成因

web站点或者第三方的服务端没有对用户输入的参数进行合法性校验,或者校验不严格,在URL跳转时用户可控,导致恶意参数的传入以及执行,将应用程序引导到恶意的第三方区域产生的安全问题(短链接更加难以防范)。

详细原因

  • 写代码时没有考虑过任意URL跳转漏洞,或者根本不知道/不认为这是个漏洞
  • 写代码时考虑不周,用取子串、取后缀等方法简单判断,代码逻辑可被绕过
  • 对传入参数做一些奇葩的操作(域名剪切/拼接/重组)和判断,适得其反,反被绕过
  • 原始语言自带的解析URL、判断域名的函数库出现逻辑漏洞或者意外特性,可被绕过
  • 原始语言、服务器/容器特性、浏览器等对标准URL协议解析处理等差异性导致被绕过

常见漏洞点

  • 用户登录、统一身份认证处、认证以后发生跳转
  • 用户分享、收藏内容后会发生跳转
  • 跨站点认证、在授权后会认证
  • 站内对其他网站的链接,点击后会跳转

跳转方式

实例

通过META标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
<title></title>
<?php
header("Content-Type:text/html;charset=utf-8");
if(isset($_REQUEST["url"]))
{
$url = $_REQUEST["url"];
}else{
$url = "url_meta.php";
}
?>
<meta http-equiv="Refresh" content="5; url=<?php echo $url?>" />
</head>
<body>
</body>
</html>

Alt text

通过JavaScript

1
2
3
4
5
6
7
8
9
<?php
if (isset($_GET['url'])) {
$target = $_GET['url'];
echo "<script>window.location.href=\"$target\"</script>";
exit;
} else {
echo "Please input the URL";
}
?>

Alt text

通过header

1
2
3
4
5
6
7
8
9
<?php
if (isset($_GET['url'])) {
$target = $_GET['url'];
header("Location: $target");
exit;
} else {
echo "Please input the URL";
}
?>

如果jump参数没有任何限制,用户就可以构造恶意链接进行提交造成恶意URL跳转 http://www.xxxx.com/aaa.php?url=http://www.eval.com ,通过恶意链接造成不可信的第三方跳转可以进一步钓鱼等(直接跳转)。

同时由于一些网站的安全策略,白名单中有http://www.xxxx.com/aaa.php 而导致一些安全策略被绕过,导致用户最终访问的时恶意链接。(过滤不严格的白名单)

常见的发生URL跳转的参数名

参数
redirect
redirect_to
redirect_url
url
jump
jump_to
target
to
link
linkto
domain

常见的URL跳转代码

  • java
1
response.sendRedirect(request.getParameter("url"));
  • PHP
1
2
$redirect_url = $_GET['url'];
header("Location: " . $redirect_url);
  • .NET
1
2
string redirect_url = request.QueryString["url"];
Response.Redirect(redirect_url);
  • Django
1
2
redirect_url = request.GET.get("url")
HttpResponseRedirect(redirect_url)
  • Flask
1
2
redirect_url = request.form['url']
redirect(redirect_url)

白名单限制

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<?php

// $allowedDomains 表示允许跳转的url白名单
$allowedDomains = array(
"aaaa.com"
"bbbb.com"
.......
);
function encodeUrl($urlInfo)
{/*{{{*/
$path = isset($urlInfo['path']) ? $urlInfo['path'] : '';
if(!empty($path))
{
$t = explode("/", $path);

for($i = 0; $i < count($t); $i++)
{
$t[$i] = rawurlencode($t[$i]);
}
$path = implode("/", $t);
}
$query = isset($urlInfo['query']) ? $urlInfo['query'] : '';
if(!empty($query))
{
$t = explode("&", $query);

for($i = 0; $i < count($t); $i++)
{
$tt = explode("=", $t[$i]);
$tt[1] = rawurlencode($tt[1]);
$t[$i] = implode("=", $tt);
}
$query = implode("&", $t);
}
if(!isset($urlInfo['host']) || empty($urlInfo['host']))
{
return $path. "?". $query;
}
$scheme = isset($urlInfo['scheme']) ? $urlInfo['scheme'] : 'http';
$port = isset($urlInfo['port']) ? $urlInfo['port'] : 80;


$request = $scheme . '://'. $urlInfo['host'];
$request .= ($port == 80) ? '' : ':'.$port;
$request .= $path;
$request .= (empty($query)) ? '' : '?'.$query;
return $request;
}/*}}}*/

function checkUrl($url,$domainArr=array())
{/*{{{*/
$res = array('isTrustedDomain' => false,'url' => '','domain' => '');
if(empty($url)) return $res;
$domainArr = empty($domainArr) || !is_array($domainArr) ? $allowedDomains : $domainArr;
$url = filterUrl($url);//先过滤特殊字符
$p = parse_url($url);
$scheme = $p['scheme'];
if(!in_array(strtolower($scheme),array('http','https'))){
return $res;
}

$host = $p['host'];
if(!isValidHost($host)){
return $res;
}
$hostLen = strlen($host);
foreach($domainArr as $domain){
$firstPos = strpos($host, $domain);
if($firstPos !== false && ($firstPos + strlen($domain)) == $hostLen){

if($firstPos == 0 || $domain[0] == '.' || $host[$firstPos-1] == '.'){
$res['isTrustedDomain'] = true;
$res['url'] = $url;
$res['domain'] = $domain;
break;
}
}
}
return $res;
}/*}}}*/

function filterUrl( $url )
{/*{{{*/
if(empty($url)) return $url;
// Strip all of the Javascript in script tags out...
$url = preg_replace('/<SCRIPT.*?<\/SCRIPT>/ims',"",$url);
// Strip all blank character
$url = preg_replace('/[\s\v\0]+/',"",$url);
//Strip special characters(',",<,>,\)
$url = str_replace(array("'","\"","<",">","\\"),'',$url);
return $url;
}/*}}}*/

function isValidHost($host)
{/*{{{*/
$p = "/^[0-9a-zA-Z\-\.]+$/";
return preg_match($p,$host) ? true : false;
}/*}}}*/

$url = "https://www.baidu.com";
$call_back_url = trim($url);
$call_back_url = encodeUrl(parse_url(urldecode($call_back_url)));
$res = checkUrl($call_back_url, $domainArr);

var_dump($res);

修复方案

  1. referer限制
    确定传递URL参数的引入来源,保证URL的有效性,避免恶意用户自己生成的链接(这里要注意的是,在有些特殊的环境下,URL跳转会带着HTTP referer头,这样就会使得依赖referer头验证的方式失效)
  2. 进行token验证
    保证所有的链接是可信域中的,加入用户不可控的token在服务端进行验证,防止恶意跳转
  3. 服务端做好域名白名单或跳转白名单,只对合法的URL进行跳转(常用)
  4. 对请求参数做加密和签名,防止参数被篡改,服务端要能合法正确的解析URL(不常用,多应用在跳转的URL是由后台生产,不是用户在前台输入)
CATALOG
  1. 1. 概念及危害
    1. 1.1. 攻击方式
  2. 2. 成因
    1. 2.1. 详细原因
    2. 2.2. 常见漏洞点
  3. 3. 跳转方式
    1. 3.1. 实例
      1. 3.1.1. 通过META标签
      2. 3.1.2. 通过JavaScript
      3. 3.1.3. 通过header
    2. 3.2. 常见的发生URL跳转的参数名
    3. 3.3. 常见的URL跳转代码
    4. 3.4. 白名单限制
  4. 4. 修复方案