Understanding the Native JDK 8u20 Deserialization Vulnerability Through a Case Study

Summary0x01 Preface The native JDK 8u20 deserialization vulnerability is a classic and one of the most complex vulnerabilities I have analyzed. It uses substantial low-level knowledge and assumes familiarity with the deserialization process and serialized-data structure. This article reflects my own understanding; please point out any inaccuracies. 0x02 JDK 8u20 Vulnerability Principles JDK…

DeserializationJDKJava SerializationJDK8u20

0x01 Preface

jdk8u20The native deserialization vulnerability is a classic and one of the most complex vulnerabilities I have analyzed.

This vulnerability relies on substantial low-level knowledge and assumes some understanding of the deserialization process and serialized-data structure.

This article reflects my own understanding of the vulnerability. Please point out any inaccurate descriptions or errors.

0x02 JDK 8u20 Vulnerability Principles

jdk8u20is actually a modification ofjdk7u21bypass. InAnalysis Notes on the JDK 7u21 Deserialization Vulnerability At the end of the articlejdk7u21fix:

First examine the last vulnerable version (611bcd930ed1):http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/611bcd930ed1/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java

Examine its child version (0ca6cbe3f350):http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/0ca6cbe3f350/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java

compare:

compare.png
JAVA
// before the change
        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; all bets are off
           return;
        }

// after the change
        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }

In the first fix, the official patch used the second approach discussed online: the earlier return was replaced with an exception.

Let us examine after the first fixAnnotationInvocationHandler.readObejct()method:

JAVA
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        // Check to make sure that types have not evolved incompatibly
        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }
        Map<String, Class<?>> memberTypes = annotationType.memberTypes();
        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }

inAnnotationInvocationHandlerclass, which overridesreadObejctmethod, then according to Oracle's rules for serializable object streams in Java—if a class definesreadObjectmethod, it replaces the default serialization mechanism's method for reading object state,Optional informationcan be read through these methods, whereasRequired datadepends ondefaultReadObjectmethod reads it;

Inside this class, we can seereadObjectmethod calls on its first linedefaultReadObject()method, which reads an object's from the byte streamfield value. It reads from the byte stream according to the object's class descriptor and declared orderfield nameand type information. The values are assigned by matching field names in the current class. If a field of the object is absent from the byte stream, the default value defined by the class is used.If this value appears in the byte stream but is not an object, discard it

When exploitingdefaultReadObject()After restoring some object values, it finally performsAnnotationType.getInstance(type)check. If the supplied type is notAnnotationTypetype, an exception is thrown.

In other words, in practice, withinjdk7u21vulnerability, the value we pass isAnnotationInvocationHandlerobject had already been restored from the serialized data before the exception was thrown. In other words, the malicious seed was planted in the runtime object but could not grow because of the exception. If we eliminate the exception, the original objective becomes reachable again.

This is whyjdk8u20vulnerability principle—escaping the thrown exception.

How exactly can it escape?jdk8u20author used an ingenious technique.

Before explaining this approach, let us briefly cover the fundamentals relevant to the vulnerability so the later analysis and details are easier to follow.

0x03 Fundamentals

###1、Purpose of a try/catch block

Programs inevitably encounter errors and overlooked exceptions. To handle them while allowing execution to continue, developers usually usetry ... catchsyntax. Place statements that may throw insidetry { ... }, then usecatchcatches the correspondingExceptionand its subclasses. After the JVM catches the exception, it matches from top to bottom againstcatchstatement. When a is matchedcatch, then executecatchcode block, allowing execution to continue.

JDK 7u21 uses exactly this:

JAVA
try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

When the detected result is notAbbitatuibType, it matchesIllegalArgumentExceptionexception and then executescatchcode block in.

But iftry ... catchnesting, how should it be evaluated?

Consider an example:

JAVA
package com.panda.sec;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

public class test {
    static double TEST_NUMBER = 0;
    public static void math(int a, int b){
        double c;
        if (a != b) {
            try {
                TEST_NUMBER = a*(a+b);
                c = a / b;
            } catch (Exception e) {
                System.out.println("inner block failed");
            }
        } else {
            c = a * b;
        }
    }
    public static void urlRequest(int a, int b, String url) throws IOException {

            try {
                math(a, b);
                URL realUrl = new URL(url);
                HttpURLConnection connection = (HttpURLConnection)realUrl.openConnection();
                connection.setRequestProperty("accept", "*/*");
                connection.connect();
                System.out.println("Status code: " + connection.getResponseCode());
            } catch (Exception e) {
                System.out.println("outer block failed");
                throw e;
            }

        System.out.println(TEST_NUMBER);
    }

    public static void main(String[] args) throws IOException {
        urlRequest(1,0,"https://www.cnpanda.net");
        System.out.println("all end");
    }

}

First examine the code. It begins by defining the global variableTEST_NUMBER=0, then definesmathandurlRequesttwo methods, and inurlRequestmethod callsmathmethod, finally inmainexecute inside the functionurlRequestmethod.

Before reading the analysis below, consider what the code prints for each of the following variable values.

  • Whena=1,b=0, and the URL ishttps://www.cnpanda.netwhen
  • Whena=1,b=0, and the URL ishttps://test.cnpanda.netwhen
  • Whena=1,b=2, and the URL ishttps://www.cnpanda.netwhen
  • Whena=1,b=2, and the URL ishttps://test.cnpanda.netwhen

to inspect the result:

Whena=1,b=0, and the URL ishttps://www.cnpanda.net:

1.jpg

In this case,b=0causesa/bhas a denominator of zero, causing the inner layer to fail and entercatchblock and printsinner block failedstring. But because the innercatchblock does not rethrow the error, so execution continues with the remaining code and sends tohttps://www.cnpanda.netaddress, sends an HTTP request, and prints status code 200. Because inmathin the method TEST_NUMBER = a*(a+b)=1*(1+0)=1, so it printsTEST_NUMBERto1.0, finally printall endterminate the code path.

Whena=1,b=0, and the URL ishttps://test.cnpanda.net:

2.jpg

In this case,b=0causesa/bhas a denominator of zero, causing the inner layer to fail and entercatchblock and printsinner block failedstring. But because the innercatchblock does not rethrow the error, so execution continues with the remaining code and sends tohttps://test.cnpanda.netaddress, but resolution fails, causing an error and enteringcatchblock. Incatchprint in the blockouter block failedstring, then throws an error and terminates the remaining logic.

Whena=1,b=2, and the URL ishttps://www.cnpanda.net:

3.jpg

In this case,b!=0, thereforea/boperates normally and does not entercatchblock, continues with the remaining logic, and sends tohttps://www.cnpanda.netaddress, sends an HTTP request, and prints status code 200. Because inmathin the method TEST_NUMBER = a*(a+b)=1*(1+2)=3, so it printsTEST_NUMBERto 3, finally printall endterminate the code path.

Whena=1,b=2, and the URL ishttps://test.cnpanda.net:

4.jpg

In this case,b!=0, thereforea/boperates normally and does not entercatchblock, continues with the remaining logic, and sends tohttps://test.cnpanda.netaddress, but resolution fails, causing an error and enteringcatchblock. Incatchprint in the blockouter block failedstring, then throws an error and terminates the remaining logic.

The preceding example supports the conclusion thatWithin a containingtry ... catchblock method that can throw calls another containingtry ... catchblock method that throws no exception. If it iscalleewithout an exception fails, the rest of the is still executedcallerlogic, but ifcalleralso fails, thendoesterminate the code's execution

This isthrowsCalldoes not throw, thenIf it isdoes not throwCallthrows?

the following code:

JAVA
package com.panda.sec;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

public class test {
    static double TEST_NUMBER = 0;
    public static void math(int a, int b,String url) throws IOException {
        double c;
        try {
            urlRequest(url);
            if (a != b) {
                    TEST_NUMBER = a*(a+b);
                    c = a / b;
            } else {
                c = a * b;
            }
        } catch (Exception e) {
            System.out.println("outer block failed");
        }
    }
    public static void urlRequest(String url) throws IOException {
        try {
             URL realUrl = new URL(url);
             HttpURLConnection connection = (HttpURLConnection)realUrl.openConnection();
             connection.setRequestProperty("accept", "*/*");
             connection.connect();
             System.out.println("Status code: " + connection.getResponseCode());
        } catch (Exception e) {
                System.out.println("inner block failed");
                throw e;
            }
        System.out.println(TEST_NUMBER);
    }
    public static void main(String[] args) throws IOException {
        math(1,0,"https://test.cnpanda.net");
         System.out.println("all end");
     }
}

uses essentially the same logic as the earlier example, with minor adjustments for convenience and some irrelevant code left in place. The difference is that here, inmathmethod callsurlRequestmethod.

What will the following case output?

Likewise,Before reading the analysis below, consider what the code prints for each of the following variable values.

  • Whena=1,b=0, and the URL ishttps://www.cnpanda.netwhen
  • Whena=1,b=0, and the URL ishttps://test.cnpanda.netwhen
  • Whena=1,b=2, and the URL ishttps://www.cnpanda.netwhen
  • Whena=1,b=2, and the URL ishttps://test.cnpanda.netwhen

Whena=1,b=0, and the URL ishttps://www.cnpanda.netwhen

5.jpg

In this case,urltohttps://www.cnpanda.net, so the inner layer sends an HTTP request to that address and prints status code 200. After it finishes, the outer layer continues its remaining logic,b=0causesa/bhas a denominator of zero, causing the outer layer to fail and entercatchblock and printsouter block failedstring and finally printall endterminate the code path.

Whena=1,b=0, and the URL ishttps://test.cnpanda.netwhen

6.jpg

In this case,urltohttps://test.cnpanda.net, so the inner layer sends an HTTP request to that address. Resolution fails, causing an error and enteringcatchblock. Incatchprint in the blockinner block failedstring. The inner failure causes the outer layer to fail too, immediately entering the outercatchblock and printsouter block failedstring and finally printall endterminate the code path.

Whena=1,b=2, and the URL ishttps://www.cnpanda.netwhen

7.jpg

In this case,urltohttps://www.cnpanda.net, so the inner layer sends an HTTP request to that address and prints status code 200. After it finishes, the outer layer continues its remaining logic,b!=0causesa/bhas a nonzero denominator, so the outer layer does not fail. It completes its logic and finally printsall endend the entire code path.

Whena=1,b=2, and the URL ishttps://test.cnpanda.netwhen

8.jpg

In this case,urltohttps://test.cnpanda.net, so the inner layer sends an HTTP request to that address. Because resolution fails, it enterscatchblock. Incatchprint in the blockinner block failedstring. The inner failure causes the outer layer to fail too, immediately entering the outercatchblock and printsouter block failedstring and finally printall endterminate the code path.

The preceding example supports the conclusion thatWithin a containingtry ... catchblock method that throws no exception calls another containingtry ... catchblock method that can throw. If the called method fails, it causescallerfails and does not finish executingcallerlogic, butdoes notterminate the code's execution

2. Structure of Serialized Data

For the serialized-data structure, see:

Summary of the Object Serialization Stream Protocol /talksafe/892.html

or read the official documentation directly:https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html

UseSerializationDumpertool can display the structure of serialized data, as shown below:

9.jpg

The skeleton of the serialization structure is composed ofTC_*and various field descriptors. EachTC_*and the meaning of descriptors are introduced in Summary of the Object Serialization Stream Protocol. Readers seeking more depth can consult it.

3. Two Serialization Mechanisms

#### Reference Mechanism

During serialization, the object's class, member fields, and other data are written using a fixed grammar and read by specific methods. Serialized data can contain nulls, new objects, classes, arrays, strings, back-references, and more. Each has a corresponding descriptor in the serialization structure, and every object written to the byte stream is assigned a referenceHandle, and this referenceHandlecan refer back to the object usingTC_REFERENCEstructure, which references the earlier handle value), referenceHandlestarts from0x7E0000begins sequential assignment and automatic incrementing. If the byte stream is reset, handle allocation restarts from0x7E0000begins.

member discards

During deserialization, if a field of the current object is absent from the byte stream, the default value defined by the class is used.If this value appears in the byte stream but is not an object, it is discarded. If it is an object, however, a handle is allocated to it.

4. Understand the JDK 7u21 vulnerability

This is essential to understand because JDK 8u20 bypasses the fix for JDK 7u21.

See my earlier article, Analysis Notes on the JDK 7u21 Deserialization Vulnerability:https://xz.aliyun.com/t/9704

0x04 Starting from a Simple Case

Becausejdk8u20is genuinely complex, so I wrote a small case to make the following discussion easier to understand.

Suppose two classes exist:AnnotationInvocationHandlerandBeanContextSupport. The specific content is:

AnnotationInvocationHandler.java

JAVA
package com.panda.sec;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class AnnotationInvocationHandler implements Serializable {
    private static final long serialVersionUID = 10L;
    private int zero;
    public AnnotationInvocationHandler(int zero) {
        this.zero = zero;
    }
    public void exec(String cmd) throws IOException {
        Process shell = Runtime.getRuntime().exec(cmd);
    }
    private void readObject(ObjectInputStream input) throws Exception {
        input.defaultReadObject();
        if(this.zero==0){
            try{
                double result = 1/this.zero;
            }catch (Exception e) {
                throw new Exception("Hack !!!");
            }
        }else{
            throw new Exception("your number is error!!!");
        }
    }
}

BeanContextSupport.java

JAVA
package com.panda.sec;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class BeanContextSupport implements Serializable {
    private static final long serialVersionUID = 20L;
    private void readObject(ObjectInputStream input) throws Exception {
        input.defaultReadObject();
        try {
            input.readObject();
        } catch (Exception e) {
            return;
        }
    }
}

Question: when passingAnnotationInvocationHandlerinside the methodzeroequals0, how can we call at the end of serializationAnnotationInvocationHandler.exec()method to achieveRCE

We first setzeroequal to zero, then try callingAnnotationInvocationHandler.exec()method:

JAVA
import java.io.*;
public class Main {
    public static void payload() throws IOException, ClassNotFoundException {
        AnnotationInvocationHandler annotationInvocationHandler = new AnnotationInvocationHandler(0);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("payload1"));
        out.writeObject(annotationInvocationHandler);
        out.close();
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("payload1"));
        AnnotationInvocationHandler str = (AnnotationInvocationHandler)in.readObject();
        str.exec("open /System/Applications/Calculator.app");
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        payload();
    }
}

As expected, becausezerovalue is 0, causingresulthas a denominator of zero, causing an exception and throwing Exception("Hack !!!")error.

10.jpg

Because the code generated the serialized filepayload1, so we can now useSerializationDumpertool to inspect its data structure:

JAVA
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 41 - 0x00 29
        Value - com.panda.sec.AnnotationInvocationHandler - 0x636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
      serialVersionUID - 0x00 00 00 00 00 00 00 0a
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 1 - 0x00 01
      Fields
        0:
          Int - I - 0x49
          fieldName
            Length - 4 - 0x00 04
            Value - zero - 0x7a65726f
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 01
    classdata
      com.panda.sec.AnnotationInvocationHandler
        values
          zero
            (int)0 - 0x00 00 00 00

Because this structure is short, we can examine it in detail.

STREAM_MAGIC - 0xac edis the magic number identifying the serialization format;

STREAM_VERSION - 0x00 05indicates the serialization version;

Contentsrepresents the content of the final generated sequence;

TC_OBJECT - 0x73marks the beginning of a new serialized object;

TC_CLASSDESC - 0x72marks the beginning of a new class descriptor;

classNamerepresents the fully qualified class name of the current object; the following content is alsoclassNamedescriptor information;

Length - 41 - 0x00 29indicates that the current object's class name has length41

Value - com.panda.sec.AnnotationInvocationHandler - 0x636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572indicates that the current object's class name iscom.panda.sec.AnnotationInvocationHandler; the following string is its hexadecimal representation;

serialVersionUID - 0x00 00 00 00 00 00 00 0adefinesserialVersionUIDvalue is20

newHandle 0x00 7e 00 00 indicates that the object is assigned the value007e0000ofhandle(because the referenceHandlestarts from0x7E0000begins assigning values in order and automatically increments them). Notably, thishandlewas not actually written to the file. If we change this007e0000into the serialized data causes an exception that terminates deserialization. It appears here becauseserializationDumperauthor provided it to make analyzing serialized-data structures easier;

classDescFlags - 0x02 - SC_SERIALIZABLEindicates that the class-descriptor marker isSC_SERIALIZABLE, indicating that serialization usesjava.io.Serializable(if usingjava.io.Externalizable, the marker here becomesclassDescFlags - 0x04 - SC_EXTERNALIZABLE);

fieldCount - 1 - 0x00 01indicates that there is one member field. Note that thisfieldCountLikewise,serializationDumperauthor added this descriptor to make serialized-data analysis easier; it does not exist in the official serialization specificationfieldCount;

Fieldsindicates that the following content describes all fields in the class,Fieldsmember stores metadata for every field of the class currently being analyzed. It is an array in which each element describes one field and no entries are duplicated;

0indicates that the description of the first field follows;

Int - I - 0x49indicates that the field has typeinttype;

fieldNamerepresents the current field's name information; the following content is also fieldNamedescriptor information;

Length - 4 - 0x00 04indicates that the current field name has length4

Value - zero - 0x7a65726findicates that the current field name iszero

classAnnotationsrepresents class-relatedAnnotationdescriptor information. This data value is normally written byObjectOutputStreamofannotateClass()method, but becauseannotateClass()method is empty by default, soclassAnnotationsis normally followed by settingTC_ENDBLOCKDATAmarker; for details aboutannotateClassFor details, see myA Study of the Serialization Processarticle)

TC_ENDBLOCKDATA - 0x78End-of-block-data marker, indicating that this object type's descriptor has ended;

superClassDescrepresents the parent-class descriptor, which is empty here;

TC_NULL - 0x70indicates that the current object is a null reference;

newHandle 0x00 7e 00 01indicates that the object is assigned the value007e0001ofhandle, the same as the precedingnewHandleLikewise, herehandlewas not actually written to the file;

classdataindicates that all class-data content follows;

com.panda.sec.AnnotationInvocationHandler values zero (int)0 - 0x00 00 00 00represents all content in the class data

That is the complete serialized-data structure. During deserialization, content is read and restored sequentially from top to bottom.

Consider a question: what happens during deserialization if we insert data into the serialized structure above that does not exist in the source code?

Before solving this problem, let us examine the reference mechanism mentioned earlier in more depth. Consider an example:

For example, serializing the following code once produces this structure:

JAVA
package com.panda.sec;
import java.io.*;
public class test implements Serializable {
    private static final long serialVersionUID = 100L;
    public static int num = 0;
    private void readObject(ObjectInputStream input) throws Exception {
        input.defaultReadObject();
        System.out.println("hello!");
    }
    public static void main(String[] args) throws IOException {
        test t = new test();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("testcase"));
        out.writeObject(t);
        out.close();
    }
}
11.jpg

If the code above serializes twice, what does the data structure become?

Take a look:

JAVA
package com.panda.sec;
import java.io.*;
public class test implements Serializable {
    private static final long serialVersionUID = 100L;
    public static int num = 0;
    private void readObject(ObjectInputStream input) throws Exception {
        input.defaultReadObject();
        System.out.println("hello!");
    }
    public static void main(String[] args) throws IOException {
        test t = new test();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("testcase"));
        out.writeObject(t);
        out.writeObject(t); // second serialization
        out.close();
    }
}
12.jpg

Comparison shows that the end of this serialized-data structure has an additional

TEXT
TC_REFERENCE - 0x71
    Handle - 8257537 - 0x00 7e 00 01

This corresponds to a passage about the reference mechanism under "Two Serialization Mechanisms" in the fundamentals above.

Every object written to the byte stream is assigned a referenceHandle, and this referenceHandlecan refer back to the object usingTC_REFERENCEstructure, which references the earlier handle value), referenceHandlestarts from0x7E0000begins sequential assignment and automatic incrementing. If the byte stream is reset, handle allocation restarts from0x7E0000begins.

How does deserialization processTC_REFERENCEblock?

In myA Study of the Deserialization Process article described the following process:

inreadObject0method contains this check:

13.jpg

Yes: during deserialization, execution entersreadObject0method checks whether the byte stream being read contains aTC_REFERENCEmarker; if present, callsreadHandlefunction, although I did not explain in detail in the articlereadHandlefunction. Let us examine it:

JAVA
    private Object readHandle(boolean unshared) throws IOException {
        if (bin.readByte() != TC_REFERENCE) {
            throw new InternalError();
        }
        passHandle = bin.readInt() - baseWireHandle;
        if (passHandle < 0 || passHandle >= handles.size()) {
            throw new StreamCorruptedException(
                String.format("invalid handle value: %08X", passHandle +
                baseWireHandle));
        }
        if (unshared) {
            // REMIND: what type of exception to throw here?
            throw new InvalidObjectException(
                "cannot read back reference as unshared");
        }

        Object obj = handles.lookupObject(passHandle);
        if (obj == unsharedMarker) {
            // REMIND: what type of exception to throw here?
            throw new InvalidObjectException(
                "cannot read back reference to unshared object");
        }
        return obj;
    }

This method reads from the byte streamTC_REFERENCEmarker section, which changes the reference read from the streamHandleassign topassHandlevariable and pass it intolookupObject(), inlookupObject()method, if the referencedhandleis non-null and has no associatedClassNotFoundExceptionstatus[handle] != STATUS_EXCEPTION), then returns the givenhandlereferenced object, finally returned byreadHandlemethod returns it to the object.

In other words, when deserialization reachesTC_REFERENCE, it attempts to restore the referencedhandleobject.

Having discussed references, let us now return to inserting data.How can we, inside classAnnotationInvocationHandlerserialized data that does not exist in the source code?

UseobjectAnnotation

Consider another example:

JAVA
package com.panda.sec;
import java.io.*;
public class test implements Serializable {
    private static final long serialVersionUID = 100L;
    public static int num = 0;
    private void readObject(ObjectInputStream input) throws Exception {
        input.defaultReadObject();
        System.out.println("hello!");
    }
    private void writeObject(ObjectOutputStream output) throws IOException {
        output.defaultWriteObject();
        output.writeObject("Panda");
        output.writeUTF("This is a test data!");
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        test t = new test();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("testcase_new"));
        out.writeObject(t);
        out.writeObject(t);
        out.close();
    }
}

In this example we overridewriteObjectmethod and use within itwriteObjectandwriteUTFmethod writesPandaobject andThis is a test data!string. This portion of serialized data is:

14.jpg

To see the change more directly, we can usecomparetool to compare:

15.jpg

We can see that the class-descriptor marker was changed from0x02 - SC_SERIALIZABLEbecomes0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE, and below the original serialized-data structure there is an additional section produced byobjectAnnotationmarked content segment. During deserialization this segment is restored

Why does this change occur?

**Point 1:** if a serializable class overrideswriteObjectmethod and writes additional data to the byte stream, then it setsSC_WRITE_METHODmarker. In this case the terminator normally used isTC_ENDBLOCKDATAto mark the end of this object's data;

**Point 2:** if a serializable class overrideswriteObjectmethod, at this serialized data'sclassdatasection also contains an additionalobjectAnnotationsection, and if the overriddenwriteObject()method does more than calldefaultWriteObject()method writes object-field data and also writes custom data to the byte stream, then inobjectAnnotationsection contains the structures and values corresponding to custom data written to the stream;

Does that make the idea clearer?

Normally we cannot modify the serializable class itself and therefore cannot override itswriteObjectmethod and therefore cannot add an extra to the serialized dataobjectAnnotationContent segment

Is there really no solution? Of course there is.

Serialized data is just binary data. Modifying its hex representation according to the serialization grammar is effectively the same as writing into the overriddenwriteObjectmethod to add data

Before writing data, we must answer one question: **which object writes into which?** Do we serialize firstAnnotationInvocationHandlerclass and insert into itBeanContextSupportobject, or serialize firstBeanContextSupportclass and insert into itAnnotationInvocationHandlerobject?

First considerjdk7u21Why was the vulnerability fixed? Because an exception thrown during deserialization terminated the process.

This immediately recalls the from the fundamentalsPurpose of a try/catch blockconclusion reached in:

Within a containingtry ... catchblock method that throws no exception calls another containingtry ... catchblock method that can throw. If the called method fails, it causescallerfails and does not finish executingcallerlogic, butdoes notterminate the code's execution

Our objective is simply to keep deserialization from terminating so that we can obtain the deserialized class object.

We therefore need to serialize firstBeanContextSupportclass that throws no exception, then insert into itAnnotationInvocationHandlerobject that throws an exception

Another point matters here: according tomember discardsmechanism tells us that if the newly added serialized value is an object, it receives a Handle, but because we manually insertedHandle, so the reference must be modifiedHandlevalue, which isTC_ENDBLOCKDATAinside the blockhandlereference value) isAnnotationInvocationHandlerobject'shandleaddress

The process is:

**Step 1:** first serializeBeanContextSupportclass, then useSerializationDumpertool produces the following data structure:

JAVA
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 32 - 0x00 20
        Value - com.panda.sec.BeanContextSupport - 0x636f6d2e70616e64612e7365632e4265616e436f6e74657874537570706f7274
      serialVersionUID - 0x00 00 00 00 00 00 00 14
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 0 - 0x00 00
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 01
    classdata
      com.panda.sec.BeanContextSupport
        values

**Step 2:** serializeAnnotationInvocationHandlerclass, then useSerializationDumpertool produces the following data structure:

JAVA

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 41 - 0x00 29
        Value - com.panda.sec.AnnotationInvocationHandler - 0x636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
      serialVersionUID - 0x00 00 00 00 00 00 00 0a
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 1 - 0x00 01
      Fields
        0:
          Int - I - 0x49
          fieldName
            Length - 4 - 0x00 04
            Value - zero - 0x7a65726f
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 01
    classdata
      com.panda.sec.AnnotationInvocationHandler
        values
          zero
            (int)0 - 0x00 00 00 00

**Step 3:** useobjectAnnotationInsertAnnotationInvocationHandlerobject:

JAVA
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 32 - 0x00 20
        Value - com.panda.sec.BeanContextSupport - 0x636f6d2e70616e64612e7365632e4265616e436f6e74657874537570706f7274
      serialVersionUID - 0x00 00 00 00 00 00 00 14
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 0 - 0x00 00
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 01
    classdata
      com.panda.sec.BeanContextSupport
        values
      objectAnnotation   	// 	start here
            TC_OBJECT - 0x73
            TC_CLASSDESC - 0x72
              className
                Length - 41 - 0x00 29
                Value - com.panda.sec.AnnotationInvocationHandler - 0x636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
              serialVersionUID - 0x00 00 00 00 00 00 00 0a
              newHandle 0x00 7e 00 00
              classDescFlags - 0x02 - SC_SERIALIZABLE
              fieldCount - 1 - 0x00 01
              Fields
                0:
                  Int - I - 0x49
                  fieldName
                    Length - 4 - 0x00 04
                    Value - zero - 0x7a65726f
              classAnnotations
                TC_ENDBLOCKDATA - 0x78
              superClassDesc
                TC_NULL - 0x70
            newHandle 0x00 7e 00 01
            classdata
              com.panda.sec.AnnotationInvocationHandler
                values
                  zero
                    (int)0 - 0x00 00 00 00
              TC_ENDBLOCKDATA - 0x78
  TC_REFERENCE - 0x71
    Handle - 8257539 - 0x00 7e 00 03

Step 4: modifyhandlevalue and its correspondingclassDescFlagsvalue:

JAVA
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 32 - 0x00 20
        Value - com.panda.sec.BeanContextSupport - 0x636f6d2e70616e64612e7365632e4265616e436f6e74657874537570706f7274
      serialVersionUID - 0x00 00 00 00 00 00 00 14
      newHandle 0x00 7e 00 00
      classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
      fieldCount - 0 - 0x00 00
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 01
    classdata
      com.panda.sec.BeanContextSupport
        values
      objectAnnotation			// start here
            TC_OBJECT - 0x73
            TC_CLASSDESC - 0x72
              className
                Length - 41 - 0x00 29
                Value - com.panda.sec.AnnotationInvocationHandler - 0x636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
              serialVersionUID - 0x00 00 00 00 00 00 00 0a
              newHandle 0x00 7e 00 02
              classDescFlags - 0x02 - SC_SERIALIZABLE
              fieldCount - 1 - 0x00 01
              Fields
                0:
                  Int - I - 0x49
                  fieldName
                    Length - 4 - 0x00 04
                    Value - zero - 0x7a65726f
              classAnnotations
                TC_ENDBLOCKDATA - 0x78
              superClassDesc
                TC_NULL - 0x70
            newHandle 0x00 7e 00 03
            classdata
              com.panda.sec.AnnotationInvocationHandler
                values
                  zero
                    (int)0 - 0x00 00 00 00
              TC_ENDBLOCKDATA - 0x78
  TC_REFERENCE - 0x71
    Handle - 8257539 - 0x00 7e 00 03

Note: the finalHandle - 8257539 - 0x00 7e 00 03in8257539isserializationDumpervalue generated in. I did not investigate exactly how it appears in serialization and deserialization because it does not affect the final serialized data. Its generation algorithm is:

JAVA
    public static void num(){
        byte b1 = 0 ;
        byte b2 = 126;
        byte b3 = 0;
        byte b4 = 3;
        int handle = (
                ((b1 << 24) & 0xff000000) +
                        ((b2 << 16) &   0xff0000) +
                        ((b3 <<  8) &     0xff00) +
                        ((b4      ) &       0xff)
        );
        System.out.println("Handle - " + handle + " - 0x" + byteToHex(b1) + " " + byteToHex(b2) + " " + byteToHex(b3) + " " + byteToHex(b4));

    }

Here b1 b2 b3 b4 combine into 00 7e 00 xx, representing the reference-handle value. Applying the calculation to these bytes yields the final value 8257539.

Converting that data structure into hexadecimal gives:

JAVA
ac ed 00 05 73 72 00 20 636f6d2e70616e64612e7365632e4265616e436f6e74657874537570706f7274
00 00 00 00 00 00 00 14 03 00 00 78 70 73 72 00 29 636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
00 00 00 00 00 00 00 0a 02 00 01 49 00 04 7a65726f 78 70 00 00 00 00 78 71 00 7e 00 03

Note that earlier we mentioned

newhandlewas not actually written to the file. If we change this007e0000into the serialized data causes an exception that terminates deserialization. It appears here becauseserializationDumperauthor provided it to make analyzing serialized-data structures easier;

Therefore, while constructing the hexadecimal data, we must discardnewhandlecorresponding hexadecimal data

Finally, grouping four characters at a time and eight groups per line gives:

JAVA
aced 0005 7372 0020 636f 6d2e 7061 6e64
612e 7365 632e 4265 616e 436f 6e74 6578
7453 7570 706f 7274 0000 0000 0000 0014
0300 0078 7073 7200 2963 6f6d 2e70 616e
6461 2e73 6563 2e41 6e6e 6f74 6174 696f
6e49 6e76 6f63 6174 696f 6e48 616e 646c
6572 0000 0000 0000 000a 0200 0149 0004
7a65 726f 7870 0000 0000 7871 007e 0003

Replace this content in0x04 Starting from a Simple Casegenerated at the beginningpayload1content in

16.jpg

Then run the following code again:

JAVA
package com.panda.sec;
import java.io.*;
public class Main {
    public static void payload() throws IOException, ClassNotFoundException {
//        AnnotationInvocationHandler annotationInvocationHandler = new AnnotationInvocationHandler(0);
//
//        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("payload1"));
//        out.writeObject(annotationInvocationHandler);
//        out.writeObject(annotationInvocationHandler);
//        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("payload1"));
        System.out.println(in.readObject().toString());
        AnnotationInvocationHandler str = (AnnotationInvocationHandler)in.readObject();
        System.out.println(str.toString());
        str.exec("open /System/Applications/Calculator.app");
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        payload();
    }
}

The result is:

17.jpg

RCE succeeds. The first object restored by deserialization iscom.panda.sec.BeanContextSupport, while the second restored object iscom.panda.sec.AnnotationInvocationHandler, exactly matching the order in which we manually inserted the data

0x05 JDK 8u20 Vulnerability Analysis

We in0x02 JDK 8u20 Vulnerability Principlessection mentionsescaping the thrown exceptionis the key to the vulnerability. With the earlier case understood, how do we escape the thrown exception? Exactly: injdkFind a similar to this in the sourcecaseinBeanContextSupportclass, causingBeanContextSupportbecomes the outer layer and callsjdkin the sourceAnnotationInvocationHandlerclass. With no exception thrown, deserialization continues, a new gadget chain forms, and the deserialization attack completes.

Then injdkDoes the source contain a similar to thiscaseinBeanContextSupportclass? The answer is clear. To help readers understand, I already used its name in the case: yes, it is java.beans.beancontext.BeanContextSupporclass. We use itsreadChildrenmethod. Examine it in detail:

JAVA
 public final void readChildren(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        int count = serializable;
        while (count-- > 0) {
            Object                      child = null;
            BeanContextSupport.BCSChild bscc  = null;
            try {
                child = ois.readObject();
                bscc  = (BeanContextSupport.BCSChild)ois.readObject();
            } catch (IOException ioe) {
                continue;
            } catch (ClassNotFoundException cnfe) {
                continue;
            }
            synchronized(child) {
                BeanContextChild bcc = null;
                try {
                    bcc = (BeanContextChild)child;
                } catch (ClassCastException cce) {
                    // do nothing;
                }
                if (bcc != null) {
                    try {
                        bcc.setBeanContext(getBeanContextPeer());

                       bcc.addPropertyChangeListener("beanContext", childPCL);
                       bcc.addVetoableChangeListener("beanContext", childVCL);

                    } catch (PropertyVetoException pve) {
                        continue;
                    }
                }
                childDeserializedHook(child, bscc);
            }
        }
    }

Line 7 of the method shows that the suppliedObjectInputStreamobject callsreadObjectmethod performs deserialization, and if an exception occurs during it, usescontinuehandling, perfectly matching our requirements.

We noted above thatObjectAnnotationthis concept. We can also see that if there is aObjectAnnotationstructure, normally written byTC_ENDBLOCKDATA - 0x78to mark the end, but this creates a problem. We know the JDK 7u21 fix worked becauseIllegalArgumentExceptionAfter the exception is caught, it throwsjava.io.InvalidObjectException. Although we can useBeanContextSupportto force serialization to continue, but the thrown exception causesBeanContextSupportofObjectAnnotationinTC_ENDBLOCKDATA - 0x78ending marker cannot be processed correctly. If we do not manually remove thisTC_ENDBLOCKDATA - 0x78then the following structure is grouped underObjectAnnotationstructure, causing it to be read incorrectly and producing deserialized data different from what we expect. Therefore, when generatingBeanContextSupportofObjectAnnotationcannot use the normal serialization structure; the ending marker structure must beTC_ENDBLOCKDATA - 0x78Delete

Precisely because we putTC_ENDBLOCKDATA - 0x78is removed, which means that when usingSerializationDumpertool to inspectjdk8u20serialized-data structure to become malformed, as shown below:

18.jpg

One more tip: when insertingBeanContextSupportobject is not inserted directly as in the case; instead, it is inserted using the idea of a fake attribute. Inmember discardswe mentioned in

During deserialization, if a field of the current object is absent from the byte stream, the default value defined by the class is used.If this value appears in the byte stream but is not an object, it is discarded. If it is an object, however, a handle is allocated to it.

We therefore insert an arbitrary value of typeBeanContextSupportfield can form a gadget chain without affecting the original serialization process

This may be difficult to understand, so here is more detail.

A normal gadget chain is tightly connected through calls between classes. JDK 8u20 is different becauseLinkedHashSetcannot inprovided that the exception-escape condition is satisfiedDirectly callBeanContextSupportmethod, butBeanContextSupportcan callAnnotationInvocationHandlermethod. This causes our gadget chain to stop atLinkedHashSetThe next step of the chain is broken. What can we do?

can only modify the serialized-data structure by placing withinLinkedHashSetforcibly insert a intoBeanContextSupportfield value. Java deserialization normally restores object fields before restoringobjectAnnotationvalues in the structure, in serialized-data order. It therefore first deserializesLinkedHashSet, then deserializeLinkedHashSetfield value. Because this value contains aBeanContextSupportfield of type, so deserialization restoresBeanContextSupportobject, namelyobjectAnnotationdata in

When deserializingBeanContextSupportprocess first deserializesBeanContextSupportfield value, one of whose values is Templates.class of AnnotationInvocationHandler class object's fields, after which deserialization restoresAnnotationInvocationHandlerobject, successfully linking to the next chain.

Finally, just likeJdk7u21same process, using a dynamic proxy to triggerProxy.equals(EvilTemplates.class), reaching the final goal of injecting a malicious class and achieving RCE.

At presentjdk8u20A deserialization payload can be written in the following ways:

Each method above has tradeoffs: some are easy to understand but cumbersome to construct, while others are harder to understand but easier to build. Readers who followed the full article may notice an even clearer approach: first generatejdk7u21serialized-data structure for the exploit payload, then manually insert objects into it using a method like the case above. This is a large amount of work, so I did not implement it manually. Readers with time and interest can try generating a payload this way—it will certainly deepen your understanding ofjdk8u21, this is the most straightforward approach)

Another special aspect of JDK 8u20 is that, in practice,BeanContextSupportis not truly a separate chain. The gadget chain is still the JDK 7u21 chain; onlyBeanContextSupportis the intermediary used to avoid throwing an exception.

0x06 Summary

This article analyzesjdk8u20native deserialization vulnerability. Unlike other analyses, this article does not follow the conventional path. It focuses on one minimal case to explainjdk8u20core issue, then explained from the overall perspectivejdk8u20what a deserialization vulnerability is and how the process works

Thinking from the reader's perspective and finding the clearest way to explain an analysis also helps me understand the vulnerability and remember what I have written. Every analysis article is, for me, a complete review.

0x07 References

https://github.com/pwntester/JRE8u20_RCE_Gadget

http://wouter.coekaerts.be/2015/annotationinvocationhandler

https://paper.seebug.org/456/

https://github.com/potats0/javaSerializationTools

https://mp.weixin.qq.com/s/SMq6aE5-qV9cINv1-74RgA

https://xz.aliyun.com/t/8277

https://mp.weixin.qq.com/s/3bJ668GVb39nT0NDVD-3IA