URL 跳转漏洞也叫作 URL 重定向漏洞,由于服务端未对传入的跳转地址进行检查和控制,从而导致攻击者可以构造任意一个恶意地址,诱导用户跳转至恶意站点。因为是从用户可信站点跳转出去的,用户会比较信任该站点,所以URL跳转漏洞常用于钓鱼攻击,通过转到攻击者精心构造的恶意网站来欺骗用户输入信息,从而盗取用户的账号和密码等敏感信息,更甚者会欺骗用户进行金钱交易。
可能产生漏洞的函数:
redirect url redirectUrl callback return_url toUrl ReturnUrl fromUrl redUrl request redirect_to redirect_url jump jump_to target to goto link linkto domain
1. 登陆跳转我认为是最常见的跳转类型,认证完后会跳转,所以在登陆的时候建议多观察url参数 2. 用户分享、收藏内容过后,会跳转 3. 跨站点认证、授权后,会跳转 4. 站内点击其它网址链接时,会跳转 5. 在一些用户交互页面也会出现跳转,如请填写对客服评价,评价成功跳转主页,填写问卷,等等业务,注意观察url。 6. 业务完成后跳转这可以归结为一类跳转,比如修改密码,修改完成后跳转登陆页面,绑定银行卡,绑定成功后返回银行卡充值等页面,或者说给定一个链接办理VIP,但是你需要认证身份才能访问这个业务,这个时候通常会给定一个链接,认证之后跳转到刚刚要办理VIP的页面。
redirect、response.setHeader("Location", url)、sendRedirect
这是一个处理 URL 重定向的 Java Spring 控制器类。当使用“url”参数向“/urlRedirect/redirect”端点发出 GET 请求时,该方法将使用“redirect:”前缀返回对指定 URL 的重定向响应。例如,如果向“ http://localhost:8080/urlRedirect/redirect?url=http://www.baidu.com ”发出请求,该方法会将用户重定向到“ http://www.baidu” .com ”。
//redirect 重定向 @Controller @RequestMapping("/urlRedirect") public class URLRedirect { /** * http://localhost:8080/urlRedirect/redirect?url=http://www.baidu.com */ @GetMapping("/redirect") public String redirect(@RequestParam("url") String url) { return "redirect:" + url; }
response.setHeader,然后进行重定向。
/** * http://localhost:8080/urlRedirect/setHeader?url=http://www.baidu.com */ @RequestMapping("/setHeader") @ResponseBody public static void setHeader(HttpServletRequest request, HttpServletResponse response) { String url = request.getParameter("url"); response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301 redirect 状态码 response.setHeader("Location", url); //设置访问的url }
sendRedirect重定向
/** * http://localhost:8080/urlRedirect/sendRedirect?url=http://www.baidu.com */ @RequestMapping("/sendRedirect") @ResponseBody public static void sendRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException { String url = request.getParameter("url"); response.sendRedirect(url); // 302 redirect }
安全防护代码
这是另一个处理 URL 转发的 Java Spring 控制器类。当使用“url”参数向“/urlRedirect/forward”端点发出请求时,该方法将使用 RequestDispatcher 对象将请求转发到指定的 URL。例如,如果向“ http://localhost:8080/urlRedirect/forward?url=/urlRedirect/test”发出请求,该方法会将请求转发到同一 Web 应用程序中的“/urlRedirect/test”。
这种方法比 URL 重定向更安全,因为它只能将请求转发到同一 Web 应用程序内的路径,而不能将请求转发到外部 URL。此外,用户浏览器地址栏中的 URL 不会更改,从而使敏感信息更加安全。
/** * Safe code. Because it can only jump according to the path, it cannot jump according to other urls. * http://localhost:8080/urlRedirect/forward?url=/urlRedirect/test */ @RequestMapping("/forward") @ResponseBody public static void forward(HttpServletRequest request, HttpServletResponse response) { String url = request.getParameter("url"); RequestDispatcher rd = request.getRequestDispatcher(url); try { //属于转发,也可以称为内部重定向,相当于方法的调用,服务端跳转时,用户浏览器的地址栏的URl是不会变化的。 //这个请求不能转向到本web应用之外的页面和网站。 rd.forward(request, response); } catch (Exception e) { e.printStackTrace(); } }
这是另一个处理 URL 重定向的 Java Spring 控制器类,但具有额外的安全措施。当使用“url”参数向“/urlRedirect/sendRedirect/sec”端点发出请求时,该方法将首先使用 SecurityUtil.checkURL() 方法检查 URL 以确保它是安全的 URL。如果 URL 是安全的,该方法将使用 response.sendRedirect() 方法将请求重定向到指定的 URL。如果 URL 不安全,该方法将返回“403 Forbidden”响应。
通过在重定向前检查 URL,此方法有助于防止跨站点脚本 (XSS) 等攻击,并确保用户仅被重定向到安全的 URL。
/** * Safe code of sendRedirect. * http://localhost:8080/urlRedirect/sendRedirect/sec?url=http://www.baidu.com */ @RequestMapping("/sendRedirect/sec") @ResponseBody public void sendRedirect_seccode(HttpServletRequest request, HttpServletResponse response) throws IOException { String url = request.getParameter("url"); //checkURL 对URL做检验 if (SecurityUtil.checkURL(url) == null) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write("url forbidden"); return; } response.sendRedirect(url); } }
@GetMapping("/redirect") public String redirect(@RequestParam("url") String url) { return "redirect:" + url; }
@RequestMapping("/setHeader") @ResponseBody public static void setHeader(HttpServletRequest request, HttpServletResponse response) { String url = request.getParameter("url"); response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301 redirect response.setHeader("Location", url); }
此处以Servlet的redirect 方式为例,示例代码片段如下:
String site = request.getParameter("url"); if(!site.isEmpty()){ response.sendRedirect(site); }
@RequestMapping("/redirect1") public ModelAndView ModelAndView(HttpServletRequest request, HttpServletResponse response){ String url = request.getParameter("url"); url = "redirect:" url; return new ModelAndView(url); }
通过String返回
@RequestMapping("/redirect2") public String redirect(@RequestParam("url") String url){ return "redirect:" url; }
@RequestMapping("/redirect3") public static void sendRedirect(HttpServletRequest request,HttpServletResponse response) throws IOException { String url = request.getParameter("url"); response.sendRedirect(url); }
RedirectAttributes跟sendRedirect相比多了参数传递的过程。如下传递了id=2到/hello对应的页面
@RequestMapping("/redirect4") public String RedirectAttributes(RedirectAttributes redirectAttributes,String url){ redirectAttributes.addAttribute("id",url); return "redirect:/index"; }
根据指定的url跳转到127.0.0.1:8081/index?id=url
可以通过设置Header来进行跳转 response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARIL Y)设置返回的状态码: SC_MOVED_PERMANENTLY 是301永久重定向 SC_MOVED_TEMPORARILY 是302临时重定向
@RequestMapping("/redirect5") public static void setHeader(HttpServletRequest request, HttpServletResponse response){ String url = request.getParameter("url"); response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); response.setHeader("Location",url); }
java程序中URL重定向的方法均可留意是否对跳转地址进行校验、重定向函数如下:
sendRedirect setHeader forward ...
利用URL跳转漏洞可以绕过一些常见的基于白名单的安全机制。如:
如:
URL跳转漏洞的危害并不只会影响到用户或其他网站。
当底层操作类库支持其他协议时,URL跳转漏洞可能导致本地的读取或网络信息被侦测等问题。如:
如:
即使底层库不支持其他协议或者已对其他协议做了限制,如未限制网络边界也可能会产生问题。如:
视图解析的过程是发生在Controller处理后,Controller处理结束后会将返回的结果封装为ModelAndView对象,再通过视图解析器ViewResovler得到对应的视图并返回。分析的栗子使用上面的Demo。
封装ModelAndView对象
在ServletInvocableHandlerMethod#invokeAndHandle中,做了如下操作:
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { //调用Controller后获取返回值到returnValue中 Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs); this.setResponseStatus(webRequest); //判断returnValue是否为空 if (returnValue == null) { //判断RequestHandled是否为True if (this.isRequestNotModified(webRequest) || this.getResponseStatus() != null || mavContainer.isRequestHandled()) { this.disableContentCachingIfNecessary(webRequest); //设置RequestHandled属性 mavContainer.setRequestHandled(true); return; } } else if (StringUtils.hasText(this.getResponseStatusReason())) { mavContainer.setRequestHandled(true); return; } mavContainer.setRequestHandled(false); Assert.state(this.returnValueHandlers != null, "No return value handlers"); try { //通过handleReturnValue根据返回值的类型和返回值将不同的属性设置到ModelAndViewContainer中。 this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception var6) { if (logger.isTraceEnabled()) { logger.trace(this.formatErrorForReturnValue(returnValue), var6); } throw var6; }
下面分析handleReturnValue方法。
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { //获取handler HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType); if (handler == null) { throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); } else { //执行handleReturnValue操作 handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } }
ViewNameMethodReturnValueHandler#handleReturnValue
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { if (returnValue instanceof CharSequence) { String viewName = returnValue.toString(); //设置返回值为viewName mavContainer.setViewName(viewName); //判断是否需要重定向 if (this.isRedirectViewName(viewName)) { mavContainer.setRedirectModelScenario(true); } } else if (returnValue != null) { throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod()); } }
通过上面的操作,将返回值设置为mavContainer.viewName,执行上述操作后返回到RequestMappingHandlerAdapter#invokeHandlerMethod中。通过getModelAndView获取ModelAndView对象。
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ... ModelAndView var15; invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]); if (asyncManager.isConcurrentHandlingStarted()) { result = null; return (ModelAndView)result; } //获取ModelAndView对象 var15 = this.getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } return var15; }
getModelAndView根据viewName和model创建ModelAndView对象并返回。
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { modelFactory.updateModel(webRequest, mavContainer); //判断RequestHandled是否为True,如果是则不会创建ModelAndView对象 if (mavContainer.isRequestHandled()) { return null; } else { ModelMap model = mavContainer.getModel(); //创建ModelAndView对象 ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus()); if (!mavContainer.isViewReference()) { mav.setView((View)mavContainer.getView()); } if (model instanceof RedirectAttributes) { Map<String, ?> flashAttributes = ((RedirectAttributes)model).getFlashAttributes(); HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class); if (request != null) { RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } } return mav; } }
获取ModelAndView后,通过DispatcherServlet#render获取视图解析器并渲染。
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { Locale locale = this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale(); response.setLocale(locale); String viewName = mv.getViewName(); View view; if (viewName != null) { //获取视图解析器 view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + this.getServletName() + "'"); } } else { view = mv.getView(); if (view == null) { throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a View object in servlet with name '" + this.getServletName() + "'"); } } if (this.logger.isTraceEnabled()) { this.logger.trace("Rendering view [" + view + "] "); } try { if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } //渲染 view.render(mv.getModelInternal(), request, response); } catch (Exception var8) { if (this.logger.isDebugEnabled()) { this.logger.debug("Error rendering view [" + view + "]", var8); } throw var8; } }
获取视图解析器在DispatcherServlet#resolveViewName中完成,循环遍历所有视图解析器解析视图,解析成功则返回。
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception { if (this.viewResolvers != null) { Iterator var5 = this.viewResolvers.iterator(); //循环遍历所有的视图解析器获取视图 while(var5.hasNext()) { ViewResolver viewResolver = (ViewResolver)var5.next(); View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } } return null; }
在Demo中有5个视图解析器。
本以为会在ThymeleafViewResolver中获取视图,实际调试发现ContentNegotiatingViewResolver中已经获取到了视图。
ContentNegotiatingViewResolver视图解析器允许使用同样的数据获取不同的View。支持下面三种方式。
ContentNegotiatingViewResolver#resolveViewName
public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes"); List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest()); if (requestedMediaTypes != null) { //获取可以解析当前视图的列表。 List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes); //根据Accept头获取一个最优的视图返回 View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs); if (bestView != null) { return bestView; } } ... }
得到View后,调用render方法渲染,也就是ThymleafView#render渲染。render方法中又通过调用renderFragment完成实际的渲染工作。
我这里使用spring-view-manipulation 项目来做漏洞复现。
http://127.0.0.1:9080/doForward?name=123
http://127.0.0.1:9080/redirectTest?name=123
二次请求:
http://127.0.0.1:9080//redirectController=123
http://127.0.0.1:9080/redirectController?url=/hello?name=123
http://127.0.0.1:9080/redirectController?url=http://www.baidu.com
http://127.0.0.1:9080/redirectTestController2?url=http://[email protected]
http://127.0.0.1:8090/rediectController2?url=http://www.qqq.baidu.com
@GetMapping("/path") public String path(@RequestParam String lang) { return "user/" + lang + "/welcome"; //template path is tainted }
POC
__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc.exe%22).getInputStream()).next()%7d__::.x
Java: response.sendRedirect(request.getParameter("url")); PHP: $redirect_url = $_GET['url']; header("Location: " . $redirect_url); .NET: string redirect_url = request.QueryString["url"]; Response.Redirect(redirect_url); Django: redirect_url = request.GET.get("url") HttpResponseRedirect(redirect_url) Flask: redirect_url = request.form['url'] redirect(redirect_url) Rails: redirect_to params[:url]
没做任何限制,参数后直接跟要跳转过去的网址就行:
https://www.xxx.com/redirect.php?url=http://www.evil.com/untrust.html
当程序员校验跳转的网址协议必须为https时(有时候跳转不过去不会给提示):
https://www.xxxx.com/redirect.php?url=https://www.evil.com/untrust.html
<?php $redirect_url = $_GET['url']; if(strstr($redirect_url,"127.0.0.1") !== false){ header("Location: " . $redirect_url); } else{ die("Forbidden"); }
还有的会检测域名结尾是不是当前域名,是的话才会跳转,Django示例代码如下:
redirect_url = request.GET.get("url") if redirect_url.endswith('landgrey.me'): HttpResponseRedirect(redirect_url) else: HttpResponseRedirect("https://www.landgrey.me")
绕过:
https://www.landgrey.me/redirect.php?url=http://www.evil.com/www.landgrey.me 或者买个xxxlandgrey.me域名,然后绕过:
https://www.landgrey.me/redirect.php?url=http://xxxlandgrey.me.
利用已知可重定向到自己域名的可信站点的重定向,来最终重定向自己控制的站点。
一种是利用程序自己的公共白名单可信站点,如www.baidu.com,其中百度有个搜索的缓存链接比如https://www.baidu.com/linkurl=iMwwNDM6ahaxKkSFuOG,可以最终跳转到自己网站,然后测试时:
https://www.landgrey.me/redirect.php?url=https://www.baidu.com/linkurl=iMwwNDM6ahaxKkSFuOG
就可以跳转到自己站点了。
另一种类似,但是程序的跳转白名单比较严格,只能是自己域的地址,这时需要有一个目标其它域的任意跳转漏洞,比如https://auth.landgrey.me/jump.do?url=evil.com,然后测试时:
https://www.landgrey.me/redirect.php?url=https://auth.landgrey.me/jump.do?url=evil.com
这一部分由于各种语言、框架和代码实现的不同,防护任意跳转代码的多种多样;导致绕过方式乍看起来很诡异,有多诡异?举三个案例:
案例一:这个案例 ,通过添加多余的"/"(%2F)符号,再对"."两次url编码成"%252E"绕过代码中对域名后".com"的切割, 构造类似
https://landgrey.me/%2Fevil%2Ecom 达到了任意URL跳转的目的。
案例二:这个案例,通过添加4个"/"前缀和"/.."后缀,构造类似
https://landgrey.me/redirect.php?url=////www.evil.com/.. 进行了绕过。
案例三:这个案例,通过"."字符,构造类似
https://landgrey.me/redirect.php?url=http://www.evil.com\.landgrey.me 进行绕过。
手工测试时,主要结合目标对输入的跳转处理和提示,根据经验来绕过; 自动化测试时,通常是根据目标和规则,事先生成payload,用工具(如burpsuite)在漏洞点处自动发包测试;
URL跳转漏洞复杂的真实例子也比较难找。黑盒测试,经常是测试成功也不能确定到底是哪里出的问题。要达到绕过效果,主要涉及以下9个特殊字符:
";", "/", "\", "?", ":", "@", "=", "&", "."
一个“协议型”的网址示例:
http://user:[email protected]/path/;help.php?q=abc#lastpage
10种bypass方式:
在 user->Users.php->logout() 函数中存在一个可以传入的 referurl 参数,该参数通过input()函数的
过滤后,传入到了 redirect() 函数中,我们跟进该函数:
通过这里的注释也可以发现这里是用于URL重定向的
功能点其实很容易找到user目录下所有的功能点都在前台登录后,所以我们登录普通用户再进行退出。
这里我们将重定向的地址设为baidu
拿乌云案例来说,开放了端口,可以送数据给这个端口,特定的数据可导致访问任意url
开启一个http server,来监听这个端口。端口号从本身shared_prefs目录下的
multi_process_config.xml文件内获得
漏洞代码1:
// 满足参数url可控,且未做限制 public String vul(String url) { return "redirect:" + url; }
漏洞分析1:
该函数存在一定的安全漏洞,因为函数中的url参数并没有做任何限制,这意味着它可以被控制为重定向到执行恶意代码的站点或页面。攻击者可以通过构造特殊的URL来执行各种攻击,例如:
漏洞复现1:
成功跳转到百度
漏洞代码2:
// ModelAndView public ModelAndView vul2(String url) { return new ModelAndView("redirect://" + url); }
漏洞分析2:
这是一个名为 的 Java 方法,它返回一个 类型的对象ModelAndView。该方法接受一个字符串参数url。
在方法体内,ModelAndView创建并返回了一个新的实例。的构造ModelAndView函数接受一个表示要显示的视图名称的字符串参数。在这种情况下,视图名称是通过将字符串“redirect://”与参数值连接起来构造的url。
因此,当使用 URL 作为参数调用此方法时,它将把用户的浏览器重定向到指定的 URL。
漏洞复现2:
漏洞案例3:
// response.sendRedirect public void vul3(String url, HttpServletResponse response) throws IOException { response.sendRedirect(url); }
漏洞分析3:
该方法将 URL 字符串和 HttpServletResponse 实例作为输入参数。它用于通过发送 302 响应代码并将“Location”标头设置为指定的 URL 来将用户重定向到指定的 URL。换句话说,当调用此方法时,用户的浏览器将收到来自服务器的响应,状态代码为 302(已找到),并且“Location”标头设置为指定的 URL。这将导致浏览器自动重定向到该 URL。
漏洞复现3:
public static boolean isWhite(String url) { List<String> url_list = new ArrayList<String>(); url_list.add("baidu.com"); url_list.add("www.baidu.com"); url_list.add("oa.baidu.com"); URI uri = null; try { uri = new URI(url); } catch (URISyntaxException e) { System.out.print(e); } String host = uri.getHost().toLowerCase(); System.out.println(host); return url_list.contains(host); }
漏洞分析:
此方法将 URL 字符串作为输入参数并返回一个布尔值,指示该 URL 是否被视为“白名单”。
“白名单”URL 通常是指可以不受任何限制或安全问题访问的受信任或允许的网站。该方法检查给定 URL 的主机是否存在于预定义的允许主机列表中,其中包括baidu.com、www.baidu.com和oa.baidu.com。要执行此检查,该方法首先使用 URI 构造函数从给定的 URL 字符串创建一个新的 URI 对象。如果 URL 字符串中有任何语法错误,则 URISyntaxException 将被抛出并被 try-catch 块捕获。
接下来,它使用 getHost() 方法从 URI 对象中提取主机组件。这将返回主机名的小写字符串表示形式。
最后,它使用 List 的 contains() 方法检查这个小写主机名是否存在于预定义的允许主机列表中。如果是这样,它返回 true 表示给定的 URL 被认为是“白名单”。否则,它返回 false,表示它不是允许或受信任的网站。
漏洞复现:
1、将跳转的url参数设为不可控,例:
public static void sendRedirect(HttpServletRequest request,HttpServletResponse response) throws IOException { String url = "http://sentiment.com"; response.sendRedirect(url); }
2、利用RequestDispatcher实现服务器内跳转
RequestDispatcher.forward()方法仅是容器中控制权的转向,在客户端浏览器地址栏中不会显示出转向后的地址。
public static void sendRedirect(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException { String url = request.getParameter("url"); RequestDispatcher rd = request.getRequestDispatcher(url); rd.forward(request,response); }
REF:
https://blog.51cto.com/u_13963323/5111217
https://www.cnblogs.com/N0r4h/p/15859970.html
https://blog.csdn.net/m0_62783065/article/details/130713629