catch-img

実践!安全ではないデシリアライゼーションの攻撃手法

安全でないデシリアライゼーション(Insecure Deserialization)とは、多くのプログミング言語に存在するバイト列等の表現で直列化されたデータを元のオブジェクトに変換する処理で発生する脆弱性です。

攻撃者がこの脆弱性を悪用することにより、DoS や状態の改ざん、最悪の場合は任意のコード実行を引き起こす可能性があります。

2017 年に OWASP Top 10 のひとつに数えられたこと、また、この脆弱性(CWE)が継続して報告されていることから、目にしたことがある方も多いのではないでしょうか。

この記事では、実際のコードを交えて Java における安全ではないデシリアライゼーションの解説を行い、この脆弱性を見つけた場合にどのように扱えばよいのか、どのような対策を行えばよいのかを解説します。

本記事を通してこの脆弱性の理解を進めるきっかけになっていただけたら幸いです。


シリアライゼーションとは

脆弱性について解説する前に、この脆弱性を引き起こす処理である「シリアライゼーション」と「デシリアライゼーション」について簡単に解説します。


Java のシリアライゼーション / デシリアライゼーション機構


シリアライゼーション

シリアライゼーションは、アプリケーション内のメモリ上のオブジェクトをシステムないしはプロセス間の転送やストレージへの保存に適したバイト列や XML、JSON 等の形式に変換する処理を示します。

特に本記事で例として扱う Java 標準のシリアライゼーション機構では、オブジェクト(のフィールド)をバイトストリームを通じてバイト列に変換します(上記の図のうち、左側の部分です)。

この処理によって変換されたオブジェクトデータを、後述するデシリアライゼーションによってオブジェクトとして再構築することで、アプリケーションがメモリ上で保持するオブジェクトを永続化、ないしは他のアプリケーションと共有することができるようになります。

以下は Java の ObjectOutputStream クラスを使って、オブジェクトをシリアライズしてファイルに出力する例です。

サンプルとして Reptiles(爬虫類の意)というクラスを定義し、 YamoryMaker クラスの中でそのインスタンスを生成してシリアライズしたものを、 "yamory.ser" という名前でファイル出力しています。

シリアライゼーションは「 ObjectOutputStream.writeObject 」メソッドで行っています。

public class Reptiles  implements java.io.Serializable {
    String family;
    String name;
    String nickname;

    public Reptiles(String family, String name, String nickname) {
        this.family    = family;
        this.name     = name;
        this.nickname = nickname;
    }
}
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class YamoryMaker {
    public static void main(String [] args){
        Reptiles yamory = new Reptiles("Gekkonidae", "Gekko japonicus", "yamory");

        try {
            String fileName = "yamory.ser";
            FileOutputStream fos = new FileOutputStream(fileName);
            ObjectOutputStream oos = new ObjectOutputStream(fos);

            //Serialize
            oos.writeObject(yamory);

            oos.close();
            fos.close();
            
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

上記のプログラムを実行した結果、次のファイルが作成されます。
可読出来る文字列から上記の yamory オブジェクトがファイルとして保存できたことがおわかりいただけたかと思います。



デシリアライゼーション

デシリアライゼーションは、シリアライゼーションによってオブジェクトをバイト列などの表現に変換したデータを、アプリケーションで取り扱うことができるようにオブジェクトに再構築する処理を示します。

実際に上記で生成した yamory.ser を読み取り、オブジェクトに戻す例を以下に示します。
ObjectInputStream#readObject メソッドでデシリアライズを行っています。

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class YamoryReciver{
    public static void main(String [] args){
            try {
                String fileName = "yamory.ser";

                FileInputStream fin = new FileInputStream(fileName);
                ObjectInputStream oin  = new ObjectInputStream(fin);
								
							 // deserialize
                Reptiles ps = (Reptiles) oin.readObject(); 

                oin.close();
                fin.close();

                System.out.println("Nickname: " + ps.nickname);

            } catch (Exception e){
                e.printStackTrace();
            }
        }
}

実際に yamory.ser をオブジェクトにして、アプリケーションで扱うことができるようになりました。

実行結果

$ java -cp . YamoryReceiver
Nickname: yamory


脆弱性をついた攻撃手法と影響

ここからは本題の脆弱性についてです。

この脆弱性は、デシリアライゼーションする対象を外部から受け取ったデータであること、またそれに対する検査の不備を示すもので、この脆弱性が特定の影響を示すわけではありません。
その影響は行われる攻撃の手法に左右されます。
以下に代表的な攻撃手法を紹介します。


権限昇格(状態変更)

ひとつ目は、OWASP Top 10 2017 A8 における「攻撃シナリオの例」の例 2 で記載されているような、権限昇格などの状態変更を引き起こすケースです。

権限情報を含むユーザ情報のオブジェクトをシリアライズし、そのエンコードしたシリアライズ後のデータを Cookie に付与してセッション管理を行っているアプリケーションが該当します(Web アプリではほぼないと思います)。

このようなケースでは、攻撃者が Cookie の権限情報を編集しその値を送るするなどして、意図せぬ状態変更を引き起こすことができます。

実際のコードを踏まえて解説します。

以下のコードは先ほどの Reptiles クラスに type フィールドを追加し、 YamoryReciver に type が "RedList" であれば文字列を出力する処理を加えたものです。

public class YamoryReciver{
    public static void main(String [] args){
            try {
               //省略
                if(ps.type.equals("RedList")){
                    System.out.println(ps.nickname+" is a rare specie. ");
                }
               //省略
       }
}

public class Reptiles  implements java.io.Serializable {
    String family;
    String name;
    String nickname;
    String type;

    //省略

先ほどの YamoryMaker で次のオブジェクトを作成しシリアライズしたとします。

Reptiles yamory = new Reptiles("Gekkonidae", "Gekko japonicus", "yamory" , "RedList");



仮に上記のシリアライズ後のデータを編集して以下のように RedList を書き換え、アプリケーションの挙動を変えることも可能です。



上記の YamoryReciver では、単純に文字列の出力だけにしか影響を及ぼしませんでしたが、これが権限管理などに使われている場合、権限昇格に繋げることができます。


リモートコード実行(RCE)

この脆弱性がもたらす影響の中でもっとも危険なのがリモートコード実行です。
この脆弱性がなぜリモートコード実行に繋がるのかを 3 ステップに分けて順を追って解説します。

1 : Magic Method

こういうケースはまずありえませんが、デシリアライゼーションの対象クラスで次のようなメソッドを実装している場合です。
以下の例では、デシリアライズ時に Reptiles オブジェクトの名前( name )をログ出力するために、 Runtime#exec メソッドを経由して bash コマンドでログを出力しています。

package UsoSerial;

import java.io.IOException;

public class Reptiles  implements java.io.Serializable {
    private static final long serialVersionUID = 4516675496922141272L;
    String family;
    String name;
    String nickname;
    String type;

    public Reptiles(String family, String name, String nickname, String type) {
        this.family    = family;
        this.name     = name;
        this.nickname = nickname;
        this.type = type;
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        in.defaultReadObject();
        Runtime.getRuntime().exec(new String[] {"bash", "-c", "echo " + name +" > ./sample.log"});
    }
}

上記のようなクラスが存在していることを把握した攻撃者が、 name に攻撃者のコマンドを設定した Reptiles オブジェクトをシリアライズし、それを標的に送ることで標的となったサーバは任意のコードが実行されます。

name に yamory.io に HTTP リクエストを行い page.html に出力するコマンドを指定したシリアライズ済みデータを、YamoryReciver がデシリアライズした実行結果を以下に示します。

Reptiles yamory = new Reptiles("Gekkonidae", "Gekko japonicus ; curl https://yamory.io > page.html ;echo something ", "yamory" , "RedList" );
実行結果

なぜデシリアライズ時に Runtime#exec メソッドが実行されたのか

YamoryReveiver を見ると分かるとおり、 YamoryReveiver 側はデシリアライズしているのみであり、それ以上特別な操作は行っていません。
それにも関わらず、上記の Runtime#exec メソッドが実行されたのは、ObjectInputStream が、デシリアライズの過程でデシリアライズ対象のクラスでオーバーライドされた readObject メソッドを実行するからです。

このような、デシリアライズの処理であたかも自動的に実行されるメソッドのことをマジックメソッドとよび、攻撃のエントリーポイントとして用いられています。

なお、エントリーポイントとなるものをキックオフガジェット(kick-off gadget)と呼びます。

マジックメソッドとして挙げらたものには、上記の readObject メソッド以外に以下のものがあります。

マジックメソッドの一例

・readResolve
・readExternal(Externalizable)
・Object#finalize
・HashMap
 ・Object#equals
 ・Object#hashCode

2 : Sink

上記の例では、シリアライズ対象クラス Reptiles に任意のコード実行につながるコードをマジックメソッド内に書いていました。コード実行のパラメータ( name フィールドの値)は、外部から指定可能であるため、クラスの仕様を把握している人であれば誰でも攻撃できるという稀有な状況でした。このような状況で脆弱になっているアプリケーションはまずありえないと思います。

次に、先ほどの例で実行コマンドのパラメータを外部から指定できないような状況で、リモートコード実行する例を示します。

まず先ほどの Reptelies クラスからマジックメソッドをなくしました。
次に、コマンド実行に繋がる Runtime#exec メソッドをマジックメソッド内で呼び出す Snake クラスを新たに作成しました。

YamoryReveiver はこの Snake メソッドに関して何ら受付や処理を行っておらず、 Reptiles クラスのオブジェクトとしてデシリアライズするコードのままにしています。

package yamo;

import UsoSerial.Reptiles;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class YamoryReceiver{
    public static void main(String [] args){
        try {
            String fileName = "snake.ser";

            FileInputStream fin = new FileInputStream(fileName);
            ObjectInputStream oin  = new ObjectInputStream(fin);
            Reptiles ps = (Reptiles) oin.readObject(); // Reptiles クラスとしてデシリアライズする
            oin.close();
            fin.close();

        } catch (Exception e){
            e.printStackTrace();
        }
    }
}
package UsoSerial;

import java.io.IOException;
import java.io.Serializable;

public class Snake implements Serializable {
    String name ;

    public Snake(String name){ this.name = name ;}

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        in.defaultReadObject();
        System.out.println("hi");
        Runtime.getRuntime().exec(new String[] {"bash", "-c", "echo " + name+" >./sample.log"});
    }
}
package UsoSerial;

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class Serial {
    public static void main(String [] args){
        Snake snake = new Snake("Gekko japonicus ; echo hi > snake_greeting.txt ;echo something ");

        try {
            String fileName = "snake.ser";
            //省略
            //Serialize
            oos.writeObject(snake);
            //省略
        
    }
}
実行結果


上記データを YamoryReceiver がデシリアライズしようとした結果、 YamoryReceiver 上で任意のコードを実行できることがわかりました。

Serial#main メソッドを実行して出力したデータを、YamoryReceiver がデシリアライズしようとした結果、YamoryReceiver 上は Snake クラスについて一切関知していないにも関わらず、 Snake クラスを経由して任意のコードを実行(snake_greeting.txt に hi が出力)できることがわかりました。

なぜ使ってもいないクラスを実行できたのか

これはデシリアライズ処理の実装において、ストリームから読み取ったクラス名をクラスパスから取得した後、先に述べたその中のマジックメソッドが実行されているためです。

このことから、アプリケーションで使用していないクラスであっても、クラスパスに存在するクラスであれば悪用される恐れがあることを示します。
デシリアライゼーションが危険と言われている所以はここにあります。

なお、上記のような攻撃者の目的を果たせるようなメソッドを実装したクラスは Sink と呼ばれています。Runtime#exec 以外に以下を呼び出している場合にSinkになり得ます。

・java.lang.reflect.Method.invoke
・java.lang.reflect.InvocationHandler.invoke
・java.net.URL
・java.io.ObjectInputStream
・java.net.URLClassLoader

3 : Gadget Chain(Property oriented programing)

ここまでマジックメソッドとシンクについて、簡単にそのイメージを紹介しました。

その 2 の例のように Sink となりうるメソッドが直接的にマジックメソッドに書かれているケースもほぼありません。
そのため、攻撃者はガジェットチェーンと呼ばれるキックオフガジェットのマジックメソッドから Sink に到るまでのパス(クラスの連鎖)を見つけ、これを攻撃に用います。

長くなりましたが、デシリアライゼーションが実際に RCE に繋がりうるのはこの方法によってです。

以下はイメージを掴んでいただくことを目的とした、かなりシンプルなガジェットチェーンのサンプルです。
先ほどと登場人物を変え、さらに Sink ガジェット( Dog )はシリアライズ可能であるものの、コード実行部分がマジックメソッドにないため、 Human を Kickoff ガジェットとして使っています。

import java.io.IOException;
import java.io.Serializable;

//kickoff gadget
public class Human implements Serializable {
    private final Runnable dog ;

    public Human(Runnable dog){
        this.dog = dog;
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        dog.run();
    }
}
package UsoSerial;

import java.io.IOException;
import java.io.Serializable;


//Sink gadget
@SuppressWarnings("serial")
public class Dog implements Runnable, Serializable {

    private final String[] order ;

    Dog(String[] order){
        this.order = order;
    }

    @Override
    public void run() {
        try {
            System.out.println("Bow!");
            Runtime.getRuntime().exec(order);
        } catch (IOException e){
            e.printStackTrace();
        }
    }

}
Human someone = new Human(new Dog( new String[]{"/bin/bash","-c","echo  Go! > ./result.log"}));
実行結果


上記では、Human → Dog をガジェットチェーンとし任意のコードを実行する流れを形成しました。
実際の世界でこの様なシンプルな形でガジェットチェーンを構築できる可能性はほぼないでしょう。

デシリアライゼーションの脆弱性がコード実行に繋がりうることに関してイメージを持っていただけたら幸いです。
なお、こうした攻撃手法は「Property oriented Programing」とも呼ばれています。


実際の例 Apache Commons Collection

現実のガジェットチェーンに関して、過去に報告された例を参考までに紹介します。

Apache Commons Collection

Appache Common Collections に含まれるリフレクション機能を持つ InvokerTransformer がシリアライズ可能であったことで、リフレクションによって Runtime の exec メソッド等を実行できるというものでした。

この脆弱性に関しては、以下の記事がとても分かりやすく説明していますので、ぜひご覧ください。


デシリアライゼーションの脆弱性への対策

デシリアライゼーションの脆弱性への対策は、第一に信頼できないソースから受け取ったデータをデシリアライズしないことです。

デシリアライズ処理をライブラリなどで間接的に使っている場合などもあるかと思います。

ライブラリ側では、上記の具体的な方法としてデシリアライズ可能なクラスを制御するホワイトリスト方式や逆にデシリアライズしない対象を定義するブラックリスト方式(バイパス可能であることを指摘されいますが)を用いていたりします。
詳細は今後の記事で解説したいと思います。

今回の記事では、Javaバイト列を例にして解説しましたが、実際に Java バイト列を外部から受け取っていることは稀かと思います。
実際にバイト列を受け取る例としては JMX やRMIなど外向きに公開しないサービスで、通常はそれらに対して前段で何らかの対策が行われていることが一般です。

しかしながら、オブジェクトを受け渡しする別の方法として、JSON、XML 形式を扱うケースは少なくないと思います。
今後の記事では、それらのデータに関する本脆弱性についても紹介しようと思います。


さいごに

今回は デシリアライゼーションの脆弱性について解説しました。

シリアライゼーション / デシリアライゼーション自体は便利な機能ですが、利用にあたって注意が必要です。
それらの機能がオープンソースライブラリの内部で意図せず使われており、ガジェットチェーンを形成していないか人手で確認することはとても困難です。

今後の記事では、ガジェットチェーンが形成されていないか確認するツールの紹介を含めて、どのように向き合えば良いか、その参考となる情報を提供できればと思います。

さいごになりますが、yamory はオープンソースソフトウェアの脆弱性と、その脆弱性に関連する攻撃情報のモニタリングをすることができる脆弱性管理ツールです。

Web アプリケーションで危険となりうる脆弱性を自動的に検出できるため、よりセキュアなシステム構築の助けになるかと思います。

無料でトライアルもできますので、ぜひ一度お試しください。


参考記事

人気の記事

募集中のセミナー

ページトップへ戻る