G-gravity

RubyGemsサプライチェーン攻撃:盗んだ資格情報の保管場所として悪用された公開レジストリ

 パッケージレジストリを悪用する攻撃では、攻撃者がマルウェアを含むパッケージをアップロードし、何も知らない開発者がそれをインストールするという手法が一般的です。今回、研究者が確認したRubyGemsのサプライチェーン攻撃では、これとは逆の手法が使われていました。レジストリをマルウェアの配布先ではなく、窃取したデータを置いておく場所として利用していたのです。


 2026年6月、6.5時間の間に14件の.gemファイルがRubyGems.orgへアップロードされました。これらのgemはいずれもマルウェアを配布するものではありませんでした。代わりに、悪意のあるブラウザー拡張機能によって持ち出された1Passwordのvault(保管庫)の全データが、.gemファイルとして格納されていました。しかも、その.gemファイルは、被害者のブラウザー内でJavaScriptだけを使って作成されたものです。
 持ち出されたデータには、平文のパスワード、SSH秘密鍵、AWSの資格情報、暗号資産ウォレットのシードフレーズ、社会保障番号、クレジットカード番号、銀行口座情報などが含まれていました。対象となったvaultの項目は63件に及びます。
 この攻撃でRubyGemsはデータを運び出すための仕組みとして使われたのではありません。dead drop、つまり窃んだデータを置いておき、攻撃者が後から回収するための場所として利用されました。信頼性が高くアクセスの多いドメインであるため、通常の開発者によるアップロードに紛れ込ませることができたのです。

RubyGemsサプライチェーン攻撃の仕組み

 今回狙われたのは、本物の1Passwordアプリではありませんでした。実際の1Passwordは、ブラウザー拡張機能との通信にHTTPではなく、native messagingとIPCを利用します。攻撃対象となったのは、Next.jsで構築され、ローカルの.localホスト名で動作するパスワード管理アプリのモックでした。そこには、GitHubやGmailのログイン情報、AWS IAMキー、SSHキー、クレジットカード情報、銀行口座情報、暗号資産のシードフレーズなど、合成データが登録されていました。
 今回の事例がセキュリティ研究だったのか、実際の標的に対する攻撃にむけたdry run(予行演習)だったのかは、残された痕跡だけでは判断できません。ただし、攻撃に使われたツールが完全に動作する状態だったことは確認できます。
 データの抽出に特別なアクセス権は必要ありませんでした。<all_urls>への広範な権限を持つブラウザー拡張機能から、localhostや.localのアドレスへアクセスできたからです。実際には3段階でデータを取得していました。
 まず、項目インデックスを使ってvaultにどのような項目があるのかを一覧化し、含まれているデータの種類を把握しました。次に、87件の項目を1件ずつ取得しました。これにより、アプリケーションの画面上で表示されるものと同じ、完全な平文データを取り出しました。
 さらに、データベースのエンドポイントにアクセスし、基盤となるSQLiteの状態を直接ダンプしました。これにより、ユーザーがすでにアーカイブまたは削除していた19件の項目も取得しました。
 取得したデータはすべてメモリ上でまとめて圧縮した後、helper.jsへ渡されます。約10行に圧縮されたこのファイルは、CompressionStream、TextEncoder、fetchという標準のブラウザーAPIだけを使い、tarベースの有効な.gemファイルを生成します。そして、あらかじめ登録された攻撃者のAPIキーを使い、RubyGemsの公開APIへ直接POSTしました。


 ここにはサーバー側のコンポーネントも、ネイティブツールもありません。生成されたgemにはRubyのソースコードがなく、requireすることもできません。このgemは、窃取したデータを格納するためだけのコンテナとして存在していました。

攻撃の解剖:段階的なタイムライン

 14件のgemに埋め込まれたタイムスタンプを時系列に並べると、攻撃がどのように進められたのかが見えてきます。

Phase 1 経路を試す

こんにちは、webmaster さん

 最初はインフラのテストでした。3件の使い捨てgemを使ってアップロードのパイプラインが正常に動作することを確認し、その後、4分間にわたる試行錯誤を経て、問題なくアップロードできることを確認しました。

Phase 2 内部を探る

 続いて偵察が始まります。5件のgemを使って、vaultのインデックス、アプリケーションのチャンク、ルートマップ、レンダリングされたHTMLを取得しました。資格情報そのものに触れる前に、まず標的の構造を把握したのです。

Phase 3 標的を絞る

 その次に、セキュリティダッシュボードを取得するgemが登場します。これによって、攻撃者は脆弱な資格情報や使い回されている資格情報を優先して狙えるようになりました。

Phase 4 データを奪う

 その後、数時間あけて、本格的なデータの取得が始まります。完全なデータベースのダンプと、APIを対象とした一括取得が行われました。2つの独立した方法で同じデータを取得しており、持ち出しが成功したことを確認するための冗長化と考えられます。

Phase 5 成果を確かめる

 最後は、13ページの管理画面をレンダリングして取得していました。これによって、データの持ち出しが成功したことを目で確認しています。

RubyGemsが理想的なdead dropになる理由

 RubyGemsには、コマンド&コントロール(C2)サーバーにはない3つの特徴があります。

  • 通常のgem pushと見分けがつかない:
    開発者のマシンからRubyGems.orgへPOSTしても、通常のgem pushと見分けがつきません。そのため、見慣れないホストへの通信を対象にしたDLPルールやSIEMの検知では、攻撃を捉えられません。
  • 攻撃者側のインフラストラクチャが存在しない:
    停止させるべき攻撃者側のインフラストラクチャが存在しません。利用するインフラそのものが、RubyGemsであるためです。
  • 公開したgemを後から取得できる:
    いったん公開されたgemがそのまま残り続けます。そのため、数週間後でも、どのマシンからでもgem fetchによって1回のコマンドで取得できます。

つまり、窃取したデータをパッケージの中に隠していたのではありません。窃取したデータそのものを、パッケージとしてRubyGemsにアップロードしていたのです。

このサプライチェーン攻撃が検知しにくい理由

 パッケージレジストリが開発者にとって不可欠な存在であることが、そのまま持ち出し経路としての魅力につながっています。RubyGems.orgへのPOSTをブロックすれば、Rubyを使ってソフトウェアを公開している組織の正規の業務まで止めてしまいます。そのため、セキュリティチームが一律にPOSTを遮断することは現実的ではありません。
 ペイロードを調べても、検知につながるとは限りません。今回のgemは、正規のアップロードと同じドメイン、エンドポイント、コンテンツタイプ、形式、およびサイズ範囲に収まっています。さらに、Rubyのソースコードをまったく含んでいないため、バックドアを仕込んだコードを検出する目的で作られた悪意のあるgemのスキャナーも、そのまま通過してしまいます。
 さらに重要なのは、この手法がRuby固有のものではない点です。npmの.tgz、PyPIの.whl、NuGetの.nupkg、Docker Hubのtarレイヤーも、同じようにブラウザー内で組み立てられます。HTTPSによるアップロードを受け付けるレジストリであれば、同様のdead dropとして利用される可能性があります。
 現実的な防御策は、エンドポイントとレジストリの両方で講じる必要があります。たとえばEDRで、ブラウザーのプロセスが認証ヘッダーを付けてレジストリAPIへPOSTしている通信を検知する方法があります。
 加えて、レジストリ側では、公開者の行動、パッケージの構造、キーの使用期間などを対象に異常検知を行うことが考えられます。ただし、こうした仕組みはいずれも、現時点では大規模に実装されていません。

セキュリティおよびAppSecチームにとっての重要なポイント

 Web Compression APIを使ってブラウザー内で.gemを生成し、RubyGemsを匿名のdead dropとして利用する手法は、資格情報の窃取における新たな進化といえます。攻撃者が管理するサーバーを必要とせず、不審な外向き通信も発生させません。フォレンジックで残る痕跡も、通常の開発者による成果物に見えるgemだけです。
 さらに、この手法はnpm、PyPI、NuGetなど、HTTPSによるアップロードを受け付けるほかのレジストリにもそのまま応用できます。攻撃者が所有するインフラだけに注目している防御側は、このタイプの攻撃を見逃す可能性があります。
 なお、gemの名前、ハッシュ、攻撃者のAPIキー全体は公開されていません。これらの情報は、確認済みのセキュリティ研究者および影響を受けたベンダーからの要請があった場合に提供されます。