Resin解析漏洞分析

前言

前阵子看有师傅在公众号上发表了Resin解析漏洞分析,我们也知道有个常用的OA用的就是Resin,因此我认为了解它的漏洞是十分必要的。

原理分析

这个漏洞和IIS解析漏洞比较像,可以通过创建一个xxx.jsp的文件夹,并在其中放置一个txt文件,文件的内容将会被当作JSP解析。

我认为要分析这个漏洞原理,首先得先了解访问jsp文件时Resin是如何处理我们请求的。

首先看下*.jsp是被哪个Servlet处理的,从配置app- default.xml中可以看出,我们的请求会被com.caucho.jsp.JspServlet处理。

<servlet servlet-name="resin-jsp"
           servlet-class="com.caucho.jsp.JspServlet">
    <init>
      <load-tld-on-init>false</load-tld-on-init>
      <page-cache-max>1024</page-cache-max>
    </init>
    <load-on-startup/>
  </servlet>
<servlet-mapping url-pattern="*.jsp" servlet-name="resin-jsp" default="true"/>

本来以为在JspServlet下断点可以看到请求调用栈,但是在实际操作的过程中发现并没有执行到JspServlet中的方法就返回了,确实比较奇怪。

Resin中发起HTTP请求一定会经过HttpRequest#handleRequest方法处理,可以在这个方法中打断点排查问题,经过排查发现在PageFilterChain#doFilter中就完成了JSP的”编译”和执行工作,这点比较奇怪,因为之前分析Tomcat中”编译JSP”的操作是在servlet中完成的。所以其实针对ResinJSP文件处理的分析重点就在PageFilterChain#doFilter中。

  • JSP编译后会被封装到Page对象中,而Page对象的引用被保存以pageRef属性中,因此首先检测pageRef是否为空,如果是则直接通过page.pageservice(req, res);执行请求,不经过后面编译的逻辑。
  • 如果缓存中没有page对象,则通过compilePage编译JSP并封装为Page对象返回,new SoftReference创建引用对象,再通过pageservice执行请求。
public void doFilter(ServletRequest request, ServletResponse response)
    throws ServletException, IOException
  {
    HttpServletRequest req = (HttpServletRequest) request;
    HttpServletResponse res = (HttpServletResponse) response;
    FileNotFoundException notFound = null;
    SoftReference<Page> pageRef = _pageRef;
    Page page;
    //首先从换从中获取Page对象的引用,如果有就不再编译。
    if (pageRef != null)
      page = pageRef.get();
    else
      page = null;
     //如果缓存为空或者page对象被修改过则编译
    if (page == null || page._caucho_isModified()) {
      try {
        _pageRef = null;

        page = compilePage(page, req, res);
        //得到page的引用并保存
        if (page != null) {
          _pageRef = new SoftReference<Page>(page);

          _isSingleThread = page instanceof SingleThreadModel;
        }
      } catch (FileNotFoundException e) {
        page = null;
        notFound = e;
      }
    }
    if (page == null) {
      // jsp/01cg
      if (notFound == null)
        return;

      String errorUri = (String) req.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);
      String uri = (String) req.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
      String forward = (String) req.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);
      // jsp/01ch
      if (uri != null) {
        //throw new FileNotFoundException(uri);
        throw notFound;
      }
      else if (forward != null) {
        //throw new FileNotFoundException(req.getRequestURI());
        throw notFound;
      }
      else if (errorUri != null) {
        //throw new FileNotFoundException(errorUri);
        throw notFound;
      }
      else {
        log.log(Level.FINER, notFound.toString(), notFound);
      }
      ((HttpServletResponse) res).sendError(HttpServletResponse.SC_NOT_FOUND);
    }
    else if (req instanceof HttpServletRequest) {
      try {
        if (_isSingleThread) {
          synchronized (page) {
           //执行请求
            page.pageservice(req, res);
          }
        }
        else
          page.pageservice(req, res);
      } catch (ServletException e) {
      ...
  }

Page#pageService-->JavaPage#service-->_aaa#_jspService,最后通过JSP生成类的_jspService方法完成请求。

a699b43c388511ecb831fa163e8ef7b7

如何进入PageFilterChain?

通过上面的分析我们可以知道,在PageFilterChain中完成了对JSP的编译和执行,所以我们分析的重点就在于如何才能进入PageFilterChain中?

追踪创建PageFilterChain的过程,在WebApp#buildInvocation中,完成了PageFilterChain的创建,我摘了部分代码分析。

  • 首先从缓存中获取FilterChains,如果有的话则直接获取chains,缓存中保存的Chains和URL有关。
  • 如果缓存没有,则通过_servletMapper.mapServlet(invocation);获取Chains。
public Invocation buildInvocation(Invocation invocation, boolean isTop)
  {
      ...
    else {
        FilterChainEntry entry = null;
        // jsp/1910 - can't cache jsp_precompile
        String query = invocation.getQueryString();
        boolean isCache = true;
        if (query != null && query.indexOf("jsp_precompile") >= 0)
          isCache = false;
        else if (_requestRewriteDispatch != null)
          isCache = false;
        if (isCache)
          entry = _filterChainCache.get(invocation.getContextURI());
        if (entry != null && ! entry.isModified()) {
          chain = entry.getFilterChain();
          invocation.setServletName(entry.getServletName());
          if (! entry.isAsyncSupported())
            invocation.clearAsyncSupported();
          invocation.setMultipartConfig(entry.getMultipartConfig());
        } else {
          chain = _servletMapper.mapServlet(invocation);
         ...   
        }

mapServlet中,主要做了下面的操作

  • ServletInvocation中获取URL并去除;xxx的内容
String contextURI = invocation.getContextURI();
 try {
            cleanUri = Invocation.stripPathParameters(contextURI);
      } catch (Exception e) {
        log.warning(L.l("Invalid URI {0}", contextURI));

        return new ErrorFilterChain(404);
      }
  • 根据URL匹配获取ServletMapping
ServletMapping servletMap = _servletMap.map(cleanUri, vars);
  • 如果根据URL没有匹配到Servlet处理则根据URL获取资源内容,并设置使用_defaultServlet处理。
servletName = servletMap.getServletName();
if (servletName == null) {
      try {
        InputStream is;
        is = _webApp.getResourceAsStream(contextURI);
        if (is != null) {
          is.close();
          servletName = _defaultServlet;
        }
      } catch (Exception e) {
      }
  • 如果URL以j_security_check结尾则使用j_security_check作为Servlet
if (matchResult == null && contextURI.endsWith("j_security_check")) {
      servletName = "j_security_check";
    }
  • 如果匹配成功则设置servletPathservletName等属性到invocation对象中,根据Servletname_servletManager获取ServletConfigImpl对象,创建FilterChains
ArrayList<String> vars = new ArrayList<String>();
vars.add(contextURI);
String servletPath = vars.get(0);
invocation.setServletPath(servletPath);
invocation.setServletName(servletName);
ServletConfigImpl newConfig = _servletManager.getServlet(servletName);
FilterChain chain= _servletManager.createServletChain(servletName, config, invocation);

所以这个漏洞的重点在于为什么/test.jsp/xxx.txt可以被_servletMap.map(cleanUri, vars);匹配到。

进入到UrlMap#map中,发现默认情况下*.jsp会交给^.*.jsp(?=/)|^.*.jspz正则处理。

a69ac476388511ecb831fa163e8ef7b7

主要出问题的是^.*.jsp(?=/)部分,这个正则的逻辑是匹配xxxx.jsp/xxxx所以我们传入的路径会被匹配到,这也是这个漏洞的本质原因。

总结

其实我认为Resin这么写可能对作者来说这本身是个正常功能,因为之前Resin也实现了Invoker的功能,可以直接根据路径加载任意类。

参考

转载自

https://xz.aliyun.com/t/10447

本文转载自https://xz.aliyun.com/t/10447,只做本站测试使用,本文观点不代表安强科技社区立场。

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022年3月17日 上午9:40
下一篇 2022年3月17日 上午10:16

相关推荐

发表回复

您的电子邮箱地址不会被公开。