1.1 权限控制的本质
一般来说,为了防止越权操作,通常会结合filter进⾏相关接⼝的鉴权操作。其中不不外乎就是对每⼀个接口(通俗来说就是我们的URI/URL)进行业务梳理,然后判断当前URI/URL是否具有相应的业务权限。
1.2 常见权限控制的实现
一般情况下,通常是获取到当前URI/URL,然后跟需要鉴权的接口进行⽐对,或者直接结合startsWith()或者endsWith()方法,设置对应的校验名单。
例如下⾯的过滤器实现,以/login开头的不需要校验(登陆业务每个人都可以访问),所有.do/.action结尾的接⼝均需要做登陆检查,防止未授权访问等。
String uri = request.getRequestURI();
if(uri.endsWith(".do")||uri.endsWith(".action")) {
//检测当前用户是否登陆
User user =(User) request.getSession().getAttribute("user");
if(user==null|| "".equals(user)) {
errorResponse(response, paramN, "未授权访问");
return;
}
}
但是,在Java中获取当前request中的URI/URL通常会使用request.getRequestURL()和request.getRequestURI()这两个方法,但是如果没有进⾏相关的处理的话,有可能导致权限控制绕过的风险。
1.3 绕过方式
当权限过滤器获取当前request中的URI/URL使用request.getRequestURL()和request.getRequestURI()这两个方法时,可以考虑以下三种⽅式进行权限绕过:
1.3.1 非标准化绕过
相关场景:
例如/system/login开头的接口是白名单,不需要进行访问控制(登陆页面所有人都可以访问),其他接⼝都需要进⾏登陆检查,防止未授权访问:
String uri = request.getRequestURI();
if(uri.startsWith("/system/login")) {
//登陆接口设置⽩白名单
filterChain.doFilter(request, response);
}
else if(uri.endsWith(".do")||uri.endsWith(".action")) {
//检测当前⽤户是否登陆
User user =(User) request.getSession().getAttribute("user");
if(user==null|| "".equals(user)) {
errorResponse(response, paramN, "未授权访问");
return;
}
}
相关效果如下:
当未登录直接访问UserInfoSearch.do接口时,显示未授权访问:
相关原理:
中间件在进⾏解析时,会对我们URI中的../进行相关处理从⽽得到相关的servlet。也就是说尝试对我们访问的URL引入../,中间件是可以正常解析并完成正常业务的,以tomcat中的examples目录中的案例servlet访问为例,尝试访问一个不存在的目录login,然后通过../回到正常目录下,正常解析:
绕过分析:
使用request.getRequestURL()和request.getRequestURI()这两个方法进⾏访问接口的获取时,是不会对类似../等进⾏规范化处理的,也就是说刚刚我们访问的/system/login/../UserInfoSearch.do际获取到的URI为:
同样是前⾯的例子,那么我们可以通过在URI中写⼊/login/../,使得权限过滤器认为我们当前访问的接⼝为白名单接口,从而绕过权限控制,使得系统认为我们当前访问的接⼝是登陆login,不需要进行权限校验:
绕过方法:
在URI引⼊入类似/login(白名单接口,可以通过测试得出,一般登陆都是不需要权限校验的)/../的
样式,伪造⽩名单接口。
1.3.2 URL截断绕过
相关场景:
例如/system/login开头的接⼝是⽩名单,不需要进行访问控制(登陆⻚面所有人都可以访问),其他接口都需要进行登陆检查,防止未授权访问,但是考虑到了../的非法访问问题:
String uri = request.getRequestURI();
if(uri.contains("./")){
errorResponse(response, paramN, "⾮非法访问");
return;
}
else if(uri.startsWith("/system/login")) {
//登陆接口设置⽩白名单
filterChain.doFilter(request, response);
}
else if(uri.endsWith(".do")||uri.endsWith(".action")) {
//检测当前用户是否登陆
User user =(User) request.getSession().getAttribute("user");
if(user==null|| "".equals(user)) {
errorResponse(response, paramN, "未授权访问");
return;
}
}
相关效果如下:
当未登录直接访问UserInfoSearch.do接⼝时,显示未授权访问:
尝试结合../伪造白名单,失败:
相关原理:
URL中有一个保留字符分号(;),主要作为参数分隔符进行使用,有时候是请求中传递的参数太多了,所以使用分号(;)将参数对(key=value)连接起来作为一个请求参数进⾏传递。
直接在URI中引入分隔符,正常来说是不会对实际接口的访问造成影响的。
绕过分析:
对于request.getRequestURL()和request.getRequestURI()来说,使用&连接的参数键值对,其是获取不到的,但是参数分隔符(;)及内容是可以获取到的:
同样是前面的例子,访问.do结尾的接口需要进行登陆检查,否则认为未授权访问,那么此时可以利用分隔符,绕过endsWith()检测,使得权限过滤器认为我们访问的接口不是业务接口,从而达到绕过权限控制的效果:
绕过⽅法:
在URI引⼊入参数分隔符;,进⾏切割URI绕过限制,例例
如/system;Bypass/UserSearch.do;Bypass
1.3.3 URL编码绕过
相关场景:
例如/system/UserInfoSearch.do接⼝是管理员才能访问的接口,需要进⾏⽤户检查,防止越权访问:
if(uri.equals("/system/UserInfoSearch.do")){
User user =(User) request.getSession().getAttribute("user");
String role = user.getRole();
if(role.equals("admin")) {
//当前⽤用户为admin,允许访问该接⼝
filterChain.doFilter(request, response);
}
else {
errorResponse(response, paramN, "越权访问");
return;
}
}
相关效果如下:
若不是admin用户登陆,拒绝访问UserInfoSearch.do接口:
否则返回当前系统存在的用户名:
相关原理:
当filter处理完相关的流程后,中间件会对请求的URL进行一次URL解码操作,然后再找到对应的Servlet进行访问。
也就是说尝试对我们访问的URL进行一次URL编码,中间件是可以正常解析并完成正常业务的,以tomcat中的examples⽬目录中的案例servlet访问为例,尝试将HelloWorldExample进行URL编码再进行访问,正常解析:
绕过分析:
除了前面介绍的两种方式以外,这里存在一个问题,使用request.getRequestURL()和request.getRequestURI()这两个⽅法进⾏行行访问接口的获取时,是不会进行URL解码操作的,也就是说刚刚我们访问的
/system/%55%73%65%72%49%6e%66%6f%53%65%61%72%63%68%2e%64%6f实际获取到的URI为:
那么我们可以通过对URI进行URL编码,此时filter中得到的uri并不是正常的/system/UserInfoSearch.do,⽽是编码后的,但是filter转发请求后浏览器可以解码并正常解析,从而达到以低权限用户绕过权限控制访问管理员接口的效果:
绕过⽅法:
对URI进行URL编码/多重URL编码,尝试绕过。
1.3.4 Spring Web的动态Controller追加/绕过
相关场景:
一般情况下,通常是获取到当前URI/URL,然后跟需要鉴权的接⼝进行比对,结合endsWith()方法,设置对应的校验名单。
例如下面的过滤器实现,所有.do、.action结尾的接口均需要做登陆检查,防⽌止未授权访问等。
String uri = request.getServletPath()+(request.getPathInfo() == null ?
"" : request.getPathInfo());
if(uri.endsWith(".do")||uri.endsWith(".action")) {
//检测当前用户是否登陆
User user =(User) request.getSession().getAttribute("user");
if(user==null|| "".equals(user)) {
errorResponse(response, paramN, "未授权访问");
return;
}
}
相关原理:
特定情况下,Spring web在匹配url接⼝的时候会容错后⾯额外的/。
以Spring MVC为例例,如下配置的话,在实际接口访问的时候会容错后⾯额外的/:
SpringMVC
/
例如以下两种访问方式效果都是一样的:
/admin/info.do?param=value
/admin/info.do/?param=value
绕过分析:
考虑到上⾯使用request.getRequestURL()和request.getRequestURI()这两个⽅方法进行访问接⼝的获取时存在的安全隐患,这里使用 request.getServletPath()+(request.getPathInfo() == null ? "" :
request.getPathInfo())进行路径的获取,但是如果在接口URI后追加额外的/,还是可以获取到的:
根据上⾯的filter代码,正常情况下非登陆访问/admin/info.do,会提示未授权访问:
尝试在接口后追加额外的/,成功绕过权限校验:
绕过方法:
尝试在对应的URL接口后加入/,以绕过权限控制。
1.4 修复建议及防御方式
以Java为例:
1.使用如下⽅法进⾏相关路路径的获取:
request.getServletPath()+(request.getPathInfo() == null ? "" :
request.getPathInfo());
2.对相关的接口访问进行标准化处理,剔除不相关的元素,例如../,分隔符(;)后的内容以及接⼝后不必要的/等;
3.使用ESAPI的canonicalize方法进行规范化处理:
ESAPI.encoder().canonicalize(URI)
同时在对应的配置⽂文件ESAPI.properties禁⽤用双重uri编码(默认开启):
Encoder.AllowMultipleEncoding=false
具体效果如下,当接收到双重url编码时,会触发报错:
4.尽量不要使用类似startsWith()、endsWith()进⾏判断。
5.使用成熟的权限控制框架进行权限校验。(Spring Security、Shiro等)
1.5 思维导图
相关实操练习:Springboot未授权访问 ——Actuator 是 springboot 提供的用来对应用系统进行自省和监控的功能模块,非法用户可通过访问默认的执行器端点(endpoints)来获取应用系统中的监控信息从而导致信息泄露的事件发生。