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…
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.
// before the changeAnnotationTypeannotationType=null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are offreturn;
}
// after the changeAnnotationTypeannotationType=null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch outthrownewjava.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
privatevoidreadObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatiblyAnnotationTypeannotationType=null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch outthrownewjava.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()) {
Stringname= memberValue.getKey();
Class<?> memberType= memberTypes.get(name);
if (memberType != null) { // i.e. member still existsObjectvalue= memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
newAnnotationTypeMismatchExceptionProxy(
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.
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 outthrownewjava.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;
publicclasstest {
staticdoubleTEST_NUMBER=0;
publicstaticvoidmath(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;
}
}
publicstaticvoidurlRequest(int a, int b, String url)throws IOException {
try {
math(a, b);
URLrealUrl=newURL(url);
HttpURLConnectionconnection= (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);
}
publicstaticvoidmain(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:
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:
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:
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:
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?
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
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
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
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
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
UseSerializationDumpertool can display the structure of serialized data, as shown below:
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.
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.
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.
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:
As expected, becausezerovalue is 0, causingresulthas a denominator of zero, causing an exception and throwing Exception("Hack !!!")error.
Because the code generated the serialized filepayload1, so we can now useSerializationDumpertool to inspect its data structure:
JAVA
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x0005
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 41 - 0x0029
Value - com.panda.sec.AnnotationInvocationHandler - 0x636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
serialVersionUID - 0x00000000000000 0a
newHandle 0x00 7e 0000
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 1 - 0x0001
Fields
0:
Int - I - 0x49
fieldName
Length - 4 - 0x0004
Value - zero - 0x7a65726f
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 0001
classdata
com.panda.sec.AnnotationInvocationHandler
valueszero(int)0 - 0x00000000
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;
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:
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?
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
privateObjectreadHandle(boolean unshared)throws IOException {
if (bin.readByte() != TC_REFERENCE) {
thrownewInternalError();
}
passHandle = bin.readInt() - baseWireHandle;
if (passHandle < 0 || passHandle >= handles.size()) {
thrownewStreamCorruptedException(
String.format("invalid handle value: %08X", passHandle +
baseWireHandle));
}
if (unshared) {
// REMIND: what type of exception to throw here?thrownewInvalidObjectException(
"cannot read back reference as unshared");
}
Objectobj= handles.lookupObject(passHandle);
if (obj == unsharedMarker) {
// REMIND: what type of exception to throw here?thrownewInvalidObjectException(
"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 associatedClassNotFoundException(status[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?
In this example we overridewriteObjectmethod and use within itwriteObjectandwriteUTFmethod writesPandaobject andThis is a test data!string. This portion of serialized data is:
To see the change more directly, we can usecomparetool to compare:
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:
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:
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 000573720020 636f6d2e70616e64612e7365632e4265616e436f6e74657874537570706f7274
0000000000000014030000787073720029 636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
00000000000000 0a 020001490004 7a65726f 787000000000787100 7e 0003
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:
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
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:
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:
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.
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.