安全でないデシリアライゼーション(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 を実行してくれる。
そりゃそうだ。シリアライズして、デシリアライズしただけだから。久しぶりのブログだというのに、一体、何をやっているのだか。