
WebLogicでCVE-2020-2883を利用してShiro rememberMeのデシリアライゼーション脆弱性を攻撃し、ワンクリックで蚁剑フィルターメモリシェルを登録する
今回の共有で扱う内容は以下のとおりです。
Java のシリアライズとは、Javaオブジェクトをバイトシーケンスに変換するプロセスであり、メモリ、ファイル、データベースに保存しやすくするためのものです。ObjectOutputStreamクラスの writeObject() メソッドでシリアライズを実装し、Javaオブジェクトをバイトシーケンスに変換します。
Java の逆シリアライズとは、バイトシーケンスをJavaオブジェクトに復元するプロセスです。ObjectInputStream クラスの readObject() メソッドは逆シリアライズに使用されます。
簡単な例を挙げます。コードSerializeAndDeserializeを参照してください ps:ここではコード内の強制型変換に注目してください```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、配列、ObjectオブジェクトなどJava組み込みのデータ型はすべてシリアライズを実装でき、自分たちで書いたPerson、DogクラスもSerializableインターフェースを実装していればシリアライズとデシリアライズを実装できます。
## なぜデシリアライズのときに脆弱性が生じるのか?
コードを見てみましょう。今、悪意のあるエンティティクラス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});
}
}
そのreadObject内にはコマンド実行のコードRuntime.getRuntime().exec(new String[]{"cmd", "/c", name})が存在し、nameパラメータは実行するコマンドである。そこで、悪意のあるオブジェクトを構築し、そのname属性に実行したいコマンドを代入すると、逆シリアライズ時にreadObjectがトリガーされた際にRCEが発生する。以下の通りである。```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); } }

では、ここまでで逆シリアライゼーションがどのようにしてRCEにつながるのかを理解しました。しかし、実際の開発でこのまま書くことはあり得ないため、ここで利用チェーン(gadget chain)の探索が必要になります。逆シリアライゼーションの脆弱性には3つの要素が必要です。
1. 逆シリアライゼーションの入り口(source)
2. ターゲットメソッド(sink)
3. 利用チェーン(gadget chain)
上図の出力結果をよく見ると、`readObject`メソッドがトリガーされただけでなく、`toString()`、引数なしコンストラクタ、`set`、`get`メソッドもトリガーされています。つまり、実際に利用チェーンを探す際には、`readObject()`メソッドだけに注目するのではなく、これらのメソッドも考慮する必要があるということです。
そして次に、**リフレクション**について理解する必要があります。前述の**強制型変換**の問題ですが、実際の開発では`readObject`内で論理処理が行われ、渡されたオブジェクトの具体的なデータ型が不明な場合は、リフレクションを通じて型を判断して呼び出しを行います。そして、このリフレクションこそがRCEへの重要な手段となるのです。
## Javaリフレクション
リフレクションとは何でしょうか?「リフレクション(reflection)」には「反(re)」という字が含まれています。つまり、リフレクションを説明するには「正射(正規の呼び出し)」から始める必要があります。コードを見てみましょう。これは私のエンティティクラスです。```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;
}
}
正常な書き方```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 で保護されたメソッドはどうやって呼び出すのでしょうか? 反射の作用がここで現れます。次のコードを見てください。```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の2つのチェーンだ。この2つを分析する理由は、CC2ではバイトコードを定義する操作が使われており、CC5ではリフレクションとチェーン呼び出しへの理解を深めるためである。
まず、より理解しやすいCC5のチェーンを見てみよう。