Resin解析漏洞分析
前言
前陣子看有師傅在公眾號上發表了Resin解析漏洞分析,我們也知道有個常用的OA用的就是Resin,因此我認為了解它的漏洞是十分必要的。
原理分析
這個漏洞和IIS解析漏洞比較像,可以通過創建一個xxx.jsp的文件夾,并在其中放置一個txt文件,文件的內容將會被當作JSP解析。
我認為要分析這個漏洞原理,首先得先了解訪問jsp文件時Resin是如何處理我們請求的。
首先看下*.jsp是被哪個Servlet處理的,從配置app- default.xml中可以看出,我們的請求會被com.caucho.jsp.JspServlet處理。
servlet-name="resin-jsp"
servlet-class="com.caucho.jsp.JspServlet">
false
1024
url-pattern="*.jsp" servlet-name="resin-jsp" default="true"/>
本來以為在JspServlet下斷點可以看到請求調用棧,但是在實際操作的過程中發現并沒有執行到JspServlet中的方法就返回了,確實比較奇怪。
在Resin中發起HTTP請求一定會經過HttpRequest#handleRequest方法處理,可以在這個方法中打斷點排查問題,經過排查發現在PageFilterChain#doFilter中就完成了JSP的”編譯”和執行工作,這點比較奇怪,因為之前分析Tomcat中”編譯JSP”的操作是在servlet中完成的。所以其實針對Resin對JSP文件處理的分析重點就在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方法完成請求。

如何進入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";
}
- 如果匹配成功則設置
servletPath和servletName等屬性到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(?=/)|^.*\.jsp\z正則處理。

主要出問題的是^.*\.jsp(?=/)部分,這個正則的邏輯是匹配xxxx.jsp/xxxx所以我們傳入的路徑會被匹配到,這也是這個漏洞的本質原因。
總結
其實我認為Resin這么寫可能對作者來說這本身是個正常功能,因為之前Resin也實現了Invoker的功能,可以直接根據路徑加載任意類。