Java RMI服务与攻击源码解析

本来在写JNDI工具相关的内容,但是RMI其实值得重新看一遍。之前的文章《RMI、LDAP、CORBA与JNDI攻击》中也写到过RMI,当时文章介绍了很多理论,但是更像一篇概述。这篇讲讲源码层面的东西。关于JDK源码,想要看一些注解可以去看OpenJDK:https://github.com/frohoff/jdk8u-jdk/tree/master/src/share/classes。这一篇的总结中更偏向于RMI的JDK源码,对于RMI攻击的各种反序列化并不做过多的分析,希望日后再补一篇。但是在看源码之前,简单说一下RMI的调用流程和攻击历史。

1. RMI调用流程

RMI(Remote Method Invocation,远程方法调用),可以引用远程主机上对象的方法,在分布式领域应用广泛。RMI总的来说可以分为:RMI Client、RMI registry、RMI Server(有的代码中把registry和Server合成了一个)

RMI调用流程

(1)首先远程主机(RMI Server)会向RMI注册表(RMI Registry)中注册对象,给对象绑定一个名称,例如Hello代表RemoteA对象
创建对象:既然要注册一个对象,那么远程主机Server上首先要有一个对象。写一个接口、一个接口的实现类、类实例化对象。

public interface IRemoteA extends Remote{
    public String hello() throws RemoteException; // 要远程调用的方法
}

public class RemoteA extends UnicastRemoteObject implements IRemoteA { // 接口实现类
    protected RemoteA() throws RemoteException {
        super();
    }
    public void hello() throws RemoteException { System.out.println("call from");}
} 
RemoteA h = new RemoteA(); //类的实例化对象

注册对象:将实例化对象绑定到Registry中,绑定名称即为Hello

LocateRegistry.createRegistry(1099); 
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);

(2)客户端向RMI注册表中根据名称Hello去查找(lookup)该对象,并获取一个远程对象的stub实例,所谓的stub就是一种代理/引用,包含了远程对象的定位信息(服务器codebase地址等)。

RemoteA hello = (RemoteA)Naming.lookup("rmi://127.0.0.1:1099/Hello");
hello.hello();

(3)客户端向codebase地址请求远程对象的class,获取到class的同时会进行实例化,即运行类的构造方法/静态代码块static中的代码。

2. RMI攻击历史

RMI过程因为涉及对象的存储和传递,所以要依靠反序列化来进行。无论是绑定对象还是查询对象的操作都会进行反序列化。这也是RMI攻击的基础。2016年ysoserial中就集成了关于Registry攻击和DGC攻击的内容。

所谓的Registry攻击,就是攻击注册中心。(1)服务器端->注册中心。服务器端是会向注册中心注册对象的(序列化传输),注册中心RegistryImpl_Skel#dispatch拦截请求的过程中会对流进行反序列化得到对象,如果本地存在CommonsCollections这种调用链就可以造成攻击,这种攻击方式在ysoserial中名为RMIRegistryExploit。反过来讲,客户端会从注册中心查询并获取对象RegistryImpl_Stub#lookup,然后在本地反序列化得到对象,如果本地存在CommonsCollections等则可能被注册中心反打,这可以理解为注册中心->客户端。(2)客户端->注册中心。按理说客户端是从注册中心拉取对象的,并不能传递恶意对象过去。但是RMI框架采用了DGC(分布式垃圾收集机制)来管理远程对象的生命周期,所以客户端可以采用DGC通信的方式发送恶意对象,在ysoserial中名为exploit.JRMPClient。注册中心DGCImpl_Skel#dispatch拦截请求进行反序列化的过程中受到攻击,也就是DGC攻击。

但是这两种都在8u121(还有JDK6u141、JDK7u131)中被修复。修复方式就是引入了JEP290。JEP(JDK Enhancement Proposal)是用于收集JDK增强的提案,290是该此提案在其中的索引编号。它代表的议题是Filter Incoming Serialization Data(过滤传入的序列化数据),官方链接:https://openjdk.java.net/jeps/290。其核心思想是在反序列化(ObjectInputStream)过程中调用过滤器对正在反序列化的类、流中的内容等进行校验,然后返回一个接受(ALLOWED)、拒绝(REJECTED)或未决定(UNDECIDED)的状态。简单来说就是反序列化期间加了白名单。对于上面提到的ysoserial的两种RMI攻击方式来说就失效了,增加的校验方法如下:

// 注册表
sun.rmi.registry.RegistryImpl#registryFilter
// DGC
sun.rmi.transport.DGCImpl#checkInput

也就是在调用栈运行到RegistryImpl_Skel#dispatch / DGCImpl_Skel#dispatch后会通过上述两个函数进行校验(这两个函数作用于服务器端),这就限制了Registry攻击和DGC攻击。所以后来就开始研究如何绕过JEP 290带来的白名单限制。RegistryImpl#registryFilter的方法实现中,会对传入的参数的类型进行判断。

private static Status registryFilter(FilterInfo var0) {
            ...
            Class var2 = var0.serialClass();
            if (var2 != null) {
                if (!var2.isArray()) {
                    return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
                } ...
    }

看一下对var2的判断,发现白名单如下,如果不是白名单中的类就返回REJECTED

String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class

相应地诞生了一些绕过方法,例如利用JRMP。一个利用服务端攻打客户端的JRMP Demo如下。

// Server
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open -a Calculator"
// Client
Registry registry = LocateRegistry.getRegistry(1099);
registry.lookup("hello");

假如我们能够作为服务器端,目标系统作为客户端来反连我们,向其发送恶意对象就可以造成攻击效果。JEP290的限制是对于服务器端的,对于客户端没有限制,这样就能达成绕过。

JRMP Listener ——registry.bind(name, object)——> Deser Filter—> RMI Registry
JRMP Listener <——Outgoing JRMP connection(not filtered)—— RMI Registry
JRMP Listener ——Payload——> RMI Registry

想要让目标系统反连我们,就要通过代码构造,让其在反序列化过程中执行了JRMP连接的代码,如ysoserial中的payload.JRMPClient调用链。

另外,由于JEP290是针对于RMI注册中心和DGC的,客户端发送数据到服务器端并没有做直接的限制,反序列化时如果接收到的不是基础数据类型(如Integer、Boolean、Byte、Character、Short、Long、Float或Double),就会直接对数据进行反序列化,也就是如果此时传入的是Object、String类型的数据会直接反序列化。假如服务器中应接收的不是Object类型的参数,我们在客户端传入Object参数的话,会无法通过method hash校验。简单Demo如下。

public interface Hello extends Remote {
    public String hey(Object msg) throws RemoteException;
}
public class HelloImpl extends UnicastRemoteObject implements Hello {
    public void hey(Object msg){
        System.out.println(msg.toString());
    }
}
// 服务端绑定
Hello hello = new HelloImpl();
LocateRegistry.createRegistry(1234);
String url = "rmi://127.0.0.1:1234/Hello";
Naming.bind(url, hello);

// 客户端
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1234);
Hello hello = (Hello)registry.lookup("Hello");
CommonsCollections1 cc1 = new CommonsCollections1();
hello.hey(cc1.getObject("calc"));

这也能看出这种利用方式存在的问题:需要目标服务器中存在CommonsCollections等,并且!要知道服务器端起服务的接口和接口中的调用方法,如Hello.hey(Object msg),较难利用。

总的来说,攻击主要有如下几种,但使用的前提是,目标服务器中存在CommonsCollections或其他反序列化gadget的组件,后两种连接JRMPListener的还要求目标服务器能够出网。对于JEP290的绕过,Oracle建议用户手动配置过滤器sun.rmi.registry.registryFilter来防御攻击。

1. JDK < 8u121 bind、lookup、DGC对Registry端口进行反序列化攻击
2. JDK < 8u231 lookup发送UnicastRef对象,反序列化时连接JRMPListener
3. JDK < 8u241 lookup发送UnicastRefRemoteObject对象,反序列化时连接JRMPListener
4. 服务器端接口实现包含非基础类型接口,如Object,String(String< < JDK 8u242)

此外,在文章最开始的RMI调用流程图中可以看到codebase是对象的加载地址(如果本地的ClassPath中找不到就从codebase中加载),那么如果这个codebase是可控的,也就意味着会从攻击者的服务器上去拉取对象,如果这个类是恶意类就可能造成RCE,这也是JNDI与RMI攻击结合的利用场景。但是后来官方也对这样的攻击进行了限制。所以这种攻击场景的前提是:首先要安装并配置了SecurityManager,Java版本低于7u21、6u45(或者设置了java.rmi.server.useCodebaseOnly=false)。因为Java 7u21、6u45开始有了个默认设置java.rmi.server.useCodebaseOnly=true,这意味着Java虚拟机只从预设的codebase中加载对象,而不支持从RMI请求中获取。所以在之后的版本进行codebase可控攻击时需要将这个属性改为false。

5. JDK<7u21、6u45 codebase可控;或设置java.rmi.server.useCodebaseOnly=false,codebase可控

3. RMI流量

RMI流量

Client(192.168.135.1)和Server(192.168.135.142:1099)交互过程大致如下:
(1)第一阶段
Client -> Server 发送:JRMI, Version:2, StreamProtocol
Server -> Client 回应:JRMI, ProtocolAck
Client -> Server 发送:JRMI, Call
Server -> Client 回应:JRMI, ReturnData -> ReturnData中包含了codebase地址
(2)第二阶段
Client(192.168.135.1)和Server(192.168.135.142:33769)交互过程大致如下:
Client -> Server 发送:JRMI, Ping
Sevrer -> Client 回应:JRMI, PingAck
Client -> Server发送: JRMI, DgcAck

客户端和服务端之间是通过Socket通信的,这部分都位于sun.rmi.transport包中。RMI协议的格式可以参考:https://docs.oracle.com/javase/9/docs/specs/rmi/protocol.html。其输出流格式和上述流量可以对应上。

Magic:
    0x4a 0x52 0x4d 0x49 Version Protocol  -> 此十六进制转换为十进制为1246907721

Version:
    0x00 0x01 -> version == 2 

Protocol:
    StreamProtocol  -> 0x4b
    SingleOpProtocol -> 0x4c
    MultiplexProtocol -> 0x4d

Messages: -> 三种:Call、Ping、DgcAck

TransportConstants类就是上述协议格式中的常量的集合

public class TransportConstants {
    public static final int Magic = 0x4a524d49; /** Transport magic number: "JRMI"*/
    public static final short Version = 2;  /** Transport version number */
    public static final byte StreamProtocol = 0x4b;   /** Connection uses stream protocol */
    public static final byte SingleOpProtocol = 0x4c; /** Protocol for single operation per connection; no ack required */
    public static final byte MultiplexProtocol = 0x4d;   /** Connection uses multiplex protocol */
    public static final byte ProtocolAck = 0x4e;   /** Ack for transport protocol */
    public static final byte ProtocolNack = 0x4f;  /** Negative ack for transport protocol (protocol not supported) */
    public static final byte Call = 0x50;
    public static final byte Return = 0x51;
    public static final byte Ping = 0x52;
    public static final byte PingAck = 0x53;
    public static final byte DGCAck = 0x54;
    public static final byte NormalReturn = 0x01;
    public static final byte ExceptionalReturn = 0x02;
}

4. RMI Registry源码

Registry的相关内容都在java.rmi.registry包中,只有:Registry接口和LocateRegistry类。

Registry

public interface Registry extends Remote { // Remote接口:标识可以从非本地虚拟机调用的方法,任何远程对象都需要直接或间接实现此接口
    public static final int REGISTRY_PORT = 1099;
    // 绑定
    public void bind(String name, Remote obj)  //绑定的对象也要实现Remote接口
    public void unbind(String name)
    public void rebind(String name, Remote obj)
    // 查询
    public Remote lookup(String name)
    public String[] list()
}

Registry代表远程对象注册表接口,提供了向注册表中绑定对象(bind、unbind、rebind)和查询对象的方法(lookuplist)。

LocateRegistry

LocateRegistry用于定位注册表。包括获取特定主机(包括本地主机)上注册表的引用getRegistry(这样才能在本地调用Registry),或者在特定端口上创建注册表createRegistry

public final class LocateRegistry {
    private LocateRegistry() {}
    public static Registry getRegistry()
    public static Registry getRegistry(int port)
    public static Registry getRegistry(String host)
    public static Registry getRegistry(String host, int port)
    public static Registry getRegistry(String host, int port, RMIClientSocketFactory csf)
    public static Registry createRegistry(int port) throws RemoteException {
        return new RegistryImpl(port); // 创建并导出Registry实例RegistryImpl
    }
    public static Registry createRegistry(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException{
        return new RegistryImpl(port, csf, ssf);
    }

无论是哪种getRegistry最终调用的都是如下代码

public static Registry getRegistry(String host, int port, RMIClientSocketFactory csf) throws RemoteException
    {
        Registry registry = null;

        if (port <= 0)
            port = Registry.REGISTRY_PORT; //Registry类中定义了此变量默认为1099

        if (host == null || host.length() == 0) { // 如果host为空,尝试转换成真实的本地主机名
            try {
                host = java.net.InetAddress.getLocalHost().getHostAddress();
            } catch (Exception e) { 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);
    }

如果我们看一下ysoserial payload模块中的JRMPClient,会发现其核心代码与getRegistry的后几行完全一致。这几行代码会向指定的RMI Registry发起请求,也就是实现了JRMP连接的过程,实现了JEP290绕过中的反向连接操作。

ysoserial.payload.JRMPClient

这里也对这两行代码中涉及的类简要说明一下(下面提到的六个类,除ObjID外,全是传输层的内容)
(1)ObjID:标识RMI运行时的远程对象(Registry、DGC、自定义远程对象等)。所有的服务都需要通过ObjID来调用,只是Registry和DGC作为特殊的服务其ObjID是已知的REGISTRY_ID = 0 | ACTIVATOR_ID = 1| DGC_ID = 2,其他的服务其ObjID需要先从Registry中lookup查询。
(2)TCPEndpoint
sum.rmi.transport包中有个tcp文件夹。TCP协议是面向连接的传输层(Transport)协议,也就是在使用TCP协议之前必须建立TCP连接。每一条TCP连接(Connection)只能有两个端点(Endpoint),即点对点,一对一的关系。当连接流量很大时,一个Connection就会产生性能瓶颈,需要建立多个Connection ,分摊信道Channel(缓存连接服务)。这些都是通讯过程中涉及到的一些接口,接口对应的实现类分别为TCPChannel、TCPConnection、TCPEndpoint、TCPTransport。简单画了个图示如下:

    Client   |    Server
— — — — — — — — — — — — — — — —
     Stub    |    Skeleton
— — — — — — — — — — — — — — — —
        Remote Reference
— — — — — — — — — — — — — — — —
        Transport(传输层)
— — — — — — — — — — — — — — — —
           Channel
         |<——————>|
          Connection
Endpoint1|<——————>|Endpoint2
         |<——————>|

(3)LiveRef:表示对象的连线,将一个对象的ObjID和Endpoint进行连线。可以理解为上层到传输层的映射。

另外,说一下RMI服务被传输层识别的代码

// Transport.serviceCall
Target target = ObjectTable.getTarget(new ObjectEndpoint(id, transport)); //ObjID、TCP Socket
Dispatcher disp = target.getDispatcher();
disp.dispatch(impl, call); // UnicastServerRef.dispatch()

(1)ObjectEndpoint:将ObjID和Transport做封装。作为ObjectTable的键,映射一个实例到Target
(2)ObjectTable:共享对象表,将ObjID映射到远程对象目标Target。维护了两个核心表Map<ObjectEndpoint, Target> objTable、Map<WeakRef, Target> implTable。类中方法都围绕于Target的存放与获取
(3)Target:封装了与远程对象有关的信息。包含ObjID、Dispatcher、Remote、Vector<VMID>、ClassLoader等属性

RegistryImpl

RegistryImpl位于sun.rmi.registry,该包中也有三个类:RegistryImpl、RegistryImpl_Skel、RegistryImpl_Stub

Stub与Skeleton

RegistryImpl,它实现了Registry接口,也就是对该接口中的五种方法进行了实现。以绑定操作为例看一下源码,尝试从this.bindings中获取var1名称对应的对象,如果获取到了就表明绑定已经存在,否则就用put方法,绑定名称和对象。那么unbind就是如果找到了绑定关系,就移除this.bindings.remove(var1);rebind则是不做判断,直接 this.bindings.put(var1, var2);bindings是一个Hashtable结构。那么list操作就是遍历Hashtable。

    private Hashtable<String, Remote> bindings;

    public void bind(String var1, Remote var2) throws RemoteException, AlreadyBoundException, AccessException {
        synchronized(this.bindings) {
            Remote var4 = (Remote)this.bindings.get(var1);
            if (var4 != null) {
                throw new AlreadyBoundException(var1);
            } else {
                this.bindings.put(var1, var2);
            }
        }
    }

RegistryImpl构造函数,将端口和ObjID进行映射,调用UnicastServerRef.exportObject导出对象。UnicastServerRef位于Server端。RMI的核心在于调用远程的方法,而这些方法都是通过UnicastServerRef对象来引用的,它映射到一个包含远程方法的类。RegistryImpl是要调用Registry相关的类,所以此处setup创建时var1.exportObject会映射到的类是RegistryImpl_stubRegistryImpl_Skel

    private static ObjID id = new ObjID(0);

    public RegistryImpl(final int var1) throws RemoteException {
        this.bindings = new Hashtable(101);
        if (var1 == 1099 && System.getSecurityManager() != null) {
            try {
                AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                    public Void run() throws RemoteException {
                        LiveRef var1x = new LiveRef(RegistryImpl.id, var1); // 1099端口和ObjID映射
                        RegistryImpl.this.setup(new UnicastServerRef(var1x, (var0) -> { // setup
                            return RegistryImpl.registryFilter(var0);
                        }));
                        return null;
                    }
                }, (AccessControlContext)null, new SocketPermission("localhost:" + var1, "listen,accept"));
            } ...
        } else {
            LiveRef var2 = new LiveRef(id, var1);
            this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));
        }
    }

    private void setup(UnicastServerRef var1) throws RemoteException {
        this.ref = var1;
        var1.exportObject(this, (Object)null, true); // UnicastServerRef.exportObject ->创建RegistryImpl_stub、RegistryImpl_Skel
    }

所以注册中心Registry注册对象:UnicastServerRef.exportObject -> LiveRef.exportObject -> TCPEndpoint.exportObject -> TCPTransport.exportObject,最终TCPTransport.listen开启监听并创建Socket。注册中心Registry处理对象请求,则与上述过程相反,一旦TCPTransport监听到请求,创建线程进行处理,经过对Connection中的信息进行提取和处理,最终交到UnicastServerRef.dispatch,进而调用RegistryImpl_Skel.dispatch

4.1 Server—Registry源码

Server服务端,Skeleton接口如下。该接口的实现类之一就是RegistryImpl_Skel

public interface Skeleton {
    // 解组参数,调用实际的远程对象实现,并封装返回值
    void dispatch(Remote obj, RemoteCall theCall, int opnum, long hash) throws Exception;
    Operation[] getOperations(); //返回Skeleton支持的操作
}

RegistryImpl_Skel

RegistryImpl_Skel,通过dispatch方法对var3的判断调用Registry相关操作,var3和五种方法的映射关系为:0->bind、1->list、2->lookup、3->rebind、4->unbind。另外DGCImpl_Skel也实现自Skeleton,所以同样以dispatch方法调用0 -> clean、1-> dirty,后面会提到DGC相关内容。

public final class RegistryImpl_Skel implements Skeleton {
    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)")};
    private static final long interfaceHash = 4905912898345647071L;

    public RegistryImpl_Skel() {}

    public Operation[] getOperations() { return (Operation[])operations.clone();}

    public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
        if (var3 < 0) {
            ...// 根据var4的hash值给var3赋值
        } else if (var4 != 4905912898345647071L) {
            throw new SkeletonMismatchException("interface hash mismatch");
        }

        RegistryImpl var6 = (RegistryImpl)var1;
        StreamRemoteCall var7 = (StreamRemoteCall)var2;
        String var8;
        ObjectInputStream var9;
        ObjectInputStream var10;
        Remote var81;
        switch(var3) {
        case 0:
            RegistryImpl.checkAccess("Registry.bind");

            try {
                var10 = (ObjectInputStream)var7.getInputStream();
                var8 = SharedSecrets.getJavaObjectInputStreamReadString().readString(var10);
                var81 = (Remote)var10.readObject();
            } catch (IOException | ClassNotFoundException | ClassCastException var78) {
                var7.discardPendingRefs();
                throw new UnmarshalException("error unmarshalling arguments", var78);
            } finally {
                var7.releaseInputStream();
            }

            var6.bind(var8, var81);

            try {
                var7.getResultStream(true);
                break;
            } catch (IOException var77) {
                throw new MarshalException("error marshalling return", var77);
            }
        case 1: ...
        case 2:
            try {
                var9 = (ObjectInputStream)var7.getInputStream();
                var8 = SharedSecrets.getJavaObjectInputStreamReadString().readString(var9);
            } catch (IOException | ClassCastException var74) {
                var7.discardPendingRefs();
                throw new UnmarshalException("error unmarshalling arguments", var74);
            } finally {
                var7.releaseInputStream();
            }

            var81 = var6.lookup(var8);

            try {
                ObjectOutput var83 = var7.getResultStream(true);
                var83.writeObject(var81);
                break;
            } catch (IOException var73) {
                throw new MarshalException("error marshalling return", var73);
            }
        case 3: ...
        case 4: ...
    }
}

无论是哪种操作,先根据传入的RemoteCall获取输入流,如果是绑定操作对输入流进行反序列化得到Object(var81)和String(var8)将二者进行绑定。如果是查询操作,如list或lookup,则是如下代码

ObjectOutput var83 = var7.getResultStream(true);
var83.writeObject(var81);

可以看到本文的JDK8_261版本在case0后面有一句权限校验RegistryImpl.checkAccess("Registry.bind");具体看一下代码,只允许localhost发送请求,也就是只能Server端发起请求。这个限制是从jdk8u141开始的,无论bind,rebind,unbind都会进行校验,只允许服务端发出,但是lookup和list方法前并没有RegistryImpl.checkAccess("Registry.bind");这样的校验。

public static void checkAccess(String var0) throws AccessException {
        try {
            final String var1 = getClientHost();

            final InetAddress var2;
            try {
                var2 = (InetAddress)AccessController.doPrivileged(new PrivilegedExceptionAction<InetAddress>() {
                    public InetAddress run() throws UnknownHostException {
                        return InetAddress.getByName(var1);  // 根据提供的主机名创建InetAddress
                    }
                });
            } catch (PrivilegedActionException var5) {
                 ...
    }

4.2 Client—Registry源码

在看RegistryImpl_Stub源码前,先看看它底层的抽象类RemoteStub,使用远程引用RemoteRef来执行对远程对象的远程方法调用。RemoteRef表示远程对象的handler(句柄)句柄代表对象的id,可以靠这个id访问对象。也可以理解为门把手,通过门把手我们可以控制门,但它不是门本身。所以RemoteRef包含了调用对象的方法——newCall+invokenewCall第三个参数为opnum操作数,即代表了操作类型,0代表bind,2代表lookup...

abstract public class RemoteStub extends RemoteObject { // `RemoteObject`,它实现了远程对象的行为
    protected RemoteStub() { super(); } //构造RemoteStub
    protected RemoteStub(RemoteRef ref) { super(ref); } // 根据RemoteRef来构造RemoteStub
}

RegistryImpl_Stub

有了这些前置知识再来看RegistryImpl_Stub,它也是实现了五种方法,五种方法的实现比较类似,选取bindlookup的代码进行解析

    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)")};
    private static final long interfaceHash = 4905912898345647071L;

    public RegistryImpl_Stub() {}

    public RegistryImpl_Stub(RemoteRef var1) { super(var1); }

    public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException {
        try {
            StreamRemoteCall var3 = (StreamRemoteCall)this.ref.newCall(this, operations, 0, 4905912898345647071L);  // RemoteRef.newCall

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

            this.ref.invoke(var3);
            this.ref.done(var3);
        } ...
    }

    public String[] list() throws AccessException, RemoteException {...}

    public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
        try {
            StreamRemoteCall var2 = (StreamRemoteCall)this.ref.newCall(this, operations, 2, 4905912898345647071L);

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

            this.ref.invoke(var2);

            Remote var20;
            try {
                ObjectInput var4 = var2.getInputStream();
                var20 = (Remote)var4.readObject();
            } catch (IOException | ClassNotFoundException | ClassCastException var13) {
                var2.discardPendingRefs();
                throw new UnmarshalException("error unmarshalling return", var13);
            } finally {
                this.ref.done(var2); // 代表了一个远程调用的完结
            }

            return var20;
        } ...
    }
    public void rebind(String var1, Remote var2) throws AccessException, RemoteException {...}
    public void unbind(String var1) throws AccessException, NotBoundException, RemoteException {...}

上面说到我们实际操作还不是远程对象,而是this.ref—远程对象的引用RemoteRefnewCall则创建了对远程对象引用进行具体操作的对象——RemoteCall,也叫做"call Object"。RemoteCall已经被弃用了,但是StreamRemoteCall的源码还是要看看。其构造函数包含了Transport的头部、调用对象、操作方法和stub/skeleton的hash。

    private ConnectionOutputStream out = null;
    private Connection conn;

    public StreamRemoteCall(Connection c, ObjID id, int op, long hash) throws RemoteException{
        try {
            conn = c;

            // write out remote call header info...
            // call header, part 1 (read by Transport)
            conn.getOutputStream().write(TransportConstants.Call);
            getOutputStream();           // creates a MarshalOutputStream
            id.write(out);               // object id (target of call)
            // call header, part 2 (read by Dispatcher)
            // Dispatcher的实现类为UnicastServerRef/UnicastServerRef2
            out.writeInt(op);            // method number (operation index)
            out.writeLong(hash);         // stub/skeleton hash
        } catch (IOException e) {
            throw new MarshalException("Error marshaling call header", e);
        }
    }

5. Client服务调用—UnicastRef

在RegistryImpl中提到服务器端导出对象方法为UnicastServerRef.exportObject。在看UnicastServerRef之前先来看看它的父类UnicastRefUnicastRef实现自RemoteRef接口。其内部操作的对象都是LiveRef。Unicast翻译过来就是“单一传播”,即客户端和服务器端之间点对点的通信连接。那么UnicastRef就可以理解为单一传播LiveRef对象。

public class UnicastRef implements RemoteRef {
    protected LiveRef ref; 

    public Object invoke(Remote var1, Method var2, Object[] var3, long var4) throws Exception {
        ...
        Connection var6 = this.ref.getChannel().newConnection();
        StreamRemoteCall var7 = null;
        boolean var8 = true;
        boolean var9 = false;

        Object var11;
        try {
            ...
            var7 = new StreamRemoteCall(var6, this.ref.getObjID(), -1, var4);

            try { // 序列化
                ObjectOutput var10 = var7.getOutputStream();
                this.marshalCustomCallData(var10);
                var11 = var2.getParameterTypes();

                for(int var12 = 0; var12 < ((Object[])var11).length; ++var12) {
                    marshalValue((Class)((Object[])var11)[var12], var3[var12], var10);
                }
            } catch (IOException var39) {...}

            var7.executeCall(); // 从流中反序列化transport和Object

            try { //反序列化
                Class var46 = var2.getReturnType();
                if (var46 != Void.TYPE) {
                    var11 = var7.getInputStream();
                    Object var47 = unmarshalValue(var46, (ObjectInput)var11); // 反序列化var11得到对象
                    var9 = true;
                    clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");
                    this.ref.getChannel().free(var6, true);
                    Object var13 = var47;
                    return var13; // 返回unmarshalValue得到的对象
                }

                var11 = null;
            } catch (ClassNotFoundException | IOException var40) {...
            } finally {
                try {
                    var7.done();
                } catch (IOException var38) {
                    var8 = false;
                }

            }
        } ...
        return var11;
    }

invoke方法调用流程如下。invoke生成代理对象时要传入接口名、方法名称、参数类型和参数。


UnicastRef
    protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
        if (var0.isPrimitive()) {
            if (var0 == Integer.TYPE) {
                return var1.readInt();
            } ...//Boolean.TYPE |Byte.TYPE | Character.TYPE | Short.TYPE|Long.TYPE|Float.TYPE|Double.TYPE
        } else {
           // //返回var1中反序列化得到的Object
            return var0 == String.class && var1 instanceof ObjectInputStream ? SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)var1) : var1.readObject(); 
        }
    }

    protected static void marshalValue(Class<?> var0, Object var1, ObjectOutput var2) throws IOException {
        if (var0.isPrimitive()) {
            if (var0 == Integer.TYPE) {
                var2.writeInt((Integer)var1);
            } ...// Boolean.TYPE | Byte.TYPE|Character.TYPE|Short.TYPE|Long.TYPE|Float.TYPE|Double.TYPE
        } else {
            var2.writeObject(var1); //序列化写入数据
        }
    }

UnicastRefnewCall方法,为此对象上的新远程方法调用创建适当的调用对象。

    public RemoteCall newCall(RemoteObject var1, Operation[] var2, int var3, long var4) throws RemoteException {
        clientRefLog.log(Log.BRIEF, "get connection");
        Connection var6 = this.ref.getChannel().newConnection();

        try {
            StreamRemoteCall var7 = new StreamRemoteCall(var6, this.ref.getObjID(), var3, var4);
            try {
                this.marshalCustomCallData(var7.getOutputStream());
            } ...
            return var7;
        } catch (RemoteException var10) {
            this.ref.getChannel().free(var6, false);
            throw var10;
        }
    }

6. Server服务调用—UnicastServerRef

继承自UnicastRef,为UnicastRef的远程对象实现了服务器端的行为。核心方法:exportObject、dispatchexportObject导出此对象,创建对应调度的skeletonstubsdispatch分配服务器端的远程对象。

这部分的运行流程如下,服务端通过listen启动Socket,创建AcceptLoop线程接收客户端的连接,每一个连接创建一个ConnectionHandler进行处理(BIO模式)。从ObjectTable中获取相应的Target对象,根据opnum确定调用的方法,最后通过反射method.invoke(obj, params)后将结果返回给客户端。

UnicastServerRef

public class UnicastServerRef extends UnicastRef implements ServerRef, Dispatcher {
    ...
    private transient Skeleton skel;
    private final transient ObjectInputFilter filter;
    private transient Map<Long, Method> hashToMethod_Map;
    private static final WeakClassHashMap<Map<Long, Method>> hashToMethod_Maps;
    private static final Map<Class<?>, ?> withoutSkeletons;

    public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
        Class var4 = var1.getClass();

        Remote var5;
        try {
            // 为远程对象创建代理
           // this.getClientRef:return new UnicastRef(this.ref);
            var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse); 
        } catch (IllegalArgumentException var7) {
            throw new ExportException("remote object implements illegal remote interface", var7);
        }

        if (var5 instanceof RemoteStub) {
            this.setSkeleton(var1);
        }

        // Target(Remote var1, Dispatcher var2, Remote var3(stub), ObjID var4, boolean var5) 
        Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
        this.ref.exportObject(var6); //  this.ep.exportObject(var6);
        this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
        return var5;
    }

    public void dispatch(Remote var1, RemoteCall var2) throws IOException {
        try {
            int var3;
            ObjectInput var41;
            try {
                var41 = var2.getInputStream();
                var3 = var41.readInt();
            } catch (Exception var38) {
                throw new UnmarshalException("error unmarshalling call header", var38);
            }

            if (this.skel != null) {
                this.oldDispatch(var1, var2, var3);
                return;
            }

            if (var3 >= 0) {
                throw new UnmarshalException("skeleton class not found but required for client version");
            }

            long var4;
            try {
                var4 = var41.readLong();
            } catch (Exception var37) {
                throw new UnmarshalException("error unmarshalling call header", var37);
            }

            MarshalInputStream var7 = (MarshalInputStream)var41;
            var7.skipDefaultResolveClass();
            Method var42 = (Method)this.hashToMethod_Map.get(var4);
            if (var42 == null) {
                throw new UnmarshalException("unrecognized method hash: method not supported by remote object");  // 如果远程对象中不存在这样的方法,就会报错
            }

            this.logCall(var1, var42);
            Object[] var9 = null;

            try {
                this.unmarshalCustomCallData(var41);
                var9 = this.unmarshalParameters(var1, var42, var7);  // 反序列化得到参数
            } catch (AccessException var34) {
                ((StreamRemoteCall)var2).discardPendingRefs();
                throw var34;
            } catch (ClassNotFoundException | IOException var35) {
                ((StreamRemoteCall)var2).discardPendingRefs();
                throw new UnmarshalException("error unmarshalling arguments", var35);
            } finally {
                var2.releaseInputStream();
            }

            Object var10;
            try {
                var10 = var42.invoke(var1, var9); // 执行远程对象的方法
            } catch (InvocationTargetException var33) {
                throw var33.getTargetException();
            }

            try {
                ObjectOutput var11 = var2.getResultStream(true);
                Class var12 = var42.getReturnType();
                if (var12 != Void.TYPE) {
                    marshalValue(var12, var10, var11);
                }
            } catch (IOException var32) {
                throw new MarshalException("error marshalling return", var32);
            }
        } catch (Throwable var39) {
           ...
        } finally {
            var2.releaseInputStream();
            var2.releaseOutputStream();
        }
    }

    private void oldDispatch(Remote var1, RemoteCall var2, int var3) throws Exception {
        ObjectInput var6 = var2.getInputStream();

        try {
            Class var7 = Class.forName("sun.rmi.transport.DGCImpl_Skel");
            if (var7.isAssignableFrom(this.skel.getClass())) {
                ((MarshalInputStream)var6).useCodebaseOnly();
            }
        } ...

        long var4;
        try {
            var4 = var6.readLong();
        } catch (Exception var8) {
            throw new UnmarshalException("error unmarshalling call header", var8);
        }

        Operation[] var10 = this.skel.getOperations();
        this.logCall(var1, var3 >= 0 && var3 < var10.length ? var10[var3] : "op: " + var3);
        this.unmarshalCustomCallData(var6);
        this.skel.dispatch(var1, var2, var3, var4);  //DGCImpl_Skel.dispatch
    }

dispatch方法会从RemoteCall中获取输入流,读取RMI协议的头部、方法,然后调用远程对象的该方法。如果此时skel不为null,调用DGCImpl_Skel的dispatch方法。

调用远程方法的代码,构造参数,传递给方法调用,服务器返回内容

Method method = hashToMethod_Map.get(op); // op: method hash目标方法的散列
params = unmarshalParameters(obj, method, marshalStream);
result = method.invoke(obj, params);
marshalValue(rtype, result, out);

7. DGC

Transport中还有一些关于DGC的类:DGCImpl、DGCImpl_Skel、DGCImpl_Stub、DGCClient、DGCAckHandler等。DGC也称为分布式垃圾收集算法。对于本地引用的每个远程对象,垃圾收集器都维护了一个引用列表,这样才能让远程客户机保存对象的引用。java.rmi.dgc.DGC接口包含两个方法:dirty、clean。当客户端对远程对象进行unmarshaled时,会调用dirty。当客户端不再存在对远程对象的引用,调用cleandirty是有期限的(lease period),从dirty被调用开始算期限,如果到期前没有更新dirty,分布式垃圾收集器就假定远程对象不再被该客户端引用。

public interface DGC extends Remote {
    Lease dirty(ObjID[] ids, long sequenceNum, Lease lease) throws RemoteException;
    void clean(ObjID[] ids, long sequenceNum, VMID vmid, boolean strong) throws RemoteException;
}

ObjID标识了一个关联的对象。sequenceNum参数是一个序列号,用于垃圾收集器的延迟调用。Lease包含了客户端的唯一标识符VMIDVM identifier)和期限(lease period)。

public final class Lease implements java.io.Serializable {

    private VMID vmid; // a unique VM identifier 标识客户端
    private long value; // Duration of this lease

    public Lease(VMID id, long duration){
        vmid = id;
        value = duration;
    }

    public VMID getVMID(){ return vmid; }
    public long getValue(){ return value; }
}

DGCImpl

DGC的一个实现类为DCGImpl,里面包含了一个内部类LeaseInfo(其构造方法包含了标识符VMID和期限expiration),LeaseInfo中的两个方法:renew()更新期限,expired()判断是否在还有效期内。DGCImpl类中包含了一个静态代码块,获取ClassLoader,创建了Server相关的对象、注册表的代理等,并将生成的Target放入ObjectTable

final class DGCImpl implements DGC {
    ...
    private static final long leaseValue = (Long)AccessController.doPrivileged(new GetLongAction("java.rmi.dgc.leaseValue", 600000L));
    private static final long leaseCheckInterval;
    private static final ScheduledExecutorService scheduler;
    private static DGCImpl dgc;
    private Map<VMID, DGCImpl.LeaseInfo> leaseTable; // 维护客户端ID和对应期限的Map结构,并提供注册和销毁VMID的方法:registerTarget、unregisterTarget、checkLeases
    private Future<?> checker;
    ...

    static DGCImpl getDGCImpl() {
        return dgc;
    }  ...

    private DGCImpl() {
        this.leaseTable = new HashMap();
        this.checker = null;
    }

    static {
         ...
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ClassLoader var1 = Thread.currentThread().getContextClassLoader();

                try {
                    Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());

                    try {
                        DGCImpl.dgc = new DGCImpl();
                        final ObjID var2 = new ObjID(2);
                        LiveRef var3 = new LiveRef(var2, 0);
                        final UnicastServerRef var4 = new UnicastServerRef(var3, (var0) -> {
                            return DGCImpl.checkInput(var0);
                        });
                        final Remote var5 = Util.createProxy(DGCImpl.class, new UnicastRef(var3), true);
                        var4.setSkeleton(DGCImpl.dgc);
                        Permissions var6 = new Permissions();
                        var6.add(new SocketPermission("*", "accept,resolve"));
                        ProtectionDomain[] var7 = new ProtectionDomain[]{new ProtectionDomain((CodeSource)null, var6)};
                        AccessControlContext var8 = new AccessControlContext(var7);
                        Target var9 = (Target)AccessController.doPrivileged(new PrivilegedAction<Target>() {
                            public Target run() {
                                return new Target(DGCImpl.dgc, var4, var5, var2, true);
                            }
                        }, var8);
                        ObjectTable.putTarget(var9);
                    } catch (RemoteException var13) {
                        throw new Error("exception initializing server-side DGC", var13);
                    }
                } finally {
                    Thread.currentThread().setContextClassLoader(var1);
                }
                return null;
            }
        });
    }

DGCImpl的dirty和clean方法,dirty方法的实现是将对象映射到Target,并返回相应的Lease。clean则是解除这种映射

 public Lease dirty(ObjID[] var1, long var2, Lease var4) {
        VMID var5 = var4.getVMID();
        long var6 = leaseValue;
        ...
        var4 = new Lease(var5, var6);
        synchronized(this.leaseTable) { 
        // 如果var5对应的LeaseInfo不为空,就更新它对应的leaseValue。
        // 如果var5对应的LeaseInfo为空,this.leaseTable.put(var5, new DGCImpl.LeaseInfo(var5, var6));
            ...
        }

        ObjID[] var14 = var1;
        int var15 = var1.length;

        for(int var10 = 0; var10 < var15; ++var10) {
            ObjID var11 = var14[var10];
            ...
            ObjectTable.referenced(var11, var2, var5); //将对象ID映射到Target
        }

        return var4; // 返回Lease
    }

    // ObjectTable.referenced
    static void referenced(ObjID var0, long var1, VMID var3) {
        synchronized(tableLock) {
            ObjectEndpoint var5 = new ObjectEndpoint(var0, Transport.currentTransport());
            Target var6 = (Target)objTable.get(var5);
            if (var6 != null) {
                var6.referenced(var1, var3); //运行到Target.referenced,执行 DGCImpl.getDGCImpl().registerTarget(var3, this);
            }
        }
    }

 public void clean(ObjID[] var1, long var2, VMID var4, boolean var5) {
        ObjID[] var6 = var1;
        int var7 = var1.length;
        for(int var8 = 0; var8 < var7; ++var8) {
            ObjID var9 = var6[var8];
            ...
            ObjectTable.unreferenced(var9, var2, var4, var5);
        }
    }

DGCImpl_Skel

服务器端对DCG的处理类为DGCImpl_Skel、客户端的DCG实现类为DGCImpl_Stub。先来看DGCImpl_Skel,其operation属性中包含了clean和dirty方法。核心方法为dispatch,根据传入的var3参数,0调用clean1调用dirty。实际调用方法的对象还是DGCImpl

public final class DGCImpl_Skel implements Skeleton {
    private static final Operation[] operations = new Operation[]{new Operation("void clean(java.rmi.server.ObjID[], long, java.rmi.dgc.VMID, boolean)"), new Operation("java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[], long, java.rmi.dgc.Lease)")};
    private static final long interfaceHash = -669196253586618813L;

    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;
            StreamRemoteCall var7 = (StreamRemoteCall)var2;
            ObjID[] var8;
            long var9;
            switch(var3) {
            case 0:
                VMID var34;
                boolean var36;
                try {
                    ObjectInput var37 = var7.getInputStream();
                    var8 = (ObjID[])((ObjID[])var37.readObject());
                    var9 = var37.readLong();
                    var34 = (VMID)var37.readObject();
                    var36 = var37.readBoolean();
                } catch (IOException | ClassNotFoundException | ClassCastException var32) {
                    var7.discardPendingRefs();
                    throw new UnmarshalException("error unmarshalling arguments", var32);
                } finally {
                    var7.releaseInputStream();
                }

                var6.clean(var8, var9, var34, var36);

                try {
                    var7.getResultStream(true);
                    break;
                } catch (IOException var31) {
                    throw new MarshalException("error marshalling return", var31);
                }
            case 1:
                Lease var11;
                try {
                    ObjectInput var12 = var7.getInputStream();
                    var8 = (ObjID[])((ObjID[])var12.readObject());
                    var9 = var12.readLong();
                    var11 = (Lease)var12.readObject();
                } catch (IOException | ClassNotFoundException | ClassCastException var29) {
                    var7.discardPendingRefs();
                    throw new UnmarshalException("error unmarshalling arguments", var29);
                } finally {
                    var7.releaseInputStream();
                }

                Lease var35 = var6.dirty(var8, var9, var11);

                try {
                    ObjectOutput var13 = var7.getResultStream(true);
                    var13.writeObject(var35);
                    break;
                } catch (IOException var28) {
                    throw new MarshalException("error marshalling return", var28);
                }...
        }
    }
}

DGCImpl_Stub也是实现了dirty和clean,其实现方法与DGCImpl_Skel的dispatch方法类似。都是先将对象序列化或反序列化,不同在于DGCImpl_Stub对于对象的操作是通过StreamRemoteCall

StreamRemoteCall var6 = (StreamRemoteCall)this.ref.newCall(this, operations, 0, -669196253586618813L);

8. MarshalOutputStream

上面源码解析没有提到Server中的一个类MarshalOutputStream,它扩展了ObjectOutputStream,以添加远程对象的方法。如果需要序列化远程对象或包含对远程对象的引用的对象,则必须使用MarshalOutputStream而不是ObjectOutputStreamMarshalOutputStream的构造函数会根据传入的protocol version(如1或2等)来生成流。序列化流时要用ObjectOutputStream,其内部有一个方法annotateClass,如果需要在序列化后的数据里写入内容,就需要重写这个方法。MarshalOutputStream作为它的子类,重写了此方法,指定了序列化要加载类的存放位置。那么在对流进行反序列化时就可以读到这个信息。

    // 序列化要加载类的位置,也就是codebase的位置
    protected void annotateClass(Class<?> cl) throws IOException {
        writeLocation(java.rmi.server.RMIClassLoader.getClassAnnotation(cl));
    }

    protected void annotateProxyClass(Class<?> cl) throws IOException {
        annotateClass(cl);
    }

    // 将类的位置写入流。
    protected void writeLocation(String location) throws IOException {
        writeObject(location);
    }

RMI工具

RMI服务扫描工具:
https://github.com/NickstaDB/BaRMIe

参考资料
https://mogwailabs.de/en/blog/2019/03/attacking-java-rmi-services-after-jep-290/
https://github.com/frohoff/jdk8u-jdk/tree/master/src/share/classes/sun/rmi
https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容