作者:n1nty@360 A-Team

這篇筆記我盡量少貼代碼,有興趣的可以自己去跟一下。

需要知道的背景知識:

1.在 tomcat 的 conf/web.xml 文件中配置了一個如下的 servlet:

<servlet>
    <servlet-name>jsp</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    <init-param>
        <param-name>fork</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>xpoweredBy</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>3</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspx</url-pattern>
</servlet-mapping>

這意味著,tomcat 接收到的所有的 jsp 或 jspx 的請求,都會轉交給org.apache.jasper.servlet.JspServlet來處理,由它來將請求導向至最終的位置。

2.Jsp 文件會被轉換為 Java 文件,并隨后被編譯為 class。轉換后的文件與編譯后的 class 默認保存在 Tomcat 下的 work 目錄中。請求最終會被導向至從 Jsp 文件編譯出來的 class 的對象上。

3.Jsp 被編譯并被加載實例化后,會被封裝在一個 JspServletWrapper 對象中。在 Tomcat 中,每一個 Context 都對應有一個 JspRuntimeContext 對象,該對象中以 Map 的形式,以 path(如 /index.jsp) 為key 保存了當前 Context 中所有的 JspServletWrapper 對象。

4.被編譯并且被 Tomcat 加載后(創建了對應的 JspServletWrapper 對象后),Jsp 文件以及轉換出來的 Java 文件以及由 Java 文件編譯出來的 class 文件,在一定程度上來說,都是可有可無的。

這里簡述一下 Tomcat 會在什么時候對 Jsp 進行編譯:

  1. 當 Tomcat 處于 development 模式時(這是默認的),當一個 Jsp 第一次被請求時,會對被請求的文件進行編譯。隨后,每次請求時,都會對文件進行更新檢查,一旦發現源 Jsp 文件有變更,則將重新編譯。而如果發現源 Jsp 文件不存在了,則會出現 404,這是我們要 “欺騙” 的一個地方。
  2. 當 Tomcat 處于非 development 模式,且 JspServlet 的初始化參數 checkInterval 的值大于 0 的時候,Tomcat 將采用后臺編譯的方式 。這種情況下,當一個 Jsp 第一次被訪問的時候,它將會被編譯。隨后每隔指定的時間,會有一個后臺線程對這些 Jsp 文件進行更新檢查,如果發現文件有更新,則將在后臺進行重新編譯,如果發現文件不存在了,將從 JspRuntimeContext 中刪除對應的 JspServletWrapper 對象,導致我們隨后的訪問出現 404。這是我們要欺騙的另一個地方,雖然看起來與上面是一樣的,但是體現在代碼中卻不太一樣。

講到這里,所謂 “隱藏任意 Jsp 文件” 的原理也就很簡單了。只要在 Jsp編譯完成后,刪掉原有 Jsp 文件,并 “欺騙” Tomcat 讓它認為文件依然存在,就可以了。

簡述一下 Tomcat 接收請求的過程,當然這里只簡述請求到達 JspServlet 后發生的事情,之前的事情就太多了。這里從 JspServlet 的 serviceJspFile 開始說起,代碼如下:

private void serviceJspFile(HttpServletRequest request,
                            HttpServletResponse response, String jspUri,
                            boolean precompile)
    throws ServletException, IOException {

    JspServletWrapper wrapper = rctxt.getWrapper(jspUri);
    if (wrapper == null) {
        synchronized(this) {
            wrapper = rctxt.getWrapper(jspUri);
            if (wrapper == null) {
                // Check if the requested JSP page exists, to avoid
                // creating unnecessary directories and files.
                if (null == context.getResource(jspUri)) {
                    handleMissingResource(request, response, jspUri);
                    return;
                }
                wrapper = new JspServletWrapper(config, options, jspUri,
                                                rctxt);
                rctxt.addWrapper(jspUri,wrapper);
            }
        }
    }

    try {
        wrapper.service(request, response, precompile);
    } catch (FileNotFoundException fnfe) {
        handleMissingResource(request, response, jspUri);
    }

}

它的主要作用就是檢查 JspRuntimeContext 中是否已經存在與當前 Jsp 文件相對應的 JspServletWrapper(如果存在的話,說明這個文件之前已經被訪問過了)。有的話就取出來,沒有則檢查對應的 Jsp 文件是否存在,如果存在的話就新創建一個 JspServletWrapper 并添加到 JspRuntimeContext 中去。

隨后會進入 JspServletWrapper 的 service 方法,如下(我對代碼進行了刪減,只看與主題有關的部分):

public void service(HttpServletRequest request,
                    HttpServletResponse response,
                    boolean precompile)
        throws ServletException, IOException, FileNotFoundException {

    Servlet servlet;

    try {

        /*
         * (1) Compile
         */
        if (options.getDevelopment() || firstTime ) {
            synchronized (this) {
                firstTime = false;

                // The following sets reload to true, if necessary
                ctxt.compile();
            }
        } else {
            if (compileException != null) {
                // Throw cached compilation exception
                throw compileException;
            }
        }

        /*
         * (2) (Re)load servlet class file
         */
        servlet = getServlet();

        // If a page is to be precompiled only, return.
        if (precompile) {
            return;
        }

    } catch (ServletException ex) {
        .....
    }

        /*
         * (4) Service request
         */
        if (servlet instanceof SingleThreadModel) {
           // sync on the wrapper so that the freshness
           // of the page is determined right before servicing
           synchronized (this) {
               servlet.service(request, response);
            }
        } else {
            servlet.service(request, response);
        }
    } catch (UnavailableException ex) {
        ....
    }
}

可以看到,主要流程就是編譯 Jsp,然后進入編譯出來的 Jsp 的 service 方法,開始執行 Jsp 內的代碼。這里先判斷當前 Jsp 是不是第一次被訪問,或者 Tomcat 是否處于 development 模式中。如果是,則會進入 ctxt.compile();對 Jsp 進行編譯。ctxt 是 JspCompilationContext 的對象,該對象內封裝了與編譯 Jsp 相關的所有信息,每一個 JspServletWrapper 里面都有一個自己的 JspCompilationContext。也就是在 compile 方法里面,對 Jsp 文件的更改以及刪除做了檢查。

而當 Tomcat 利用后臺線程來對 Jsp 的更新刪除做檢查的時候,是不會經過這里的,而是直接進入 JspCompilationContext 的 compile 方法(也就是上文的 ctxt.compile() 方法)。代碼如下:

public void compile() throws JasperException, FileNotFoundException {
    createCompiler();
    if (jspCompiler.isOutDated()) {
        if (isRemoved()) {
            throw new FileNotFoundException(jspUri);
        }
        try {
            jspCompiler.removeGeneratedFiles();
            jspLoader = null;
            jspCompiler.compile();
            jsw.setReload(true);
            jsw.setCompilationException(null);
        } catch (JasperException ex) {
            // Cache compilation exception
            jsw.setCompilationException(ex);
            if (options.getDevelopment() && options.getRecompileOnFail()) {
                // Force a recompilation attempt on next access
                jsw.setLastModificationTest(-1);
            }
            throw ex;
        } catch (FileNotFoundException fnfe) {
            // Re-throw to let caller handle this - will result in a 404
            throw fnfe;
        } catch (Exception ex) {
            JasperException je = new JasperException(
                    Localizer.getMessage("jsp.error.unable.compile"),
                    ex);
            // Cache compilation exception
            jsw.setCompilationException(je);
            throw je;
        }
    }
}

JspCompilationContext 對象內有一個 Compile 對象,用它來對 Jsp 進行更新檢查以及編譯。jspCompile.isOutDated 方法代碼如下:

public boolean isOutDated(boolean checkClass) {

    if (jsw != null
            && (ctxt.getOptions().getModificationTestInterval() > 0)) {

        if (jsw.getLastModificationTest()
                + (ctxt.getOptions().getModificationTestInterval() * 1000) > System
                .currentTimeMillis()) {
            return false;
        }
        jsw.setLastModificationTest(System.currentTimeMillis());
    }

    Long jspRealLastModified = ctxt.getLastModified(ctxt.getJspFile());
    if (jspRealLastModified.longValue() < 0) {
        // Something went wrong - assume modification
        return true;
    }
    ......
}

我們只需要讓此方法返回 false,那么無論 Tomcat 在何時對 Jsp 文件進行編譯或者更新檢查,都會認為這個 JspServletWrapper 對象的 Jsp 文件沒有發生任何更改,所以也就不會發現文件被刪掉了。它會繼續保留這個 JspServletWrapper 對象以供客戶端訪問。

后面就沒有什么好說的了,如何進行“欺騙”,大家直接看效果吧。將 hideshell.jsp (在后面提供) 放在 webapps/ROOT 下,同目錄下有傳說中的 jspspy2011.jsp:

Tomcat 啟動后,先訪問一下 jspspy2011.jsp,目的是為了讓 Tomcat 將它編譯,并生成 JspServletWrapper 保存在 JspRuntimeContext 中(其實我們也可以自己用代碼來編譯,但是我太懶)。然后再訪問 hideshell.jsp,如下圖:

點擊 "Hide /jspspy2011.jsp",會發現 webapps/ROOT 目錄下的 jspspy2011.jsp 消失了,再訪問 jspspy2011.jsp 出現了 404。Shell 被“隱藏”了,而且訪問路徑被更改成了 hidden-jspspy2011.jsp:

同時再回到 hideshell.jsp,它會提示 /hidden-jspspy2011.jsp 是一個疑似的隱藏文件:

hideshell.jsp 會嘗試將被隱藏的 Jsp 文件與它生成的 Java 與 class 文件全部刪掉。但是我發現如果 Jsp 中使用了內部類,這些內部類所編譯出來的 class 不會被刪掉。

“隱藏” 任意 Jsp 文件到此已經實現了。可是雖然文件看不到了,當我們在訪問隱藏后的路徑的時候,依然會產生日志。那么下一篇筆記,有可能分享一下在隱藏 Shell 的同時,如何隱藏掉它們產生的訪問日志。

其實細心的話你會發現,你在訪問這個 hideshell.jsp 的時候,如果你的日志使用的是默認配置的話,Tomcat 是不會記錄你的訪問日志的。:)

hideshell.jsp 我發在了 ThreatHunter 社區,https://threathunter.org/topic/59545c18a1e4d7fc5810529a


歡迎關注作者公眾號


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/458/