Javaリフレクションメカニズムとセキュリティの問題
0x00序文
最近、Javaの逆シリアル化の脆弱性を要約する過程で、著者は「Javaリフレクションメカニズム」という言葉を回避できなかったことがわかりました。以前は、これが開発者がプログラムをデバッグするのに便利なJavaの「準動的」言語を実装するためのメカニズムであることしか知りませんでしたただし、「リフレクション」がデシリアライゼーションの脆弱性攻撃の方法の1つになるとは思っていなかったため、この記事では、リフレクションメカニズムの原則とその他のセキュリティ問題の概要を説明します。
反射メカニズムマインドマップ:
0x01 Javaリフレクションメカニズムの定義
Javaリフレクションメカニズムとは、実行状態では、任意のクラスについて、このクラスのすべてのプロパティとメソッドを知ることができ、オブジェクトについては、そのメソッドとプロパティのいずれかを呼び出すことができることを意味します。オブジェクトのメソッドを呼び出す機能をJava言語のリフレクション機構と呼びます。
リフレクションメカニズムの重要なポイントは「ランタイム」です。これにより、プログラムの実行中にコンパイル中に完全に不明な.classファイルをロード、探索、および使用できます。1つの文で要約すると、リフレクションは実行時に任意のクラスのプロパティとメソッドを実現できます。
反射メカニズムの0x02の長所と短所
リフレクションを初めて使用するときに疑問があるかもしれません。なぜリフレクションを使用してオブジェクトを直接作成するのですか?彼は香りがよいのではありませんか?今回は、Javaでの動的コンパイルと静的コンパイルの概念を取り上げます。簡単に話しましょう。
- 静的コンパイル:コンパイル時に型を決定し、オブジェクトをバインドします。
- 動的コンパイル:実行時に型を決定し、オブジェクトをバインドします。Javaの柔軟性を最大化し、ポリモーフィズムを反映し、クラス間の結合を減らします。
利点:
リフレクションメカニズムは、オブジェクトの動的な作成とコンパイルを実現でき、多くの柔軟性を示します。特に、J2EE開発者にとって、彼の柔軟性は非常に明白です。たとえば、大規模なソフトウェアの開発において、プログラムをコンパイルしてリリースする際に、将来機能を更新する必要が生じた場合、ユーザーに旧ソフトウェアをアンインストールしてから新バージョンを再インストールするように求めることはできません。静的な場合は、プログラム全体を一度再コンパイルして関数を更新する必要があります。リフレクションメカニズムを使用すると、動的なアンインストールなしで実行でき、実行時に動的に作成してコンパイルするだけで済みます。
短所:
パフォーマンスに影響を与えます。リフレクションメカニズムは実際には説明のための操作であり、JVMに何をしたいか、そしてそれらがどのように要件をグループ化するかを伝えます。このタイプの操作は、同じ操作を直接実行するよりも常に遅くなります
0x03反射メカニズムの原理
リフレクションメカニズムの基本は、java.lang.ClassクラスのインスタンスオブジェクトであるClassクラスを理解することであり、Classはすべてのクラスのクラスです。通常のオブジェクトの場合、インスタンスを作成するときは通常次のメソッドを使用します。
Demo test = new Demo();
では、上記のメソッドを使用して、クラスclassのインスタンスオブジェクトを作成することもできますか?
Class c = new Class();
答えは良くないので、Classのソースコードを見て、彼のコンストラクタがプライベートであることがわかりました。つまり、JVMだけがClassオブジェクトを作成できます。
newを使用して通常のオブジェクトを作成するようにオブジェクトをインスタンス化することはできませんが、既存のクラスをランダム化することによってClassオブジェクトを取得できます。次の3つの方法があります(Demoと呼ばれる共通クラスがあると仮定)
- 通常の他のクラスをインスタンス化し、このインスタンスでgetClass()を呼び出してClassオブジェクトを取得します
Demo test = new Demo();
Class c1 = test.getClass();
- すべてのデータ型(基本データ型を含む)には「静的」クラス属性があるため、直接.class属性を呼び出してClassオブジェクトを取得します
Class c2 = Demo.class;
- ClassクラスのforNmaeメソッドを呼び出してClassオブジェクトを取得する
Class c3 = Class.forName(“ReflectDemo.Demo”)//forName()
パラメータは、実際のパス、パッケージ名、クラス名です
3つの作成方法のうち、最初の方法でオブジェクトが作成され、リフレクションメカニズムの意味がなくなったため、3番目の方法が通常使用されます。2番目の方法ではクラスパッケージをインポートする必要があり、依存関係が強すぎて、インポートせずにパッケージがスローされますコンパイルエラー。3番目の方法は一般的に使用され、構成ファイルに文字列を渡すか書き込むことができます。
リフレクションメカニズムの原則は、Javaクラスのさまざまなコンポーネントを個々のJavaオブジェクトにマップすることです。これにより、実行時にクラスのすべてのメンバー(変数、メソッド)を呼び出すことができます。次の図は、リフレクションメカニズムでのクラスのロードプロセスです。
0x04 Javaリフレクションメカニズム操作
前の紹介でClassオブジェクトを取得する方法を学び、リフレクションメカニズムでClassオブジェクトのすべてのメンバー情報を取得してから、メンバーを取得するためのいくつかの関数を簡単に紹介しました。
- メンバーメソッドを取得する
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
//親クラスを除く、このクラスのすべてのメソッドを取得します。
public Method getMethod(String name, Class<?>... parameterTypes)
//親クラスを含む、このクラスのすべてのパブリックメソッドを取得します。
2つのパラメーターは、メソッド名と、メソッドパラメータークラス(クラスタイプ)のクラスパラメーターリストです。
クラスAに4つのメンバーメソッドがある場合、次のように:
getDeclaredMethod()およびgetMethod()関数を使用して、指定したクラスのすべての/ publicメンバーメソッドを取得します。
- コンストラクターを取得する
`public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)`
//親クラスのコンストラクターを除く、このクラスのすべてのコンストラクターを取得します。
public Constructor<T> getConstructor(Class<?>... parameterTypes)
//親クラスを含む、このクラスのすべてのパブリックコンストラクターを取得します。
クラスAに3つのコンストラクターがある場合、2つのパブリックコンストラクターと1つのプライベートコンストラクター
はgetDeclaredConstructor()関数を使用してすべてのコンストラクターを取得し、getConstructor()関数を使用してパブリックコンストラクターのみを取得します。
- メンバー変数フィールドを取得する
public Field getDeclaredField(String name)
//親クラスの変数を除く、クラス自体によって宣言されたすべての変数を取得します。
public Field getField(String name)
//親クラス変数を含む、このクラスのすべてのパブリックメンバー変数を取得します。
クラスのメンバー変数もオブジェクトです。これはjava.lang.reflect.Fieldのオブジェクトであるため、java.lang.reflect.Fieldにカプセル化されたメソッドを通じてこの情報を取得します。
クラスAに
はさまざまな属性を持ついくつかのメンバー変数があります。getDeclaredFields()関数を使用してすべてのメンバー変数を取得し、getFields()関数を使用してパブリックメンバー変数のみを取得します。
0x05リフレクションメカニズムおよび逆シリアル化の脆弱性
デシリアライズの脆弱性の主な機能:
writeObject
()シリアライズ、バイトストリームへのオブジェクトの出力readObject()デシリアライズ、オブジェクトへのバイトストリームの出力
リフレクションメカニズムを使用して、readObjectメソッドを書き換え、コマンドの実行を実行できる関数Runtime.getRuntime()を追加し、calc.exeコマンドを実行して計算機を呼び出します。
package reflectdemo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
public class demo implements Serializable{
private Integer age;
private String name;
public demo() {}
public demo(String name,Integer age){ //构造函数,初始化时执行
this.age = age;
this.name = name;
}
private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
in.defaultReadObject();//调用原始的readOject方法
try {//通过反射方法执行命令;
Method method= java.lang.Runtime.class.getMethod("exec", String.class);
Object result = method.invoke(Runtime.getRuntime(), "calc.exe");
}
catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args){
demo x= new demo();
operation.ser(x);
operation.deser();
}
}
class operation {
public static void ser(Object obj) {
try{//序列化操作,写数据
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
//ObjectOutputStream能把Object输出成Byte流
oos.writeObject(obj);//序列化关键函数
oos.flush(); //缓冲流
oos.close(); //关闭流
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void deser() {
try {//反序列化操作,读取数据
File file = new File("object.obj");
ObjectInputStream ois= new ObjectInputStream(new FileInputStream(file));
Object x = ois.readObject();//反序列化的关键函数
System.out.print(x);
ois.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
上記の逆シリアル化の脆弱性から、Javaリフレクションが実際にプライベートメソッドとプロパティにアクセスできることがわかります。これは、第2レベルのセキュリティメカニズム(1)をバイパスする方法です。これは実際には、Java自体が特定の目的のために残した、またはデバッグを容易にするための「バックドア」に似ています。いずれの場合でも、その原則はアクセスセキュリティチェックをオフにすることです。
一般に、脆弱性を見つけてプログラムにコマンド実行を実装させたい場合、私たちが懸命に取り組むことができる2つの方向があります
- 制御コードと関数:データは名前付き注入などの注入穴のようなコードとして扱われるか、またはreadObjectが上記のデモコードのように書き換えられ、カスタムコードが追加されます
- 入力、データ、変数の制御:コード内の既存の関数とロジックを使用して、入力コンテンツの形式を変更することでプロセスを制御します(入力が異なると、ロジックフローが異なり、コードブロック内のコードが実行されます)。
Javaの逆シリアル化の脆弱性の場合、これは制御データエントリのカテゴリに属します。脆弱性をトリガーするためにリフレクションメカニズムを呼び出すとき、彼は満たさなければならない2つの基本的なポイントがあります。
- シリアル化可能なクラスがあり、クラスがreadObject()メソッドを書き直した(コードインジェクションがないため、既存のコードロジックにそのようなクラスがあるかどうかのみを見つけることができます)
- method.invoke関数は、書き換えられたreadObject()メソッドのロジックに表示され、パラメーターは制御可能です。
0x06反射安全
上記を読んだ後、Javaリフレクションメカニズムが強力すぎることを心から嘆きます。しかし、ある程度の安心感があれば、Javaのメカニズムが強すぎることに気づくでしょう。
反射を処理する場合、セキュリティはより複雑な問題です。リフレクションはフレームワークタイプのコードでよく使用されます。このため、従来のアクセス制限に関係なく、フレームワークにコードへの完全なアクセス権を与えることが必要になる場合があります。ただし、他の場合では、制御されていないアクセスが重大なセキュリティリスクをもたらす可能性があります。
これらの矛盾する要件のため、Javaプログラミング言語はリフレクションのセキュリティに対処するためのマルチレベルのアプローチを定義しています。基本モードは、ソースコードアクセスに適用されるのと同じリフレクションに対する制限を実装することです。
- 任意の場所から共通コンポーネントへのアクセス
- クラス自体の外部のプライベートコンポーネントアクセスはありません
- 保護およびパッケージ化された(デフォルトのアクセス)コンポーネントへの制限付きアクセス
C ++と比較すると、Javaは比較的安全な言語です。これはオペレーティングメカニズムと密接に関連しており、C ++はローカルで実行されるため、ほとんどすべてのプログラムの権限は理論的には同じです。Javaは仮想マシンで実行されており、外部に直接接続しないため、Javaの実行環境は実際には「サンドボックス」環境です。また、Javaのセキュリティモデルとして、バイトコードベリファイア、クラスローダー、セキュリティマネージャー、アクセスコントローラーなどの一連のセキュリティコンポーネントが含まれているため、Javaのセキュリティメカニズムはより複雑に見えます。