境界防衛の向こう側:Java・PHP・Pythonにおける安全でないデシリアライゼーションを撃破する

Security tutorial - IT technology blog
Security tutorial - IT technology blog

見えない層を守る

午前2時、たった一つの不正なHTTPヘッダーが原因で本番環境が崩壊するのを目の当たりにした。あの夜、私は残酷な教訓を学んだ——従来のファイアウォールは、ロジックレベルの欠陥には無力だということを。

多くのチームがSQLインジェクションやXSSに注力する一方で、安全でないデシリアライゼーションは静かな殺し屋として潜んでいる。OWASP Top 10の第8位にランクインしているのには理由がある。過去6ヶ月間、私たちのチームはアプリケーションのロジックが根本的に壊れていれば、パッチを当てたサーバーでも脆弱であることに気づき、スタックからネイティブシリアライゼーションを取り除いてきた。

シリアライゼーションとは、アクティブなオブジェクトをバイトストリームや文字列などの保存可能な形式に変換することだ。デシリアライゼーションはその逆のプロセスである。この脆弱性は、アプリケーションがバリデーションなしにユーザーが提供したデータを信頼してオブジェクトを再構築しようとしたときに発生する。攻撃者がシリアライズされた文字列を操作すると、多くの場合root権限で任意のコマンドをアプリケーションに実行させることができる。攻撃者がシリアライズされた文字列を操作すると、多くの場合root権限で任意のコマンドをアプリケーションに実行させることができる。これはPythonのバグではなく、信頼できない入力を対象として設計されていなかったライブラリの機能なのだ。

PythonのPickle問題:10行のエクスプロイト

Pythonの開発者はその手軽さからpickleライブラリをよく使う。しかし、pickleは実質的に実行エンジンだ。データを保存するだけでなく、そのデータを再構築する方法の命令も保存する。本番環境において、クライアントからのpickleオブジェクトを信頼することは、ターミナルシェルを渡すのと同じことだ。

import pickle
import os

# このペイロードはデシリアライゼーション時にシステムコマンドを実行する
class MaliciousPayload:
    def __reduce__(self):
        # 実行する呼び出し可能オブジェクトと引数を返す
        return (os.system, ('whoami',))

# 攻撃者がこのオブジェクトをシリアライズする
attack_payload = pickle.dumps(MaliciousPayload())

# 被害者のサーバーがデータを処理する
pickle.loads(attack_payload)

__reduce__メソッドに注目してほしい。このメソッドは、再構築時にどの関数を呼び出すかをPythonに正確に伝える。'whoami'をリバースシェルコマンドに置き換えるだけで、攻撃者は100ミリ秒以内にホストの完全な制御を手に入れることができる。これはPythonのバグではなく、信頼できない入力を対象として設計されていなかったライブラリの機能なのだ。

ガジェットチェーンがライブラリを兵器に変える仕組み

現代のエクスプロイトは、1行の脆弱なコードだけに依存することはほとんどない。代わりに、ガジェットチェーンを活用する。「ガジェット」とは、ロギングユーティリティやデータベースドライバーなど、アプリケーションの依存関係内に存在する既存のクラスで、特定のアクションを実行するものだ。これらをつなぎ合わせることで、攻撃者は自分のライブラリから機能するプログラムを作り出す。

Java:ObjectInputStreamのリスク

Javaのjava.io.ObjectInputStream.readObject()は頻繁に狙われる。クラスパスにApache Commons Collectionsライブラリ(特にバージョン3.2.2以前)が含まれている場合、攻撃者はRuntime.exec()に至るチェーンを引き起こすことができる。これが主要なミドルウェアプラットフォームに影響を与えた大規模な侵害事件の根本原因だった。

// 標準的だが危険なJavaのパターン
InputStream is = request.getInputStream();
ObjectInputStream ois = new ObjectInputStream(is);
// readObject()が呼び出された瞬間にRCEが発動する
Object obj = ois.readObject(); 

PHP:マジックメソッドとPOPチェーン

PHPはunserialize()を使用し、__destruct()__wakeup()のような「マジックメソッド」を呼び出す。プロパティ指向プログラミング(POP)として知られるこの手法により、攻撃者はオブジェクトのプロパティを上書きできる。クラスがプロパティを使用してファイルパスを定義している場合、攻撃者はそのパスをリダイレクトして重要なシステムファイルを削除することができる。

class FileCleaner {
    public $path;
    public function __destruct() {
        // $pathが"index.php"に設定された場合、サイトはダウンする
        unlink($this->path);
    }
}

unserialize($_GET['payload']);

防御者のツールキット:自動化と監査

攻撃者はもはやこれらのチェーンを手動で構築することはない。特定のフレームワークバージョンをターゲットにした自動ジェネレーターを使用する。自分のスタックを守るには、ハッカーよりもこれらのツールをよく理解する必要がある。

  • Ysoserial: SpringやHibernateなどのライブラリ向けにJavaガジェットチェーンを生成するためのゴールドスタンダード。
  • PHPGGC: Laravel、Symfony、Guzzle向けのプリビルトペイロードの大規模ライブラリ。
  • Snyk/OWASP Dependency-Checkpom.xmlrequirements.txtをスキャンし、有効なガジェットとして知られているライブラリを検出するツール。

レガシーシステムを監査したところ、マイクロサービスの15%がysoserialで即座に悪用できる古いライブラリを実行していることがわかった。ライブラリにパッチを当てることは最初のステップだが、アーキテクチャを変えることが唯一の永続的な解決策だ。

本番環境のセキュリティ強化

内部システムを6ヶ月かけてリファクタリングした結果、この攻撃ベクターを完全に排除するための高インパクトな3つの戦略を特定した。

1. データのみの形式への移行

外部通信にネイティブ言語のシリアライゼーションを使用するのをやめよう。JSONProtocol Buffersを使用すること。これらの形式は厳密にデータ指向であり、クラスのインスタンス化やメソッド実行の命令を含まない。ガジェットチェーンが必要とする「ロジック」がないため、本質的に安全だ。

2. ホワイトリストベースのデシリアライゼーション(Java)

Javaのシリアライゼーションから離れられない場合は、クラスがインスタンス化される前に検証する必要がある。resolveClassメソッドをオーバーライドして厳格な許可リストを実装しよう。受け取ったクラスがリストにない場合は、即座に接続を切断すること。

public class SecureInputStream extends ObjectInputStream {
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        if (!desc.getName().equals("com.company.SafeDTO")) {
            throw new SecurityException("不正なクラス: " + desc.getName());
        }
        return super.resolveClass(desc);
    }
}

3. 暗号署名

整合性を検証せずにシリアライズされたデータを処理してはいけない。セキュアなボルトに保存された256ビットキーを使用したHMAC(ハッシュベースメッセージ認証コード)でデータに署名しよう。署名が一致しない場合、データが改ざんされているため、デシリアライズを試みることなくアプリケーションは破棄すべきだ。

セキュリティとは完璧であることではなく、攻撃のコストを報酬よりも高くすることだ。危険なシリアライゼーションパターンを排除し、厳格なバリデーションを徹底することで、基盤の亀裂を塞ぐことができる。すべてのハッカーを止めることはできないかもしれないが、簡単な獲物を探している者たちは確実に阻止できる。

Share: