安全でないデシリアライゼーション?

安全でないデシリアライゼーション(Insecure Deserialization)?
なんと初耳。「OWASP Top 10 - 2017 」で、初めて聞いた。もっと言うと、「OWASP Top 10 2017 日本語版」で、初めて聞いた。しかし、日本語版のリリースは、本当にありがたい。英語には、果敢にチャレンジしているものの、情けないことに、なかなか前に進まない。

そして、さらにありがたいことに、徳丸浩先生が、本脆弱性の入門記事「安全でないデシリアライゼーション(Insecure Deserialization) 入門」を書かれていた。自分は、情報収集の能力が低い上に、収集に十分に時間もかけられておらず、いつも情報不足に陥っている。このため、「徳丸浩の日記」には、いつも、本当に、お世話になっている。


とりあえず、原理だけでも頭に叩き込んでおく。
自分の理解が正しければ、原理は難しくない。というか、とても易しい。「リクエストに、シリアライズされたデータが含まれていて、それをサーバ側で(ノーチェックで)デシリアライズして処理を実行していたら?」まずいに決まっている。この典型例が、「安全でないデシリアライゼーション(Insecure Deserialization) 入門」の『脆弱なプログラム(1)』。つまり、『オブジェクトをシリアライズしてクッキー経由で引き渡ししている』ケース。


早速、Foo オブジェクトと Bar オブジェクトを作って、試してみる*1

さて、Java で書こうとすると、おそらく、init メソッドを持った抽象クラス(またはインターフェース)が必要となる。ちょっと 上記の記事 と名前のつけ方が変わってしまうが、この抽象クラスを Baz という名前にした(もちろん、Serializable をインプリする)。

abstract class Baz implements Serializable {
    abstract void init();
}

class Foo extends Baz {
    void init() { System.out.println("Foo#init"); }
}

class Bar extends Baz {
    void init() { System.out.println("Bar#init"); }
}


そして、次のような流れにする。

  • まず、/Deserialization/s にアクセスする
  • 次に、/Deserialization/d にアクセスする


Foo が、一般ユーザに相当するオブジェクト、Bar が、管理ユーザに相当するオブジェクトと考えると、えっと、Foo#init が、一般ユーザ権限で実行する処理で、Bar#init が、管理者権限で実行する処理、みたいに考えると、セキュリティっぽくなる。


サーブレットを書く前に、シリアライズとデシリアライズを実行する、お手軽な関数を用意しておく。こんな感じでよいと思うのだが。

class SimpleSerializer {

    static String serialize(Serializable object) throws IOException {

        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);) {

            oos.writeObject(object);

            return DatatypeConverter.printHexBinary(bos.toByteArray());
        }
    }

    static Serializable deserialize(String hexString)
        throws IOException, ClassNotFoundException {

        try (ObjectInputStream ois = new ObjectInputStream(
                new ByteArrayInputStream(
                        DatatypeConverter.parseHexBinary(hexString)));) {

            return (Serializable) ois.readObject();
        }
    }
}


いよいよ、サーブレット。まずは、SerializeServlet クラスから。

@WebServlet(name = "SerializeServlet", urlPatterns = {"/s"})
public class SerializeServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {

        res.addCookie(new Cookie("FOO", SimpleSerializer.serialize(new Foo())));
        // ...
    }
}


同様に、DeserializeServlet クラス。

@WebServlet(name = "DeserializeServlet", urlPatterns = {"/d"})
public class DeserializeServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {

        String serialized = "";

        for (Cookie c : req.getCookies()) {
            if (c.getName().equals("FOO")) {
                serialized = c.getValue();
                break;
            }
        }
        if (serialized.isEmpty()) return; // TODO

        try {
            ((Baz) SimpleSerializer.deserialize(serialized)).init();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        // ...
    }
}

これで、一応、正常系は動作する。/Deserialization/d へのリクエスト中、Cookie ヘッダ中の FOO の値を、Bar をシリアライズしたデータに書きかえて送信すると、サーバ側では、Bar#init を実行してくれる。


そりゃそうだ。シリアライズして、デシリアライズしただけだから。久しぶりのブログだというのに、一体、何をやっているのだか。

*1:比較的使い慣れた Java で試すことにする。恥ずかしながら、事情があって、何年もプログラミングをしていない。おかしな箇所があったら、ご指摘いただけるとありがたい。