开放重定向(CWE-601: URL Redirection to Untrusted Site),也叫URL跳转漏洞,是指服务端未对传入的跳转url变量进行检查和控制,导致诱导用户跳转到恶意网站,由于是从可信的站点跳转出去的,用户会比较信任。这个过程很简单,例如下述流程:
下面的例子有助于理解URL重定向的概念和原理:
public class RedirectServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String query = request.getQueryString();
if (query.contains("url")) {
String url = request.getParameter("url");
response.sendRedirect(url);
}
}
}
上述代码是一个Java Servlet,它将接受请求中带有url参数的GET请求,以将浏览器重定向到url参数指定的地址。该Servlet从请求中检索GET请求中的url参数值,并发送重定向响应将浏览器重定向到该url地址中。
此Java Servlet代码的问题在于:攻击者可能会将RedirectServlet类用作电子邮件网络钓鱼诈骗的一部分,以将用户重定向到恶意站点。例如,攻击者可以通过在电子邮件中包含以下链接来发送HTML格式的电子邮件,指导用户登录其帐户:
bank.example.com/redirect?url=http://attacker.example.net">单击此处登录
用户可以认为该链接是安全的,因为URL以其受信任的银行bank.example.com开头。但是,用户随后将被重定向到攻击者的网站(attacker.example.net),该网站可能看上去与bank.example.com非常相似。然后,用户可能不经意间将凭据输入到攻击者的网页中,从而破坏了他们的银行帐户。所以,永远不要在不验证重定向地址是否为受信任的站点的情况下将用户重定向到新的URL中。
chaturbate在购买成功后页面会发生跳转,但对于参数prejoin_data未做验证,访问:
https://64.38.230.2/tipping/purchase_success/?product_code=4137&prejoin_data=domain%2Fevil.com
页面会被重定向到:
https://evil.com/tipping/purchase_success/?product_code=4137。
mijn.werkenbijdefensie.nl站点对重定向参数未经任何校验,造成重定向漏洞。更严重的是重定向过程中为URL增加了用户ID和Token令牌,进一步造成CSRF仿冒攻击。具体的:
https://www.google.com/?user=xxx&token=xxxx&channel=mijnwerkenbijdefensie。可造成用户身份的泄露
Digits在登录成功后会通过HTTP 302重定向到业务界面,将登录凭证通过callback_url回调:
https://www.digits.com/login?consumer_key=9I4iINIyd0R01qEPEwT9IC6RE&host=https://www.digits.com&callback_url=https://www.periscope.tv
若修改修改callback_url参数为attacker.com,因域名不在同域内,系统会拒绝访问。使用下面这个URL即可绕过造成URL重定向漏洞:
callback_url=https://attacker.com%[email protected]
其中:
1、@的作用:在URL中用于标识用户的凭据,形式如:
http://user:[email protected]:8080/login
一般使用该符号绕过主机host的检测限制
2、通过通过fuzzing探测后端检测机制最后发现:系统最终返回的重定向的URL字符串会将非ASCII码转为符号“?”。
3、URL中的符号“?”:分隔实际的URL和参数,?符号后面为参数
所以上述能够重定向恶意URL的原理就是:
这里犯的错误就是在使用不可信数据前没有标准化字符串为统一确定的标准格式,上述应该是先进行了使用(根据@符号定位取出host),在格式化字符串(将非ASCII转义为?)。如果先进行格式化,则将会通过?定位HOST。任何不可信数据使用前都应该确定有一个标准的格式再进行合法性校验。
从上节案例中可以初探到漏洞产生的原因,本小节给出发生该漏洞的几个常见原因:
URL跳转漏洞本身属于低危漏洞,但可以结合其他漏洞加以深入利用,主要的利用方式不仅限于钓鱼攻击,包括:
利用姿势不局限于上述几种,不要思维定势,具体看下面的实例。
URL重定向的问题是一种通用的Web问题,不依赖于特定语言,重定向的http Response指令是没有区别的(30x代码)。区别在于不同语言实现URL重定向的方式,更具体点是使用了什么组件、函数实现了这一重定向的功能,例如JAVA中的response.sendRedirect(URL)函数。所以为讲解Python语言重定向漏洞前必须熟悉该语言中重定向功能的实现方法。
Django中在django.http模块中提供了HttpResponseRedirect/HttpResponsePermanentRedirect对象实现重定向功能,它们最终都继承自HttpResponse,被定义在django.http模块中,返回的状态码分别为302(临时重定向)/301(永久重定向)。构造函数签名如下所示
def __init__(self, redirect_to, *args, **kwargs)
其中所以第一个参数是必须的。下面给出重定向用法的例子:
1、创建Django工程
2、定义视图redirectURLDemo
def redirectURLDemo(request):#
redirectUrl = request.GET.get("url")
return HttpResponseRedirect(redirectUrl)
3、配置URL
urlpatterns = [
url(r'^redirectURLDemo/$', redirectURLDemo)
]
4、浏览器输入:10.164.146.143:8000/redirectURLDemo?url=http://www.baidu.com
回车后重定向到百度。
若将HttpResponsePermanentRedirect替换为HttpResponseRedirect上述功能实现一致,只是返回码变为301
特别地:redirect_to:表示要重定向的URL。其使用方法需要注意:参数不加协议头,则默认HOST为同域且相对于当前URI路径;若参数有该协议头(允许的协议头为ftp、http和https),则按照给出的HOST进行重定向。具体的用法请参照官方相关开发手册。例如:
10.164.XXX.XXX:8000/redirectURLDemo?url=www.baidu.com
该URL参数不带协议,所以认为该URL参数相对于当前URI,所以访问路径如上所示。
Django在django.shortcuts模块中提供了redirect方法支持和HttpResponseRedirect相同作用的重定向方法,这是一个便捷的方法。其主要的用法和HttpResponseRedirect相同。例如修改前文HttpResponseRedirect的代码:
def redirectURLDemo(request):#
redirectUrl = request.GET.get("url")
#return HttpResponseRedirect(redirectUrl)
return redirect(redirectUrl)
访问:http://10.164.146.143:8000/redirectURLDemo?url=http://www.baidu.com。效果是一样的,依然可以跳转到百度页面。
该方法不仅能根据URL重定向,还可以根据对象Object重定向和根据视图view重定向。用法详情参考官方相关开发手册。
Flask框架中为了实现重定向提供了redirect函数。示例代码如下所示:
Hello_world使用redirect函数重定向到了login界面。访问:http://127.0.0.1:5000/后被重定向到login界面
受影响的版本范围:Django1.10.7之前的1.10版本,1.9.13之前的1.9版本,1.8.18之前的1.9版本。
安装使用有问题版本的Django:
实验环境如下:
操作系统:windows10 64bits
Python版本:2.7.18
Django:1.10.0
IDE:Pycharm
一般开发人员在进行重定向时考虑到安全重定向问题,将使用is_safe_url函数进行主机名的验证。该函数如下:
def is_safe_url(url, host=None):
"""
Return ``True`` if the url is a safe redirection (i.e. it doesn't point to
a different host and uses a safe scheme).
Always returns ``False`` on an empty url.
"""
简单的例子如下所示:
def BypassIsUrlSafeCheck(request):
url001 = request.GET.get("url")
if is_safe_url(url001, host="www.baidu.com"):
return HttpResponseRedirect(url001)
else:
return HttpResponse('Fail!')
该视图将HOST:www.baidu.com定义为安全的URL,若安全则指示浏览器重定向到该URL。
访问:Host为www.csdn.net失败。
访问:Host为www.baidu.com成功。
http://127.0.0.1:8000/URLRedirect/?url=http://www.baidu.com
is_safe_url函数会把参数url进行解析,并获取以下对应部分的值:
# 协议(scheme)
# 域名(netloc)
# 路径(path)
# 路径参数(params)
# 查询参数(query)
# 片段(fragment)
对应的http url格式为:
is_safe_url若不带host参数则默认同域。is_safe_url判断若utl参数带有协议,则解析其中的host进行判断;若不带协议则默认同域。看下述例子输出:
# False 有协议 判断host是否为同域
print 1
print is_safe_url('http://baidu.com')
# False 有协议 判断host是否为同域
print 2
print is_safe_url('http:www.baidu.com')
# False 错误用法,直接返回false
print 3
print is_safe_url('//www.baidu.com')
#return HttpResponseRedirect('//www.baidu.com')
# True 无协议使用当前根目录
print 4
print is_safe_url('baidu.com')
# return HttpResponseRedirect('baidu.com')
# False
print 5
print is_safe_url('http://google.com/adadadadad', 'blog.neargle.com')
# True
print 6
print is_safe_url('http://blog.neargle.com/aaaa/bbb', 'blog.neargle.com')
# false
print 7
print is_safe_url('https://249016615')
# false
print 8
print is_safe_url('http:249016615')
注:IP地址十进制小工具: https://www.ipaddressguide.com/ip
展示
然而有如下的绕过方法:
# True有协议且十进制host
print is_safe_url('https:249016615')
# True https:249016615为百度
print is_safe_url('https:249016615', 'www.csdn.net')
例如上述开发人员限定重定向的地址为同域(7)或者仅能重定向到csdn中,但是因为is_safe_url错误的认为url参数没有带协议,所以错误认为在同域下,最终返回错误的结果使得后续程序产生逻辑错误,最终产生安全问题。
# http://127.0.0.1:8000/URLRedirect?url=https:249016615(百度)
def BypassIsUrlSafeCheck(request):
url001 = request.GET.get("url")
if is_safe_url(url001, host="www.csdn.net"):
return HttpResponseRedirect(url001)
else:
return HttpResponse('Fail!')
正常输入要访问的百度地址:
通过上述的上过方式输入百度的地址:
http://127.0.0.1:8000/URLRedirect?url=https:249016615
则绕过访问到了百度。
原理
上述从黑盒测试可知,以下形式可以绕过is_safe_url的HOST验证:
[https]|[ftp][非http均可以]:{IP的10进制形式}
该形式is_safe_url错误的认为无协议,所以认为访问同域下的资源,错误认为安全。
通过源码查看其中原理:
第一处红框看出上述3返回false的原因。然后使用urlparse解析url参数。同时也给出了返回值的原因。
1、//开头的返回false
2、netloc主机为空但是有scheme协议返回false
3、最后一个红框可以组成四种情况,但是对于需要绕过指定的host,则第一个括号中需要满足netloc主机为空;第二个括号中需要scheme协议为空(基于2的原因,所以scheme协议必须解析为空)。
如何使得正确的url无法解析出主机netloc和协议scheme,查看urlparse函数的url解析过程寻找漏洞。
该函数通过解析url参数获取对应的6元组。问题就在该函数中,通过以上的模式就可以使得netloc为空,错误认为相对路径。核心为调用urlsplit函数进行分割,该函数代码如下所示:
i = url.find(':') #1
if i > 0:
if url[:i] == 'http': # optimize the common case #2
scheme = url[:i].lower()
url = url[i+1:]
if url[:2] == '//': #3
netloc, url = _splitnetloc(url, 2) #4
if (('[' in netloc and ']' not in netloc) or
(']' in netloc and '[' not in netloc)):
raise ValueError("Invalid IPv6 URL")
if allow_fragments and '#' in url:
url, fragment = url.split('#', 1)
if '?' in url:
url, query = url.split('?', 1)
_checknetloc(netloc)
v = SplitResult(scheme, netloc, url, query, fragment)
_parse_cache[key] = v
return v
for c in url[:i]:
if c not in scheme_chars:
break
else:
# make sure "url" is not actually a port number (in which case
# "scheme" is really part of the path)
rest = url[i+1:]
if not rest or any(c not in '0123456789' for c in rest): #5
# not a port number
scheme, url = url[:i].lower(), rest
if url[:2] == '//': #6
netloc, url = _splitnetloc(url, 2)
if (('[' in netloc and ']' not in netloc) or
(']' in netloc and '[' not in netloc)):
raise ValueError("Invalid IPv6 URL")
if allow_fragments and '#' in url:
url, fragment = url.split('#', 1)
if '?' in url:
url, query = url.split('?', 1)
_checknetloc(netloc)
v = SplitResult(scheme, netloc, url, query, fragment) #5
_parse_cache[key] = v
对于https: 249016615从上述代码看出:
1、#2:如果为http,则有正确的代码解析出协议scheme。并且由于没有//,则到最后返回时netloc主机一直没有被解析出,为空值。最终返回值存在协议但没有netloc返回了false。
所以http的情况,无论怎样都不能返回true。只能使用除http外的协议:https,绕过这一过程。
2、所以https时,#5处:如果url中存在非数字字符,则把“:”前的设置为scheme。而前文可知scheme必须为空。所以跳转的主机只能包含数字,也就想到可通过IP的十进制方式进行绕过。
所以https: 249016615这种情形,最后返回的五元组scheme和netloc均为空,最优UrlParse函数中会返回true
漏洞范围与描述:A maliciously crafted URL to a Django (1.10 before 1.10.7, 1.9 before 1.9.13, and 1.8 before 1.8.18) site using the ``django.views.static.serve()`` view could redirect to any other domain, aka an open redirect vulnerability.
本次实验环境如下:
操作系统:windows10 64bits
Python版本:2.7.18
Django:1.10.0
IDE:Pycharm
1、增加static静态文件目录
2、在static目录下创建如下html静态文件
3、增加urlpattern映射,通过django.views.static.serve()函数指定web站点的静态文件目录为上述创建的static目录
4、访问test.html。
Payload如下所示:
http://127.0.0.1:8000/staticFile/%5C%5Chttps:249016615
注:249016615为百度的IP地址,可以修改成任意其他恶意网址造成钓鱼
最终浏览器会被重定向到百度页面
观察该函数是如何重定向资源的,函数内部部分实现如下所示:
def serve(request, path, document_root=None, show_indexes=False):
path = posixpath.normpath(unquote(path))
path = path.lstrip('/')
newpath = ''
for part in path.split('/'):
if not part:
# Strip empty path components.
continue
drive, part = os.path.splitdrive(part) # 1
head, part = os.path.split(part) # 2
if part in (os.curdir, os.pardir):
# Strip '.' and '..' in path.
continue
newpath = os.path.join(newpath, part).replace('\\', '/') # 3
if newpath and path != newpath: # 5
return HttpResponseRedirect(newpath) # 6
fullpath = os.path.join(document_root, newpath)
if os.path.isdir(fullpath):
该函数内部有个url重定向函数:HttpResponseRedirect。分析前面的代码发现此处包含URL重定向漏洞。
首先HttpResponseRedirect默认当前域为安全的,为了绕过进行也已HOST访问,可以根据上一个漏洞CVE-2017-7234的payload进行构造,即https:249016615。
然而并没成功,分析到进入HttpResponseRedirect分支的条件必须为
1、part必须为url整体,也就是本例子的https:249016615
2、path != newpath。
上述操作part关键的两个步骤为# 1和# 2。
# 1通过“:”分割出驱动盘
所以输入https:249016615到最后part必定仍然为https:249016615。
但是这不满足第二个条件path != newpath。然而最终肯定是要求part(或者newpath)为https:249016615。起初的path必须增加点可以被前文代码过滤的字符而又不会拆分。
从上述#2处的os.path.split(part)函数执行可以看出:会将正反斜杆和后面的内容分开
def split(p):
"""Split a pathname.
Return tuple (head, tail) where tail is everything after the final slash.
Either part may be empty."""
d, p = splitdrive(p) #就是上述的splitdrive方法,又调用了一次
# set i to index beyond p's last slash
i = len(p)
while i and p[i-1] not in '/\\': #2
i = i - 1
head, tail = p[:i], p[i:] # now tail has no slashes #3
# remove trailing slashes from head, unless it's all slashes
head2 = head
while head2 and head2[-1] in '/\\':
head2 = head2[:-1]
head = head2 or head
return d + head, tail
所以此处若输入\\ https:249016615,则返回的part变为https:249016615,并且\\ https:249016615在前文的流程中不会被拆分。
所以Payload为:
%5C%5C[ https][非http协议]:{十进制IP地址}
URL重定向需结合黑白盒进行测试,黑盒可以发现简单的URL重定向问题,有经验者可能从黑盒就可以通过各种方式绕过渗透。白盒源码审计可以阅读代码找到全文所有重定向的函数并且阅读代码以找到漏洞利用的方法。下面从黑白盒两个方面给出测试方法。
测试URL重定向第一步必须找到重定向功能出现的地方,所以给出URL的特征和常见场景。
URL重定向特征:
常见场景:
大多数的跳转漏洞都发生在登录功能处,其他存在漏洞的功能处有:注册、注销、改密,第三方应用交互,页面切换,业务完成跳转、返回上级、账号切换、保存设置、下载文件等等。
黑盒测试方法如下:
步骤一、操作网站所有功能,对于上述提到的场景必须操作一遍。并使用Berpsuit进行抓包
步骤二、筛选出所有的301和302 Response报文,并设法定位到对应的Request报文。一般这类报文中含有字段return、redirect、url、jump、goto、target、link、callback等
步骤三:修改参数到其它相对于该网站不合法的HOST,查看是否跳转成功,若可以则存在URL重定向漏洞
步骤四:若不能跳转,尝试下节方式进行绕过
下述绕过的原因基本上都是开发人员通过contains、startwith、endwiths、indexof等校验字符串部分的方法错误认为全局安全,导致了因为前后台处理不一致的原因导致了绕过。下面给出了几种常见的绕过方式,但不限于此。
问号指示query参数的开始,后续将被分割。
http://www.aaa.com/acb?Url=http://login.aaa.com
问号的绕过方式为:
http://www.aaa.com/acb?Url=http://test.com?login.aaa.com 。
最终重定向地址:
http://www.test.com/?login.aaa.com
@符号的作用为分割认证凭据和URL。
绕过案例例如前文案例3。
例如:
http://www.aaa.com/acb?Url=http://login.aaa.com
绕过方式:
http://www.aaa.com/acb?Url=http://[email protected]
最终重定向地址:
http://www.test.com
#号对服务器无任何作用,仅起效于浏览器侧,用于定位页面的位置。
http://www.aaa.com/acb?Url=http://login.aaa.com/
绕过方式为:
http://www.aaa.com/acb?Url=http://test.com#login.aaa.com
最终导致重定向
原URL:
http://www.aaa.com/acb?Url=http://login.aaa.com/
绕过方式1:斜杆+点
http://www.aaa.com/acb?Url=http://test.com\.login.aaa.com
绕过方式2:反斜杠
http://www.aaa.com/acb?Url=http://test.com\login.aaa.com
绕过方式3:两个反斜杠
http://www.aaa.com/acb?Url=http://test.com\\login.aaa.com
开发人员设置可以重定向的URL的HOST为:
aaa.com
则注册另一个恶意域名为:
testaaa.com
白名单检查是否包含aaa.com这个域名,则认为是安全的。导致绕过。
1、搜索源码关键函数
Django下:搜索HttpResponseRedirect、HttpResponsePermanentRedirect、redirect(一般导入django.shortcuts模块)函数
Flask下:搜索redirect(是否import redirect)
建议直接搜索redirect函数,并确定该函数功能是否为URL重定向
2、查看这些函数的第一个参数是否外部可控,若不可控则无问题
3、若可控,则查看是否经过严格的校验。或者通过相关函数的机制设置了可访问的白名单HOST。对于自行编写校验机制的部分,关注是否存在通过contains、startwith、endwiths、indexof等函数仅校验局部字符串的部分,确认是否可以绕过。