安全矩阵

 找回密码
 立即注册
搜索
查看: 4595|回复: 0

Shiro权限绕过合集

[复制链接]

991

主题

1063

帖子

4315

积分

论坛元老

Rank: 8Rank: 8

积分
4315
发表于 2021-2-24 22:13:06 | 显示全部楼层 |阅读模式
本帖最后由 gclome 于 2021-2-24 22:15 编辑

原文链接:Shiro权限绕过合集

CVE-2020-1957


影响版本

Apache Shiro <= 1.5.1

Shiro处理
  1. org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain,
复制代码
其中getPathWithinApplication方法处理路径,pathMatches方法匹配路由
  1. public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
  2. FilterChainManager filterChainManager = getFilterChainManager();
  3. if (!filterChainManager.hasChains()) {
  4. return null;
  5. }


  6. String requestURI = getPathWithinApplication(request);


  7. // in spring web, the requestURI "/resource/menus" ---- "resource/menus/" bose can access the resource
  8. // but the pathPattern match "/resource/menus" can not match "resource/menus/"
  9. // user can use requestURI + "/" to simply bypassed chain filter, to bypassed shiro protect
  10. if(requestURI != null && !DEFAULT_PATH_SEPARATOR.equals(requestURI)
  11. && requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) {
  12. requestURI = requestURI.substring(0, requestURI.length() - 1);
  13. }




  14. //the 'chain names' in this implementation are actually path patterns defined by the user.  We just use them
  15. //as the chain name for the FilterChainManager's requirements
  16. for (String pathPattern : filterChainManager.getChainNames()) {
  17. if (pathPattern != null && !DEFAULT_PATH_SEPARATOR.equals(pathPattern)
  18. && pathPattern.endsWith(DEFAULT_PATH_SEPARATOR)) {
  19. pathPattern = pathPattern.substring(0, pathPattern.length() - 1);
  20. }


  21. // If the path does match, then pass on to the subclass implementation for specific checks:
  22. if (pathMatches(pathPattern, requestURI)) {
  23. ......
  24. return null;
  25. }
复制代码
  1. org.apache.shiro.web.util.WebUtils#getPathWithinApplication
复制代码
  1. public static String getPathWithinApplication(HttpServletRequest request) {
  2. String contextPath = getContextPath(request);
  3. String requestUri = getRequestUri(request);
  4. if (StringUtils.startsWithIgnoreCase(requestUri, contextPath)) {
  5. // Normal case: URI contains context path.
  6. String path = requestUri.substring(contextPath.length());
  7. return (StringUtils.hasText(path) ? path : "/");
  8. } else {
  9. // Special case: rather unusual.
  10. return requestUri;
  11. }
  12. }


  13. public static String getContextPath(HttpServletRequest request) {
  14. String contextPath = (String) request.getAttribute(INCLUDE_CONTEXT_PATH_ATTRIBUTE);
  15. if (contextPath == null) {
  16. contextPath = request.getContextPath();
  17. }
  18. contextPath = normalize(decodeRequestString(request, contextPath));
  19. if ("/".equals(contextPath)) {
  20. // the normalize method will return a "/" and includes on Jetty, will also be a "/".
  21. contextPath = "";
  22. }
  23. return contextPath;
  24. }


  25. public static String getRequestUri(HttpServletRequest request) {
  26. String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE);
  27. if (uri == null) {
  28. uri = request.getRequestURI();
  29. }
  30. return normalize(decodeAndCleanUriString(request, uri));
  31. }


  32. public static boolean startsWithIgnoreCase(String str, String prefix) {
  33. if (str == null || prefix == null) {
  34. return false;
  35. }
  36. if (str.startsWith(prefix)) {
  37. return true;
  38. }
  39. if (str.length() < prefix.length()) {
  40. return false;
  41. }
  42. String lcStr = str.substring(0, prefix.length()).toLowerCase();
  43. String lcPrefix = prefix.toLowerCase();
  44. return lcStr.equals(lcPrefix);
  45. }


  46. public static boolean hasText(String str) {
  47. if (!hasLength(str)) {
  48. return false;
  49. }
  50. int strLen = str.length();
  51. for (int i = 0; i < strLen; i++) {
  52. if (!Character.isWhitespace(str.charAt(i))) {
  53. return true;
  54. }
  55. }
  56. return false;
  57. }


  58. private static String normalize(String path, boolean replaceBackSlash) {


  59. if (path == null)
  60. return null;


  61. // Create a place for the normalized path
  62. String normalized = path;


  63. if (replaceBackSlash && normalized.indexOf('\\') >= 0)
  64. normalized = normalized.replace('\\', '/');


  65. if (normalized.equals("/."))
  66. return "/";


  67. // Add a leading "/" if necessary
  68. if (!normalized.startsWith("/"))
  69. normalized = "/" + normalized;


  70. // Resolve occurrences of "//" in the normalized path
  71. while (true) {
  72. int index = normalized.indexOf("//");
  73. if (index < 0)
  74. break;
  75. normalized = normalized.substring(0, index) +
  76. normalized.substring(index + 1);
  77. }


  78. // Resolve occurrences of "/./" in the normalized path
  79. while (true) {
  80. int index = normalized.indexOf("/./");
  81. if (index < 0)
  82. break;
  83. normalized = normalized.substring(0, index) +
  84. normalized.substring(index + 2);
  85. }


  86. // Resolve occurrences of "/../" in the normalized path
  87. while (true) {
  88. int index = normalized.indexOf("/../");
  89. if (index < 0)
  90. break;
  91. if (index == 0)
  92. return (null);  // Trying to go outside our context
  93. int index2 = normalized.lastIndexOf('/', index - 1);
  94. normalized = normalized.substring(0, index2) +
  95. normalized.substring(index + 3);
  96. }


  97. // Return the normalized path that we have completed
  98. return (normalized);


  99. }


  100. private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {
  101. uri = decodeRequestString(request, uri);
  102. int semicolonIndex = uri.indexOf(';');
  103. return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);
  104. }
复制代码
   hasText方法判断path是否只有空格,是返回根路径.getContextPath方法处理context_path(返回站点的根路径,也就是项目的名字)其中/./、/../、//与/..getRequestUri与getContextPath方法基本相同,处理request_uri(返回整个请求路径).decodeAndCleanUriString对uri进行url解码并根据分号切割,取出分号之前的字符串.
        springboot处理分号,

  1. org.springframework.web.util.UrlPathHelper#removeSemicolonContentInternal
复制代码
  1. private String removeSemicolonContentInternal(String requestUri) {
  2. for(int semicolonIndex = requestUri.indexOf(59); semicolonIndex != -1; semicolonIndex = requestUri.indexOf(59, semicolonIndex)) {
  3. int slashIndex = requestUri.indexOf(47, semicolonIndex);
  4. String start = requestUri.substring(0, semicolonIndex);
  5. requestUri = slashIndex != -1 ? start + requestUri.substring(slashIndex) : start;
  6. }


  7. return requestUri;
  8. }
复制代码

漏洞点


        在shiro处理路径时,/..;/会变为/..,从而匹配到未需授权路由,再springboot处理时,会根据;截断并重新拼接之前字符串,/..会向上跳跃目录,进一步显示页面.

修复方式

        修改了获取uri的方式



CVE-2020-11989

影响范围

Apache Shiro < 1.5.3

Shiro处理

        根据之前的修复手段,可以看到uri的获取已经完善,但是对于路径中带分号的情况并未处理,进而导致此次绕过,当存在context-path时,通过访问/;/的情况直接访问到根目录,而springboot会将分号删除拼接,进一步导致绕过.还有一种利用方式在于shiro中*与**路由的区别,当为*时,只对路由下的第一个路径进行鉴权,当存在/admin/a%25%2f%2f/a时,由于shiro会进行url解码,而springboot不会,在springboot设置为/admin/{name}时,导致差异解析.

修复方式
        不单独对context-path以及url解码做处理.



CVE-2020-13933


影响版本

Apache Shiro < 1.6.0

Shiro处理

        在除去url解码context-path解析处理后,对分号还是没有进行处理,在shiro处理uri时,当路径以/为结尾时,会截取到最后一个/之前的字符串为uri,这时如果鉴权以*为末尾,就会产生绕过无法匹配到处理后类似/admin这样的路径,而在springboot中未进行处理,导致差异解析,这个问题在1.7.0后版本中才正式得到修复.同时再此问题上,可以配合分号,因一直未对其处理,直接截断同样适用此方式.




修复手段



        增加了InvalidRequestFilter类,全局判断是否存在;、\和其余不可见字符.





CVE-2020-17523

影响版本

Apache Shiro < 1.7.1

Shiro处理

        经过之前的修复,对于分号和路径都进行了处理,此次问题出现在pathMatches方法匹配路由中,
  1. org.apache.shiro.util.AntPathMatcher#doMatch
复制代码
  1. protected boolean doMatch(String pattern, String path, boolean fullMatch) {
  2. if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
  3. return false;
  4. }


  5. String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);
  6. String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator);


  7. int pattIdxStart = 0;
  8. int pattIdxEnd = pattDirs.length - 1;
  9. int pathIdxStart = 0;
  10. int pathIdxEnd = pathDirs.length - 1;


  11. // Match all elements up to the first **
  12. while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
  13. String patDir = pattDirs[pattIdxStart];
  14. if ("**".equals(patDir)) {
  15. break;
  16. }
  17. if (!matchStrings(patDir, pathDirs[pathIdxStart])) {
  18. return false;
  19. }
  20. pattIdxStart++;
  21. pathIdxStart++;
  22. }


  23. if (pathIdxStart > pathIdxEnd) {
  24. // Path is exhausted, only match if rest of pattern is * or **'s
  25. if (pattIdxStart > pattIdxEnd) {
  26. return (pattern.endsWith(this.pathSeparator) ?
  27. path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator));
  28. }
  29. if (!fullMatch) {
  30. return true;
  31. }
  32. if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") &&
  33. path.endsWith(this.pathSeparator)) {
  34. return true;
  35. }
  36. for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
  37. if (!pattDirs[i].equals("**")) {
  38. return false;
  39. }
  40. }
  41. return true;
  42. } else if (pattIdxStart > pattIdxEnd) {
  43. // String not exhausted, but pattern is. Failure.
  44. return false;
  45. } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
  46. // Path start definitely matches due to "**" part in pattern.
  47. return true;
  48. }


  49. // up to last '**'
  50. while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
  51. String patDir = pattDirs[pattIdxEnd];
  52. if (patDir.equals("**")) {
  53. break;
  54. }
  55. if (!matchStrings(patDir, pathDirs[pathIdxEnd])) {
  56. return false;
  57. }
  58. pattIdxEnd--;
  59. pathIdxEnd--;
  60. }
  61. if (pathIdxStart > pathIdxEnd) {
  62. // String is exhausted
  63. for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
  64. if (!pattDirs[i].equals("**")) {
  65. return false;
  66. }
  67. }
  68. return true;
  69. }


  70. while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
  71. int patIdxTmp = -1;
  72. for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
  73. if (pattDirs[i].equals("**")) {
  74. patIdxTmp = i;
  75. break;
  76. }
  77. }
  78. if (patIdxTmp == pattIdxStart + 1) {
  79. // '**/**' situation, so skip one
  80. pattIdxStart++;
  81. continue;
  82. }
  83. // Find the pattern between padIdxStart & padIdxTmp in str between
  84. // strIdxStart & strIdxEnd
  85. int patLength = (patIdxTmp - pattIdxStart - 1);
  86. int strLength = (pathIdxEnd - pathIdxStart + 1);
  87. int foundIdx = -1;


  88. strLoop:
  89. for (int i = 0; i <= strLength - patLength; i++) {
  90. for (int j = 0; j < patLength; j++) {
  91. String subPat = (String) pattDirs[pattIdxStart + j + 1];
  92. String subStr = (String) pathDirs[pathIdxStart + i + j];
  93. if (!matchStrings(subPat, subStr)) {
  94. continue strLoop;
  95. }
  96. }
  97. foundIdx = pathIdxStart + i;
  98. break;
  99. }


  100. if (foundIdx == -1) {
  101. return false;
  102. }


  103. pattIdxStart = patIdxTmp;
  104. pathIdxStart = foundIdx + patLength;
  105. }


  106. for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
  107. if (!pattDirs[i].equals("**")) {
  108. return false;
  109. }
  110. }


  111. return true;
  112. }
复制代码
  1. org.apache.shiro.util.StringUtils#tokenizeToStringArray
复制代码

  中适用了trim会清除空格,当请求路径中存在空格时,返回之前的情况,shiro鉴权适用*时,存在/admin/*无法匹配到/admin/,而在springboot中可以正确匹配,导致差异解析绕过.


修复方式


        直接设置清除空格为false,默认不清除.





回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-3-28 19:48 , Processed in 0.017321 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表