FlawOpen

flawopen.com/Incidents/equifax-struts-ognl-rce-july-2017

● Equifax · CVE-2017-5638 · July 2017 · CVSS 9.8 · 緊急
セキュリティ研究 · FlawOpen

Equifax:データ約1億5,000万人に影響

CVE-2017-5638 により、未修正の Equifax Struts ポータルで Content-Type ヘッダー経由の OGNL インジェクションが可能でした。攻撃は約 76 日続き、約 1 億 5,000 万人のデータが影響を受けました。[1], [4]

💡 わかりやすい解説 (ELI5)

配送ソフトが、拒否した送り状ラベルをエラー通知に取り込むと想像してください。その通知がラベルを実行可能な命令テンプレートとして扱うと、差出人は住所の誤りを知らせるだけでなく、ソフトに命令を実行させられます。Struts の修正はすでにありましたが、Equifax は古い管理者リストに通知を送り、ネットワークスキャンでもポータル上の脆弱性を検出できませんでした。[1], [4]

主要な概念と専門用語

OGNL
Object-Graph Navigation Language。Struts が Java オブジェクトのプロパティの読み書きに使う式言語です。攻撃者の文字列を OGNL として評価することは、攻撃者のコードを実行することを意味します。[1], [6]
Jakarta マルチパートパーサー
ファイルのアップロードを扱う Struts のコンポーネント。不正な Content-Type を拒否するとき、後で評価されるエラーメッセージを組み立てていました。[1], [3], [2]
式インジェクション(CWE-917)
信頼できない入力がインタープリターの評価する式の中に入り込み、データが命令になってしまうことです。[6]
資産インベントリ
どのシステムでどのソフトウェアのどのバージョンが動いているかの記録。これがないと、パッチの通知を必要なマシンに結びつけられません。[4]

フレームワーク対応

インシデントのタイムライン

2017-03-07

Apache がセキュリティ公告 S2-045 と、修正版の Struts 2.3.32 および 2.5.10.1 を公開する。[1]

2017-05-13

Equifax は後に GAO に対し、別の侵入が 5 月 13 日に始まり、攻撃者が個人データの抽出を開始したと説明しました。それ以前にも不正アクセスはありましたが、その時点ではデータは盗まれていません。[4]

2017-07-29

Equifax が、トラフィック検査装置の期限切れ証明書を更新した後に不審な通信に気づく。[4]

2017-09-07

Equifax が侵害を公表する。後に GAO は、米国内の少なくとも 1 億 4,550 万人と国外の約 100 万人のデータが影響を受けたと報告する。[4]

ステップ・バイ・ステップの攻撃フロー

ステップ 1

細工した Content-Type ヘッダー

攻撃者は、Content-Type ヘッダーにメディアタイプではなく OGNL 式を入れたアップロード要求を送ります。[1]

ステップ 2

エラーメッセージにヘッダーが入り込む

Jakarta パーサーはそのヘッダーを拒否し、ヘッダーの生の文字列を含むローカライズされたエラーメッセージを組み立てます。[1], [3], [2]

ステップ 3

メッセージが評価される

Struts はこのメッセージ内の式を評価するため、攻撃者の OGNL が Web アプリケーションの権限で Java コードとして実行されます。[1], [3], [2], [6]

ステップ 4

静かで長いデータ持ち出し

攻撃者はデータ保管場所で見つけた認証情報を使い、ポータルに関連する 3 つのデータベースからさらにポータルとは無関係な 48 個へアクセスを広げました。GAO によると約 9,000 件のクエリが実行され、攻撃は約 76 日続きました。期限切れの証明書により暗号化通信を検査できませんでした。[4]

ソースコード比較:脆弱 vs 堅牢化

説明用の Java で、Struts のソースコードではありません。2.3.32 と 2.5.10.1 の実際の修正は、マルチパートのエラーの組み立て方を変えるものでした。強化版は固定のメッセージキーを使い、ヘッダーは位置引数としてだけ渡すので、テキストとして整形されるだけで式の一部にはなりません。さらに、メディアタイプらしくない値はプレースホルダーに置き換えるため、後でメッセージを変更しても、ヘッダーが評価される文字列に戻ってくることはありません。[3], [2], [6]

脆弱な実装
import com.opensymphony.xwork2.TextProvider;
import javax.servlet.http.HttpServletRequest;

public final class UploadErrors {
    private final TextProvider text;   // resolves messages and evaluates expressions in them

    public UploadErrors(TextProvider text) {
        this.text = text;
    }

    public String describe(HttpServletRequest request) {
        String contentType = request.getHeader("Content-Type");
        // 生のヘッダーが評価対象のメッセージの一部になる。
        return text.getText("Invalid content type: " + contentType);
    }
}
堅牢化されたセキュアパッチ
import com.opensymphony.xwork2.TextProvider;
import java.util.Locale;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;

public final class UploadErrors {
    private static final Pattern MEDIA_TYPE = Pattern.compile(
        "^[a-z]+/[a-z0-9.+-]+(;\\s*[a-z0-9-]+=[a-z0-9._-]+)*$");
    private final TextProvider text;

    public UploadErrors(TextProvider text) {
        this.text = text;
    }

    public String describe(HttpServletRequest request) {
        String contentType = request.getHeader("Content-Type");
        boolean wellFormed = contentType != null && contentType.length() <= 256
            && MEDIA_TYPE.matcher(contentType.toLowerCase(Locale.ROOT)).matches();
        // 固定のメッセージキーを使い、ヘッダーは位置引数としてのみ渡す。
        // さらに、メディアタイプの形式である場合に限る。
        return text.getText("upload.invalid.content.type",
            new String[] { wellFormed ? contentType : "(malformed)" });
    }
}

エンジニアリング&システム堅牢化チェックリスト

参考資料