0x01 重定向漏洞概念与原理 开放重定向(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中。
0x01 重定向案例展示 一、案例1:未做校验直接使用用户的输入进行URL重定向 chaturbate在购买成功后页面会发生跳转,但对于参数prejoin_data未做验证,访问:
https://64.38.230.2/tipping/purchase_success/?product_code=4137&prejoin_data=https://www.it610.com/article/domain%2Fevil.com
页面会被重定向到:
https://evil.com/tipping/purchase_success/?product_code=4137。
二、案例2:泄露凭证 mijn.werkenbijdefensie.nl站点对重定向参数未经任何校验,造成重定向漏洞。更严重的是重定向过程中为URL增加了用户ID和Token令牌,进一步造成CSRF仿冒攻击。具体的:
- 请求链接:https://mijn.werkenbijdefensie.nl/login?redirect_url=https://google.com
- redirect_url为重定向参数,服务器后台直接使用该参数进行重定向
- 客户端收到Response的重定向指示,将uid和Token作为重定向URL的参数发送该URL
- 最后客户端访问了这样一个URL:
三、案例3:设计与实现缺陷的绕过 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%ff@www.periscope.tv
其中:
1、@的作用:在URL中用于标识用户的凭据,形式如:
http://user:password@XXX.com:8080/login
一般使用该符号绕过主机host的检测限制
2、通过通过fuzzing探测后端检测机制最后发现:系统最终返回的重定向的URL字符串会将非ASCII码转为符号“?”。
3、URL中的符号“?”:分隔实际的URL和参数,?符号后面为参数
所以上述能够重定向恶意URL的原理就是:
- 系统接收到callback_url参数值:https://attacker.com%ff@www.periscope.tv
- 识别到@符号,取@符号后面的URL进行host识别,www.periscope.tv识别通过
- 程序将字符串“https://attacker.com{%ff}@www.periscope.tv”进行转义:将非ASCII码转为符号“?”,所以最后的代重定向的URL字符串为“https://attacker.com?@www.periscope.tv”。(注:{}表示此处应该为url解码后的值,因为%ff解码后为不可见字符,所以这里为了文档化写成该表示)
- 浏览器接收到https://attacker.com?@www.periscope.tv,并访问该地址。由于?是分割URL参数的标志,此时@符号转为参数部分,最终attacker.com变为了host,客户端浏览器访问了攻击者定义好的恶意网页
0x02 漏洞产生原因 从上节案例中可以初探到漏洞产生的原因,本小节给出发生该漏洞的几个常见原因:
- 代码层忽视URL跳转漏洞,或不知道/不认为这是个漏洞; 属于未校验的情况,例如上节例子所示
- 代码层过滤不严,用取子串、取后缀等方法简单判断,代码逻辑可被绕过; 属于开发人员意识到该风险已经设计实现了重定向校验,但是校验方法有漏洞,导致绕过
- 对传入参数操作(域名剪切/拼接/重组)和判断不当,导致绕过;属于开发人员意识到该风险已经设计实现了重定向校验,但是校验方法有漏洞,导致绕过
- 原始语言自带的解析URL、判断域名的函数库出现逻辑漏洞或者意外特性; 该问题源于开发者使用函数库自带的安全特性去校验URL合法时存在漏洞,造成校验的绕过。可见下文的CVE-2017-7233讲解
- 服务器/容器特性、浏览器等对标准URL协议解析处理等差异性导致被绕过;
- xss漏洞:通过javascript:alert(0)或CRLF;
- 获取用户权限(伪造钓鱼网站、窃取登录凭证token);
- 绕过检测(窃取CSRF token,绕过SSRF、RCE黑名单);
- 高级利用方法(配合其他功能/漏洞)。
0x04 Python语言重定向使用 URL重定向的问题是一种通用的Web问题,不依赖于特定语言,重定向的http Response指令是没有区别的(30x代码)。区别在于不同语言实现URL重定向的方式,更具体点是使用了什么组件、函数实现了这一重定向的功能,例如JAVA中的response.sendRedirect(URL)函数。所以为讲解Python语言重定向漏洞前必须熟悉该语言中重定向功能的实现方法。
一、Django框架之HttpResponseRedirect与HttpResponsePermanentRedirect 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框架之redirect方法 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 Flask框架中为了实现重定向提供了redirect函数。示例代码如下所示:
文章图片
Hello_world使用redirect函数重定向到了login界面。访问:http://127.0.0.1:5000/后被重定向到login界面
文章图片
0x05 Python语言重定向案例 一、is_safe_url函数绕过造成的重定向(CVE-2017-7233) 受影响的版本范围: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
1、is_safe_url函数
一般开发人员在进行重定向时考虑到安全重定向问题,将使用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
2、is_safe_url函数问题
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 错误用法,直接返回falseprint 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')# falseprint 8print is_safe_url('http:249016615')
注:IP地址十进制小工具: https://www.ipaddressguide.com/ip
文章图片
3 、重定向漏洞案例
展示
然而有如下的绕过方法:
# True有协议且十进制host
print is_safe_url('https:249016615')# Truehttps: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 vfor 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(), restif 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
二、django.views.static.serve()函数URL重定向漏洞(CVE-2017-7234) 漏洞范围与描述: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、Serve函数用法
1、增加static静态文件目录
文章图片
2、在static目录下创建如下html静态文件
文章图片
3、增加urlpattern映射,通过django.views.static.serve()函数指定web站点的静态文件目录为上述创建的static目录
文章图片
4、访问test.html。
文章图片
2、Serve函数漏洞
Payload如下所示:
http://127.0.0.1:8000/staticFile/%5C%5Chttps:249016615
注:249016615为百度的IP地址,可以修改成任意其他恶意网址造成钓鱼
最终浏览器会被重定向到百度页面
3、漏洞原理
观察该函数是如何重定向资源的,函数内部部分实现如下所示:
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通过“:”分割出驱动盘
文章图片
# 2通过斜线(包括正反斜线)分割:
文章图片
所以输入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 Redirection to Untrusted Site)-Python】
0x06 安全编码建议
- 若跳转的URL事先是可以确定的,包括url和参数的值,则可以在后台先配置好,url参数只需传对应url的索引即可,通过索引找到对应具体url再进行跳转;
- 若跳转的URL事先不确定,但其输入是由后台生成的(不是用户通过参数传入),则可以先生成好跳转链接然后进行签名,而跳转时首先需要进行验证签名通过才能进行跳转;
- 若1和2都不满足,url事先无法确定,只能通过前端参数传入,则必须在跳转的时候对url进行按规则校验:即控制url是否是授权的白名单或者是符合合法规则的url。编码时一般会在校验符合规则的URL时犯错误,例如使用indexof、contains、startwith等局部校验,必须全量校验URL中的所有字符满足规则,否则很容易绕过。
- XSS漏洞的注意事项 :跳转url检测中也建议加入CRLF头部注入漏洞的检测逻辑。具体就是在请求参数中加入了%0d%0a这种测试代码,需要对这些参数进行删除处理(事实上:在判断到一个参数中包含 %00 -> %1f 的控制字符时都是不合法的,需对其进行删除,因为一个URK中总是为可见的字符)。
- 开源项目及时进行升级,如Django升级 pip install django –upgrade,防止框架本身的漏洞造成的重定向问题。
一、黑盒测试方法 1、一般出现的场景
测试URL重定向第一步必须找到重定向功能出现的地方,所以给出URL的特征和常见场景。
URL重定向特征:
- 浏览器在没有用户交互的情况下自动跳转到其它页面
- 使用抓包工具定位Response为301和302的报文,定位到Resquet请求
大多数的跳转漏洞都发生在登录功能处,其他存在漏洞的功能处有:注册、注销、改密,第三方应用交互,页面切换,业务完成跳转、返回上级、账号切换、保存设置、下载文件等等。
2、通用测试方法
黑盒测试方法如下:
步骤一、操作网站所有功能,对于上述提到的场景必须操作一遍。并使用Berpsuit进行抓包
步骤二、筛选出所有的301和302 Response报文,并设法定位到对应的Request报文。一般这类报文中含有字段return、redirect、url、jump、goto、target、link、callback等
步骤三:修改参数到其它相对于该网站不合法的HOST,查看是否跳转成功,若可以则存在URL重定向漏洞
步骤四:若不能跳转,尝试下节方式进行绕过
3、常用黑盒绕过手法
下述绕过的原因基本上都是开发人员通过contains、startwith、endwiths、indexof等校验字符串部分的方法错误认为全局安全,导致了因为前后台处理不一致的原因导致了绕过。下面给出了几种常见的绕过方式,但不限于此。
- 利用问好绕过限制
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://login.aaa.com@test.com
最终重定向地址:
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
最终导致重定向
- 利用正反斜杆绕过
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
- 利用白名单检测缺陷机制绕过
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等函数仅校验局部字符串的部分,确认是否可以绕过。
推荐阅读
- web挖洞|HACK学习黑帽子Python--漏洞检测脚本快速编写
- git创建本地分支、提交到远程分支
- Python之条件竞争
- 二进制安全|格式化字符串漏洞研究(C/C++、Python)
- 负载均衡的实验